Skip to main content

[course] Fullstack TypeScript

keywords: typescript, graphQL

本篇非原創文章,內容整理自 Fullstack TypeScript @ Frontend Masters

練習用專案:fem-fullstack-ts @ gitlab

GraphQL Intro

Why GraphQL

想像原本的 API 能夠滿足 UI 的需求,但在某一次改版時,UI 需要額外多一個欄位的資料,但這個欄位是來自另一個 resource / model 的。

這時候我們可以有幾種不同的作法:

  1. 使用原本的 API endpoint,並且多加上所需的新欄位
    • 可能會有 over-fetching 的情況,同一支 API 回傳的內容越來越多,甚至可能包含到 UI 上使用不到的資料
  2. 建立一支新的 API,在那支 API 只回傳所需的新欄位,在載入 UI 的時候同時打兩支不同的 API
    • 很難維護,將會不清楚那些 API 在那裡被使用
    • client 需要打很多 API,server 的 QPS 會變高,並且增加了 race condition 問題發生的機可能性
  3. 建立一支新的 API endpoint,並且在這支 API 回傳所有需要的資料,包含原有的欄位和新欄位,以滿足該頁面的需求
    • 違反了原本 RESTful model 的慣例
  4. 調整現有的 API,讓它可以根據需要回傳不同類型的資料(例如,使用 quertParam)
    • 增加測試的複雜度

從上面的情境可以看到,不論做的是哪一種選擇,都還是有其缺點,而 GraphQL 的目的就是要解決這樣的問題 —— client 只去請求它所需的資料,且這個資料可以是來自許多不同的 resources / models。

Basic Syntax

const typeDefs = gql`
type Query {
// 驚嘆號表示 required (non-nullable),預設都是 nullable
currentUser: User!

// array of Suggestion
// []! 的意思是,一定會有 Array 存在
// [Suggestion!] 的意思是,在 Array 中不會有 null
// 所以空陣列還是合法的
suggestions: [Suggestion!]!
}
type User {
id: String!
name: String!
handle: String!
coverUrl: String!
avatarUrl: String!
createdAt: String!
updatedAt: String!
}
type Suggestion {
name: String!
handle: String!
avatarUrl: String!
reason: String!
}
`;

Server: Setup Apollo Server

使用 apollo-server-express 提供的 ApolloServer 來建立 Apollo Server。

Server: Resolver

Write Schema

feat(resolver): move schema into a single file @ gitlab commit

可以把 graphQL schema 的檔案拆成獨立的檔案(例如,xxx.graphql),這裡需要搭配 @graphql-tools 提供的方法,像是 loadSchemaSyncGraphQLFileLoaderaddResolersToSchema 等等。

Write Resolver

feat(resolver): change resolver as some modules

最後,也可也把一開始定義好的 resolver 拆成獨立的檔案。

// https://gitlab.com/PJCHENder/fem-fullstack-ts/-/commit/b3338de9ec2d5acaffa17acd21788a904a781a28
const resolvers = {
Query: {
currentUser: () => {
return {
id: '123',
name: 'John Doe',
handle: 'johndoe',
coverUrl: '',
avatarUrl: '',
createdAt: '',
updatedAt: '',
};
},
suggestions: () => {
return [];
},
},
};

Typed Resolver

可以使用 @graphql-codegen 這個套件來自動根據 resolver 產生出對應的 TypeScript 型別,例如 resolvers-types.generated。接著即可使用這支檔案中所定義好的型別,像是 ResolversQueryResolvers

info

@graphql-codegen 會去讀取專案中 codegen.yml 的設定檔。

caution

如果是用 VSCode 的話,跑完 codegen 後,有很高的機會要記得 restart TS server 和 restart ESLint server。

Typed ResolverContenxt

feat(resolver): typed the ResolverContext @ gitlab commit

我們可以根據需要自己定義 Context 的 interface:

export interface TwitterResolverContext {
db: Db;
}

Client: Setup Apollo Client

feat(apollo-client): setup apollo-client @ gitlab commit

使用 @apollo/client 提供的 ApolloClientApolloProviderInMemoryCache 來建立與 ApolloServer 的連線。

Client: Query Data

一般來說,不論是 Query 或 Mutation 都可以分成這三個步驟:

  1. Create Query:使用 gql 來撰寫 GraphQL 用的 Query 或 Mutation
  2. GraphQL Codegen:透過 @graphql-codegen 來產生對應的型別和可以用來呼叫的方法
  3. Query Data:使用 codegen 產生的檔案中所提供的方法 useGetCurrentUserQuery 來 query 我們所需的資料。
caution

如果是用 VSCode 的話,跑完 codegen 後,有很高的機會要記得 restart TS server 和 restart ESLint server。

GraphQL

幾個關鍵字:

  • Query
  • Nested Resolver
  • Mutation
  • Union

資料來源