內容來源:2018 年 06 月 09 日,有數派聯合創始人周文宇在“杭州第一屆 GraphQLParty—GraphQL與領域驅動帶來的協同價值”進行《Stratup使用GraphQL的姿勢》演講分享。IT 大咖說作為獨家視訊合作方,經主辦方和講者審閱授權釋出。
閱讀字數:6109 | 16分鐘閱讀
摘要
本次演講主要介紹如何使用GraphQL,分別從前後端兩個角度分析GraphQL的優劣勢,對比Restful又能夠給前後端協同開發帶來哪些好處。
為什麼使用GraphQL
之所以要使用GraphQL主要出於幾方面的考慮。首先我們的業務複雜度高,應用本身的業務場景極其複雜,涉及到紡織行業大大小小几十個業務場景和十幾個不同工種功能之間的聯動與互動。
其次是由於Tech Team有一半同事Remote在全國各地,因此對協同效率與開發工具鏈的要求很高,而GraphQL恰好能解決溝通成本的問題。第三是因為終端過於繁雜,既有b2b的平臺也有SaaS產品和硬體產品,各個終端的連線非常多,如果用傳統的Restful API開發會用到非常多的膠水程式碼。
傳統介面的問題(後端)
這裡大概介紹一下傳統介面在我們這個領域中應用所會遇到的一些問題。
溝通成本高。前後端需要反覆溝通介面結構和資料型別,但是對於B端這種相對複雜的業務場景,資料和業務場景都是多變的。同時前端對資料介面結構的掌控有限,前後端Pipeline也不一致,傳統BBF對前端不夠友好,更多的還得依賴後端。
開發效率低。文件維護成本高,一致性和同步性問題很難解決。
一些技術債。比如對多終端和多場景支援不友好,缺乏標準化的約束,前後端都需要重複工作。
以上是從後端的角度來對這些問題進行的分析,接下來由我們的前端方面的負責人介紹下我們在前端方向上的一些實踐。
傳統介面的問題(前端)
剛才周老師已經從後端的角度分析了這些痛點,我這裡就從前端的層面來重新談論下這些問題。
第一個要提的是反覆溝通介面的問題,對於後端來說他們更關注的是資料的表結構,而前端所需要的僅僅是介面的展示資料。有時這些資料並不能夠從某個表中直接獲取到,可能要跨很多的表,這就需要前端和後端之間進行相互的磨合溝通才能獲得最終的結果。
其次是前端對介面的結構掌控有限,當前端的請求傳送出去後,介面所返回的資料形式有可能並不符合預期,比如本該返回的陣列變成了物件。
再來看下開發效率低的問題。前端開發中UnderFetching和OverFetching一直無法避免。
UnderFetching指的從介面中取到的資料遠低於需要展現的資料,由此引發了N+1問題,即需要根據已取得的介面中的ID或者詳細資訊再去請求對應的介面,這就導致了前端的請求從非同步變成了同步。
原本頁面的渲染時間是取決於最慢的介面響應時間,而同步模式下則是所有介面序列返回的時間。OverFetching則正好相反,介面的返回資料遠高於需要展現的資料,對前端和伺服器端的資源造成了雙重消耗。
Node中間層 vsGraphQL(前端)
面臨傳統介面所帶來的這些痛點,一般團隊都會選擇NodeJs作為中間層的解決方案。那麼Node和GraphQL相比有那些優缺點呢?下面來一起看下。
Node.js在一定程度上減輕了underFetching和overFetching問題,尤其是解決了N+1的問題,後端可以通過Node.Js中間層來進行資料資源的整合,但是此時仍舊只能返回一個固定的資料結構。
Node.js缺陷也很明顯,比如對多端需要做額外的適配,難以適應前端的快速迭代,需要花費大量時間維護,由此還會引發介面文件的斷層。
GraphQL對於Node碰到的這些問題基本上都能夠很好的解決。GraphQL中可以由前端來定義Query,頁面和資料能完美匹配。同時一旦Schema確定,前後端就可以快速並行開發。前端對欄位及返回型別也能夠了如指掌,GUI清晰的展現了欄位的型別結構。
如何給高速執行的汽車更換輪子(前端)
給高速執行的汽車更換輪子是一件相當危險的事情,而我們早期所面臨的情況和這差不多。當時我們的產品已經推出有一段時間了,資料庫非常龐大,功能也很繁雜,而且資料完全是基於Restful。此時需要將Restful完全替換成GraphQL,這無疑對前後端來說都是一個非常大的挑戰。
在跟換輪子之前,我先比較了一下前端資料快取的框架,目前主流的有Relay和Apolloo。Relay由Facebook官方推出,支援的框架有React和React Native,Apolloo則支援絕大多數主流框架。
Router方面Relay官方支援React Route,新版本中還支援一個新的路由Found。Apollo由於本身的執行方式和生命週期已經完全和路由割離開,所以能夠支援任何Route。
Relay的資料快取由官方提供的RelayStore完成,Apollo則是基於Redux。基於以上幾點考慮,我最終選擇了Apollo。
之前我們在使用Redux加Redux-saga的時候,倒沒有遇到什麼特別大的問題,主要還是特別麻煩。需要手動管理和儲存返回資料,還要為每個資源建立一套Action,Reducer,Redux-saga,同時要針對每個請求進行異常及資料處理,針對頁面需要的資料觸發多次序列請求。
在使用GraphQL和Apollo之後,前端方面只需要全域性定義一個URL,接下來就是定義每個Query需要取得的資料,根據頁面定製介面資料。同時還可以做全域性的異常處理,介面請求的合併。
如何安裝輪子(前端)
接下來將講一下我是如何安裝輪子的,會涉及到一些面向場景的解耦操作,還有具體的程式碼演示。
對於我們應用的訂單頁面資料,在Restuful場景下首先會根據訂單ID請求訂單資訊,接著依據從訂單資訊中得到的產品ID獲取產品詳細資料,之後還需要根據建立人ID獲取客戶詳細資料,最後將這些資料結合起來才能渲染頁面。
上圖展示的是通過GraphQL來做訂單頁面時後端定義的一些類。最下方的員工類是一個基類,它包含了id、性別、部分、姓名這些通用的欄位。
Employee物件被巢狀在Order類和Peuduct類中,在這兩個類中能夠很輕易的通過creator欄位獲取到Employee的資料資訊。
而Order類和Peuduct類之間是相互引用的關係,通過items欄位分別定義對應的物件資料。
這張圖分別展示了前端Fragment和Query。前端Fragment是基於物件的,左邊的第一段程式碼就是一個基於Employee物件的employee Fragment,我們可以從中獲取到的id、gende、name等資料。在其他的Fragment中能夠自由的引用employee物件。
Query其實就是GraphQL對傳統前端Fragment的定義,它可以使用GraphQL官方提供的方法將關聯的資料欄位繫結給某個component。
鑑權及續租方案(前端)
在結合GraphQL和Apollo的情況下我們的鑑權方案主要依賴於AppAsyncStorage和web localStorage這兩個資料可持久化的方案。
在登入過程中產生的token快取到App AsyncStorage或web localStorage中,註冊GraphQL Server的時候通過outLink來setContext,setContext方法主要用來重寫header,在header中新增資源欄位。這個資源欄位一般是和後端商議後決定,不過Apollo官方的推薦通過傳入token來實現整個鑑權方案。
如何使用輪子
工業控制裝置(前端)
前面提到過我們的終端裝置中還包含樹莓派,這是一個工業控制裝置,一般被放置在使用者的廠房中,用來列印記錄庫房資料。
我們通過阿里雲物聯網套件來實現伺服器端和樹莓派之間的通訊,裝置可以釋出和訂閱一些資料到MQTT中 ,每隔一段時間就會有心跳包從裝置上傳到MQTT,以此來更新頁面資料。之後MQTT會將心跳包傳遞到訊息佇列中,然後再通過API到應用層來同步訊息。
最後app端或web端通過暴露給他們的GraphQLAPI來讀取到裝置的資訊,比如裝置編號、韌體版本、公網內網IP等。
當使用者發現裝置版本和伺服器版本存在差異,執行需要更新的時候,本質上是傳送動態操作到GraphQL API層,然後由GraphQL API層傳送訊息到裝置 ,之後訊息通過SDK API到達MQTT,接著釋出一系列的請求到裝置。最後裝置重啟完成之後會再重新發布心跳包到MQTT來更新一系列的操作。
使用輪子過程中的注意事項
使用新輪子的問題(前端)
在使用新輪子的過程中碰到的第一個問題就是在學習成本和團隊適應方面。一是文件不夠不夠健全,相關的學習資料偏少,可能產品已經推出了2年時間,但是文件卻是剛推出時寫的。
除開文件問題之外,本質上還有一個思路的改變。原先使用Redux傳送請求時,雖然和後端溝通麻煩了一點,但是畢竟已經和熟悉了。現在轉換到GraphQL後,請求發起機制、資料重新整理、檔案上傳等等都完全不同,相當於要從頭開始學習新的東西。
在使用Apollo的過程中我們也遇到了一些坑。比如多次請求觸發導致返回結果為underfined,之所以會這樣是由於第一個介面請求傳送出去後,還在loading階段時,同一個介面又傳送了第二次,導致返回資料發生衝突變成undefined。
還有資源物件和id重複導致資源資料被覆蓋的問題,這是由Apollo的資料儲存的特性所造成的,Apollo的每個資源物件的型別和id是定義資料欄位唯一的標識。
Component的設計思路改變(前端)
以前設計的Component有容器元件和展示元件這兩個概念,容器元件和Redux store之間存在互動,用來更新資料,展示元件則是單純的展示資料。現在使用GraphQL之後,我們發現了一個更優的解決方案。
因為每個物件的資源欄位固定,完全可以讓每個Component和GraphQL 的Query片段一一對應。這意味著Component不再是為了請求而定義的,而是根據物件型別來定義
過於依賴Fragment導致的效能問題(前端)
我們在使用GraphQL 的過程中曾出現過一次非常大的效能問題。當時為了更高效的開發,我們將每個資源物件全部使用Fragment來寫,且每個物件都取到了所有頁面中全部的資料。
此時一個要動態計算的欄位被放在了一個基類中,在多個Fragment中迴圈呼叫,甚至巢狀呼叫。這時候後端就可能接收到一個需要計算n方次動態計算的結果的請求,伺服器的負載壓力可想而知。
以上這種情況對於前端來說,操作的只是某個實體的一個或幾個資源欄位。但是對於後端來說,每個實體背後可能對應著不同的資料庫甚至不同型別的儲存叢集,同時後端叢集間的海量資料自由join得到的結果。前端氾濫資料會導致後端負載急劇上升。
以上大概就是前端方面要講的全部內容,接下請繼續看下關於後端方面的問題。
使用新輪子的問題(後端)
使用GraphQL的過程中遇到的第一個比較嚴重的問題就是介面設計思路轉變困難,之前在寫RestfulAPI的時候想的更多的是面向資源,而GraphQL的設計思路則是面向場景,這完全顛覆了後端設計介面的哲學。
其次是開發feature時要更多考慮效能的問題,避免被查當機。另外GraphQL相比Restful在鑑權模型設計上要投入更多的精力。
說完了這些弊端,再來看下有那些有利的地方。一是介面開發工作量大幅減少,重複性工作得以減少,避免了出錯。其次是錯誤返回和文件等標準化工作可以藉助工具本身完成。最後是對API多版本管理更加友好。
效能問題(後端)
從後端來看早期的效能問題都是因為急於快速上線引起的,主要有以下幾個因素。
第一點就是GraphQL的N+1場景,即前端在查詢資料的時候可能首先要查到IDS陣列,然後再map IDS陣列重新對後端發起請求,最後後端通過多條SQL取到的可能是列表資料。
第二點是在前期開發的時候沒有做請求層級限制,導致前端查詢多層巢狀,伺服器無法承受壓力。
第三點是沒有做分頁的數目限制,一般來說前端可以通過傳遞特定的引數給介面來設定每頁的接收數量,但是有些前端人員可能會為了快速上線新功能將每頁的數量直接設定為9999,這樣伺服器就又會被搞當機。
安全問題
為避免發生一些安全問題,需要在認證、授權和請求頻率限制幾個方面多加註意。認證上每次都要在Header上加上token,認證機制應改為JWT,我們這邊由於涉及到的終端較多,所以還會按平臺檢驗合法性。
授權上可以針對操作也可以針對欄位進行具體的授權。請求頻率限制上既要防止惡意刷請求也要實現基於IP和UserID的限制。
安裝工人的心得
這裡的標題是安裝工人的心得,其實指的就是我個人在使用GraphQL過程的一些感悟和總結。
先來看看GraphQL還有那些弊端。
第一,雖然後端已經做了一些優化,但是還是沒有完全實現前端的按需查詢,當資料量達到一定級別的時候,資料庫查詢可能會成為效能瓶頸。
第二,生態群並不完善,文件和例項缺乏,可能在google查詢到的資料無法確切的解決問題。
第三,評價一個技術的實用性,不光要看技術力還要看它能否快速落地。一個現有的專案要從Restful切換到GraphQL,其實重構的壓力很大,且重構和業務並行推進困難。
第四,由於目前國內使用GraphQL的團隊不是很多,所以很難招聘到有經驗的工程師,需要從零開始積累。
談完了弊端再來說下個人的感悟,總結起來就是三個確實。確實提升了前後端的協作開發效率;確實降低了在前後端協作過程中的錯誤率;確實提高了介面複用度和開發效率。
個人認為用開發工具本身提高無論是前端、後端還是DevOps的工作效率是未來的發展趨勢,我也相信GraphQL未來會支援更多的框架,解決更多的問題,並在效能和安全上有進一步提升。
關於我們
有數派是專業的紡織SaaS產品,具備工業(硬體)控制、樣品管理、倉庫儲存、銷售管理、設計輔助等功能,應用場景覆蓋了行業幾十個場景,近十個終端。
我們在技術棧的選擇上,web用的是React,App用的React Native,後端主要的API源是用Ruby寫的,還有部分Python用來做資料分析,目前所有的API都被遷移到了GraphQL上。硬體部分用到了C++,伺服器的容器管理用的是Docker,DB則是PostgreSQL。
我們目前在終端上有樹莓派、tv、pad、web、app、微信端、ipad。
以上為全部分享內容,謝謝大家!
編者:IT大咖說,轉載請標明版權和出處