Categories
程式開發

通過前端工程化將 Apollo 引入現有 React 技術棧


GraphQL 作為一種新的通信協議自 2015 年 Facebook 開源之初,就開始受到技術社區的關注。Apollo 作為目前較為成熟的 GraphQL Client 解決方案,成為騰訊 NOW 直播 Web 業務的首選 。在 GMTC 全球大前端技術大會(深圳站)2019 上,朱林結合騰訊 NOW 直播的實踐分享了:GraphQL Client 解決的問題是什麼? Apollo 的優勢是什麼?本文即根據朱林的演講整理而成。以下為正文:

大家下午好,我是來自騰訊 NOW 直播的高級前端工程師朱林。我是 2015 年進入騰訊的,目前在騰訊最大的前端團隊之一的 IVWEB 團隊,我在團隊主要負責 PWA 以及 GraphQL 等技術棧的業務落地 。

今天我分享的主題是《通過前端工程化將 Apollo 引入現有 React 技術棧》,主要分為三個部分:

  • 首先,介紹什麼是 GraphQL。
  • 其次,GraphQL 在前端的選型,即GraphQL Client 端的選型,目前業界最火的有兩個方案,一個是Apollo,另一個是Relay,我會對比這兩個方案,大家可以結合自己的業務作出選擇。
  • 最後,確定好選型之後,我們希望把整個團隊的技術棧統一,那麼手工拷貝代碼或者口口相傳的方式肯定是不可取的,我們希望通過類似前端工程化的腳手架這樣的方式在新項目中快速推廣,統一整體的技術棧。

一、什麼是 GraphQL ?

首先,我簡單介紹一下什麼是 GraphQL。在了解 GraphQL 之前,我們先來看下這個頁面(也可以掃描二維碼)。這個頁面其實是一個比較典型的列表頁面或者說 feeds 頁面,很多卡片其實很類似,可能也會有一些分頁的需求,每個卡片的元素或者說信息的數據結構比較統一。

通過前端工程化將 Apollo 引入現有 React 技術棧 1

單獨拿一個卡片來說,這裡有主播的頭像、暱稱,還有你是否關注主播,以及這個主播是否處於開播狀態。

通過前端工程化將 Apollo 引入現有 React 技術棧 2

對於我們的後端來說,後端服務都是追求盡量的原子化,或者說微服務;所以用戶資料可能是一個獨立的服務,關係鏈關係(即你跟主播的關注關係)是一個獨立的服務;主播的開播狀態,或者說我們叫房間服務 / 開播服務,也是一個獨立的服務。

依據之前很火的 Restful 這樣的一個接口風格,可能很多同學沒有實踐過,它會要求把所有的接口都看做資源,每個資源都是獨立、解耦的。可能一個首頁的頁面會拉三條後台接口,但是,作為前端性能優化的一個最基礎的指標來說,你希望你的HTTP 請求能夠盡量的少,這樣一方面性能更好,另一方面,前端編寫代碼的時候不用再去做數據聚合,這樣可以降低前端代碼的複雜度。

然後,你希望後端的同學聚合下接口。於是得到這樣一個接口,如下圖所示。

通過前端工程化將 Apollo 引入現有 React 技術棧 3

這個接口字段非常多。為什麼?因為不止你向這位後端同學提了需求,還有其他同學也提了需求。很多其他的頁面可能不是長這樣子,其他的同學也希望能夠聚合一下。他們頁面的有些數據是跟你一樣的,比如:主播的頭像、暱稱。可是也有一些差異,比如:主播拍的短視頻、主播開播的房間的封面,後端同學就把這些都聚合到了這個接口,這個接口當然能夠滿足大家的需求,可多出了很多冗餘數據。下面我們簡單看一下接口文檔。

1. 接口文檔

當接口非常複雜時,就會暴露出兩個問題。如果數據要解耦,我們就會請求很多次接口;即使後端的同學做了聚合,又會有很多冗餘的字段,下面是數據聚合後的接口文檔,還是非常複雜的,我們看看這樣會帶來什麼問題。

