GraphQL and Relay 淺析

Facebook 在去年夏天公佈了 GraphQL,就像往前端深潭砸下了一顆巨石,人們都被水聲吸引到了湖邊,觀望是否會出現什麼,有些人期待,有些人猜疑。過了半年多,社區已經慢慢的摸清這個石頭的材質,本文但願在你入門 GraphQL 和 Relay 的過程當中能幫你清除一些障礙。前端

GraphQL

GraphQL 是在 Facebook 內部應用多年的一套數據查詢語言和 runtime。
初次入門者建議先把官網的資料都讀一遍,難度不大(specification 和 API 能夠後面再看)。node

GraphQL 包括什麼

  1. 類型系統 - GraphQL 是強類型語言,強類型雖然寫時會稍微累點,但就不用寫一堆類型檢測的代碼了;react

  2. 驗證 - GraphQL 提供機制對你的語法和請求作必定層度的校驗;git

  3. introspection - 一個讓你能經過幾行代碼就能瞭解整個資源提供方的細節的 API。github

GraphQL 優點

官網已經列舉了,我用更簡練的語言描述下。數據庫

GraphQL 與 REST

同類型協議目前最出名的是 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 的說明文檔在哪裏,看個例子:
pic

自定義 schema

不必像 REST 這樣固定且通用的語法。

其餘專有方案(Ad Hoc Endpoints)

和專有方案對比:

  1. 專有方案每一個接口都本身定義獲取數據,後端代碼不能獲得重用;

  2. 和 REST 對比的第二點同樣;

  3. 每一個接口的數據不能複用;

  4. 對比其餘現有的專有方案,要麼沒有強類型,要麼沒有 GraphQL 這麼昂貴,並且前面3點也仍是沒有解決。

與圖數據庫的關係

首先,介紹下什麼是圖數據庫,能夠參考neo4j的介紹,一圖勝千言:
pic

pic
上邊是關係數據庫,下邊是圖數據庫。

GraphQL 爲何有 Graph,是由於它的 query 是以圖的形式來組織的:

user
┖-OWNS-> playlist
         ┖-CONTAINS-> track
                      ┖-LIKED_BY-> users

GraphQL 並不要求後臺必定要是圖數據庫,關係數據庫也能夠,它只是一套查詢數據的語言而已。

DataLoader

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 提到一些思路:

  1. 對語句作 AST 分析,太複雜的就拒絕了;

  2. 作超時限制,對容量也能夠作限制;

  3. 客戶端記得要作 cache(如 Relay)。

Relay

Relay 是鏈接 GraphQL 和 React 的一座橋樑。不過,除了讓 React 認識 GraphQL 服務器以外,它還作了什麼呢?

建議先把官網的資料都讀一遍,Relay 相對來講比 GraphQL 複雜一些,並且文檔並不詳細(截至截稿時,Relay的版本是 v0.6.1),也缺失了關於 graphql-relay 庫的詳細介紹,掃一遍後,結合本文最後的學習資料的代碼加深理解。

Relay 怎麼用?

使用 Relay 是要侵入先後端的:

  • 在後端你得經過 graphql-relay-js 讓 GraphQL schema 更適合 Relay;

  • 在前端再經過 react-relay 來配合 React。

Relay 包括什麼?

Relay 把關於數據獲取的事情都接管過來,好比說請求異常,loading,請求排隊,cache,獲取分頁數據。我這裏重點講一下如下幾個方面:

client-side 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 裏的也不會獲取。

Relay 的聲明式數據獲取

React 是按 Component 組織 view 的,最好的方式也是把 view 須要的數據寫在 view。若是用常規的作法,view 負責本身的 Data-fetch,那麼,因爲 React 是一層一層的往裏深刻 Component 的,那麼也就意味着每一層 Component 都本身發請求去了,是不可能作到用一個網絡請求來獲取全部數據的。

因此,Relay 經過抽象出一個 container 的概念,讓每一個模塊提早聲明本身須要的數據,Relay 會先遍歷全部 container,組成 query tree,這樣就達到了只使用一個網絡請求的目的。

另外,經過聲明式數據獲取還能夠更好的對組件約束,只能獲取它聲明的數據,而且 Relay 也能夠作些驗證。

graphql-relay-js

在看一些 React 和 Relay 協做的例子時,常常發現這個庫的存在,這個庫究竟是幹什麼的?

經過查看源碼後發現,裏面實際上是各類 helper 方法,負責生成一些 GraphQL 的類,爲何須要這樣作?其實,這是由於 Relay 提供的一些功能(好比 ID handling,分頁)須要 GraphQL 服務器提供特定的代碼結構。若是你要開發一個 GraphQL 的前端,就算它基於其餘框架,基於其餘語言,實現一個像 graphql-relay-js 所實現的 Relay-compliant 的 server 是頗有幫助的,好比graphql-go/relay

babel-relay-plugin

Relay 的 container 依賴的數據資源是經過聲明的,但客戶端是不知道後端的數據結構的。爲了讓客戶端了解整個後臺結構,就要引入這個 bable 插件,這個插件經過讀取服務端的 schema,就可讓客戶端正確理解它所須要的資源在服務端是長什麼樣的。

optimistic UI update

咱們看下例子:

<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 的將來還任重而道遠。

學習資料

相關的庫

  • 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

相關文章
相關標籤/搜索