杭州 GraphQLParty 第五場-GraphQL 基於 SPA 架構的工程實踐文字版

本文爲 2018 年 6 月 9 日,宋小菜與 Coding 共同舉辦的第一屆 GraphQLParty ,下午第五場國內某大型電商前端開發專家鄧若奇的演講稿,現場反響效果極好,對於想要嘗試 GraphQL 和在公司初步實踐的團隊有很大的借鑑意義。前端

image.png | left | 747x421

你們好,我是阿里的鄧若奇。我和 Scott 是好友,很是幸運今天站在這裏和你們面對面的交流。我是一名前端,據說今天來的前端特別多,很是高興,壓力也很大。node

image.png | left | 747x421

我今天分享的主題是基於 SPA 架構的 GraphQL 工程實踐。主要從一名前端的視角來看 GraphQL 在整個 web 鏈路中包括前端和後端協同效率的問題。 react

image.png | left | 747x421

先介紹一下我本身,我叫鄧若奇,作前端以前作過幾年的後端開發,如今在阿里 CBU 體驗技術部,主要 B2B 前端工程體系基礎建設,同時是集團 nodejs 中間件客戶端維護者(之一)及 devops 接口人。 git

image.png | left | 747x421

    今天分享大概分爲五個部分: github

    一、談談我對 GraphQL 哲學的一些理解。web

    二、基於先後端分離一些架構設計與技術選型。redis

    三、詳細介紹基於 GraphQL 構建 BFF 這一層,個人一些分層設計和思考。算法

    四、介紹一下先後端協做一些效率方面的問題。數據庫

    五、講一下引入 GraphQL 以後須要解決的問題。express

image.png | left | 747x421

image.png | left | 747x421

前面其實不少講師已經講到了 GraphQL 一些東西,GraphQL 其實就是經過一套 schema 作領域模型的定義,官方稱之爲 SDL,同時引入一套類型系統。經過這套類型系統來對模型進行約束,就像 PPT 展現的這三個類型同樣。

image.png | left | 747x421

在實際使用過程當中客戶端經過把想要獲取的字段經過 schema 文本發送給服務端,服務端通過處理以後以 json 格式返回。這是最多見的一種使用方式。

image.png | left | 747x421

經過以上的描述大概知道 GraphQL 有如下幾個特色:

第一,它提供一套統一的模型定義。第二,相比 REST 提供了靈活的按需查詢的能力。第三點其實也是容易被你們忽略的一點,就是它經過這套類型系統提供了模型和模型之間關係的描述。這就引入了一個不爭的事實,

image.png | left | 747x421

application data graph,雖然服務器是以 json 數據返回的,但咱們的應用數據是一個圖或者說是一個網。這也是 GraphQL 成爲描述應用數據的一個極佳選擇。我認爲也是它之因此叫 GraphQL 而不是叫 TreeQL 的緣由。

image.png | left | 747x421

架構設計與技術選型,先後端分離,提及先後端分離是一個老生常談的問題,自從我開始作前端一直到如今,我認爲先後端分離大體分爲四個階段:

image.png | left | 747x421

第一階段前端異步去請求數據接口,而後刷新局部的 UI;第二階段前端接管 view 層,這個時候基於 spa 框架開始涌現,而且一直流行到今天;第三和第四階段隨着 nodejs 技術的興起,咱們開始思考與後端的協同效率問題,經過引入 BFF 這一層實現,可讓前端進行快速的業務迭代,同時後端下沉爲服務或者微服務,可以變得更加穩定和高效。

image.png | left | 747x421

這個架構圖相信不少人已經看到過,就很少說了。

這是技術選型,顯然它不是惟一的,由於前面不少講師有他們本身的選型。前端選擇 react 和 relay,relay 實際上是一種基於 react 和 GraphQL 的一種數據整合方案,前面有講師有提到relay的一些痛點,其實在我看來並非徹底的痛點,relay 最大痛點是文檔太少了(笑)。BFF 這一層引入 eggjs,eggjs 是阿里開源的一個面向企業級開發的一個外部框架,能夠理解成它就是一個 koa 或者是一個 express,亦或者是一個 mvc 的框架就好了。

image.png | left | 747x421

如何設計BFF。

image.png | left | 747x421

先來看一下基於傳統的 mvc 模式的 web service 怎樣受理一個 rest 請求的,首先請求進入到 middleware,咱們叫中間件或者是攔截器,在中間件處理一些通用的邏輯,好比說用戶登錄判斷和 api 鑑權,而後請求到 router,router 經過 url 把請求分發到不一樣的 controller,在 controller 這一層調用 model 進行業務處理,而後 model 再調用 service 層進行取數,數據返回了以後在 controller 層完成數據拼裝而且返回。