通過前端工程化將 Apollo 引入現有 React 技術棧 4

2. 業務報錯

下圖是我們團隊自研的一個前端腳本錯誤監控的方案,已在 GitHub 開源了,叫 Aegis

突然有一天你的業務報錯了,比較典型的就是 null is not an object,那為什麼會出現這樣的報錯呢?

因為,複雜的接口一般都對應著防禦性的編程。但是你寫代碼時,默認就相信後端的接口一定是按照接口文檔的形式返回的,沒有做防禦性編程。

通過前端工程化將 Apollo 引入現有 React 技術棧 5

為了解決這個問題,防止某些字段不是你期望所返回的樣子,你做了一些防禦性編程(如下圖)。代碼非常醜陋,不僅要判斷嵌套對像中的字段是否存在,而且還判斷這個字段的值是不是數字類型

通過前端工程化將 Apollo 引入現有 React 技術棧 6

3. API 聯調痛點

在上面的小故事中,可以看到一些問題。

通過前端工程化將 Apollo 引入現有 React 技術棧 7

(1)請求多個 API

請求多個 API,影響前端頁面性能。

(2)冗餘字段

當你讓後端的同學幫你做聚合時,又多了冗餘字段。因為多了冗餘字段,可能在實際業務的性能度量中,頁面的首屏渲染速度並沒有得到提升。

(3)API 文檔更新不及時,甚至沒有文檔

剛剛看到的那個文檔非常複雜,不僅要判斷嵌套對像中的字段是否存在,而且還判斷這個字段的值是不是數字類型;當需求變更時,文檔還會新增字段或者改變字段的類型。當再把文檔交接給其他同事時,因為文檔更新不及時,新的問題會隨之產生,畢竟錯誤 / 過時的文檔,還不如沒有文檔。

(4)參數類型校驗

最後,就是剛剛說的所謂的防禦性編程。在前端代碼裡面,因為可能不會信任後端返回的數據,你會寫很多所謂的參數類型校驗,或者字段是否存在等防禦性編程代碼,而這些代碼跟你的業務邏輯是沒有關係的,徒增代碼的複雜度。

4.API 聯調痛點解決方案

(1)沒有 GraphQL 時的解決方案

假設大家都不知道 GraphQL,那麼針對這上面這 4 個問題,我們從零開始去想的話,如何解決這個問題? (如圖所示)

通過前端工程化將 Apollo 引入現有 React 技術棧 8

(2)GraphQL 解決方案

在有 GraphQL 的情況下,我們又是怎麼去解決的。

通過前端工程化將 Apollo 引入現有 React 技術棧 9

  • Query

針對請求多個 API 和冗餘字段等問題, 在結構化查詢語言中,​​通過 GraphQL 的聚合,不需要寫很多冗餘的邏輯,也不用手動去聚合,只需要通過寫 JSON 的方式配置 GraphQL schema 文件,它可以自動幫你處理這些過濾邏輯以及類型校驗邏輯,請求多個 API 時只返回你想要的接口,不多也不少。

  • introspection(自檢性)

針對 API 文檔更新不及時,GraphQL 的解決方案是,在寫好後端代碼後,不需要寫接口文檔,因為已經和前端的同學定義好字段的類型。寫過 TypeScript 的同學應該知道,每個字段對應的是一個強類型的字段校驗規則。其次,利用GraphQL 的自檢性,你就可以完全的看到這個接口可以提供哪些字段,每個字段代表的含義是什麼,每個字段返回的參數的類型是什麼,以及它可以接收怎樣的參數。

  • 類型系統

針對參數類型校驗, 是說每個字段返回的參數的類型是什麼,以及它可以接收怎樣的一個參數。 GraphQL 是一個強類型的協議,有自己的類型系統。

通過前端工程化將 Apollo 引入現有 React 技術棧 10

5. 什麼是 GraphQL?

(1)GraphQL 定義

GraphQL 是 Facebook 在 2015 年開源的,那麼先來看一下它是什麼?

