當前位置:
首頁 > 最新 > 一位前端專家構建GraphQL工程的心路歷程

一位前端專家構建GraphQL工程的心路歷程

內容來源:2018 年 6 月 9 日,國內某大型電商公司用戶體驗部門前端開發專家鄧若奇在「杭州第一屆 GraphQLParty—GraphQL與領域驅動帶來的協同價值」進行《基於SPA架構的GraphQL工程實踐》演講分享。IT 大咖說(微信id:itdakashuo)作為獨家視頻合作方,經主辦方和講者審閱授權發布。

閱讀字數:3838 | 10分鐘閱讀

摘要

主要演講主要介紹基於SPA架構的GraphQL工程實踐,從前端視角來分析GraphQL在整個鏈路中的協同效率問題。

獲取嘉賓演講視頻及PPT,掃一掃下方二維碼即可。

GraphQL的哲學

GraphQL是通過一套Schema來定義領域模型,官方稱之為SDL。它引入了一套類型系統來對模型進行約束,如上圖展示的3個類型。

在實際應用中客戶端將要獲取的欄位通過Schema文本的方式發送給服務端,服務端接收處理後返回json格式的數據。

GraphQL提供了一套統一模型定義,擁有靈活的按需查詢的能力。還有個容易被大家忽視的特性——通過類型系統提供了模型之間的關係描述,由此可以看出雖然數據以json格式返回,但是實際的應用數據呈現的應該是網狀架構,這使得GraphQL成為描述應用數據的極佳選擇,也是它名字的由來。

架構設計與技術選型

從前端視角看前後端分離

以我個人經歷來看,前後端分離可以分為4個階段。

第一階段前端非同步請求數據介面刷新局部UI。

第二階段前端接管View層,這是很多基於MVC的框架採用的模式。

第三、四階段隨著nodeJS技術的興起,前後端的協同效率問題開始受到關注,後續通過引入BFF這層讓前端能夠快速迭代,同時後端下沉為服務或微服務。

上圖是我的技術選型方案。前端為React和relay,relay是基於GraphQL和React的數據整合方案。BFF這層引入的是Egg.js,它是阿里開源的面向企業級開發的web框架。

如何設計BFF

基於REST的分層設計

先來看下傳統的基於MVC模式的web server受理REST請求過程。首先請求進入middleware(中間件),在此處理一些通用邏輯,比如用戶登錄態判斷或API鑒權。接著進入Router將請求分布到不同的controller,controller這層調用model進行業務處理,然後model再調用service層取數據,最後數據在controller層完成封裝並返回。

基於GraphQL的分層設計

引入GraphQL之後Router和controller不再被需要,因為首先GraphQL並不基於endpoint,其次它自身的resolver可以完成數據封裝。此架構中我們引入了兩個模塊connector和Schema Loader。connector模塊一方面針對GraphQL的一些特點做了特殊緩存設計,另一方面制定了前後端協作的規範。

構建schema

這是我最初寫的GraphQL代碼,借鑒與GraphQL-js的官方repo。現在看來這段代碼存在2個問題,首先schema應與語言無關而只是模型的描述,其次開發的時候應該遵守設計先行的原則,先確定模型然後再寫代碼。

理想情況應該是這樣的,先確定模型描述和關係,然後再編寫resolver決定具體處理方案,最後在應用載入的時候使用schema Loader將他們綁定在一起。

鑒權與授權

鑒權和授權的區別在於,鑒權主要針對通用邏輯,是粗粒度的,授權則是定製邏輯,粒度較細。

在GraphQL中授權可能針對的是某個欄位,如圖所示query查詢的是小明的工資,由於工資只能自己查看,所以要在resolver中加入一段授權邏輯保證查詢者為本人。這裡的設計理念是將授權邏輯封裝在model層,讓它在不同的resolver中得以復用。

緩存設計

上圖是資料庫中的兩條用戶記錄,他們互為friend,通過兩段代碼分別查詢用戶和他們的friend。

這是上面代碼請求的時序圖,可以看到一共發出了4次請求,但最終獲取到的數據只有兩條。

引入緩存之後,第二輪的請求就都可以在第一輪的查詢緩存中找到。

還可以再進行優化,將兩段代碼的第一輪請求合併在一起,這才是最優解。

為實現以上的效果,首先需要使用緩存。然後還要有請求隊列,將同一個周期中的所有load或query全部緩存起來,然後在下一個周期中合併成一個請求放出。最後是批量處理的能力,用於處理附帶批量key的請求。