image.png | left | 747x421

在引入 GraphQL 以後發生了一些變化。首先 router 不須要了,由於 GraphQL 並非基於 endpoint 的,controller 這一層也不須要了,由於 GraphQL 天生的 resolver 會幫咱們搞定數據拼裝的功能,另外還引入兩個模塊:第一個是 connector,第二個是 schema loader,之因此有 connector這個模塊,主要是由於基於兩點考慮:第一點是出於性能考慮,針對 GraphQL 的一些特色須要進行特殊的緩存設計,另外在作先後端協做的時候,須要有一些約定或者規範好比說分頁,跟客戶端進行約定分頁邏輯的時候,須要有這麼一些規範,須要在 connector 層實現。

另外,schema loader 就很是好理解,在應用啓動的時候須要加載 schema,因此咱們須要有一個模塊來加載它。

image.png | left | 747x421

下面將我在構建 BFF 層當中體會比較深的點跟你們交流一下。第一,如何構建 schema?

image.png | left | 747x421

這實際上是一個開發模式的問題,我在剛開始寫 GraphQL 代碼的時候,代碼是這樣的,其實這段代碼也是在 graphql-js 的官方 repo 上抄來的,可是個人代碼跟它是同樣的,可是如今看來我不會這樣寫。

image.png | left | 747x421

由於它存在兩個問題:首先,schema 是一個與語言無關的,只是模型的一個描述,其次,作開發的時候本着設計先行的原則,先肯定模型是長什麼樣的,而後纔開始動手寫代碼。

image.png | left | 747x421

因此比較贊同:SDL First Philosophy。這個不是我提的,這個是老外提的。

image.png | left | 747x421

首先先肯定模型的描述,以及它們之間的關係,等肯定模型之間的關係以後,咱們再來書寫 resolver 具體應該如何處理。

image.png | left | 747x421

最後在應用加載的時候 schema loader 把二者綁定。這些都不是問題。

image.png | left | 747x421
    

第二,鑑權與受權。

image.png | left | 747x421

鑑權稱之爲 authentication,受權稱之爲 authorization,說實話我一開始老是搞錯,而且這兩個概念其實在中國人看來其實能夠互換或者說是相同的意思,

image.png | left | 747x421

我認爲鑑權主要是針對一些通用的邏輯,好比剛纔說的用戶登錄的一些邏輯,是一些比較粗粒度的。

image.png | left | 747x421

這是剛纔的那張圖,把用戶登錄判斷和 API 鑑權能夠放在 middleware 裏面實現。

image.png | left | 747x421

受權是什麼呢?受權實際上是具備一些定製的邏輯,它的粒度可能比較細,針對 GraphQL 來講多是細化到某一個字段。

image.png | left | 747x421

來看一個例子,這個 query 要查詢小明的工資,小明的工資只能小明本身看,若是服務端給它放出來就出現水平權限的問題。

image.png | left | 747x421

因此咱們在 resolver 裏面須要加入受權邏輯,來保證必定是小明本身(才能看),這裏抽象了一個 User.isSelf 接口,這裏的設計,咱們把受權的邏輯把它封裝在 model,讓它在不一樣的 resolver 裏面均可以複用。User.isSelf 不光在 salary,可能在 login password 這種地方均可以用到。

image.png | left | 747x421

    第三,緩存設計,

image.png | left | 747x421

這是數據庫裏面兩條用戶記錄,用戶 1 和用戶 2 他們互爲 friend。

image.png | left | 747x421

而後程序裏面有這麼兩段代碼:第一段代碼先去查詢用戶 1,查詢回來以後再查詢它的 friend,也就是用戶 2,第二段代碼正好反過來。那麼,

image.png | left | 747x421

它的請求時序圖是這樣的,能夠看到一共發了四個請求,而且咱們最終查到的數據只有兩條,能夠看到這是很是浪費的,因此考慮優化一下,

image.png | left | 747x421

先引入緩存,在引入緩存以後第二輪的查詢在第一輪緩存結果中找到,很是幸運的是第二輪不須要發新的請求了。

image.png | left | 747x421

再進一步深挖一下,若是第一輪請求可以合併成一個請求,這樣就太棒了。

image.png | left | 747x421