GraphQL 的定義是一種 API 查詢語言,它其實是一種語言的規範,跟具體的實現是沒有關係的。而且它是一層非常薄的數據抽象層,它既不會負責存儲數據,也不會負責渲染數據;簡單說就是一層 Proxy(代理),它把後端的數據做一些聚合、校驗、判斷,再返回給前端。對於前端來說,它是一種結構化的查詢語言,長得很像 JSON,其實不是。

(2)數據抽象層

通過前端工程化將 Apollo 引入現有 React 技術棧 11

數據抽象層就是中間這一層 GraphQL Server。對於前端來說,你可能是一個iOS 的App,安卓的App,也可能是移動端的H5 頁面,通過一個Http 協議層把GraphQL 的Query(查詢參數)請求到了GraphQL Server,對於GraphQL Server 來說,它取數據的Data 層是什麼它一點都不關心,但是它吐回給前端的就是一個JSON。

(3)語言無關

GraphQL 是一個所謂的數據抽象層,它的邏輯輕及薄,且與語言無關。

Facebook 官方推薦的實現是 JavaScript,即 Node.js 的實現;在社區這個方案非常活躍,包括 RubyGoPythonPHPC# 等,在社區都有各式各樣的對於 GraphQL 規範的實現。

通過前端工程化將 Apollo 引入現有 React 技術棧 12

(4)結構化查詢語言

通過前端工程化將 Apollo 引入現有 React 技術棧 13

上圖就是在前端發起一個 GraphQL 查詢的樣子,它其實不是 JSON,但是非常像 JSON 包裹起來的。看上圖,users 就是 list,它想請求一個用戶的列表,那麼傳入的參數是一個數組,但我這裡只傳了一個數組元素,這裡 unis 即用戶唯一標識符。返回的數據就包裹在下面這個花括號裡,包括姓名、頭像 logo、是否在線、是否在開播,以及用戶是不是關注了他(isListen)。

二、Apollo VS Relay

在前端社區裡面,兩個最典型的 GraphQL Client 方案就是 Apollo 和 Relay。

Apollo 是由社區驅動的,文檔非常豐富。據我所知,目前已經有一個類似於創業公司的組織在驅動它不斷迭代,它提供了一整套解決方案,包括Apollo Client 和Apollo Server;它也是前後端一體的打包方案,甚至提供了整個鏈路的一個監控平台,可以監控Apollo Server 或者說GraphQL Server 的性能指數。

Relay 是 Facebook 官方出的,只是針對前端,而且只針對 React 的技術棧,它不僅能讓你更好、更方便地寫 GraphQL Query,甚至還接管了前端的數據管理。

1. Relay

(1) Relay 示例

那麼我們先來看一下,沒有 GraphQL Client,前端該怎麼發起一個 Graphql Query?

通過前端工程化將 Apollo 引入現有 React 技術棧 14

上圖是一個 Relay 的 action。

我請求了一個 url(即 GraphQL 的一個接口),上圖是直接拼接字符串。

大家可能會問,為什麼不直接用模板字符串?直接用模板字符串當然可以,但是會帶來一個問題,前端頁面會在Html 這一層去inline 一段後台接口預加載的邏輯,並把它緩存到內存裡(比如掛載到window 對像上),這樣當你的JS 文件加載完成,發起後台接口請求時,可以優先讀取內存中緩存好的接口數據,但是取緩存的key 值是接口url + 接口參數。

當使用模板字符串時,因為構建器不一樣,或構建配置不一樣,沒有辦法保證這一段Query 模板字符串最後編譯出來的格式,比如有幾個換行、空格,這樣讀取接口緩存時可能會出現key 值不匹配的情況。

通過前端工程化將 Apollo 引入現有 React 技術棧 15

如果沒有 GraphQL Client ,但是又想做 Http 接口預加載,用拼接字符串方式可能會更靠譜。

(2) Relay 的缺點

通過前端工程化將 Apollo 引入現有 React 技術棧 16

