從前端踏入後端

先交代一下心路歷程,佔個版面

去年三月開始進公司寫前端

  • html, css, javascript

  • react, redux

  • restful, graphQL

lodash, moment, third party library

other knowledge......

中間花了點時間參與糞 game

突然來了個訊息

今年8月開始寫後端 + typescript

mysql

apollo-server

sequelize, typeorm

koa, express

nginx, docker, gcp

linux

讓後端大概知道前端在想什麼

前端大概知道後端在想什麼

今日主題

以及我當中的開發心得 ><

Application

 完成功能且讓使用者愉悅

UI/UX + front-end + back-end

使用畫面/流程/體驗

在對的時間點呈現合邏輯的畫面

功能正確性

PM?

UI/UX + front-end + back-end

使用畫面/流程/體驗

front-end + back-end

使用體驗

差的體驗?研訓院

  • 等待時間過長 (資訊載入/寫入速度)
  • 狀態置入 (initialValue/initialState)

舉個例子

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

front-end

state

back-end

request

response

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

function Component(props): JSX.Element {
  const { id } = useParams();
  const { state, setState } = useState();

  const { dataA } = useQuery(A)
  const { dataB } = useQuery(B)

  const aggregateData = useMemo(() => {
    /** 複雜運算 */
  }, [dataA, dataB, state]);

  return (
    <div>
      ...
    </div>
  )
}

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

function Component(props): JSX.Element {
  const { id } = useParams();
  const { state, setState } = useState();

  const { dataA } = useQuery(A)
  const { dataB } = useQuery(B)

  const aggregateData = useMemo(() => {
    /** 複雜運算 */
  }, [dataA, dataB, state]);

  return (
    <div>
      ...
    </div>
  )
}

apollo client cahce

redux 

狀態

state

routing

auth

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

function Component(props): JSX.Element {
  const { id } = useParams();
  const { state, setState } = useState();

  const { dataA } = useQuery(A)
  const { dataB } = useQuery(B)

  const aggregateData = useMemo(() => {
    /** 複雜運算 */
  }, [dataA, dataB, state]);

  return (
    <div>
      ...
    </div>
  )
}

狀態

auth

singleton

single source of truth

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

function Component(props): JSX.Element {
  const { id } = useParams();
  const { state, setState } = useState();

  const { dataA } = useQuery(A)
  const { dataB } = useQuery(B)

  const aggregateData = useMemo(() => {
    /** 複雜運算 */
  }, [dataA, dataB, state]);

  return (
    <div>
      ...
    </div>
  )
}

發 request

注意 cache 和畫面是否同步 

local cache 一致性

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

function Component(props): JSX.Element {
  const { id } = useParams();
  const { state, setState } = useState();

  const { dataA } = useQuery(A)
  const { dataB } = useQuery(B)

  const aggregateData = useMemo(() => {
    /** 複雜運算 */
  }, [dataA, dataB, state]);

  return (
    <div>
      ...
    </div>
  )
}
  • 注意時間複雜度
  • 熟悉 data structure
  • lodash 好幫手

重組資料

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

重組資料

  • 注意時間複雜度
  • 熟悉 data structure
  • lodash 好幫手

可讀性 > 時間複雜度

寫一個很屌的 reduce,

但可能你過了一個月後就要花更多時間去看懂它 

=> 去權衡增加時間複雜度換個可讀性更好的 function

reduce 通常可以取代 map + filter

groupBy/OrderBy

Map, WeakMap

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (前端視角)

正確的使用 framework 裡的 method

所達成的優化更加明顯

useEffect/useMemo

 render/更新狀態

注意重新 render 的次數

useMemo 盡量不要存 DOM,

應該是存複雜運算的結果

前端

完成功能的前提下,

在對的時間點呈現合邏輯的畫面

啊優化就大概兩個面向

降低 render 所需時間,減少 bundleSize

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (後端視角)

function Component(props): JSX.Element {
  const { id } = useParams();
  const { state, setState } = useState();

  const { dataA } = useQuery(A)
  const { dataB } = useQuery(B)

  const aggregateData = useMemo(() => {
    /** 複雜運算 */
  }, [dataA, dataB, state]);

  return (
    <div>
      ...
    </div>
  )
}

發 request

restful

graphql

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (後端視角)

front-end

state

back-end

狀態 -> 發 request -> 重組資料 -> render/更新狀態

call API (後端視角)

front-end

state

back-end

Web server

database

back-end

Web server

database

SQL

SELECT column-names
  FROM table-name
 WHERE condition

(ORM) 

ORM 做了啥?

先看看 database 在做什麼

=> 資料該怎麼存

