Facebook 在去年夏天公佈了 GraphQL,就像往前端深潭砸下了一顆巨石,人們都被水聲吸引到了湖邊,觀望是否會出現什麼,有些人期待,有些人猜疑。過了半年多,社區已經慢慢的摸清這個石頭的材質,本文但願在你入門 GraphQL 和 Relay 的過程當中能幫你清除一些障礙。前端
GraphQL 是在 Facebook 內部應用多年的一套數據查詢語言和 runtime。
初次入門者建議先把官網的資料都讀一遍,難度不大(specification 和 API 能夠後面再看)。node
類型系統 - GraphQL 是強類型語言,強類型雖然寫時會稍微累點,但就不用寫一堆類型檢測的代碼了;react
驗證 - GraphQL 提供機制對你的語法和請求作必定層度的校驗;git
introspection - 一個讓你能經過幾行代碼就能瞭解整個資源提供方的細節的 API。github
官網已經列舉了,我用更簡練的語言描述下。數據庫
同類型協議目前最出名的是 REST,特色是資源可定位,使用 HTTP verbs。REST 具體應該怎麼寫有不少爭議,但簡單的例子是沒有爭議的:express
GET /users/1
REST 優勢是簡單明瞭,缺點也是太簡單明瞭,致使語法可擴充性不強。
咱們來看看 GraphQL 官網是怎麼和 REST 對比的:後端
語法靈活數組
GraphQL 只須要一次請求就可以得到你全部想要的資源。這裏舉一個和 REST 對比的例子 讓你們有直觀的認識。瀏覽器
如今,我想獲取id爲1的用戶的名字,年齡和他全部朋友的名字
GraphQL 實現的方案:
{ user(id: 1) { name age friends { name } } }
REST 實現的方案:
GET /users/1 and GET /users/1/friends
或
GET /users/1?include=friends.name
發現區別了嗎?用 REST 要不就發屢次請求,要不就得用一個不方便擴展的語法。
沒有冗餘
往後擴充資源也沒有冗餘,你只會得到你想要的資源。仍是用上面的例子,若是 user 多了個屬性 gender 會怎麼樣?
在 REST 的方案中,若是客戶端不變,取到的結果是會多了 gender 屬性,而在 GraphQL 方案中,客戶端是不會獲取到 gender 屬性的。
強類型
有 introspection 機制,代碼即文檔,方便快捷,而不須要去找這個 API 的說明文檔在哪裏,看個例子:
自定義 schema
不必像 REST 這樣固定且通用的語法。
和專有方案對比:
專有方案每一個接口都本身定義獲取數據,後端代碼不能獲得重用;
和 REST 對比的第二點同樣;
每一個接口的數據不能複用;
對比其餘現有的專有方案,要麼沒有強類型,要麼沒有 GraphQL 這麼昂貴,並且前面3點也仍是沒有解決。
首先,介紹下什麼是圖數據庫,能夠參考neo4j的介紹,一圖勝千言:
上邊是關係數據庫,下邊是圖數據庫。
GraphQL 爲何有 Graph,是由於它的 query 是以圖的形式來組織的:
user ┖-OWNS-> playlist ┖-CONTAINS-> track ┖-LIKED_BY-> users
GraphQL 並不要求後臺必定要是圖數據庫,關係數據庫也能夠,它只是一套查詢數據的語言而已。
Dataloader 是一個小工具,幫你把你的請求轉成批量請求的形式,和 GraphQL 搭配的也挺好,看個例子:
query FetchPlaylist { playlist(id: "e66637db-13f9-4056-abef-f731f8b1a3c7") { id name tracks { id title viewerHasLiked } } }
這個 query 是要獲取某個用戶的歌單。
注意一個細節,這個 query 想獲取每一個 track 的一些屬性。咱們定義一下 Track 這個類型:
import { GraphQLString, GraphQLBoolean, GraphQLObjectType } from 'graphql'; export default new GraphQLObjectType({ name: 'Track', description: 'A Track', fields: () => ({ id: { type: GraphQLString, resolve: it => it.uuid } title: { type: GraphQLString }, viewerHasLiked: { type: GraphQLBoolean, resolve: (it, _, { rootValue: { ctx: { auth } } }) => ( (auth.isAuthenticated) ? it.userHasLiked(auth.user) : null ) } }) });
resolve 函數調用的是後端 API,注意這裏的 it 就是 track 的對象。
咱們獲取 viewerHasLiked 這個屬性須要調用 it.userHasLiked (auth.user)。那麼,個人歌單裏有 50 首歌的話,就要調用 50 次it.userHasLiked(auth.user),這樣訪問數據庫的性能是沒法接受的。合理的想法是變成批量的。那要怎麼作呢?這就是 DataLoader 發揮做用的時候了:
import DataLoader from 'dataloader'; import BaseModel from './BaseModel'; const likeLoader = new DataLoader((requests) => { // requests is now a an array of [track, user] pairs. // Batch-load the results for those requests, reorder them to match // the order of requests and return. }) export default class Track extends BaseModel { userHasLiked(user) { return likeLoader.load([this, user]); } }
在一個 event loop 裏每次調用 dataloader,dataloader 會記下你的請求參數,在下次 event loop 的時候把這麼屢次的請求參數變成一個數組提供你操做,你就能夠拿這個數組對數據庫執行批量的操做了。並且,它還對結果按你的請求參數進行了緩存,是居家必備的殺人利器。
或許有人有疑問,感受 GraphQL 把我所擁有的資源所有都暴露了,別人不僅一覽全局,並且還能一次過所有拉下來,那還得了?
事實上,GraphQL 提供的資源不必定要和你數據庫同樣,由於它只是扮演中間層的角色,雖然也可能很像。因此,你要想好哪些資源能夠被看。
至於獲取,其實看到上面的例子裏有這句 auth.isAuthenticated
。
能夠看到你能夠在裏面插入權限限制的。至於獲取資源太多拖垮服務器?
Jacob Gillespie 提到一些思路:
對語句作 AST 分析,太複雜的就拒絕了;
作超時限制,對容量也能夠作限制;
客戶端記得要作 cache(如 Relay)。
Relay 是鏈接 GraphQL 和 React 的一座橋樑。不過,除了讓 React 認識 GraphQL 服務器以外,它還作了什麼呢?
建議先把官網的資料都讀一遍,Relay 相對來講比 GraphQL 複雜一些,並且文檔並不詳細(截至截稿時,Relay的版本是 v0.6.1),也缺失了關於 graphql-relay 庫的詳細介紹,掃一遍後,結合本文最後的學習資料的代碼加深理解。
使用 Relay 是要侵入先後端的:
在後端你得經過 graphql-relay-js 讓 GraphQL schema 更適合 Relay;
在前端再經過 react-relay 來配合 React。
Relay 把關於數據獲取的事情都接管過來,好比說請求異常,loading,請求排隊,cache,獲取分頁數據。我這裏重點講一下如下幾個方面:
Relay 獲取數據固然離不開 cache,能夠看到 GraphQL 再也不依賴 URL cache,而是按照 Graph 來 cache,最大的保證 cache 沒有冗餘,發最少的請求,我舉一個例子:
好比下面這個請求:
query { stories { id, text } }
若是利用 URL 請求(好比說瀏覽器的 cache),那麼這個請求下次確實命中 cache 了,那麼假如我還有一個請求是:
query { story(id: "123") { id, text } }
看得出,下面這個請求獲取的數據是上面請求的子集,這裏有兩個問題:
若是第一第二兩個請求獲取的數據不一致怎麼辦?
原本就是子集,爲何我還要發請求?
這兩個想法催生出來了 GraphQL 的解決方案:按照 Graph 來 cache,也就是說子集不須要再發請求了,固然你也能夠強制發請求來更新局部或者整個 cache。
具體作法是經過拍平數據結構(相似數據庫的幾個範式)來 cache 整個 Graph。
view 經過訂閱他須要的每一個 cache record 來更新,只要其中一個 record 更新了,也只有訂閱了這個 record 的 view 纔會獲得更新。
最後,聊到修改,咱們能夠看到 mutation 有個反直覺的地方是請求的 query 裏包括了須要獲取的數據。爲何不直接返回你的修改影響的那些數據? 由於服務端實現這個太複雜了,有的時候一個簡單的修改會影響到很是多的後臺數據,而不少數據 view 是不須要知道它變化了。
因此,Relay 團隊最後選擇的方案是,讓客戶端告訴服務器端你認爲哪些數據你想從新獲取。具體到實現,Relay 採用的方案是獲取 cache 和 fat query 有交集的部分,這樣既更新了 cache,並且不在 cache 裏的也不會獲取。
React 是按 Component 組織 view 的,最好的方式也是把 view 須要的數據寫在 view。若是用常規的作法,view 負責本身的 Data-fetch,那麼,因爲 React 是一層一層的往裏深刻 Component 的,那麼也就意味着每一層 Component 都本身發請求去了,是不可能作到用一個網絡請求來獲取全部數據的。
因此,Relay 經過抽象出一個 container 的概念,讓每一個模塊提早聲明本身須要的數據,Relay 會先遍歷全部 container,組成 query tree,這樣就達到了只使用一個網絡請求的目的。
另外,經過聲明式數據獲取還能夠更好的對組件約束,只能獲取它聲明的數據,而且 Relay 也能夠作些驗證。
在看一些 React 和 Relay 協做的例子時,常常發現這個庫的存在,這個庫究竟是幹什麼的?
經過查看源碼後發現,裏面實際上是各類 helper 方法,負責生成一些 GraphQL 的類,爲何須要這樣作?其實,這是由於 Relay 提供的一些功能(好比 ID handling,分頁)須要 GraphQL 服務器提供特定的代碼結構。若是你要開發一個 GraphQL 的前端,就算它基於其餘框架,基於其餘語言,實現一個像 graphql-relay-js 所實現的 Relay-compliant 的 server 是頗有幫助的,好比graphql-go/relay。
Relay 的 container 依賴的數據資源是經過聲明的,但客戶端是不知道後端的數據結構的。爲了讓客戶端了解整個後臺結構,就要引入這個 bable 插件,這個插件經過讀取服務端的 schema,就可讓客戶端正確理解它所須要的資源在服務端是長什麼樣的。
咱們看下例子:
<Relay.RootContainer Component={ProfilePicture} route={profileRoute} renderLoading={function() { return <div>Loading...</div>; }} renderFailure={function(error, retry) { return ( <div> <p>{error.message}</p> <p><button onClick={retry}>Retry?</button></p> </div> ); }} />
能夠看到在 Relay 裏能夠很簡單的處理請求整個請求過程當中的 UI 變化。
相信閱讀本文的讀者都是對這二者有必定興趣的人,但在我上手以後,個人心情是複雜的。GraphQL 和 Relay 帶來了一些優點,最重要的是能夠一次性獲取資源,看上去是將來之路,但這優點其實用些不優雅的方法來解決也沒什麼問題,但爲了這些優點須要編寫大量與業務邏輯無關的代碼,讓我真心憂慮它的路能走多遠,相信看過一個官方的 TODOList的例子 的入門者很容易就能感受到。REST 如此簡單,普及開來尚且用了幾年,複雜好多倍的 GraphQL 的將來還任重而道遠。
GraphQL 和 Relay學習資源彙總:這裏列舉了比較全的相關學習資源,5顆星。
搭建你的第一個 GraphQL 服務器:這篇文章從0開始幫你搭建一個 GraphQL,比較淺,3顆星。
relay-starter-kit:這個例子簡單的描述了 Relay 和 GraphQL 的關係,但沒有 mutation,3顆星。
From rest to GraphQL:提到了rootValue,dataloader,講了比較真實的例子,5顆星。
Relay 官方例子 TODOlist:比較完整的增刪改查的官方例子,5顆星。
Unofficial Relay FAQ:這篇 FAQ 是 Facebook 員工寫的,裏面提到 Relay 是要取代 Flux,並且 routing 還在積極修改中。
server:好比 express-graphql。
ORM:好比 graffiti。
facebook/dataloader。
adrenaline:React bindings for Redux with Relay。
react-router-relay:結合 react-router,介紹。
graphql-relay-js
babel-relay-plugin