Relay 帶來的問題主要為以下三個:

  • 可讀性差;
  • 缺少校驗;
  • 手動處理緩存。

首先,不光可讀性非常差,而且寫代碼也非常痛苦;

其次,也是最重要的一點,Relay 在前端完全沒有利用到 GraphQL Client 的類型校驗能力。如果傳的參數錯了,就沒有這個字段,一定要到請求完後端之後,才能感知到這代碼寫錯了;如果有GraphQL Client 方案的話,在構建編譯時就會直接報錯,不用等到請求後台接口這一步。

最後,在前端可能需要自己手動處理後台接口的緩存。

##2. Apollo Client

(1)Apollo

好,我們來我們先簡單介紹一下 GraphQL Client 。

GraphQL Client 的用法很簡單,直接安裝 apollo-boostreact-hooks、graphql 三個包就可以了。

通過前端工程化將 Apollo 引入現有 React 技術棧 17

這三個包怎麼用呢?

  • apollo-boost 這個包,提供了所有前端需要的 API,包括轉換,構造 GraphQL Query 的請求參數,以及設置 GraphQL 後台接口等。
  • apollo/react-hooks 是基於 react-hooks 的視圖層,如果現有業務代碼裡面沒有用到 react-hooks ,可以先不用管它。
  • graphql 是 Facebook 官方提供的一個包,用來解析官方查詢語言,因為它畢竟不是一個 JSON,當發起請求時 ,它會做一些處理。

如果用 GraphQL Client,剛剛那種代碼可能就會變成這樣(如下圖)。

通過前端工程化將 Apollo 引入現有 React 技術棧 18

先看左邊代碼,與剛剛定義了 GraphQL Query 的模樣不太一樣,你會發現:

  • users 的 uins 不是一個寫死的固定的值,而是一個變量的 anchorUinList,能夠通過參數動態化傳入 Query 是非常重要的一個點;
  • anchorUinList 被定義為了 float(浮點)。

就像我剛剛說的,定義好了前端的GraphQL Schema,如果實際傳參有問題,比如說, User 唯一ID 有問題,其實根本不用等到跟後台聯調階段才發現問題,寫代碼編譯時就會報錯了。那麼具體怎麼發起呢?在這裡設置 GraphQL 後台接口的地址,然後用 Client.Query 的方式發起。當然,這是一個最簡單的 demo。可以用 GraphQL.Query 這樣一個公共的方法去抽離出來,抽成一個 redux 的 middlerware,也可以通過 dispatch 一個 action 去發起請求。

(2)Relay

Relay 有一個最重要的特點,即強制約定大於配置

什麼意思呢?即在 Relay 的規範裡面,它規定了很多東西,如命名、查詢規範等。

通過前端工程化將 Apollo 引入現有 React 技術棧 19

上圖是一個 Relay Query 的例子,跟剛剛的 GraphQL Schema 長得差不多,但是不同之處在於:

  • 命名格式

一定要按照它的格式命名。上圖,你會發現這個文件叫Query.js,但是你的Query 就一定要叫Query,且下劃線後面的駝峰一定不能變,它表示前面那塊是modules,後面的駝峰的大寫的首字母表示這是一個讀操作。因為 GraphQL 不止支持讀操作,還支持寫操作,在 GraphQL 裡面叫 mutation(突變)。

  • 查詢規範

在 Relay 裡面還定義了一個點,就是說它的分頁方式跟我們在 Restful 的那種風格不一樣。它認為你請求的每一個資源都是一條邊,前端到後台的一條邊。所以,你會看到 connection 包裹的對象叫 edges,然後每一項就是我剛剛頁面裡面的每一張的卡片,它就是一個 node ,也就是節點。包括你的 GraphQL Client 去定義分頁參數的時候,也要遵循這樣的查詢規範。

GraphQL 還支持第3 種基本的操作類型,叫subscription(訂閱),如果是Web 應用的話,可能會基於WebSocket 的做訂閱,取代長輪詢這種比較浪費資源的方式,來主動監聽頁面數據的變更。

3.Apollo VS Relay