規劃 table 和 column 以及對應關聯

  1. 建立Model 和 Table 去對應

2. 寫 resolver 時去實作 model 進行 CRUD, 並組資料

而 ORM 做了什麼

=> 中間人去做 CRUD 

常見的後端日常

  1. 建立Model 和 Table 去對應

2. 寫 resolver 時去實作 model 進行 CRUD, 並組資料

@Entity({ name: 'Activities' })
export class Activity extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({
    type: 'enum',
    enum: ActivityType,
    default: ActivityType.SIGN_UP,
  })
  type: ActivityType;

  @OneToOne(() => SignUpActivity, (signUpActivity) => signUpActivity.activity)
  signUpActivity: SignUpActivity;

  @OneToOne(
    () => AnnouncementActivity,
    (announcementActivity) => announcementActivity.activity,
  )
  announcementActivity: AnnouncementActivity;

  @OneToOne(() => InformationActivity, (informationActivity) => informationActivity.activity)
  informationActivity: InformationActivity;

  @ManyToMany(() => Hashtag)
  @JoinTable({
    name: 'ActivityHashtags',
    joinColumn: {
      name: 'ActivityId',
      referencedColumnName: 'id',
    },
    inverseJoinColumn: {
      name: 'HashtagId',
      referencedColumnName: 'name',
    },
  })
  hashtags: Hashtag[];
}
  1. 建立Model 和 Table 去對應

2. 寫 resolver 時去實作 model 進行 CRUD, 並組資料

@Resolver(() => ActivityInterface)
export class ActivityResolver {
  @Query(() => [ActivityInterface])
  async activities(
  @Arg('type', () => ActivityType, { nullable: true }) type: ActivityType,
    @Arg('category', () => ActivityCategory, { nullable: true }) category: ActivityCategory,
    @Arg('searchTerm', { nullable: true }) searchTerm: String,
    @Args() {
      offset, limit,
    }: OffsetLimitArg,
  ) {
    const qb = getRepository(Activity)
      .createQueryBuilder('activity')
      .leftJoinAndSelect('activity.signUpActivity', 'signUpActivity')
      .offset(offset)
      .limit(limit)
      .where('activity.releasedAt <= CURRENT_TIMESTAMP')
      .orderBy('activity.releasedAt', 'DESC');

    if (type !== undefined) qb.andWhere('activity.type =:type', { type });
    if (category !== undefined) qb.andWhere('activity.category =:category', { category });

    if (searchTerm) {
      qb.andWhere(
        new Brackets(
          (queryBuilder) => queryBuilder
            .orWhere('activity.title LIKE :like')
            .orWhere('EXISTS (SELECT * FROM ActivityHashtags WHERE HashtagId LIKE :like AND ActivityId = activity.id)'),
        ),
        { like: `%${searchTerm.replace('%', '\\%').replace('_', '\\_')}%` },
      );

      qb.andWhere('activity.type !=:type', { type: 'INFORMATION' });
    }

    return qb.getMany();
  }
}

那什麼時候要決定多開什麼 API ?

所以當有 order, filter 時的條件,就是需要後端開 API 的時候

要先知道拉多筆資料時要設定 offset, limit,

才不會拉整個 table

剛剛看完是前端和後端喬 API 的溝通

讓我們拉回前端

 

前端從頭開發大概是怎麼回事

前端架構指引 ->

要做哪些共用 UI 元件, 頁面對應層級

瀏覽過一遍設計圖

釐清需求,瀏覽過一遍 wireframe

要採用哪些技術(框架以及其生態系套件)

後端 開發

-> API 設計

-> 要注意會不會拉太多資料以及 table

-> transaction

瀏覽過一遍設計圖

釐清需求,瀏覽過一遍 wireframe

要採用哪些技術(框架以及其生態系套件)

建 table, 確認每個 table 的關聯性

CPU bound

I/O

bound

我們一樣從輸入網址列開始講起 XD

阿還有沒有其他優化系統的方向

browser

wifi

router

DNS

server

host(url) -> IP

DNS不是url 換IP
是 host

url 跟 host 不一樣
url包含 協定, port, path...
DNS不會知道那些

TCP, TLS

client

AP server

AP server

AP server

AP server

單機

nginx

rytass.com

rytass.admin.com

1.2.3.4:6666

1.2.3.4:8888

reverse proxy

client

load balance

AP server

AP server

AP server

AP server

分散式

client

AP server

database

CDN

Redis

cache

redis -> 記憶體

database -> 硬碟

全端 開發

client

isomorphic

server

enum, error

official

admin

UI, asset

model

Text

official

admin

Thanks

Reference

從前端踏入後端

By Jay Chou

從前端踏入後端

  • 712