GraphQL 為 Facebook 所發表 API 查詢語言,在 2018 年成立 GraphQL Foundation 後,各語言的後端與前端都有 GraphQL 實作,本文將對 GraphQL 做簡單介紹,並以 Apollo 平台建立基本 GraphQL 開發環境。
Version
macOS Catalina 10.15
WebStorm 2019.2.3
Node 10.16.3
Apollo GraphQL 2.9.6
Vue CLI 4.0.5
Vue 2.6.10
Vue Apollo 3.0.0-beta.11
History of GraphQL
2012 年時,Facebook 開始提供 native app 版本,當時只是將 browser 版本包成 native app 而已,但執行效能很差,Facebook 發現主要原因是 REST API 傳輸很沒效率導致,因此開始著手以 mobile client 角度設計 GraphQL API 取代 REST API。
GraphQL 初步構想是既然能以 SQL 對 relational database 查詢,而不用在乎實際 database 實作,能否也設計一個新的 query language 對 internet 查詢,而不用在乎 backend 實作,至少目前 REST API 不是語言。
2015 年 Facebook 對外發表以 Node + JavaScript 實作的第一個 GraphQL Server,在 client 則有 relay,主要都是 React ecosystem 在使用。
2018 年 Facebook 將 GraphQL 開放成為 GraphQL Foundation 非營利組織,負責建立 GraphQL 語言標準,自此 GraphQL 開始出現各種實作版本,在 server 端如 JavaScript、Go、PHP、C#、Java、Python、Ruby 都能實現 GraphQL Server;在 Client 端如 React、Angular、Vue、iOS 與 Android native app 也都能成為 GraphQL Client。
目前 GraphQL ecosystem 除了 Facebook 平台外,Meteor 的 Apollo 平台也很受歡迎,本文將以 Apollo 平台為主。
Why GraphQL ?
GraphQL 全名為 Graph Query Language,如同 SQL 也是 Query Language,但為什麼稱為 Graph
呢 ?
query {
me {
name
location
birthday
friends {
name
location
birthday
}
}
}
一個典型的 GraphQL。
- me
- name
- location
- birthday
- friends
- name
- location
- birthday
若抽象化來看,GraphQL 結構很樹狀結構。
將其轉 90 度,其結構就更像 tree
了,而 tree
是一種 graph
,因此 Facebook 稱為 GraphQL。
REST API vs GraphQL API
Overfetching of Data
REST API
https://swapi.co/api/people/1
以 REST API 讀取 SWAPI,想要取得 Luke Skywalker
的資料。
REST API 回傳了一堆資料,但事實上我們只想要 name
、mass
與 height
三個 property,剩下資料都只是浪費網路頻寬而已。
GraphQL API
https://graphql.org/swapi-graphql
以 GraphQL API 讀取 SWAPI,一樣取得 Luke Skywalker
的資料。
呼叫 person
query,傳入 personID: 1
,指定回傳 name
、mass
與 height
3 個 field,JSON 只會回傳所需資料而已,節省網路頻寬。
Multiple Round Trips
若需求時除了顯示 Luke Skywalker
,還想顯示在哪些電影曾經出現過。
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
https://swapi.co/api/people/1/
回傳的 films
property 下,包含所有電影的 endpoint。
https://swapi.co/api/films/1/
回傳了一堆資料,但事實上我們只要 title
而已。
且為了得到所有 title
,總共要發 1
+ 5
次 API request。
在 person
query 下呼叫其 filmConnection
sub query,指定回傳其 films
property 下的 title
property,只要發 1
次 API request 就可得到我們要的結果,而且完全沒有多餘的 field,速度更快也節省頻寬。
Overfetching 與 Multiple round trips 也是當時 Facebook 發明 GraphQL 最想解決的問題
API Versioning
對於已發佈 REST API,若想刪除 property,會造成既有 client 的 breaking change,REST API 的解法是從 endpoint 定義 v1
、 v2
…,原來 client 使用 v1
,新 client 使用 v2
…。
若使用 GraphQL API,就沒有 versioning 問題,可一直新增 property,對 client 沒有影響,若要刪除 property,也不必從 server 動手,client 自行從 GraphQL 刪除 property 即可。
API Document
REST API 並沒有定義 document,傳統會另外提供 document 或使用 Swagger。
定義 schema 時可同時建立 document,user 可在 GraphiQL 或 GraphQL Playground 內建的Document Explorer 讀取最新 document。
Black Magic of GraphQL
REST API vs Graph API
REST API 有眾多 endpoint,但 Graph API 只有一個 endpoint
REST API 有 verb 與 URI,但 Graph API 有 query 與 mutation
REST API 與 Graph API 都回傳 JSON
REST API 與 Graph API 都不局限於特定 server 與 client 技術
REST API 與 Graph API 都是 stateless
URL-Driven vs Query-Language
GraphQL 底層一樣使用 REST API 與 HTTP,只是僅使用 POST,且 body 用來傳送標準化的 GraphQL,GraphQL Server 本質上是一個負責解譯 GraphQL 的 parser。
http://snowtooth.herokuapp.com
可使用 GraphQL API。
$ curl 'http://snowtooth.herokuapp.com' -H 'Content-Type: application/json' --data '{"query": "{ allLifts { name }}"}'
亦可使用 REST API 以 POST 與 body 傳送 GraphQL。
再次證明 GraphQL 是基於 REST API 與 HTTP 的產物
Fullstack GraphQL
接下來將以 Vue Apollo + Apollo GraphQL + GraphQL Playground 建立 GraphQL 開發環境。
Browser
Vue 使用 GraphQL 對 Apollo GraphQL 要資料並顯示。
Vue CLI
$ vue create vue-grapql
使用 Vue CLI 建立 vue-gql
project。
Vue Apollo
$ vue add apollo
安裝 apollo
plugin 自動設定 Vue Apollo 。
是否安裝 sample code ? 直接按 ↩
選擇 N
不安裝。
是否安裝 GraphQL Server ? 直接按 ↩
選擇 N
不安裝。
是否設定 Apollo Engine ? 直接按 ↩
選擇 N
不設定。
apollo
plugin 除了安裝 Vue Apollo 外,還幫我們設定了以上檔案。
Apollo GraphQL
$ mkdir server
$ cd server
$ yarn init --yes
$ yarn add apollo-server graphql
以 Apollo GraphQL 提供 GraphQL API 供 Vue Apollo 使用。
- 在 Vue project 下建立
server
目錄 - 使用
yarn init --yes
在server
目錄下建立package.json
- 使用 Yarn 安裝
apollo-server
與graphql
server/index.js
let { ApolloServer, gql } = require('apollo-server')
let data = [
{ title: 'FP in JavaScript', category: 'FP'},
{ title: 'RxJS in Action', category: 'FRP'},
{ title: 'Speaking JavaScript', category: 'JS'}
]
let typeDefs = gql`
type Query {
"""
根據書本種類查詢所有書籍
"""
books(category: BookCategory!): [Book]
}
"""
書本 type
"""
type Book {
"""
書本名稱
"""
title: String
"""
書本種類
"""
category: BookCategory
}
"""
書本種類 enum
"""
enum BookCategory {
"""
函數式編程
"""
FP
"""
函數響應編程
"""
FRP
"""
JavaScript
"""
JS
}
`
let books = (_, { category }) => data.filter(x => x.category === category)
let resolvers = {
Query: {
books
}
}
let apolloServer = new ApolloServer({ typeDefs, resolvers })
apolloServer.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
在 server
目錄下建立 index.js
。
第 1 行
let { ApolloServer, gql } = require('apollo-server')
從 apollo-server
require 進 ApolloServer
與 gql
。
第 3 行
let data = [
{ title: 'FP in JavaScript', category: 'FP'},
{ title: 'RxJS in Action', category: 'FRP'},
{ title: 'Speaking JavaScript', category: 'JS'}
]
原始資料為 array of object,包含 title
與 category
兩個 property。
第 9 行
let typeDefs = gql`
type Query {
books(category: BookCategory!): [Book]
}
type Book {
title: String
category: BookCategory
}
enum BookCategory {
FP
FRP
JS
}
`
GraphQL 在 server 端第一步是建立 schema,使用的是 GraphQL SDL (GraphQL Schema Definition Language),類似 relational database 使用 SQL 的 DDL (Data Definition Language) 定義 schema。
以 gql
開頭,後面加上 ES6 的 template string 定義 schema。
Schema 內會定義三種型別:
- Query:查詢資料的 function
- Mutation:修改資料的 function
- Custom Type:自行定義的 type
第 10 行
type Query {
"""
根據書本種類查詢所有書籍
"""
books(category: BookCategory!): [Book]
}
以 type Query
開頭定義 query,目前我們只有一個 books
。
- 其 argument 為
category
,型別為BookCategory!
,!
表示不能為 null,一定得提供,否則 GraphQL 會幫你擋下來 - 回傳為
Book
型別的 array
GraphQL 為 strongly typed language,須明確定義 type
可在 schema 內以 """
定義註解,將顯示在 GraphQL Playground 的 Docs 內。
20 行
type Book {
title: String
category: BookCategory
}
定義 Book
type,包含 object 內的 title
與 category
,其中 category
型別為 BookCategory
enum。
34 行
enum BookCategory {
FP
FRP
JS
}
定義 BookCategory
enum,包含 FP
、FRP
與 JS
三個 enumeration。
52 行
let resolvers = {
Query: {
books
}
}
GraphQL 在 server 端第二步是建立 resolver,有別於 typeDefs
為 string,resolvers
為 object。
Schema 只定義了 query 與 mutation 的 signature,其實際內容要在 resolver 實作。
定義 Query
property,再以 object 定義 books()
resolver。
50 行
let books = (_, { category }) => data.filter(x => x.category === category)
Resolver 依序有 4 個 argument: parent
、args
、context
與 info
,讀取 argument 只會用到第二個 args
,而 parent
目前用不到可用 _
表示, context
與 info
目前可忽略。
57 行
let apolloServer = new ApolloServer({ typeDefs, resolvers });
建立 ApolloServer
object,將 typeDefs
與 resolvers
組合成 object 傳進其 constructor。
60 行
apolloServer.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
以 server.listen()
啟動 Apollo GraphQL,其回傳為 promise,將其 url
destructure 之後以 console.log()
顯示。
Apollo server 預設啟動在
4000
port
GraphQL Playground
$ node server/index.js
直接以 Node 執行 Apollo GraphQL。
http://localhost:4000
Apollo Server 已經內建 GraphQL Playground 供我們測試 GraphQL API 與查詢文件。
query {
books(category: FP) {
title
}
}
使用 books
query 查詢資料,category
argument 傳入 FP
,雖然有 title
與 category
兩個 property,但只要回傳 title
property 即可。
query {
books {
title
}
}
若 books
query 沒傳入任何 argument,GraphQL 會自動幫你攔下來。
type Query {
books(category: BookCategory!): [Book]
}
回想我們並沒有在 server 端寫任何判斷 category
是否為 null,只有宣告 category
為 BookCategory!
,因為有 !
所以 GraphQL 會自動在 runtime 幫你檢查,而不用寫任何程式碼。
GraphQL Playground 可自動將 schema 內的註解顯示在 DOCS
內。
Vue Apollo
.env
VUE_APP_GRAPHQL_HTTP=http://localhost:4000
在 project 根目錄建立 .env
,設定 Apollo Server 位置。
App.vue
<template>
<div>
<ul>
<li v-for="(item, index) in books" :key="index">
{{ item.title }}
</li>
</ul>
</div>
</template>
<script>
import gql from 'graphql-tag';
let books = gql`
query {
books(category: FP) {
title
}
}
`;
export default {
name: 'app',
apollo: {
books
}
}
</script>
12 行
import gql from 'graphql-tag';
從 Graphql-tag
import 進 gql
。
21 行
export default {
name: 'app',
apollo: {
books
}
}
在 Vue instance 的 apollo
property 內宣告 books
query。
14 行
let books = gql`
query {
books(category: FP) {
title
}
}
`;
定義 books
query 所使用的 GraphQL,將 GraphQL Playground 的 GraphQL 複製貼上即可。
第 1 行
<template>
<div>
<ul>
<li v-for="(item, index) in books" :key="index">
{{ item.title }}
</li>
</ul>
</div>
</template>
由於 books
query 回傳為 array,因此一樣使用 v-for
顯示所有資料。
Yarn Script
package.json
{
"name": "vue-gql",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"start": "nodemon ./server/index.js",
"all": "yarn start & yarn serve"
},
}
在 project 根目錄下的 package.json
增加啟動 Apollo GraphQL 的 Yarn script。
第 9 行
"start": "nodemon ./server/index.js",
增加 start
,可輸入 yarn start
可使用 nodemon
啟動 Apollo Server。
當 code 有任何修改,nodemon 會自動重新啟動 node 執行
第 10 行
"all":"yarn start & yarn serve"
增加 all
,可輸入 yarn all
同時啟動 Apollo Server 與 Vue。
Start All Server
$ yarn all
使用 yarn all
一次啟動所有 server。
Summary
Backend
- 不必再用 code 檢查 argument 是否有傳,檢查 type 正不正確, GraphQL 會幫你擋掉
- 可盡情提供所有 property,不用擔心網路頻寬問題,前端可自行選擇要使用的 property
- 為了相容已發佈 REST API,無法刪除 field,導致沒用的 field 越來越多浪費頻寬,除非在 endpoint 加上
v1
、v2
…,但 GraphQL 只要在 client 移除 field 即可,server 不用修改 - 不必再使用 Swagger,GraphQL 已經內建 GraphQL Playground
- Schema 可直接寫 API 註解,GraphQL Playgound 與 client 都可直接顯示
- Schema 直接整合文件,bug 修改時就可直接修改文件
- 設計時不必使用 route 與 body 概念,改用與程式設計更接近的 function 與 argument,DX 更佳
- 在 microservice 架構下,本來就需要 API gateway 角色面對 client,GraphQL 適合做 API gateway,而原有 service 可繼續使用 REST / gRPC / Socket 溝通
- GraphQL
適當
彌補了 JavaScript 沒型別的缺憾
Frontend
- 可自行選擇 field 下載,不必再將不必要的資料下載浪費網路頻寬,大幅提升 UX
- 多個 API 只需使用一次 query 即可,不必多次 API 來回浪費時間
- 單一 endpoint 就可測試 API 與查詢文件
- Client 也能讀到 server 寫的 schema 註解
- 設計時不必使用 route 與 body 概念,改用與程式設計更接近的 function 與 argument,DX 更佳
Conclusion
- GraphQL 從 2018 年成立 GraphQL Foundation 後,GraphQL 從此脫離 React ecosystem,其他語言 / framework 也能受惠
- Apollo 平台在使用上比 Facebook 平台簡單,這也是為什麼 GraphQL 在 2018 年開始爆紅
- GraphQL 特別適合 API 使用對象為 mobile client,若 API 使用對象為其他 service,則 REST / gRPC / Socket 都有它適用場合
Sample Code
完整範例可在我的 GitHub 上找到
Reference
Alex Banks & Eve Porcello, GraphQL 學習手冊
Eve Porcello, GraphQL Query Language
GraphQL
Apollo Docs
Vue Apollo
Academind, REST vs GraphQL - What’s the best kind of API ?
Program With Erik, What is GraphQL and Why You Should Choose It