通過前端工程化將 Apollo 引入現有 React 技術棧 20

這是 Relay 的 GraphQL 形式,它實際是 Query 是通過一個 React Apollo 這樣的組件發起的,然後直接把後端請求的數據以 props 的形式傳遞到真正的業務組件裡面去。

以上是通過一個 demo 簡單的對 Apollo 和 Relay 做一個對比。

Apollo 優點:

  • 文檔豐富
  • 社區驅動,生態豐富
  • 代碼侵入性低(相比 Relay)

Apollo 缺點:

  • 非官方出品

Relay 優點:

  • Facebook 官方出品約定大於配置

Relay 缺點:

  • 文檔複雜
  • 侵入性較強
  • 手動處理緩存

通過前端工程化將 Apollo 引入現有 React 技術棧 21

這是前端的一個比較,對於在後端的業務遷移來說,我個人的建議是,前端同學可以先把現有的HTTP 的後台接口作為GraphQL Server 的數據源,多一層很薄的一層,然後慢慢的把整個前後端的技術棧往GraphQL 技術棧平滑遷移,這樣的話,你可以一個接口一個接口的遷移,步子不會邁得太大,也不會出太大的問題,改造成本也不會太高。

4.N+1 Query 的問題

GraphQL 的特性所帶來一個問題,即“N+1 Query”的問題,就是我請求 4 個字段,前端請求了 1 次,後台請求了 4 次”,遇到這個問題怎麼解決?

Facebook 提供了一個解決方案,一個非常薄的工具,才 300 多行代碼,裡面還有很多是註釋叫 dataloader。

解決原理:

第一是緩存,既然你每次都會發起 5 次請求,但其實很多字段它的 Key 是一樣的。比如說,剛剛那個例子,某一個特定主播的頭像、名稱,其實只要請求一次,就可以緩存住了。緩存策略還可以自己控制。
第二點是,它會在 Node.js 那一層,也就是 GraphQL Server 那一層會批量的請求數據。

三、總結

通過前端工程化將 Apollo 引入現有 React 技術棧 22

做一個簡單的總結:

  1. 我們可以基於之前的 Restful 接口,或者這些基於之前的後台的 Http 接口,漸進式改造,把現有前後端的通信鏈路改造為 GraphQL 技術棧。
  2. 使用到 GraphQL 之後,未來研發模式一定是 schema 優先。前後端共同維護 GraphQL schema,也就不存在接口文檔了;代碼寫好了,文檔就自然而然的生成好了,包括需要什麼樣的字段,字段是什麼類型,是否允許否為空。
  3. 選擇合適自身業務特點的技術棧。
  4. 我們是可以通過業務抽象的腳手架去統一前端模板。可能通過yeoman 去生成一個統一的規範的腳手架,來在新項目中去統一大家發起的GraphQL 請求的方式,或者統一大家GraphQL Client 一個技術選型,包括構建工具,而不是說每個同學去寫一個新頁面的時候,都要重新去搭建前端技術的開發環境,或者說重新安裝一遍開發環境。
  5. 我個人認為 GraphQL Server 其實不是說讓前端去侵入後端,更多的是去提升前後端雙方的研發體驗,以及未來項目上線之後的運營質量。

作者介紹

朱林,騰訊高級前端工程師,騰訊 IVWEB 團隊負責人之一。先後負責過 QQ 群活動、花樣直播、NOW 直播、QQ 群視頻等業務的前端開發和架構。在前端性能優化和 Node.js 方面有較深入的研究,對音視頻相關的前端開發、服務端渲染、GraphQL 等技術棧有豐富的實踐經驗。

活動推薦

大前端工程化是指移動端、前端在項目規模、工程複雜度、快速迭代等相同背景下,對一些共性問題的思考。工程化是與實踐密不可分的,GMTC 全球大前端技術大會(北京站)設置“大前端工程化”專題,本專場將通過分享業內一些經過實踐檢驗的工程化方案,希望能夠為大家在大前端工程化的探索道路上提供借鑒和幫助。