因此總結一下,咱們爲了達成這個目的可能須要緩存,這是毫無疑問的。而後咱們須要一個請求隊列,請求隊列什麼意思?咱們在同一個隊列當中,node 的 event loop 是分爲一個週期一個週期的,同一個週期裏把全部請求所有放在一個隊列裏,在下一個週期合併成一個請求發出。最後,須要批量處理的能力。一個請求帶着批量的 key 過來的時候應該怎麼表現,

image.png | left | 747x421

索性的是 Facebook 提供這樣的解決方案,data loader,

image.png | left | 747x421

data loader 接收,這個就是面對批量 Key 請求過來的時候應該如何處理,而且每一個 data loader 的實例下面都有一個 cache。

image.png | left | 747x421

因此,剛纔的需求引入 data loader 以後實現起來就變成這樣。

image.png | left | 747x421

這段代碼最終的效果把三個請求合併成一個請求,在咱們的後端咱們實際上是執行一條指令把它搞定的。

image.png | left | 747x421

可是,在實際使用的時候感受結合關係型數據庫使用起來仍是有一點複雜。

image.png | left | 747x421

首先,一張關係型數據庫表你們會設置一些 key,除了有 primary key 以外,還會設置 unique key,咱們常常會根據 pk 進行查詢,或者根據 uk 進行查詢,像這段代碼裏面會根據 id 去查詢用戶,也可能會根據 mobile 去查詢用戶,咱們的代碼根據 data loader 的哲學不得不初始化兩個實例。

image.png | left | 747x421

這種方式帶來的最大問題由於是不一樣的 data loader 實例,緩存是不一樣的緩存,即便記錄是徹底同樣的,可是你無法利用到,致使緩存的利用率並不高。

image.png | left | 747x421

爲了解決這個問題,我書寫 rdb-dataloader 這個模塊,

image.png | left | 747x421

這個模塊其實它的做用很簡單,就是我不管查詢PK仍是查詢 UK,在同一個實例裏面搞定,讓它複用緩存,看紅框中的代碼,先經過「name」來查詢一條記錄,而後把這條記錄經過它的ID來進行第二次查詢,第二次查詢顯然是不會發出去,它會用緩存。

image.png | left | 747x421

因此要實現這樣的功能其實這裏面有一個問題,你緩存記錄的時候是緩存每條記錄的所有字段,讓這些字段覆蓋你的 UK 和 PK,你可能會擔憂數據量的問題,可是它其實真的不是問題,數據量控制應該由你的分頁邏輯來關心的。

image.png | left | 747x421

這裏拋出一個思考 data loader 這種形式是請求級別的緩存,當一個請求進來的時候初始化一個 data loader 的實例,當請求結束以後它就銷燬,這個和咱們平時用的基於 redis 中心化緩存有什麼不一樣,可不能夠切成 redis 的方式,留給你們思考。

image.png | left | 747x421

先後端如何協做,

image.png | left | 747x421

做名一個前端其實在用了 GraphQL 以後必需要思考,對於瀏覽器性能怎麼樣,這是進一步挖掘 relay 的緣由,下面給你們簡單分享一下 relay 的部分特性。

image.png | left | 747x421

這是一個最普通的 react component,一個最普通的訴求就是組件須要異步取數,而後把數據進行渲染,因此在 componentDidMount 裏面去把異步取數邏輯加上。

image.png | left | 747x421

因此現實中隨着組件數愈來愈深,頁面加載時間也就愈來愈長,由於子組件必需要等到父組件加載完它的數據以後纔開始渲染,這個問題我想優化一下,應該怎麼優化,

image.png | left | 747x421

最簡單的方式把全部組件所須要的數據所有放在首個請求裏面,這個項目交付了以後很不錯,後面產品經理找我要搞一個需求,

image.png | left | 747x421

結果我搞了一個 bug,由於我已經搞不清楚這個 query 裏的哪些字段是對應哪些組件的。

image.png | left | 747x421

咱們看一下 relay 的方式,relay 這裏有一個 create Fragment Container這個方法,經過這個方法傳入一個react組件,而後傳入 GraphQL schema 來返回一個 relay container,其實這是一個高階組件,經過這種方式,咱們實現了依賴注入,也沒有打破數據封裝性。

image.png | left | 747x421

這個 fragment 在首個最初始的 query 裏面把它內聯進去,這樣就知道這個玩意是哪一個組件發出來的。

image.png | left | 747x421

 relay 有一個很是 smart,很是智能的特色,應用的數據是一個圖狀的,

image.png | left | 747x421

