什麼是 GraphQL?為什麼要用 GraphQL?
GraphQL - A Query Language for APIs
GraphQL is a new API standard that provides a more efficient, powerful and flexible alternative to REST. It was developed and open-sourced by Facebook and is now maintained by a large community of companies and individuals from all over the world. At its core, GraphQL enables declarative data fetching where a client can specify exactly what data it needs from an API. Instead of multiple endpoints that return fixed data structures, a GraphQL server only exposes a single endpoint and responds with precisely the data a client asked for.
這是官方關於 GraphQL 的介紹,濃縮成一句話解釋: an API defines how a client can load data from a server
大概可以達到這些優點:
① It lets the client specify exactly what data it needs. / 由前端決定需要哪些資料 ② It uses a type system to describe data. / 資料是具有嚴謹的型態規範 ③ It makes it easier to aggregate data from multiple sources. / 只需要單一的 API 端口
GraphQL is the better REST 跟傳統的 REST 的相比,可以用這張圖解釋:
根據 REST 的做法,我們依照每一個 Resource 為單位(通常 depend on 資料庫的表格)。也就是說,前端需要一個資源時即需要呼叫一次 API,N 個資源時需要呼叫 N 次。可能會隱性的增加 Request 與增加新資源的開發成本。 RESTFul 分為幾個階段:Request → URL Route → Handle Controller -> Multiple Endpoint
在 GraphQL 的架構中將 URL Route → Handle Controller 的動作直接壓縮設計在 API 中透過 Schema 跟 Resolver 取代,將 Resources 視為是一個 Graph,僅需要透過 One Endpoint API 就可以做到對資料存取的操作。像這樣使用:
為什麼要用 GraphQL? 目前應用程式的發展是很快速的,資料的定義也很難在一開始就設定得很完美,很常都是前、後端同時開發同時串接。而因為 API 的興起,一個後端通常需要應付很多前端平台,像是網頁前、後台、Android、iOS app 之類的。多個呈現畫面的前端平台仰賴於一個後端與一個資料庫的來源,RESTFul 其實是一個相對舒適的被動解法。反正後端就是開好在那邊,請大家依照文件各取所需就好。但是隨著開發時程拉長、規模變大之後,會發現不同的前端可能會有不同的要求,此時後端可能就需要一個每個來源進行調整或是吵架 (?)
GraphQL 可以優雅地處理這個問題,大概有以下幾點好處:
① No more Over and Less - Overfetching and Underfetching / 前端需要什麼自己決定,可以避免 Response 太肥或不足的問題 ② Benefits of a Schema & Type System / 資料類型的定義可以讓達到初步的驗證效果 ③ Rich open-source ecosystem and an amazing community / 開源的社群資料很多,也有許多有趣的配套解法可以混搭
Thinking in graphs 前面有提到, GraphQL 將資源視為一個 Graph,這邊的資源可以想成是資料庫中的每一種資料。
怎麼開始、怎麼使用 GraphQL 由幾個部分所組成:
TypeDefs
Resolvers
Queries (Query、Mutation)
Schema
=> API contain Schema contain Queries contain TypeDefs and Resolvers
TypeDefs 資料可以定義成下面這樣子,Query 跟 Mutation 作為 Graph 的 Root,其中在包含 Resource 的組合:
1 2 3 4 5 6 7 8 9 10 11 12 type Query { person: person! persons: [person] } type Mutation { create(name: String!, age: Int!): String } type Person { name: String! age: Int! }
每一種資料都可以設置屬性與類型,可以當作初步的驗證!
Resolvers 可以想成一個 Function,負責解析怎樣的需求該對應怎樣的操作。
Queries (Query、Mutation) Queries 分為 Query、Mutation 兩種,定義前端的 API 要如何跟後端溝通。
Fetching Data with Queries
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 query { Person(last: 1 ) { name }, Persons { id name } } # Return: # { # "data": { # "Person": { # name: "Sarah" # }, # "Persons": [{ # "id": 1, "name": "Johnny" # }, { # "id": 2, "name": "Sarah" # }] # } # }
Writing Data with Mutations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 mutation { create(name: "Bob" , age: 36 ) { id } } # Return: # { # "data": { # "create": { # "id": 3 # } # } # }
Schema Schema 定義 API 的抽象層,將最上層的 Query Root 往下視為一個 API Graph。
簡單總結如下:
TypeDefs ➞ 定義資料 & Query 的型態
Resolvers ➞ 定義操作資料的方式
Queries (Query、Mutation) ➞ 資料操作的最上層
Schema ➞ 定義 API 的抽象層
實踐第一個 GraphQL Server 開始 目前常見的 GraphQL API Server 大概有以下幾種做法與分別來自不同的套件:
第一種寫法:GraphQLObjectType + GraphQLSchema / graphql
第二種寫法:buildSchema + typeDefs + rootValue / graphql
第三種寫法:makeExecutableSchema + typeDefs + resolvers / graphql-tools
第四種寫法:ApolloServer + typeDefs + resolvers / apollo-server
第五種寫法:GraphQLServer + typeDefs + resolvers / graphql-yoga
這邊都是以 Node + Express 為範例,也先將資料來源假設是靜態變數,實務應用的時候可以把 Resolver 改為直接存取資料庫的 Function。
第一種寫法:GraphQLObjectType + GraphQLSchema 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 // 1 . 先定義資料來源 & 操作資料的 resolvers let users = [ { id: 0 , name: 'Tom' , sex: 0 , }, { id: 1 , name: 'Bob' , sex: 0 , }, { id: 2 , name: 'Alick' , sex: 1 , }, ]; const userResolver = ({id}) => users.filter(u => u.id == id)[0]; const usersResolver = () => users; // 2 . 利用 GraphQLObjectType 定義 data 的 TypeDefs const userType = new graphql.GraphQLObjectType({ name: 'user' , fields: { id: { type: graphql.GraphQLInt }, name: { type: graphql.GraphQLString }, sex: { type: graphql.GraphQLInt }, } }) const usersType = new graphql.GraphQLList(userType) // 3 . 再利用 GraphQLObjectType 定義 Query 的 TypeDefs 且將 resolvers 定義在其中 const queryType = new graphql.GraphQLObjectType({ name: 'Query' , fields: { user: { type: userType, resolve: (_, args) => userResolver(args), args: { id: { type: graphql.GraphQLInt } }, }, users: { type: usersType, resolve: () => usersResolver() } } }) // 4 . 利用 GraphQLSchema 將 Query 封裝成 Schema const schema = new graphql.GraphQLSchema({ query: queryType }); // 5 . 利用 graphqlHTTP 將 Schema 封裝成 API app.use( '/' , graphqlHTTP({ schema: schema, graphiql: true }) );
第二種寫法:buildSchema + typeDefs + rootValue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // 1 . 先定義資料來源 & 操作資料的 resolvers let users = [ { id: 0 , name: 'Tom' , sex: 0 , }, { id: 1 , name: 'Bob' , sex: 0 , }, { id: 2 , name: 'Alick' , sex: 1 , }, ]; const userResolver = ({id}) => users.filter(u => u.id == id)[0]; const usersResolver = () => users; const resolvers = { Query: { user: (_, args) => userResolver(args), users: () => usersResolver() }, } // 2 . 利用 template string 定義 data 跟 Query 的 TypeDefs const typeDefs = ` type Query { user(id: Int!): User users: [User] }, type User { id: Int name: String sex: Int } `; // 3 . 利用 buildSchema 將 Query 封裝成 Schema const schema = graphql.buildSchema(typeDefs); // 4 . 利用 graphqlHTTP + rootValue 將 Query 與 resolver 串接且封裝成 API app.use( '/' , graphqlHTTP({ schema: schema, rootValue: { user: userResolver, users: usersResolver }, graphiql: true }) );
第三種寫法:makeExecutableSchema + typeDefs + resolvers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // 1 . 先定義資料來源 & 操作資料的 resolvers let users = [ { id: 0 , name: 'Tom' , sex: 0 , }, { id: 1 , name: 'Bob' , sex: 0 , }, { id: 2 , name: 'Alick' , sex: 1 , }, ]; const userResolver = ({id}) => users.filter(u => u.id == id)[0]; const usersResolver = () => users; const resolvers = { Query: { user: (_, args) => getCourse(args), users: () => getCourses() }, } // 2 . 利用 template string 定義 data 跟 Query 的 TypeDefs const typeDefs = ` type Query { user(id: Int!): User users: [User] }, type User { id: Int name: String sex: Int } `; // 3 . 利用 makeExecutableSchema 將 Query 跟 resolver 封裝成 Schema const schema = makeExecutableSchema({ typeDefs, resolvers, }); // 4 . 利用 graphqlHTTP 將 Schema 封裝成 API app.use( '/' , graphqlHTTP({ schema: schema, graphiql: true }) );
第四種寫法:ApolloServer + typeDefs + resolvers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 let users = [ { id : 0 , name : 'Tom' , sex : 0 , }, { id : 1 , name : 'Bob' , sex : 0 , }, { id : 2 , name : 'Alick' , sex : 1 , }, ]; const userResolver = ({id} ) => users.filter(u => u.id == id)[0 ];const usersResolver = () => users;const resolvers = { Query: { course: (_, args ) => getCourse(args), courses: () => getCourses() }, } const typeDefs =gql` type Query { user(id: Int!): User users: [User] }, type User { id: Int name: String sex: Int } ` ;const server = new ApolloServer({ typeDefs, resolvers, }); const app = express();server.applyMiddleware({ app });
第五種寫法:GraphQLServer + typeDefs + resolvers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // 1 . 先定義資料來源 & 操作資料的 resolvers let users = [ { id: 0 , name: 'Tom' , sex: 0 , }, { id: 1 , name: 'Bob' , sex: 0 , }, { id: 2 , name: 'Alick' , sex: 1 , }, ]; const userResolver = ({id}) => users.filter(u => u.id == id)[0]; const usersResolver = () => users; const resolvers = { Query: { course: (_, args) => getCourse(args), courses: () => getCourses() }, } // 2 . 利用 template string 定義 data 跟 Query 的 TypeDefs const typeDefs = ` type Query { user(id: Int!): User users: [User] }, type User { id: Int name: String sex: Int } `; // 3 . 利用 makeExecutableSchema 將 Query 跟 resolver 封裝成 Schema API const server = new GraphQLServer({ typeDefs, resolvers, }) server.start()
架構與實現 官方將常見的使用案例分為三種,也提出三種可以導入的方式:
① GraphQL server with a connected database ② GraphQL layer that integrates existing systems ③ Hybrid approach with connected database and integration of existing system
GraphQL 也可以分為前端與後端:
① A GraphQL server that serves your API. => 套件有: Apollo Client、Relay ② A GraphQL client that connects to your endpoint. => 套件有: express-graphql、 apollo-server、 graphql-yoga
除了前後端之外,也可以加一些額外的工具:
前端後端間也可以有一個 Cache Gateway: Apollo Engine
後端與資料庫間也可以用一個 ORM Layer:Prisma、join-monster
Then ? GraphQL 的第一步可以這樣開始:
規劃資料庫與資料的關係,Thinking in graphs
配置後端 GraphQL Server + GraphiQL 測試
開始串前端!
可以搭配 投影片 閱讀:)
Reference [1] GraphQL [2] The Fullstack Tutorial for GraphQL
License 本著作由 Chang, Wei-Yaun (v123582) 製作, 以創用CC 姓名標示-相同方式分享 3.0 Unported 授權條款釋出。