初識 GraphQL
第一次聽到 GraphQL 是在 Facebook 使用 React Native 做了一個 F8 App ,但那時候都把精神都花再學習 Reactjs 上,所以後來也沒有特別去了解。
最近這陣子剛退伍,整理一下了 Instapaper,發現了好幾篇跟 GraphQL 相關的文章(雖然還沒仔細閱讀 😛),GraphQL 真的慢慢變成趨勢了,所以想趁這段空窗期好好學習一下 GraphQL,順便記錄一下學習的過程,希望可以幫助對於 GraphQL 沒有概念的朋友可以對 GraphQL 有多一點認識!
GraphQL 是什麼
GraphQL 是一個 「query language」,是由 Facebook 所提出的,GraphQL 主要是為了解決在 REST 上所遇到的一些問題,待會在下面文章我們會繼續討論到,對於 GraphQL 想要更深入的了解請參考以下連結:
- facebook/graphql — https://github.com/facebook/graphql
- GraphQl — https://facebook.github.io/graphql/
// 假想取得一篇文章的資訊,包含文章作者、時間、日期、留言。{posts {idauthortitledatecomments {commentIdusertimestamp}}}
GraphQL 不是一個程式語言或是框架,它是一個概念和規範,就像 REST 那樣,每個不同的程式語言都有實作自己的 GraphQL,更多請參閱:Code | GraphQL。
GraphQL 解決了哪些問題
當我們在設計 API 的時候,通常都會遵循 REST 的風格來設計,在 REST 中,我們把這些不同的資源利用四個動詞:GET、POST、UPDATE、DELETE 來做不同的 state transfer,這些不同的搭配產生的 URI 讓我們來作為 API,例如:
GET /posts // <- 取得所有文章POST /posts/ // <- 新增文章UPDATE /posts/1 // <- 更新 id 為 1 的文章DELETE /posts/1 // <- 刪除 id 為 1 的文章
上面例子是很簡單的 RESTful API,但是當需求變得比較複雜的時候,可能會用到多個 API,或者是透過 Model 關聯拿回需要的資料,在這個部份有可能會回傳多餘的欄位資料,造成網路資源的浪費;又或者在處理巢狀的 Route 不是這麼的容易,當應用程式越來越複雜時,會發現 Endpoints 會變得非常龐大。
從上圖來比較 REST 和 GraphQL,當我們透過 REST 來取得 album 的資料和評論時,我們需要透過兩次的 GET 來取得;透過 GraphQL 我們可以明確的告訴 Server 我們需要回傳哪些欄位,一次就可以拿回全部的資料,相較於 REST 更加靈活。
就我對 REST 的了解,和現在目前對 GraphQL 的認識,我的想法是:
RESTful 所有的 resource 都是圍繞著被定義的 route 在進行,而 GraphQL 則是透過定義不同的 Type,然後建立一個 Schema,如 GraphQL 其名,由每個不同的 Type 內的欄位來組織成不同的資料。
以下內容的相關程式碼在:graphql-emoji-for-learning
建立 GraphQL Server
前面大概解釋了一下 GraphQL 解決了在 REST 上所遇到的一些問題,接下來還是透過實作簡單的範例來感受一下 GraphQL 所帶來的好處,我將使用 JavaScript 來實做並利用一個 emoji.json
來作為假想的 Database,再透過 GraphQL 來取得 emoji 的資料。
首先,我們先完成前置的準備,先建立一個 GraphQL 的 Server:
# 安裝 Koa$ npm install koa@2 koa-convert koa-mount koa-graphql --save-dev# or use yarn$ yarn add -D koa@^2.0.0 koa-convert koa-mount koa-graphql
接著,我們安裝由 JavaScript 實作 GraphQL 核心的 package — graphql
$ npm install graphql --save# or use yarn$ yarn add graphql
完成 package 安裝後,我們來建立一個 schema.js
和 server.js
來測試 GraphQL Server:
// schema.jsconst { graphql, buildSchema } = require('graphql');exports.schema = buildSchema(`type Query {hello: String}`);exports.rootValue = {hello: () => 'Hello world!',};
// server.jsconst Koa = require('koa');const mount = require('koa-mount');const convert = require('koa-convert');const graphqlHTTP = require('koa-graphql');const { schema, rootValue } = require('./schema');const app = new Koa();app.use(mount('/graphql', convert(graphqlHTTP({schema,rootValue,graphiql: true,}))));app.listen(3000);
執行 node server.js
,接著打開 http://localhost:3000/graphql
確認;
GraphiQL 可以讓你很方便執行 query、mutation,並可以在右邊馬上看到你的執行結果,有助於你在 Debug 時找到問題。
如何定義 Type
Type System 是 GraphQL 的核心之一,用來定義你的資料類型,在 query 後該回傳什麼類型的資料回來,我們將這些所有 Type 組合起來就是變成了一個 Schema 了!
在 Schema 中,我們都會有一個最基本的 Query Type,它是一個 Key-Value 的形式,每個 Key 會對應到一個 Type;GraphQL 本身內建了五種 Scalar Type,你可以在你的 Schema 內直接使用它們:
- String
- Int
- Float
- Boolean
- ID
預設上,每個 GraphQL Type 都可以是 null,可以透過加入 ! 來表示該欄位不可以為 null:
// 以 schema.js 為範例type Query {hello: String!}
如果需要使用回傳的並不是單一的值,我們可以使用 List 這個,只要把 Type 加入 []
就可以了:
type Query {hello: [String!]! // hello 是 String List,且該欄位和回傳值不能為 null}
定義 Object Type
這個範例使用了 emoji.json,裡面有一些 Emoji 相關資訊,透過 fetch 將資料抓回來後,做一些處理,並以前面幾筆作為範例 data。
處理完成後的資料結構如下:
{1: {"no": 1,"code": "1F600","char": "😀","name": "GRINNING FACE","date": "2012ˣ","keywords": ["face", "grin"]},3: {"no": 2,"code": "1F601","char": "😁","name": "GRINNING FACE WITH SMILING EYES","date": "2010ʲ","keywords": ["eye", "face", "grin", "smile"]},3: {"no": 3,"code": "1F602","char": "😂","name": "FACE WITH TEARS OF JOY","date": "2010ʲ","keywords": ["face", "joy", "laugh", "tear"]}}
很多時候我們需要用到的 Type 可能不是單純只有上面的 Scalar Type,所以我們可以定義自己所需的 Type,定義新的 Object Type 就像在定義 type Query 一樣,將你需要的欄位寫上,並給定型別(ex:String、ID ...etc),Type 中的欄位型別也可以是其他你所定義的 Type,並不拘限內建的 Type。
依據整理後的 JSON 結構,新增 type Emoji 並更新我們前面的 schema.js:
const { graphql, buildSchema } = require('graphql');const data = require('./data.json');exports.schema = buildSchema(`type Emoji {no: ID!code: String!char: String!name: String!date: String!keywords: [String!]!}type Query {emojis: [Emoji!]!}`);exports.rootValue = {emojis: () => Object.keys(data).map(id => data[id]),};
接著打開 GraphiQL 輸入以下 query,確認執行結果:
query {emojis {nocodecharnamedatekeywords}}
在前面提到了:「Type 中的欄位型別也可以是其他你所定義的 Type」,聽起來有點繞,讓我們看一下是怎麼回事,假設我們今天有另外一個 data 是長成這樣的:
{1: {"name": "face","ids": [1, 3],"list": [{"no": 1,"code": "1F600","char": "😀","name": "GRINNING FACE","date": "2012ˣ","keywords": ["face", "grin"]}, {"no": 3,"code": "1F602","char": "😂","name": "FACE WITH TEARS OF JOY","date": "2010ʲ","keywords": ["face", "joy", "laugh", "tear"]}]},2: {"name": "grin","ids": [1, 2],"list": [{"no": 1,"code": "1F600","char": "😀","name": "GRINNING FACE","date": "2012ˣ","keywords": ["face", "grin"]}, {"no": 2,"code": "1F601","char": "😁","name": "GRINNING FACE WITH SMILING EYES","date": "2010ʲ","keywords": ["eye", "face", "grin", "smile"]}]}}
有注意到 list 這個 key 嗎?是否和我們所定義的 type Emoji 相似,沒錯!所以我們可以這樣去定義這個資料的 type:
type XXX {name: String!ids: [Int!]!list: [Emoji!]!}
傳入參數
在 GraphQL 也可以使用傳入參數的搜尋方式,我們利用上面的範例再做一些小改進,首先我們在 type Query 的部份再新增一個 relatedEmoji(keyword: String!)
:
type Query {emojis: [Emoji!]!relatedEmoji(keyword: String!): [Emoji!]!}
我們將 keyword 傳給 Resolver Function,讓 Resolver Function 去作應對的處理:
exports.rootValue = {emojis: () => { ... },relatedEmoji: ({ keyword }) => Object.keys(data).map(id => data[id]).filter(emoji => emoji['keywords'].includes(keyword) ? emoji : null),};
What’s resolver function? resolver function 就是當我們這個 field(ex:relatedEmoji) 在執行時,所對應到的 f unction 來處理並產生結果。
我們再透過下面的方式在 query 一次,確認 query 的結果:
Mutation
Mutation 翻譯過來的意思是:突變、變化;當我們在新增、修改、刪除資料時,我們在這都稱這個動作為為 Mutate,而在 GraphQL 主要的動作就是 Query 和 Mutation,在這個部份簡單介紹如何使用 Mutation。
首先,我們必須在 buildSchema
新增另一個 Top-Level Type — Mutation:
exports.schema = buildSchema(`type Emoji { ... }type Query { ... }type Mutation { ... }`)
新增 Emoji
這裡我們先來實作新增(Create)資料的部份,在 SQL 中也就是所謂的 Insert,我們更新 type Mutation 的內容:
type Mutation {createNewEmoji(input: EmojiInput): Emoji}
在 createEmoji(input: EmojiInput)
的地方這裡有個小細節,這裡的 EmojiInput 是定義一個 input 的 type( input-types),我們可以預期得到一個我們所要的輸入 type:
input Emoji {code: String!char: String!name: String!date: String!keywords: [String!]!}
所以在新增一筆新的 Emoji 之後,回傳的 Type 是 Emoji,在這裡可以很明顯感受到 GraphQL 的 Type System 所帶來的好處;接著我們來實作 createNewEmoji
的 Resolver Function:
exports.rootValue = {emojis: () => { ... },relatedEmoji: ({ keyword }) => { ... },createNewEmoji: ({ input }) => {const newEmoji = data[Object.keys(data).length + 1] = input;const id = Object.keys(data).length;newEmoji['no'] = id;return newEmoji;},};
打開 GraphiQL 輸入以下來新增一筆資料,並確認執行結果:
mutation {createNewEmoji(input: {code: "1F61C",char: "😜",name: "FACE WITH STUCK-OUT TONGUE AND WINKING EYE\n≊ face with stuck-out tongue & winking eye",date: "2010ʲ",keywords: ["eye","face","joke","tongue", "wink"]}) {nocodecharnamekeywords}}
看起來是新增成功了,我們透過 query 確認是否正確:
更新 Emoji
寫到這發現篇幅有點長了😮,這邊就繼續延續上面的部份,直接實作更新(Update)資料的部份。
我們要在 type Mutation 再新增一個 updateEmoji(id, input)
,給定我們要更新資料的 id 和所要更新的資料:
type Mutation {createNewEmoji(input: EmojiInput): EmojiupdateEmoji(id: ID!, input: EmojiInput): Emoji}
在 exports.rootValue
新增 updateEmoji 的 Resolver Function:
exports.rootValue = {emojis: () => { ... },relatedEmoji: { ... },createNewEmoji: { ... },updateEmoji: ({ id, input }) => {data[id] = input;data[id]['no'] = id;return data[id];},};
再使用 query 來確認第四筆的 Emoji 資訊是否被更新了:
刪除 Emoji
最後就是刪除(Delete)資料了,我們在 type Mutation 新增一個 deleteEmoji(id)
,結果回傳的是刪除後其他的資料:
type Mutation {createNewEmoji(input: EmojiInput): EmojiupdateEmoji(id: ID!, input: EmojiInput): EmojideleteEmoji(id: ID!): [Emoji]}
更新 exports.rootValue
,新增 deleteEmoji 的 Resolver Function:
exports.rootValue = {emojis: () => { ... },relatedEmoji: ({ keyword }) => { ... },createNewEmoji: ({ input }) => { ... },updateEmoji: ({ id, input }) => { ... },deleteEmoji: ({ id }) => {delete data[id];return Object.keys(data).map(id => data[id]);}};
執行刪除資料後的回傳結果如下:
介紹 GraphQL 大概就到這裡了,把我們最常使用的 CRUD 給走過了一遍,當然 GraphQL 的功能還不只這樣,還有一些像是:Fragments、Interfaces…etc,當然更多相關的資料還是到 GraphQL 官方網站去學習囉!
這次對 GraphQL 的學習算是相當的淺的,還有許多沒有 touch 到的地方希望有時間再來學習,這篇文章是一個 GraphQL 初學者所寫的,我想內容應該會有不少錯誤(笑),如果有任何表達或是思考上的錯誤,歡迎糾正我,感謝你(妳)的閱讀!