是怎樣去存取應用的一個數據的?這是一個僞代碼,可是表示 relay 底層的一種協做方式,從上面的例子能夠看到存儲三個對象,第一個對象是博客,有內容,也有做者,可是這個做者是一個 user 類型,博客不會直接存儲 user 的所有數據,而是經過引用的方式引用到第二個對象,同理,在給博客裏面下面添加評論,評論的做者和它屬於哪一個博客,一樣是用引用的方式,這個有什麼好處,

image.png | left | 747x421

當我好比說做者改頭像,好比說 github 大家常常改頭像嗎,反正我是不常常,可是我發現只要改了頭像任何地方都會修改,提的 issue,代碼的提交者所有都會改掉。可是我能夠肯定 github 不是用的 relay(笑),只是說表達這個意思,只要你改了這個對象,在界面任何引用它的地方所有都更新,這就是視圖一致性。

image.png | left | 747x421

返回來看一下在 cache 裏面 123 是什麼東西,是緩存 key,它的緩存 key 是一個全局惟一的,由於把全部的實體所有都塞到緩存裏面去了,我不能用數據庫的 ID,不然用 ID 爲 1 的 user 和 ID 爲 1 的博客它們之間怎麼搞,

image.png | left | 747x421

因此須要實現 relay 的規範,Global Identifier,咱們要保證每一個對象它的 ID 是惟一的。 

image.png | left | 747x421

這是一種簡單的方式,能夠定義任何一種算法或者方式來肯定你的 global ID。這裏我只是把簡單的類型加上它數據庫的 ID 進行 base64 了,

image.png | left | 747x421

因此在 relay store 裏面我看到都是這些東西。

image.png | left | 747x421

剛纔說了是 global ID 是須要後端來進行配合,咱們須要在後端定義兩個方法,fromGlobalId 就是在 relay 發請求的時候會把 ID 帶過來,而後我在後端我只識別數據庫 ID,因此要把它解包,把它解成數據庫的 ID,在吐出去的時候,須要給每一個數據庫 ID 給裝包有一個 toGlobalId,這兩個方法,若是使用剛纔個人方法,graphql-relay 這個包已經提供。

image.png | left | 747x421

image.png | left | 747x421

當客戶端把文本發送到服務端,服務端通過處理的時候,咱們每每發現這種文本是很是大的,特別是對於網絡環境很差的一些無線終端它的體驗是很是差的。

image.png | left | 747x421

而且它們是一種靜態的文本,能不能把它優化一下,好比說傳一個 ID 過去,給每個 query 賦予一個 ID,服務端根據 ID 反查成爲 query,而後再把它處理出來。

image.png | left | 747x421

所幸 relay 也提供這樣的方式。它給出的答案是在構建時期作,構建 relay 腳本的時候,本質上它是一個模塊,會給這個模塊作一個 hash,標識當前的 schema 給它加一個指紋,

image.png | left | 747x421

這個 hash 在後端能夠去 load 到內存裏,在前端也能夠把它經過打包打進去,這個時候先後端就對應起來了。

image.png | left | 747x421

因此在真實的場景中是經過 hash,而不是經過 schema text。

image.png | left | 747x421

須要解決的問題,

image.png | left | 747x421

前面的講師都已經有提到過 DOS Attack。

image.png | left | 747x421

說白了就是這種嵌套攻擊,請注意這並非死循環,這只是一個攻擊者故意經過你的 query 無限寫的很是複雜的嵌套,讓你的服務器消耗殆盡。

image.png | left | 747x421

最簡單的你設置 query 文本大小來作預防確定不行,而設置白名單那麼咱們使用 GraphQL 的意義又在何處,因此咱們仍是對 query 的深度進行控制,

image.png | left | 747x421

這裏給出一個例子,後面你們其實能夠看到。

image.png | left | 747x421

rate limiting 限流,

image.png | left | 747x421

由於 GraphQL 它不是基於 rest 的,因此確定不能對於 /graphql 這個路由你去限制它每分鐘只能調用多少次,你真正要限制是讀寫和操做。

image.png | left | 747x421

這是一個例子,這個例子表示每分鐘最多隻能添加 20 個評論,經過 directive來作,

image.png | left | 747x421

可是限流實現成本比較大,若是你是專門實現限流這個功能,它須要依賴第三方的一些服務,好比說你在計算限流次數的時候,還有計算時間窗口的時候,由於你的應用是一個集羣,你不可能作在應用裏面。因此我在代碼裏面尚未實現。

相關文章
相關標籤/搜索