本文首先介紹了 GraphQL,再經過 MongoDB + graphql + graph-pack 的組合實戰應用 GraphQL,詳細闡述如何使用 GraphQL 來進行增刪改查和數據訂閱推送,並附有使用示例,邊用邊學印象深入~javascript
若是但願將 GraphQL 應用到先後端分離的生產環境,請期待後續文章。php
本文實例代碼:Githubhtml
GraphQL 是一種面向數據的 API 查詢風格。前端
傳統的 API 拿到的是先後端約定好的數據格式,GraphQL 對 API 中的數據提供了一套易於理解的完整描述,客戶端可以準確地得到它須要的數據,沒有任何冗餘,也讓 API 更容易地隨着時間推移而演進,還能用於構建強大的開發者工具。java
前端的開發隨着 SPA 框架全面普及,組件化開發也隨之成爲大勢所趨,各個組件分別管理着各自的狀態,組件化給前端仔帶來便利的同時也帶來了一些煩惱。好比,組件須要負責把異步請求的狀態分發給子組件或通知給父組件,這個過程當中,由組件間通訊帶來的結構複雜度、來源不明的數據源、不知從何訂閱的數據響應會使得數據流變得雜亂無章,也使得代碼可讀性變差,以及可維護性的下降,爲之後項目的迭代帶來極大困難。git
試想一下你都開發完了,產品告訴你要大改一番,從接口到組件結構都得改,後端也罵罵咧咧不肯配合讓你從好幾個 API 裏取數據本身組合,這酸爽 😅github
在一些產品鏈複雜的場景,後端須要提供對應 WebApp、WebPC、APP、小程序、快應用等各端 API,此時 API 的粒度大小就顯得格外重要,粗粒度會致使移動端沒必要要的流量損耗,細粒度則會形成函數爆炸 (Function Explosion);在此情景下 Facebook 的工程師於 2015 年開源了 GraphQL 規範,讓前端本身描述本身但願的數據形式,服務端則返回前端所描述的數據結構。web
簡單使用能夠參照下面這個圖:mongodb
好比前端但願返回一個 ID 爲 233
的用戶的名稱和性別,並查找這個用戶的前十個僱員的名字和 Email,再找到這我的父親的電話,和這個父親的狗的名字(別問我爲何有這麼奇怪的查找 🤪),那麼咱們能夠經過 GraphQL 的一次 query 拿到所有信息,無需從好幾個異步 API 裏面來回找:數據庫
query { user (id : "233") { name gender employee (first: 10) { name email } father { telephone dog { name } } } }
返回的數據格式則恰好是前端提供的數據格式,很少很多,是否是心動了 😏
這裏先介紹幾個對理解 GraphQL 比較重要的概念,其餘相似於指令、聯合類型、內聯片斷等更復雜的用法,參考 GraphQL 官網文檔 ~
GraphQL 的操做類型能夠是 query
、mutation
或 subscription
,描述客戶端但願進行什麼樣的操做
這些操做類型都將在後文實際用到,好比這裏進行一個查詢操做
query { user { id } }
若是一個 GraphQL 服務接受到了一個 query
,那麼這個 query
將從 Root Query
開始查找,找到對象類型(Object Type)時則使用它的解析函數 Resolver 來獲取內容,若是返回的是對象類型則繼續使用解析函數獲取內容,若是返回的是標量類型(Scalar Type)則結束獲取,直到找到最後一個標量類型。
type
String
、Int
、Float
、Boolean
、ID
,用戶也能夠定義本身的標量類型好比在 Schema 中聲明
type User { name: String! age: Int }
這個 User
對象類型有兩個字段,name 字段是一個爲 String
的非空標量,age 字段爲一個 Int
的可空標量。
若是你用過 MongoOSE,那你應該對 Schema 這個概念很熟悉,翻譯過來是『模式』。
它定義了字段的類型、數據的結構,描述了接口數據請求的規則,當咱們進行一些錯誤的查詢的時候 GraphQL 引擎會負責告訴咱們哪裏有問題,和詳細的錯誤信息,對開發調試十分友好。
Schema 使用一個簡單的強類型模式語法,稱爲模式描述語言(Schema Definition Language, SDL),咱們能夠用一個真實的例子來展現一下一個真實的 Schema 文件是怎麼用 SDL 編寫的:
# src/schema.graphql # Query 入口 type Query { hello: String users: [User]! user(id: String): [User]! } # Mutation 入口 type Mutation { createUser(id: ID!, name: String!, email: String!, age: Int,gender: Gender): User! updateUser(id: ID!, name: String, email: String, age: Int, gender: Gender): User! deleteUser(id: ID!): User } # Subscription 入口 type Subscription { subsUser(id: ID!): User } type User implements UserInterface { id: ID! name: String! age: Int gender: Gender email: String! } # 枚舉類型 enum Gender { MAN WOMAN } # 接口類型 interface UserInterface { id: ID! name: String! age: Int gender: Gender }
這個簡單的 Schema 文件從 Query、Mutation、Subscription 入口開始定義了各個對象類型或標量類型,這些字段的類型也多是其餘的對象類型或標量類型,組成一個樹形的結構,而用戶在向服務端發送請求的時候,沿着這個樹選擇一個或多個分支就能夠獲取多組信息。
注意:在 Query 查詢字段時,是並行執行的,而在 Mutation 變動的時候,是線性執行,一個接着一個,防止同時變動帶來的競態問題,好比說咱們在一個請求中發送了兩個 Mutation,那麼前一個將始終在後一個以前執行。
前端請求信息到達後端以後,須要由解析函數 Resolver 來提供數據,好比這樣一個 Query:
query { hello }
那麼同名的解析函數應該是這樣的
Query: { hello (parent, args, context, info) { return ... } }
解析函數接受四個參數,分別爲
parent
:當前上一個解析函數的返回值args
:查詢中傳入的參數context
:提供給全部解析器的上下文信息info
:一個保存與當前查詢相關的字段特定信息以及 schema 詳細信息的值解析函數的返回值能夠是一個具體的值,也能夠是 Promise 或 Promise 數組。
一些經常使用的解決方案如 Apollo 能夠幫省略一些簡單的解析函數,好比一個字段沒有提供對應的解析函數時,會從上層返回對象中讀取和返回與這個字段同名的屬性。
GraphQL 最多見的是經過 HTTP 來發送請求,那麼如何經過 HTTP 來進行 GraphQL 通訊呢
舉個栗子,如何經過 Get/Post 方式來執行下面的 GraphQL 查詢呢
query { me { name } }
Get 是將請求內容放在 URL 中,Post 是在 content-type: application/json
狀況下,將 JSON 格式的內容放在請求體裏
# Get 方式 http://myapi/graphql?query={me{name}} # Post 方式的請求體 { "query": "...", "operationName": "...", "variables": { "myVariable": "someValue", ... } }
返回的格式通常也是 JSON 體
# 正確返回 { "data": { ... } } # 執行時發生錯誤 { "errors": [ ... ] }
若是執行時發生錯誤,則 errors 數組裏有詳細的錯誤信息,好比錯誤信息、錯誤位置、拋錯現場的調用堆棧等信息,方便進行定位。
這裏使用 MongoDB + graph-pack 進行一下簡單的實戰,並在實戰中一塊兒學習一下,詳細代碼參見 Github ~
MongoDB 是一個使用的比較多的 NoSQL,能夠方便的在社區找到不少現成的解決方案,報錯了也容易找到解決方法。
graph-pack 是集成了 Webpack + Express + Prisma + Babel + Apollo-server + Websocket 的支持熱更新的零配置 GraphQL 服務環境,這裏將其用來演示 GraphQL 的使用。
首先咱們把 MongoDB 啓起來,這個過程就不贅述了,網上不少教程;
搭一下 graph-pack 的環境
npm i -S graphpack
在 package.json
的 scripts
字段加上:
"scripts": { "dev": "graphpack", "build": "graphpack build" }
建立文件結構:
. ├── src │ ├── db // 數據庫操做相關 │ │ ├── connect.js // 數據庫操做封裝 │ │ ├── index.js // DAO 層 │ │ └── setting.js // 配置 │ ├── resolvers // resolvers │ │ └── index.js │ └── schema.graphql // schema └── package.json
這裏的 schema.graphql
是 2.3 節的示例代碼,其餘實現參見 Github,主要關注 src/db
、src/resolvers
、src/schema.graphql
這三個地方
src/db
:數據庫操做層,包括 DAO 層和 Service 層(若是對分層不太瞭解能夠看一下最後一章)src/resolvers
:Resolver 解析函數層,給 GraphQL 的 Query、Mutation、Subscription 請求提供 resolver 解析函數src/schema.graphql
:Schema 層而後 npm run dev
,瀏覽器打開 http://localhost:4000/
就可使用 GraphQL Playground 開始調試了,左邊是請求信息欄,左下是請求參數欄和請求頭設置欄,右邊是返回參數欄,詳細用法能夠參考 Prisma 文檔
首先咱們來試試 hello world
,咱們在 schema.graphql
中寫上 Query 的一個入口 hello
,它接受 String 類型的返回值
# src/schema.graphql # Query 入口 type Query { hello: String }
在 src/resolvers/index.js
中補充對應的 Resolver,這個 Resolver 比較簡單,直接返回的 String
// src/resolvers/index.js export default { Query: { hello: () => 'Hello world!' } }
咱們在 Playground 中進行 Query
# 請求 query { hello } # 返回值 { "data": { "hello": "Hello world!" } }
Hello world 老是如此愉快,下面咱們來進行稍微複雜一點的查詢
查詢入口 users
查找全部用戶列表,返回一個不可空但長度能夠爲 0 的數組,數組中若是有元素,則必須爲 User 類型;另外一個查詢入口 user
接受一個字符串,查找 ID 爲這個字符串的用戶,並返回一個 User 類型的可空字段
# src/schema.graphql # Query 入口 type Query { user(id: String): User users: [User]! } type User { id: ID! name: String! age: Int email: String! }
增長對應的 Resolver
// src/resolvers/index.js import Db from '../db' export default { Query: { user: (parent, { id }) => Db.user({ id }), users: (parent, args) => Db.users({}) } }
這裏的兩個方法 Db.user
、Db.users
分別是查找對應數據的函數,返回的是 Promise,若是這個 Promise 被 resolve,那麼傳給 resolve 的數據將被做爲結果返回。
而後進行一次查詢就能夠查找咱們所但願的全部信息
# 請求 query { user(id: "2") { id name email age } users { id name } } # 返回值 { "data": { "user": { "id": "2", "name": "李四", "email": "mmmmm@qq.com", "age": 18 }, "users": [{ "id": "1", "name": "張三" },{ "id": "2", "name": "李四" }] } }
注意這裏,返回的數組只但願拿到 id
、name
這兩個字段,所以 GraphQL 並無返回多餘的數據,怎麼樣,是否是很貼心呢
知道如何查詢數據,還得了解增長、刪除、修改,畢竟這是 CRUD 工程師必備的幾板斧,不過這裏只介紹比較複雜的修改,另外兩個方法能夠看一下 Github 上。
# src/schema.graphql # Mutation 入口 type Mutation { updateUser(id: ID!, name: String, email: String, age: Int): User! } type User { id: ID! name: String! age: Int email: String! }
同理,Mutation 也須要 Resolver 來處理請求
// src/resolvers/index.js import Db from '../db' export default { Mutation: { updateUser: (parent, { id, name, email, age }) => Db.user({ id }) .then(existUser => { if (!existUser) throw new Error('沒有這個id的人') return existUser }) .then(() => Db.updateUser({ id, name, email, age })) } }
Mutation 入口 updateUser 拿到參數以後首先進行一次用戶查詢,若是沒找到則拋錯,這個錯將做爲 error 信息返回給用戶,Db.updateUser
這個函數返回的也是 Promise,不過是將改變以後的信息返回
# 請求 mutation UpdataUser ($id: ID!, $name: String!, $email: String!, $age: Int) { updateUser(id: $id, name: $name, email: $email, age: $age) { id name age } } # 參數 {"id": "2", "name": "王五", "email": "xxxx@qq.com", "age": 19} # 返回值 { "data": { "updateUser": { "id": "2", "name": "王五", "age": 19 } } }
這樣完成了對數據的更改,且拿到了更改後的數據,並給定但願的字段。
### 3.4 Subscription
GraphQL 還有一個有意思的地方就是它能夠進行數據訂閱,當前端發起訂閱請求以後,若是後端發現數據改變,能夠給前端推送實時信息,咱們用一下看看。
照例,在 Schema 中定義 Subscription 的入口
# src/schema.graphql # Subscription 入口 type Subscription { subsUser(id: ID!): User } type User { id: ID! name: String! age: Int email: String! }
補充上它的 Resolver
// src/resolvers/index.js import Db from '../db' const { PubSub, withFilter } = require('apollo-server') const pubsub = new PubSub() const USER_UPDATE_CHANNEL = 'USER_UPDATE' export default { Mutation: { updateUser: (parent, { id, name, email, age }) => Db.user({ id }) .then(existUser => { if (!existUser) throw new Error('沒有這個id的人') return existUser }) .then(() => Db.updateUser({ id, name, email, age })) .then(user => { pubsub.publish(USER_UPDATE_CHANNEL, { subsUser: user }) return user }) }, Subscription: { subsUser: { subscribe: withFilter( (parent, { id }) => pubsub.asyncIterator(USER_UPDATE_CHANNEL), (payload, variables) => payload.subsUser.id === variables.id ), resolve: (payload, variables) => { console.log('🚢 接收到數據: ', payload) } } } }
這裏的 pubsub
是 apollo-server 裏負責訂閱和發佈的類,它在接受訂閱時提供一個異步迭代器,在後端以爲須要發佈訂閱的時候向前端發佈 payload。withFilter
的做用是過濾掉不須要的訂閱消息,詳細用法參照訂閱過濾器。
首先咱們發佈一個訂閱請求
# 請求 subscription subsUser($id: ID!) { subsUser(id: $id) { id name age email } } # 參數 { "id": "2" }
咱們用剛剛的數據更新操做來進行一次數據的更改,而後咱們將獲取到並打印出 pubsub.publish
發佈的 payload,這樣就完成了數據訂閱。
在 graph-pack 中數據推送是基於 websocket 來實現的,能夠在通訊的時候打開 Chrome DevTools 看一下。
目前先後端的結構大概以下圖。後端經過 DAO 層與數據庫鏈接實現數據持久化,服務於處理業務邏輯的 Service 層,Controller 層接受 API 請求調用 Service 層處理並返回;前端經過瀏覽器 URL 進行路由命中獲取目標視圖狀態,而頁面視圖是由組件嵌套組成,每一個組件維護着各自的組件級狀態,一些稍微複雜的應用還會使用集中式狀態管理的工具,好比 Vuex、Redux、Mobx 等。先後端只經過 API 來交流,這也是如今先後端分離開發的基礎。
若是使用 GraphQL,那麼後端將再也不產出 API,而是將 Controller 層維護爲 Resolver,和前端約定一套 Schema,這個 Schema 將用來生成接口文檔,前端直接經過 Schema 或生成的接口文檔來進行本身指望的請求。
通過幾年一線開發者的填坑,已經有一些不錯的工具鏈可使用於開發與生產,不少語言也提供了對 GraphQL 的支持,好比 JavaScript/Nodejs、Java、PHP、Ruby、Python、Go、C# 等。
一些比較有名的公司好比 Twitter、IBM、Coursera、Airbnb、Facebook、Github、攜程等,內部或外部 API 從 RESTful 轉爲了 GraphQL 風格,特別是 Github,它的 v4 版外部 API 只使用 GraphQL。據一位在 Twitter 工做的大佬說硅谷很多一線二線的公司都在想辦法轉到 GraphQL 上,可是同時也說了 GraphQL 還須要時間發展,由於將它使用到生產環境須要先後端大量的重構,這無疑須要高層的推進和決心。
正如尤雨溪所說,爲何 GraphQL 兩三年前沒有普遍使用起來呢,可能有下面兩個緣由:
- GraphQL 的 field resolve 若是按照 naive 的方式來寫,每個 field 都對數據庫直接跑一個 query,會產生大量冗餘 query,雖然網絡層面的請求數被優化了,但數據庫查詢可能會成爲性能瓶頸,這裏面有很大的優化空間,但並非那麼容易作。FB 自己沒有這個問題,由於他們內部數據庫這一層也是抽象掉的,寫 GraphQL 接口的人不須要顧慮 query 優化的問題。
- GraphQL 的利好主要是在於前端的開發效率,但落地卻須要服務端的全力配合。若是是小公司或者整個公司都是全棧,那可能能夠作,但在不少先後端分工比較明確的團隊裏,要推進 GraphQL 仍是會遇到各類協做上的阻力。
大約能夠歸納爲性能瓶頸和團隊分工的緣由,但願隨着社區的發展,基礎設施的完善,會漸漸有完善的解決方案提出,讓廣大先後端開發者們能夠早日用上此利器。
網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~
參考:
PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~
另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~