內容來源:2018 年 6 月 9 日,國內某大型電商公司用戶體驗部門前端開發專家鄧若奇在「杭州第一屆 GraphQLParty—GraphQL與領域驅動帶來的協同價值」進行《基於SPA架構的GraphQL工程實踐》演講分享。IT 大咖說(微信id:itdakashuo)做爲獨家視頻合做方,經主辦方和講者審閱受權發佈。前端
閱讀字數:3838 | 10分鐘閱讀node
主要演講主要介紹基於SPA架構的GraphQL工程實踐,從前端視角來分析GraphQL在整個鏈路中的協同效率問題。web
GraphQL是經過一套Schema來定義領域模型,官方稱之爲SDL。它引入了一套類型系統來對模型進行約束,如上圖展現的3個類型。數據庫
在實際應用中客戶端將要獲取的字段經過Schema文本的方式發送給服務端,服務端接收處理後返回json格式的數據。json
GraphQL提供了一套統一模型定義,擁有靈活的按需查詢的能力。還有個容易被你們忽視的特性——經過類型系統提供了模型之間的關係描述,由此能夠看出雖然數據以json格式返回,可是實際的應用數據呈現的應該是網狀架構,這使得GraphQL成爲描述應用數據的極佳選擇,也是它名字的由來。後端
以我我的經從來看,先後端分離能夠分爲4個階段。瀏覽器
第一階段前端異步請求數據接口刷新局部UI。緩存
第二階段前端接管View層,這是不少基於MVC的框架採用的模式。服務器
第3、四階段隨着nodeJS技術的興起,先後端的協同效率問題開始受到關注,後續經過引入BFF這層讓前端可以快速迭代,同時後端下沉爲服務或微服務。微信
上圖是個人技術選型方案。前端爲React和relay,relay是基於GraphQL和React的數據整合方案。BFF這層引入的是Egg.js,它是阿里開源的面向企業級開發的web框架。
先來看下傳統的基於MVC模式的web server受理REST請求過程。首先請求進入middleware(中間件),在此處理一些通用邏輯,好比用戶登陸態判斷或API鑑權。接着進入Router將請求分佈到不一樣的controller,controller這層調用model進行業務處理,而後model再調用service層取數據,最後數據在controller層完成封裝並返回。
引入GraphQL以後Router和controller再也不被須要,由於首先GraphQL並不基於endpoint,其次它自身的resolver能夠完成數據封裝。此架構中咱們引入了兩個模塊connector和Schema Loader。connector模塊一方面針對GraphQL的一些特色作了特殊緩存設計,另外一方面制定了先後端協做的規範。
這是我最初寫的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實例,請求結束後就銷燬。
做爲一名前端在使用GraphQL的時候首先要是思考的是對瀏覽器的性能有何影響,這也是接下來進一步挖掘relay的緣由。
在使用React組件時,最廣泛的訴求就是須要異步取數據,而後對數據進行渲染,常規的作法是在componentDidMount中添加異步取數的邏輯。所以實際應用中隨着頁面層級的深刻,加載時間會隨之變長,子組件必須等待父組件的數據加載完以後才能開始渲染。
對此最簡單的優化方案是將全部組件須要的數據所有放在第一次請求中,如上所示。但是在後續要新增需求的時候我卻搞出了bug,由於此時已經分不清哪些字段對應哪些組件。
再來看下relay的實現方式,relay有一個creatFragmenContainer方法,能夠向該方法傳入React組件,而後經過GraphQL的scheam返回relay component。這種方式不只實現了依賴注入也沒有打破組件的數據封裝性。
在最初的query中嵌入上面的fragment後,咱們就知道了字段是由哪一個組件發出的。
上圖是一段僞代碼,表示的是relay底層的協做方式。第一個對象是博客,有內容,也有做者,可是這個做者是一個 user 類型,博客不會直接存儲 user 的所有數據,而是經過引用的方式引用到第二個對象。同理評論的做者和它屬於哪一個博客,一樣是用引用的方式。這樣的好處在於只要對象發生改動,全部引用該對象的地方都會同步更新。
請注意圖中一、二、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實現。
不過實際上限流的實現成本是比較大的,若是要專門實現限流功能,須要依賴第三方的一些服務。