Facabook提供了一種批量處理的解決方案DataLoader,它接收一個用來處理批量key的方法,每個DataLoader的實例下方都有一個cache。最初的需求在引入DataLoader之後代碼如下圖所示。

這段代碼的最終效果是把三個請求合併成一個請求,在後端執行的是一條SQL語句。

不過在實際結合關係型資料庫使用的時候還是略微有些複雜。一般我們對關係型資料庫進行查詢的時候即會依據PK(primary key)也會依據UK(unique key)。如上代碼關於用戶的查詢既可以通過ID也可以通過Mobiles,這就不得不實例化兩個DataLoader實例。由於是不同DataLoader實例,所以用的是不同的緩存,導致緩存利用率不高。

為此我編寫了rdb-dataloader模塊,讓PK和UK的查詢都在同一個實例中,達到復用緩存的目的。注意紅框中的代碼,這裡先通過name查詢出一條記錄,然後對這條記錄經由ID做第二次查詢,顯然第二次查詢不會發出,而是會使用緩存。方案的核心在於緩存記錄的全部欄位,數據量的控制應該由分頁邏輯來關心。

DataLoader是請求級別的緩存,請求進來的時候初始化DataLoader實例,請求結束後就銷毀。

前後端如何協作

Relay

作為一名前端在使用GraphQL的時候首先要是思考的是對瀏覽器的性能有何影響,這也是接下來進一步挖掘relay的原因。

在使用React組件時,最普遍的訴求就是需要非同步取數據,然後對數據進行渲染,常規的做法是在componentDidMount中添加非同步取數的邏輯。因此實際應用中隨著頁面層級的深入,載入時間會隨之變長,子組件必須等待父組件的數據載入完之後才能開始渲染。

對此最簡單的優化方案是將所有組件需要的數據全部放在第一次請求中,如上所示。可是在後續要新增需求的時候我卻搞出了bug,因為此時已經分不清哪些欄位對應哪些組件。

再來看下relay的實現方式,relay有一個creatFragmenContainer方法,可以向該方法傳入React組件,然後通過GraphQL的scheam返回relay component。這種方式不僅實現了依賴注入也沒有打破組件的數據封裝性。

在最初的query中嵌入上面的fragment後,我們就知道了欄位是由哪個組件發出的。

上圖是一段偽代碼,表示的是relay底層的協作方式。第一個對象是博客,有內容,也有作者,但是這個作者是一個 user 類型,博客不會直接存儲 user 的全部數據,而是通過引用的方式引用到第二個對象。同理評論的作者和它屬於哪個博客,同樣是用引用的方式。這樣的好處在於只要對象發生改動,所有引用該對象的地方都會同步更新。

請注意圖中1、2、3這幾個數字,他們是全局唯一的緩存key。由於所有的數據都在緩存中,所以不能再使用資料庫中的ID,否則對於ID相同的博客和用戶就無法處理了。唯一ID的實現有各種方案,可以使用base64(type+」:」+id)這種形式。

全局ID需要後端來配合,定義fromGlobalId和toGobalId這兩個方法。fromGlobalId負責將relay發請求時帶來的ID解包成資料庫ID,toGobalId負責返回的時候對資料庫ID裝包。

客戶端將schema文本發送到服務端,然後由服務端進行處理的這一過程中,文本量其實是相當大的,對於網路環境不好的用戶體驗會非常差。

那麼能不能直接發送query id,在服務端通過id解析出文本呢?所幸relay提供了這種方式,在構建relay腳本的時候會給模塊注入hash標識當前schema,通過這個hash前後端就對應起來了。

需要解決的問題

首要解決的是DOS Attack,說白了就是上圖這種嵌套攻擊,請注意這並不是死循環,這只是一個攻擊者故意通過你的 query 無限寫的非常複雜的嵌套,讓你的伺服器消耗殆盡。顯然設置query文本長度和query白名單無益於解決問題,正確的做法是控制query的深度。

對於rate limiting限流,由於GraphQL並非是基於Rest,所以不能通過限制路由每分鐘的調用次數來解決。而應該是限制讀寫操作,上面的例子表示的就是每分鐘最多只能添加20個評論,通過directive實現。

不過實際上限流的實現成本是比較大的,如果要專門實現限流功能,需要依賴第三方的一些服務。

以上為今天的分享內容,謝謝大家!

IT大咖說 |關於版權

感謝您對IT大咖說的熱心支持!


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 IT大咖說 的精彩文章:

流計算,不止於流——阿里雲流計算峰會圓滿舉行
大數據時代,如何根據業務選擇合適的分散式框架

TAG:IT大咖說 |