GraphQL
在近幾年被提到的次數越來越多,最近參加過的幾次技術大會前端分會場均提到過。對於這種光看名字並不容易想到它是什麼的東西,還是存在些神祕感的。於是,打算去了解一下GraphQL
到底是什麼。
什麼是GraphQL?
首先,GraphQL
來自Facebook,如果你也跟我一樣完全沒了解過它,不知道它到底是幹什麼的,那麼你一定聽說過另一個叫做 Structured QL
的東西。WHAT? 其實就是SQL了。
- 嗯,和
SQL
一樣,GraphQL
是一門查詢語言(Query Language) - 同樣和
SQL
一樣的是,GraphQL
也是一套規範,就像MySQL
是SQL
的一套實現一樣,Apollo
,Relay
...也是GraphQL
規範的實現 - 與
SQL
不同的是,SQL
的資料來源是資料庫,而GraphQL的資料來源可以是各種各樣的REST API,可以是各種服務/微服務,甚至可以是資料庫
這裡借Apollo官網的一張圖來說明GraphQL在網際網路應用架構中所處的位置
幾個時間點
- GraphQL規範於2015年開源
- Subscriptions操作於2017年被加入到規範中
那麼為什麼叫 Graph呢?
Graph是圖的意思,在GraphQL
的世界裡,萬物皆為圖,也就是說你可以把你的業務模型建模為圖。
圖是將很多真實世界現象變成模型的強大工具,因為它們和我們自然的心智模型和基本過程的口頭描述很相似。
這裡就涉及到了圖論,Graph Database之類的知識,感興趣可以某歌學習一下。
這樣我們可以對GraphQL大體有了一個概念了。那麼我們來大概瞭解一下GraphQL。
GraphQL為我們解決了什麼問題呢?
簡單的說,站在前端角度,很多文章都會提到過為了取代Restful API,稍微具體點:
- API欄位的定製化,按需取欄位
- API的聚合,一次請求拿到所有的資料
- 後端不再需要維護介面的版本號了
- 完備的型別校驗機制,提供了更健壯的介面
- 。。。
在知道以上這些優點是如何做到的之前,我們先來對GraphQL做一個簡單的學習
前端學習GraphQL
做為一名前端開發人員,只站在前端的角度上來說,更多的時候,我們需要關心的只有兩個操作:
-
query: 在GraphQL中這個關鍵字屬於schema(可以理解為協議)中的一種,代表你要執行的是查詢動作,即增刪改查中的查
-
mutation: 代表你要執行的動作是增刪改
query
和mutation
統稱為schema。其實還有一個subscriptions在2017年被加入到規範(spec)中,讓我們可以更輕鬆的實現推送功能
這裡我們以一個公司內部的分享平臺的兩個場景為例,來介紹一下這兩個操作如何使用。
query操作
首先,最基礎的一個場景,分享平臺首頁需要調一個介面,獲取全部的分享列表,目前這個介面的呼叫方式是:
GET /api/share/allShares
複製程式碼
返回
{
"shares": [
{
"shareId": 1238272,
"title": "分享一下Vue3.0",
"desc": "Vue3.0就要釋出了,帶來了哪些新功能呢?",
"where": "6F-19會議室",
"startTime": 1548842400
},
{
"shareId": 1238272,
"title": "用flutter寫app頁面是一種什麼樣的體驗",
"desc": "用跨平臺框架flutter來寫app頁面的初體驗",
"where": "6F-17會議室",
"startTime": 1548842400
},
{
"shareId": 1238272,
"title": "Cordova原理",
"desc": "一起來了解一下Cordova",
"where": "6F-19會議室",
"startTime": 1548842400
}
]
}
複製程式碼
那麼換成GraphQL的方式,我們可以這麼寫
query {
shares {
title
desc
where
startTime
}
}
複製程式碼
咦?發現漏掉了一個欄位,如果我們要跳至詳情頁,需要知道分享的id,改造一下
# 給一個查詢起一個名字是一個好習慣
query findAllShares {
shares {
# 為id起了一個別名,叫shareId
shareId: id
title
desc
where
startTime
}
}
複製程式碼
到此,一個基礎的查詢操作就完成了。
分頁
通常,如果列表類資料量比較大的話,我們會採用分頁的方式獲取資料,而非一次性獲取全部資料,依然以剛才的分享列表獲取為例,如果用傳統的介面呼叫的方式,通常是要這樣去調介面:
GET /api/share/allShares?star=0&limit=10
複製程式碼
返回
{
"allShares": {
"totalCount": 3,
"shares": [
{
"shareId": 1238272,
"title": "分享一下Vue3.0",
"desc": "Vue3.0就要釋出了,帶來了哪些新功能呢?",
"where": "6F-19會議室",
"startTime": 1548842400
},
{
"shareId": 1238273,
"title": "用flutter寫app頁面是一種什麼樣的體驗",
"desc": "用跨平臺框架flutter來寫app頁面的初體驗",
"where": "6F-17會議室",
"startTime": 1548842400
},
{
"shareId": 1238274,
"title": "Cordova原理",
"desc": "一起來了解一下Cordova",
"where": "6F-19會議室",
"startTime": 1548842400
}
]
}
}
複製程式碼
我們來繼續改造GraphQL的方式,分頁的方式:
# 分頁方式
query findAllShares($start: Int!, $limit: Int = 10) {
allShares (start: $start, limit: $limit) {
totalCount
shares {
shareId: id
title
desc
where
startTime
}
}
}
複製程式碼
GraphQL提供了完備的分頁解決方案,可參考 Pagination
下一場景,得到了所有的分享列表,可以進入詳情頁了。目前詳情頁有三個主要的查詢介面:獲取分享詳情,獲取分享的評論列表和獲取分享者所有的分享列表。如果是傳統的方式,我們需要調三個介面:
// 獲取分享的詳情
GET /api/share/:shareId
複製程式碼
// 分享詳情返回
{
"shareDetail": {
"shareId": 1238274,
"title": "Cordova原理",
"desc": "一起來了解一下Cordova",
"where": "6F-19會議室",
"startTime": 1548842400,
"attchments": "",
"creatorId": 321,
"lastUpdateTime": 1548842400,
"logoUrl": "",
...
}
}
複製程式碼
// 獲取分享的評論列表
GET /api/share/comments/:shareId
複製程式碼
// 分享評論列表返回
{
"commentInfo": {
"totalCount": 5,
"comments": [
{
"id": 1,
"content": "非常不錯",
"userId": 213,
"commentTime": 1548842400,
},
{
"id": 2,
"content": "很好",
"userId": 214,
"commentTime": 1548842400,
},
{
"id": 3,
"content": "不錯",
"userId": 216,
"commentTime": 1548842400,
},
{
"id": 4,
"content": "Very GOOD!",
"userId": 2313,
"commentTime": 1548842400,
}
]
}
}
複製程式碼
// 分享的建立者的建立的全部分享列表
GET /api/share/shares/:creatorId
複製程式碼
// 分享建立者的全部分享返回
{
"hisShares": [
{
"shareId": 1238272,
"title": "分享一下Vue3.0",
"desc": "Vue3.0就要釋出了,帶來了哪些新功能呢?",
"where": "6F-19會議室",
"startTime": 1548842400
},
{
"shareId": 1238273,
"title": "用flutter寫app頁面是一種什麼樣的體驗",
"desc": "用跨平臺框架flutter來寫app頁面的初體驗",
"where": "6F-17會議室",
"startTime": 1548842400
},
{
"shareId": 1238274,
"title": "Cordova原理",
"desc": "一起來了解一下Cordova",
"where": "6F-19會議室",
"startTime": 1548842400
}
]
}
複製程式碼
那麼如果用GraphQL的方式呢?
query shareDetailPage($shareId: Int!, $creatorId:ID!, $start: Int!, $limit: Int = 10) {
# 分享詳情
shareDetail: share (shareId: $shareId) {
shareId: id
title
desc
where
logoUrl
attchments
}
# 評論資訊
commentInfo(shareId: $shareId, start: $start, limit: $limit) {
totalCount
comments {
id
userId
content
commentTime
}
}
# 他的分享
hisShares (creatorId: $creatorId) {
shares {
title
desc
where
startTime
}
}
}
複製程式碼
一個查詢即可搞定。
mutation操作
變更操作,這裡只介紹一種場景。到了分享詳情頁,我們可能會需要編輯這個分享,在傳統的方式中,需要調一個更新操作的介面:
POST /api/share/update/:shareId
FormData:
title=xxx&desc=xxx&where=xxx
複製程式碼
調完此介面後為了確認確實已經更新成功了,我們可能還會調一次獲取分享詳情介面:
GET /api/share/:shareId
複製程式碼
接下來我們換成GraphQL的方式:
mutation editShareInfo($shareObj: ShareInput!) {
editShareInfo(shareInfo: $shareObj) {
shareId: id
title
desc
where
logoUrl
attchments
}
}
複製程式碼
這樣,便可以直接將分享內容修改並返回修改後的分享詳情
其他的功能
為了我們寫查詢語句部分程式碼能有更好的可複用性,GraphQL
還提供了Fragments
(片段), Inline Fragments
(內聯片段)和Directives
(指令)功能。前兩者可以類比為JavaScript中的function
(函式)和anonymous function
(匿名函式),Directives
(指令)可以根據我們傳的引數來決定某些欄位是否需要返回。這裡就不做過多介紹了。
以上的功能如何實現?
schema
通過上面的例子,肯定會產生些疑問,我們要如何知道可以查詢哪些欄位?使用哪些引數?這就需要引入schema
了。
通俗點說,schema
就是協議,規範,或者可以當他是介面文件。
GraphQL規定,每一個schema
有一個根(root)query和根(root)mutation。
我們先來看Root Query怎麼寫,依然是上面的查詢的例子
# 定義一個根查詢
type Query {
# 可以查詢的欄位和引數
shares(start: Int = 0, limit: Int = 10, creatorId: ID): [Share!]!
share(shareId: ID!): Share!
commentInfo(shareId: ID!, start: Int = 0, limit: Int = 10): CommentInfo!
}
複製程式碼
資料型別
如果你熟悉TypeScript或Flow的話可能會發現上面的寫法似曾相識,是的,裡面的含義就是你想的那樣。每一個可以查詢的欄位的引數後面會跟標明這個引數的型別,!
用來表示這個引數不可以是空的。[]
表示查詢這個欄位返回的是陣列,[]
裡面是陣列的型別。
上面我們還看到了一些在TypeScript中不存在的型別,比如ID
,ID
我們暫且把他當成字串String
型別就可以了。類似我們熟悉的JavaScrpit或TypeScript,GraphQL
也有幾個基礎型別,在GraphQL
中他們統稱叫標量型別
(Scalar Type),主要包括:Int(整型), Float(浮點型), String(字串), Boolean(布林型)和ID(唯一識別符號型別)。同時,GraphQL
也允許你去自定義標量型別,例如:Date型別,只需實現相關的序列化,反序列化和驗證的功能即可。
物件型別
上面的根查詢定義中,我們還看到了一些與業務相關的型別,比如Share, Comment,這些統稱為物件型別
。物件型別也是GraphQL
中的schema
的基本元件,他可以告訴我們在服務上可以獲得到哪些物件,以及這個物件有哪些欄位。接下來我們要做的就是定義這些物件型別,直到全部為基礎型別。
# 定義Share的物件型別
type Share {
id: ID!
title: String!
desc: String!
startTime: Int!
where: String
attchments: String
logoUrl: String
creatorId: ID!
lastUpdateTime: Int
is_delete: Int
score: Int
createTime: Int!
}
# 定義評論資訊物件型別
type CommentInfo {
totalCount: Int!
comments: [Comment!]!
}
# 定義評論物件型別
type Comment {
id: ID!
content: String!
commentTime: Int!
userId: ID!
shareId: ID!
}
複製程式碼
這樣,我們就完成了schema的定義。
其他型別和功能
GraphQL
其實還有Enumeration types
(列舉型別),Union types
(聯合型別)。同時,為了程式碼能更好的複用,GraphQL
還提供了 Interface
(介面)功能。這裡就不做過多介紹了。
實現執行
GraphQL
約定,我們需要為Root Query(根查詢)和Root Mutation(根變更)裡面的每一個欄位提供一個resolver
的函式。幷包裝成一個物件暴露出去,就像這樣:
const resolvers = {
// 這裡面寫查詢操作欄位的resolver函式
Query: {},
// 這裡面寫變更操作欄位的resolver函式
Mutation: {},
}
export default resolvers
複製程式碼
讓我們繼續寫完整:
// 一些載入資料的async function
import { loadSharesFromDB, loadShareById, loadCommentsByShareId } from './datasource'
const resolvers = {
// 這裡面寫查詢操作欄位的resolver函式
Query: {
shares: (parent, { start, limit, creatorId }, context, info) => {
return loadSharesFromDB(start, limit, creatorId)
.then(...)
},
share: (parent, { shareId }, context, info) => {
return loadShareById(shareId)
.then(...)
},
commentInfo: (parent, { shareId, start, limit }, context, info) => {
return loadCommentsByShareId(shareId, start, limit)
.then(...)
},
},
// 這裡面寫變更操作欄位的resolver函式
Mutation: {
// ...
},
}
複製程式碼
同樣的,對於mutation
(變更)操作,我們也是先把schema
完成:
# 定義Mutation根入口
type Mutation {
editShareInfo(shareInfo: ShareInput!): Share!
}
input ShareInput {
id: ID!
title: String!
desc: String!
where: String
}
複製程式碼
然後,補全resolver
函式:
import { updateShareInfo, loadShareById } from './datasource'
const resolvers = {
Query: {
// ...
},
Mutation: {
editShareInfo: (parent, { shareInfo }, context, info) => {
// 更新分享詳情,then獲取更新後的分享詳情
return updateShareInfo(shareInfo.id, shareInfo)
.then(loadShareById(shareInfo.id))
},
},
}
export default resolvers
複製程式碼
到此,我們就實現了這個簡單的GraphQL
的Server了。
結語
GraphQL
還有很多內容可以探索,使用。比如,如果用Schema構建函式來生成物件型別,可以標記某一個欄位為廢棄,並給出廢棄原因。這樣,在版本迭代時,就可以友好的提示到舊版本的使用者,促使其升級到最新的介面,通過某些檢測手段,我們也能很輕鬆的知道舊版本的使用頻率,從而方便的讓我們在某一個時間徹底刪掉這個欄位。
如果說GraphQL
有什麼缺點,那可能就是上手確實沒那麼容易,而且對於後端同學來說,還是有很多坑要踩的,比如快取,效能問題等。好在目前的GraphQL
的資料已經不像幾年前那樣的匱乏,不管是官方還是社群,GraphQL
可以參考的資源和解決方案都越來越多了。
不管怎樣,單純的對於前端er來說,如果說上一次前端的技術變革是SPA的普及的話,相信當下一次變革到來時,一定有GraphQL
的影子。
一些連結
- GraphQL Tutorials
- GraphQL 中文教程
- 知乎上很火的GraphQL 為何沒有火起來?
- graphpack 一個0配置的
GraphQL server
工具,非常適合初學者去了解GraphQL
,並提供了Codepen的方式線上編輯程式碼,本篇中的示例就是在這個工具的基礎上實現的