自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

攜程度假基于 RPC 和 TypeScript 的 BFF 設(shè)計(jì)與實(shí)踐

人工智能 新聞
在這篇文章中,我們將介紹一種基于 RPC 和 TypeScript 的 BFF 設(shè)計(jì)與實(shí)踐。我們稱(chēng)之為 RPC-BFF,它利用前后端都采用同一語(yǔ)言(TypeScript)的優(yōu)勢(shì),實(shí)現(xiàn)了其它 BFF 技術(shù)方案所不具備的多項(xiàng)功能,可以顯著提升前后端數(shù)據(jù)交互的性能、效率以及類(lèi)型安全。

作者簡(jiǎn)介

工業(yè)聚,攜程高級(jí)前端開(kāi)發(fā)專(zhuān)家,react-lite, react-imvc, farrow, remesh 等開(kāi)源項(xiàng)目作者,專(zhuān)注 GUI 開(kāi)發(fā)、框架設(shè)計(jì)、工程化建設(shè)等領(lǐng)域。

一、前言

隨著多終端的發(fā)展,前后端的數(shù)據(jù)交互的復(fù)雜性和多樣性都在急劇增加。不同的終端,其屏幕尺寸和頁(yè)面 UI 設(shè)計(jì)不一,對(duì)接口的數(shù)據(jù)需求也不盡相同。構(gòu)建一套接口滿(mǎn)足所有場(chǎng)景的傳統(tǒng)方式,面對(duì)新的復(fù)雜性日益捉襟見(jiàn)肘。

在這個(gè)背景下,BFF 作為一種模式被提出。其全稱(chēng)是 Backend for frontend,即為前端服務(wù)的后端。它的特點(diǎn)是考慮了不同端的數(shù)據(jù)訪(fǎng)問(wèn)需求,并給予各端針對(duì)性的優(yōu)化。

在這篇文章中,我們將介紹一種基于 RPC 和 TypeScript 的 BFF 設(shè)計(jì)與實(shí)踐。我們稱(chēng)之為 RPC-BFF,它利用前后端都采用同一語(yǔ)言(TypeScript)的優(yōu)勢(shì),實(shí)現(xiàn)了其它 BFF 技術(shù)方案所不具備的多項(xiàng)功能,可以顯著提升前后端數(shù)據(jù)交互的性能、效率以及類(lèi)型安全。

二、為什么會(huì)需要 BFF?

用發(fā)展的視角來(lái)看,業(yè)界存在的兩大趨勢(shì)催生了 BFF 模式:

  • 硬件行業(yè)的多終端發(fā)展趨勢(shì)
  • 軟件行業(yè)的微服務(wù)發(fā)展趨勢(shì)

其中,微服務(wù)化以及中臺(tái)化的趨勢(shì),改變了后端團(tuán)隊(duì)構(gòu)建服務(wù)的方式。整個(gè)系統(tǒng)將按照領(lǐng)域模型分隔出多個(gè)微服務(wù),這增強(qiáng)了各個(gè)服務(wù)的內(nèi)聚性和可復(fù)用性的同時(shí),也給下游的接口調(diào)用者增加了數(shù)據(jù)聚合的成本。

多終端的發(fā)展,又讓數(shù)據(jù)聚合的需求進(jìn)一步多樣化。使得處于微服務(wù)和多終端之間的團(tuán)隊(duì),不管是前端團(tuán)隊(duì)還是后端團(tuán)隊(duì),他們面對(duì)的問(wèn)題日趨復(fù)雜化。

圖片

系統(tǒng)復(fù)雜度的增加,將體現(xiàn)在代碼復(fù)雜度和團(tuán)隊(duì)協(xié)作復(fù)雜度上。這意味著,沒(méi)有采用有效手段應(yīng)對(duì)復(fù)雜度的團(tuán)隊(duì),自身將成為產(chǎn)研流程中的瓶頸。他們既要面對(duì)上游多個(gè)微服務(wù)的聯(lián)調(diào)需求,又要應(yīng)對(duì)下游多個(gè)端的數(shù)據(jù)消費(fèi)需求;有更多的會(huì)議要參加,更多的需求文檔要讀,更多的代碼、單元測(cè)試和技術(shù)文檔要寫(xiě),然而敏捷開(kāi)發(fā)模式的交付周期卻不隨之增加。

此時(shí)一般有兩個(gè)應(yīng)對(duì)策略。一種是去掉數(shù)據(jù)聚合層,讓各個(gè)下游前端應(yīng)用自行對(duì)接微服務(wù)接口,將聚合數(shù)據(jù)的業(yè)務(wù)邏輯轉(zhuǎn)移到前端,增加它們的代碼體積,拖慢其加載速度,同時(shí)顯著增加客戶(hù)端向服務(wù)端發(fā)起的請(qǐng)求數(shù),達(dá)到拉低頁(yè)面渲染性能,破壞用戶(hù)體驗(yàn)的效果。

另一種做法則是采用 BFF 模式,降低前后端數(shù)據(jù)交互的復(fù)雜度。微服務(wù)強(qiáng)調(diào)按領(lǐng)域模型分隔服務(wù),BFF 則強(qiáng)調(diào)按終端類(lèi)型分隔服務(wù)。將原本單個(gè)團(tuán)隊(duì)處理“多對(duì)多”的復(fù)雜關(guān)系,轉(zhuǎn)變成多個(gè)團(tuán)隊(duì)處理的“一對(duì)多”關(guān)系。因此,本質(zhì)上,BFF 模式的優(yōu)化方式是通過(guò)調(diào)整開(kāi)發(fā)團(tuán)隊(duì)在人力組織關(guān)系層面的職能分工而實(shí)現(xiàn)的。

圖片

也就是說(shuō),BFF 不是作為新技術(shù)用以提升生產(chǎn)力,而是通過(guò)改變生產(chǎn)關(guān)系去解放生產(chǎn)力。從單一團(tuán)隊(duì)成為產(chǎn)研流程中的單點(diǎn)瓶頸,轉(zhuǎn)變成多個(gè) BFF 研發(fā)團(tuán)隊(duì)各自應(yīng)對(duì)某一端的數(shù)據(jù)聚合需求。

這種轉(zhuǎn)變所爭(zhēng)取到的是增加人手以提升效率的空間。一個(gè)工作任務(wù)相耦合的研發(fā)團(tuán)隊(duì)不能無(wú)限加人提效,但若能拆成多個(gè)研發(fā)團(tuán)隊(duì),則可以擴(kuò)大每個(gè)子團(tuán)隊(duì)的提效空間。當(dāng)然,代價(jià)是團(tuán)隊(duì)之間可能存在重復(fù)工作,只是相比效率瓶頸而言,這些問(wèn)題可能不屬于現(xiàn)階段主要矛盾,值得取舍。

盡管 BFF 不是新技術(shù),也不要求新技術(shù),但不意味著不能引入新技術(shù),或者引入新技術(shù)無(wú)法提效。我們?nèi)钥梢杂眯录夹g(shù)去實(shí)現(xiàn) BFF,解決因團(tuán)隊(duì)拆分而產(chǎn)生的問(wèn)題,收獲研發(fā)效率(生產(chǎn)力)和產(chǎn)研流程(生產(chǎn)關(guān)系)兩方面的提升。

接下來(lái),我們先看一下實(shí)現(xiàn) BFF 的幾種不同方式,然后介紹作為新技術(shù)出現(xiàn)的 RPC-BFF。

三、BFF 的實(shí)現(xiàn)方式

如前所述,BFF 是作為模式(Pattern)被提出,而非一種新技術(shù)。在技術(shù)層面上,任何支持服務(wù)端開(kāi)發(fā)的語(yǔ)言和運(yùn)行時(shí),不管是 Java、Python、Go 還是 Node.js,都可以開(kāi)發(fā) BFF 服務(wù)。只要它們所實(shí)現(xiàn)的這個(gè)服務(wù),是面向前端需求的。

BFF 的實(shí)現(xiàn)方式多種多樣,不僅跟技術(shù)選型有關(guān),跟研發(fā)團(tuán)隊(duì)的分工方式、職能邊界、協(xié)作流程等因素也息息相關(guān)。

3.1 樸素模式

所謂的樸素模式,是指 BFF 的實(shí)現(xiàn)方式不改變前后端的分工方式、職能邊界和協(xié)作流程。前端不介入 BFF 層的實(shí)現(xiàn),BFF 層的需求由后端團(tuán)隊(duì)自行消化。

也就是說(shuō),BFF 在這里僅僅是后端團(tuán)隊(duì)的內(nèi)部分工:

  • 開(kāi)發(fā)微服務(wù)應(yīng)用(后端團(tuán)隊(duì))
  • 開(kāi)發(fā) BFF 服務(wù)(后端團(tuán)隊(duì))
  • 消費(fèi) BFF 接口(前端團(tuán)隊(duì))

前端的工作方式跟之前一樣,后端負(fù)責(zé)開(kāi)發(fā) BFF 為前端團(tuán)隊(duì)提供面向前端的數(shù)據(jù)聚合接口。開(kāi)發(fā) BFF 的編程語(yǔ)言由后端決定。

圖片

如上圖所示,紫框?yàn)檠邪l(fā)團(tuán)隊(duì)(前端或后端),藍(lán)框?yàn)?BFF 服務(wù),黃框?yàn)榍岸藨?yīng)用,綠色箭頭表示“開(kāi)發(fā)”,紫色箭頭表示“調(diào)用”。

在圖示中,BFF 按照終端尺寸分為 Mobile BFF 和 PC BFF 兩類(lèi),它背后的假設(shè)是:相近終端尺寸的前端應(yīng)用擁有相近的數(shù)據(jù)訪(fǎng)問(wèn)需求。移動(dòng)端應(yīng)用調(diào)用 Mobile BFF,PC 端應(yīng)用調(diào)用PC BFF。

3.2 解耦模式

解耦模式,相比樸素模式而言,它改變了分工方式、職能邊界和協(xié)作流程。后端不介入 BFF 層的實(shí)現(xiàn),BFF 層的需求由前端團(tuán)隊(duì)自行消化。

也就是說(shuō),BFF 是前后端團(tuán)隊(duì)共同完成的一種新的分工:

  • 開(kāi)發(fā)微服務(wù)應(yīng)用(后端團(tuán)隊(duì))
  • 開(kāi)發(fā) BFF 服務(wù)(前端團(tuán)隊(duì))
  • 消費(fèi) BFF 服務(wù)(前端團(tuán)隊(duì))

前端的工作方式跟之前有所不同,前端團(tuán)隊(duì)自己開(kāi)發(fā) BFF 服務(wù)以供自己的前端應(yīng)用消費(fèi)聚合好的數(shù)據(jù)。開(kāi)發(fā) BFF 的編程語(yǔ)言由前端決定。

圖片

如上圖所示,前端團(tuán)隊(duì)同時(shí)開(kāi)發(fā)了 App 及配套的 BFF 服務(wù)。前端開(kāi)發(fā)的 BFF 服務(wù)背后調(diào)用了后端團(tuán)隊(duì)提供的各個(gè)微服務(wù)接口,將它們聚合起來(lái),轉(zhuǎn)換為前端應(yīng)用可直接消費(fèi)的數(shù)據(jù)形態(tài)。

3.3 樸素模式 VS 解耦模式

兩種模式都有其適用場(chǎng)景,具體要看不同研發(fā)團(tuán)隊(duì)的人力資源、應(yīng)用類(lèi)型和技術(shù)文化風(fēng)格等多個(gè)因素。

從技術(shù)發(fā)展的角度,解耦模式更能代表“徹底的前后端分離”的趨勢(shì)和目標(biāo)。

前后端分離可以大體分為兩個(gè)階段:

  • 渲染服務(wù)回歸前端團(tuán)隊(duì)(SSR)
  • 數(shù)據(jù)服務(wù)回歸前端團(tuán)隊(duì)(BFF)

部分開(kāi)發(fā)者可能認(rèn)為完成了第一階段,就達(dá)到了前后端分離的目標(biāo)。此時(shí)前端團(tuán)隊(duì)可以自行構(gòu)建SSR 服務(wù)器,更早介入渲染流程,不必等到瀏覽器加載頁(yè)面的 JavaScript 腳本后才開(kāi)始發(fā)揮作用。不必再跟后端頻繁溝通,交代他們?cè)?html 文件里添加指定的 id 或class 名;前端團(tuán)隊(duì)可以全權(quán)處理,后端團(tuán)隊(duì)只需要提供數(shù)據(jù)接口即可。

然而,“前后端分離”不只是把前端代碼(如 html 文件)從后端代碼倉(cāng)庫(kù)轉(zhuǎn)移到前端代碼倉(cāng)庫(kù)里,這只是形式和手段。前后端分離是指職能的分離,是為了讓前端研發(fā)人員不必低效率地遙控后端研發(fā)人員,讓他們?nèi)C(jī)械地調(diào)整面向前端需求的代碼。前端團(tuán)隊(duì)可以自主負(fù)責(zé)、自主修改、快速迭代,專(zhuān)業(yè)的人做專(zhuān)業(yè)的事兒。

面向前端需求的代碼,不僅僅是 html 文件,也包括了面向渲染優(yōu)化的數(shù)據(jù)聚合代碼。前端的職能是為用戶(hù)開(kāi)發(fā)出體驗(yàn)更好的 GUI 應(yīng)用,有助于這個(gè)目標(biāo)的所有合理的技術(shù)手段都在其職能范圍之內(nèi)。

讓 SSR 服務(wù)從后端團(tuán)隊(duì)轉(zhuǎn)到前端團(tuán)隊(duì),是為了得到面向前端需求的界面渲染優(yōu)化空間。讓更加理解渲染優(yōu)化原理的前端團(tuán)隊(duì),可以在 SSR 服務(wù)上應(yīng)用新的渲染優(yōu)化措施,包括但不限于 Streaming SSR、Suspense、Selective Hydration、Server Component 等技術(shù)可以得以應(yīng)用。它們可以顯著提升頁(yè)面的各項(xiàng)性能指標(biāo),令用戶(hù)更早看到內(nèi)容,更早看到有意義的內(nèi)容,更早跟界面自由交互。

讓 BFF 服務(wù)從后端團(tuán)隊(duì)轉(zhuǎn)到前端團(tuán)隊(duì),則是為了獲得面向前端需求的數(shù)據(jù)聚合優(yōu)化空間,可以提高代碼的跨端復(fù)用率、減少前端應(yīng)用的代碼體積、減少前端應(yīng)用的加載時(shí)間,提升用戶(hù)體驗(yàn)。

當(dāng)研發(fā)團(tuán)隊(duì)完成了 SSR 和 BFF 兩個(gè)階段的前后端分離后,前端團(tuán)隊(duì)同時(shí)掌握了 SSR 和 BFF 兩重優(yōu)化空間。他們既不需要讓后端團(tuán)隊(duì)幫忙添加 id 和 class 到 html 文件中,也不需要讓他們幫忙把某個(gè)文案添加到某個(gè)接口里。面向前端的渲染優(yōu)化需求和數(shù)據(jù)優(yōu)化需求,都能在前端團(tuán)隊(duì)職責(zé)范圍內(nèi)自主解決,顯著減少前后端的溝通頻次和成本,分離彼此的關(guān)注度,提升雙方的專(zhuān)業(yè)聚焦水平。

更重要的是,SSR 和 BFF 在渲染優(yōu)化上有著不可分割的關(guān)系。視圖的渲染不是憑空的,它依賴(lài)數(shù)據(jù)的準(zhǔn)備,有意義的內(nèi)容才得以呈現(xiàn);當(dāng)視圖需要流式渲染,數(shù)據(jù)請(qǐng)求也需要做相應(yīng)配合。假設(shè)我們頁(yè)面的首屏所依賴(lài)的數(shù)據(jù),都被聚合到一個(gè)單一接口里,其后果便是 Streaming SSR 無(wú)法發(fā)揮充分價(jià)值;所有組件都在等待單一聚合接口的響應(yīng)數(shù)據(jù),才開(kāi)始進(jìn)行渲染。

圖片

如上圖所示,紫色箭頭表示“時(shí)間”,粉色框?yàn)閿?shù)據(jù)獲取,橙色框?yàn)榻M件渲染,綠色框?yàn)榘l(fā)送 HTML 到瀏覽器。我們可以看到,樸素的 SSR 是一個(gè)串行過(guò)程,組件渲染階段需要等待數(shù)據(jù)獲取全部結(jié)束。而充分的 Streaming SSR 則有多次數(shù)據(jù)獲取的并發(fā)任務(wù)(調(diào)用了多次后端接口),組件渲染并不需要等待所有數(shù)據(jù)獲取任務(wù)結(jié)束,只需相關(guān)數(shù)據(jù)就位即可進(jìn)入組件渲染及后續(xù)發(fā)送 HTML 的流程。用戶(hù)可以更早看到內(nèi)容。

在前端團(tuán)隊(duì)缺乏 BFF 掌控能力的情況下,他們無(wú)法自主控制 SSR 的數(shù)據(jù)獲取過(guò)程,只能被動(dòng)接受后端提供的接口。即便視圖層框架(如 React)支持 Streaming rendering,也僅僅是把一大塊 html 分多次發(fā)送給瀏覽器,它仍受制于 data-fetching 的阻塞時(shí)間,無(wú)法做到充分的 Streaming SSR。

因此,技術(shù)層面更合理的做法是,每個(gè)組件描述它自身所需的數(shù)據(jù)依賴(lài),頁(yè)面渲染時(shí)遇到?jīng)]有數(shù)據(jù)依賴(lài)的靜態(tài)組件,立即發(fā)送給用戶(hù),遇到有數(shù)據(jù)依賴(lài)的組件則發(fā)起請(qǐng)求(Render-As-You-Fetch),不同組件可以獨(dú)立發(fā)起不同請(qǐng)求,每個(gè)組件數(shù)據(jù)請(qǐng)求完畢后即刻開(kāi)始自身渲染,最終得到頁(yè)面流式渲染,用戶(hù)漸進(jìn)式地看到一塊塊成形的界面渲染的效果。

我們可以看到,在這個(gè)渲染優(yōu)化需求下,傳統(tǒng)意義上的數(shù)據(jù)聚合思路(盡可能少的接口)反而是不利的;微服務(wù)式的多個(gè)接口,反而是有利的。當(dāng)然,這不意味著前端直接對(duì)接微服務(wù)接口,不需要 BFF;這其實(shí)意味著我們需要 Streaming BFF 去解放 Streaming SSR 的潛力。

總的而言,徹底的前后端分離是指前端掌握了面向渲染優(yōu)化的充分條件,包括 SSR 和 BFF 兩個(gè)彼此緊密關(guān)聯(lián)的優(yōu)化空間,缺少任意一個(gè),都難以獲得充分的渲染優(yōu)化能力。從這個(gè)角度來(lái)看,前端團(tuán)隊(duì)開(kāi)發(fā) BFF 是一個(gè)未來(lái)的技術(shù)方向。研發(fā)團(tuán)隊(duì)的分工模式隨著技術(shù)發(fā)展,將從樸素模式逐漸轉(zhuǎn)向解耦模式。

四、解耦模式的 BFF 技術(shù)選型

解耦模式的 BFF 服務(wù)由前端團(tuán)隊(duì)開(kāi)發(fā),所用的編程語(yǔ)言也由前端團(tuán)隊(duì)決定。大部分情況下前端團(tuán)隊(duì)會(huì)采用相同的編程語(yǔ)言(JavaScript/TypeScript),基于 Node.js 運(yùn)行時(shí)開(kāi)發(fā)相應(yīng)的 BFF 服務(wù)?;谶@個(gè)前提,我們討論幾種技術(shù)選型。

4.1 RESTful API

處于模仿階段的前端團(tuán)隊(duì),往往會(huì)采用 RESTful API 方式實(shí)現(xiàn) BFF 服務(wù)。技術(shù)目標(biāo)是把后端之前做但現(xiàn)在不做的功能,用同樣的方式和思路讓前端團(tuán)隊(duì)用Node.js 實(shí)現(xiàn)一遍。

圖片實(shí)現(xiàn) BFF 服務(wù)的前端開(kāi)發(fā)人員,其心智模型跟普通后端無(wú)異,涉及 URL,HttpRequest, HttpResponse, RequestHeader, Query, Body, Cookie, Authorization, CORS,Service, Controller 等等。

然而,即便是現(xiàn)在,掌握成熟后端接口開(kāi)發(fā)能力的前端開(kāi)發(fā)人員,依舊是稀缺的。所以,往往這種方式開(kāi)發(fā)的 BFF 服務(wù)的質(zhì)量,劣于專(zhuān)業(yè)后端開(kāi)發(fā)的,并且?guī)缀醪豢紤]面向前端的極致優(yōu)化,能滿(mǎn)足需求已經(jīng)達(dá)到了目標(biāo)。

此外,從語(yǔ)義角度看,RESTful API 是面向資源的,跟面向前端需求的 BFF 場(chǎng)景并不契合。很多時(shí)候,前端需要的數(shù)據(jù)并不是后端資源的直接映射,而是經(jīng)過(guò)聚合、轉(zhuǎn)換、過(guò)濾、排序等處理后的結(jié)果。

因此,一開(kāi)始基于 RESTful API 開(kāi)發(fā)的 BFF 服務(wù),最終將有意或無(wú)意、主動(dòng)或被動(dòng)地演變成只有一半功能的 RPC 服務(wù)。它的 url 參數(shù)設(shè)計(jì)是函數(shù)語(yǔ)義,而非資源語(yǔ)義,但調(diào)用這些遠(yuǎn)程函數(shù)時(shí)仍然要考慮 server-client 之間底層通訊細(xì)節(jié)。既沒(méi)有被封裝,也缺少優(yōu)化。

4.2 GraphQL

GraphQL是一個(gè)面向前端數(shù)據(jù)訪(fǎng)問(wèn)優(yōu)化的數(shù)據(jù)抽象層,它相當(dāng)適合作為 BFF 技術(shù)選型,并且也是我們之前包括現(xiàn)在仍在使用的 BFF 方案。

圖片

它的技術(shù)特點(diǎn)是,在數(shù)據(jù)訪(fǎng)問(wèn)層實(shí)現(xiàn)了一定的控制反轉(zhuǎn)(Inversion of control,IOC)的能力。

GraphQL 服務(wù)的開(kāi)發(fā)者負(fù)責(zé)構(gòu)建一個(gè)數(shù)據(jù)網(wǎng)絡(luò)結(jié)構(gòu)(Graph),支持其消費(fèi)者根據(jù)自身需求編寫(xiě) GraphQL Query 語(yǔ)句查詢(xún)所需 JSON 數(shù)據(jù)(Tree)。這種靈活的查詢(xún)能力,實(shí)際上是將前后端數(shù)據(jù)交互相關(guān)的代碼分成了兩部分,一部分是關(guān)于通用性的,放在數(shù)據(jù)提供方(GraphQL 服務(wù))里,一部分是關(guān)于特殊性的,放在數(shù)據(jù)消費(fèi)方(前端應(yīng)用的查詢(xún)語(yǔ)句)里。

如此,GraphQL BFF 可以將按終端尺寸分類(lèi)的多個(gè) BFF 整合成一個(gè) BFF。從之前Mobile BFF 和 PC BFF,變成統(tǒng)一的GraphQL BFF。減少了兩個(gè) BFF 之間重復(fù)的部分。

  • Mobile BFF = GraphQL BFF + Mobile GraphQL Query
  • PC BFF = GraphQL BFF + PC GraphQL Query

通過(guò)統(tǒng)一的 GraphQL-BFF 配合差異的 *-GraphQL-Query 實(shí)現(xiàn)了之前多個(gè)         BFF服務(wù)提供的數(shù)據(jù)訪(fǎng)問(wèn)能力。

前端團(tuán)隊(duì)開(kāi)發(fā) GraphQL BFF 應(yīng)用時(shí),其心智模型不再是純粹的后端概念,而是 GraphQL 相關(guān)的概念,如 Schema, Query, Mutation, Resolver,DataLoader, Directive, SelectionSet 等等,它們更加利于前后端數(shù)據(jù)訪(fǎng)問(wèn)的優(yōu)化。更多詳情可以閱讀另一篇文章《GraphQL-BFF:微服務(wù)背景下的前后端數(shù)據(jù)交互方案》。

4.3 RPC

RPC 是指 Remote Procedure Call,即遠(yuǎn)程過(guò)程調(diào)用。這個(gè)模式也適用于 BFF 服務(wù)的實(shí)現(xiàn)。

圖片

它的技術(shù)特征是,將實(shí)現(xiàn)端和調(diào)用端之間的通訊過(guò)程封裝,作為技術(shù)細(xì)節(jié)隱藏起來(lái),并不暴露給調(diào)用者。對(duì)于調(diào)用者而言,仿佛像調(diào)用本地函數(shù)。

正因如此,它不要求調(diào)用一次 RPC 函數(shù)即發(fā)起一次獨(dú)立的通訊過(guò)程。它可以將多次 RPC 函數(shù)批量化(Batching)并流式響應(yīng)(Streaming),進(jìn)一步減少反復(fù)重建通訊過(guò)程的開(kāi)銷(xiāo)。

前端團(tuán)隊(duì)基于 RPC 模式開(kāi)發(fā) BFF時(shí),其心智模型跟開(kāi)發(fā)傳統(tǒng)后端服務(wù)不同。所謂的接口設(shè)計(jì),被轉(zhuǎn)化為函數(shù)參數(shù)設(shè)計(jì)和返回值設(shè)計(jì),沿襲了前端開(kāi)發(fā)者熟悉的標(biāo)準(zhǔn)編程語(yǔ)言特性。相比 RESTful API 和 GraphQL 而言,RPC 引入更少概念和領(lǐng)域知識(shí)要求。

4.4 GraphQL VS RPC

我們團(tuán)隊(duì)從四年前開(kāi)始使用 GraphQL-BFF,并成功落地到多個(gè)項(xiàng)目,取得了不錯(cuò)的效果。我們看到了它帶來(lái)的好處和價(jià)值,同時(shí)也發(fā)現(xiàn)了它當(dāng)前的一些局限性。為了突破桎梏,爭(zhēng)取到更廣闊的優(yōu)化空間,我們開(kāi)始探索 RPC-BFF 方案,并試圖克服 GraphQL-BFF 方案未能解決的問(wèn)題。

值得提醒的是,本文提到的 GraphQL-BFF 面臨的難題,是在精益求精的層面上的探討,并非否定和質(zhì)疑 GraphQL 作為 BFF 方案的合理性。

GraphQL-BFF 的第一個(gè)難題是類(lèi)型問(wèn)題。GraphQL 是一個(gè)跨編程語(yǔ)言的數(shù)據(jù)抽象層,它自帶了一套 Type System。使用具體某個(gè)帶類(lèi)型的編程語(yǔ)言(如 TypeScript)開(kāi)發(fā) GraphQL 服務(wù)時(shí),就存在兩個(gè)類(lèi)型系統(tǒng),因此難免有一些語(yǔ)言特性無(wú)法對(duì)齊以及類(lèi)型重復(fù)定義的地方。

圖片

比如,GraphQL 的類(lèi)型系統(tǒng)支持代數(shù)數(shù)據(jù)類(lèi)型(Algebraic data type),可以用 union 定義 A 或 B 的類(lèi)型關(guān)系。這在 Rust和 TypeScript 中有不錯(cuò)的支持,在 Java 或 Go 中還沒(méi)有直接的支持。然而即便是 TypeScript,很多類(lèi)型聲明也得在 GraphQL 和 TypeScript 中分別定義一份。

此外,GraphQL 的 Client-side 類(lèi)型問(wèn)題也是一個(gè)挑戰(zhàn)。由于 GraphQL 服務(wù)的返回值取決于發(fā)送過(guò)來(lái)的查詢(xún)語(yǔ)句,因此其響應(yīng)的數(shù)據(jù)類(lèi)型不是固定的,而是隨著查詢(xún)語(yǔ)句代碼的修改而變化的。

盡管 GraphQL 由于提供了內(nèi)省機(jī)制(Introspection),可以構(gòu)建出專(zhuān)門(mén)根據(jù) GraphQL Schema + GraphQL Query 生成 TypeScript 類(lèi)型定義的 Code Generator 工具。但其中包含很多手動(dòng)處理或復(fù)雜的工程化配置環(huán)節(jié)。

開(kāi)發(fā)者有可能需要手動(dòng)從 TypeScript 代碼里復(fù)制出 GraphQL Query 語(yǔ)句,在 GraphQL Code Generator 工具里生成 TypeScript 類(lèi)型后,復(fù)制該類(lèi)型定義到項(xiàng)目中,然后傳入 GraphQL Client 調(diào)用函數(shù)標(biāo)記其返回值類(lèi)型。

或者像 Facebook/Meta 公司推出的 Relay 框架那樣,實(shí)現(xiàn)一個(gè) Compiler 去掃描代碼里的 GraphQL Query 語(yǔ)句,自動(dòng)生成類(lèi)型到指定目錄,讓開(kāi)發(fā)者可以直接使用。這塊對(duì)研發(fā)團(tuán)隊(duì)的技術(shù)能力和工程化水平有較高要求。

GraphQL-BFF 的第二個(gè)難題是,缺乏 Streaming 優(yōu)化支持。當(dāng)前的 GraphQL 數(shù)據(jù)響應(yīng),是由查詢(xún)語(yǔ)句中最慢的節(jié)點(diǎn)決定的,尚未支持已 resolved 的節(jié)點(diǎn)提前返回給調(diào)用端消費(fèi)的能力。

雖然 GraphQL 有 @stream/@defer 相關(guān)的 RFC,但目前并未進(jìn)入 GraphQL 規(guī)范中,也未在相關(guān) GraphQL 庫(kù)中得到實(shí)現(xiàn)或推廣。

GraphQL-BFF 的第三個(gè)難題是缺少 Client-side Data-Loader 優(yōu)化支持。

在 Server-side 的GraphQL 有相關(guān) Data-Loader,支持在一次查詢(xún)請(qǐng)求處理過(guò)程中,相同資源的訪(fǎng)問(wèn)可以被去重。但是在調(diào)用端,一次 GraphQL Query 就對(duì)應(yīng)著一次 Http 請(qǐng)求與響應(yīng)。多次 GraphQL Query 很難被自動(dòng) merge 和 batching,遑論 streaming 優(yōu)化。

如前所述,為了渲染上的進(jìn)一步優(yōu)化,前端組件實(shí)踐的流行趨勢(shì)是,每個(gè)組件可以將自己的數(shù)據(jù)需求定義在自己的組件代碼中,而非托管給父級(jí)組件。如此,可以方便組件自身做 streaming 優(yōu)化;當(dāng)它自身的數(shù)據(jù)已經(jīng)獲取完畢,它可以先行渲染,不必等待。

相關(guān) GraphQL Client(如Relay)的處理辦法是,讓視圖組件用 GraphQL Fragment 而非 GraphQL Query 去表達(dá)數(shù)據(jù)需求。通過(guò)編譯器處理后,它們將可以提取到父級(jí)組件或者根組件里合并為 GraphQL Query。實(shí)現(xiàn)編寫(xiě)時(shí)在組件內(nèi),運(yùn)行時(shí)托管在父級(jí)組件中獲取數(shù)據(jù)。

圖片

這種策略在實(shí)踐上是可行的,然而既有較高的工程化門(mén)檻,難以普遍推廣,又不是 GraphQL 規(guī)范所定義的標(biāo)準(zhǔn)行為,甚至需要額外自定義很多指令以達(dá)到目標(biāo)(如  Relay 框架的 @argument, @argumentDefinitions 等),這進(jìn)一步損害了它的易用性。

我們需要的一種 BFF 技術(shù)方案是:

  • 支持使用標(biāo)準(zhǔn)的語(yǔ)言特性解決問(wèn)題
  • BFF 實(shí)現(xiàn)端類(lèi)型定義不必編寫(xiě)兩份
  • BFF 調(diào)用端可無(wú)縫復(fù)用 BFF 實(shí)現(xiàn)端的定義,不必重復(fù)定義
  • 支持 Client-side Data-Loader 機(jī)制,可以將客戶(hù)端的多個(gè)數(shù)據(jù)訪(fǎng)問(wèn)調(diào)用自動(dòng)
  •     merging
  •     batching
  •     streaming

RPC-BFF 技術(shù)方案,可以滿(mǎn)足上述目標(biāo)。

五、RPC-BFF 的技術(shù)選型

基于 RPC 實(shí)現(xiàn) BFF 的思路和方案,也有很多種選擇。

5.1 gRPC

gRPC 是一個(gè)非常優(yōu)秀的 RPC 技術(shù)方案,但它跟 GraphQL 一樣是跨編程語(yǔ)言的,需要額外使用一種 DSL 定義類(lèi)型(Interface Definition Language,IDL),因此有類(lèi)似的重復(fù)定義類(lèi)型的問(wèn)題,也未對(duì)前端常用語(yǔ)言(如TypeScript)做充分的針對(duì)性?xún)?yōu)化,并且它主要服務(wù)于分布式系統(tǒng)這類(lèi) server-to-server 的調(diào)用,對(duì) client/ui-to-server 的 BFF 場(chǎng)景沒(méi)有特殊處理。

在很多基于 gRPC 的 BFF 實(shí)踐中,BFF 跟背后的領(lǐng)域服務(wù)之間是 RPC 模式,BFF 跟前端之間則回到 RESTful API 模式。而我們所謂的 RPC-BFF,其實(shí)是指前端和 BFF 之間是 RPC 模式的調(diào)用關(guān)系。

5.2 tRPC

tRPC 的設(shè)計(jì)目的則跟我們的目標(biāo)更加貼合,它是基于 TypeScript 實(shí)現(xiàn)的端到端類(lèi)型安全方案(End-to-end type-safe APIs)。

然而,tRPC 的技術(shù)實(shí)現(xiàn)方式,跟我們的需求場(chǎng)景卻不契合。

tRPC 跟前文的 Relay 框架某種意義上是兩個(gè)極端。Relay 框架完全依賴(lài)它的 Relay Compiler 去掃描代碼,充分分析和收集代碼里的 GraphQL 配置,有很多編譯、構(gòu)建和代碼生成的環(huán)節(jié)。而 tRPC 則相反,它目前沒(méi)有構(gòu)建、編譯和代碼生成的步驟,正如其官方文檔里所言:

tRPC has no build or compile steps, meaning no code generation, runtime bloat or build step.

tRPC 假設(shè)了開(kāi)發(fā)者的項(xiàng)目是全棧項(xiàng)目(full-stack application),或者前后端代碼都在一個(gè)倉(cāng)庫(kù)。

圖片

因此,前端項(xiàng)目可以直接 import 后端項(xiàng)目里的 TypeScript 類(lèi)型。

如果我們構(gòu)建的 BFF 項(xiàng)目,不只為了一個(gè)前端項(xiàng)目服務(wù),而是多個(gè)前端項(xiàng)目共用的,涉及跨端、跨語(yǔ)言、跨團(tuán)隊(duì)等復(fù)雜組合。使用 tRPC 時(shí),可能就得采取 Monorepo 模式,把 BFF 項(xiàng)目及其所有調(diào)用方的代碼項(xiàng)目,放到一個(gè) Git 倉(cāng)庫(kù)中管理。這將顯著地增加多個(gè)團(tuán)隊(duì)之間的 Git 工作流、CI/CD、研發(fā)和部署流程等多方面的協(xié)調(diào)問(wèn)題,要求處理新的工程化復(fù)雜度(Monorepo),能滿(mǎn)足這個(gè)條件的團(tuán)隊(duì)不多。

我們的場(chǎng)景所期望的技術(shù)方案是,允許 BFF 項(xiàng)目及各個(gè)下游前端項(xiàng)目在不同倉(cāng)庫(kù)中管理,它們無(wú)法直接 import BFF 項(xiàng)目的類(lèi)型,不滿(mǎn)足 tRPC 的項(xiàng)目假設(shè)。

5.3 DeepKit

DeepKit 是一個(gè)富有野心的項(xiàng)目,它在 TypeScript 基礎(chǔ)上增加了很多特性,力圖打造一個(gè)更加完備的 TypeScript 后端基礎(chǔ)設(shè)施。

圖片

它也有 RPC 模塊的部分,但出于以下幾個(gè)原因,最后未被我們選擇。

第一個(gè)原因是,DeepKit 的 RPC 實(shí)踐方式跟 tRPC 有類(lèi)似的項(xiàng)目結(jié)構(gòu)假設(shè)。

圖片

如上所示,rpc server 和 rpc client 之間需要有個(gè)共享的接口,一個(gè)負(fù)責(zé)實(shí)現(xiàn)該接口,一個(gè)負(fù)責(zé)消費(fèi)該接口。這也要求前后端需要放到一個(gè)倉(cāng)庫(kù)中?;蛘卟捎?npm 包這類(lèi)更低效的代碼共享途徑,對(duì)庫(kù)和框架這種變動(dòng)比較不頻繁的場(chǎng)景來(lái)說(shuō)是合適的,對(duì)業(yè)務(wù)迭代這種更新頻率則難以接受。

另一個(gè)原因則是,選擇 DeepKit 并不是選擇一個(gè)庫(kù)或者框架這種小決策,它從編程語(yǔ)言開(kāi)始侵入,然后到運(yùn)行時(shí)以及庫(kù)和框架等方方面面。有較強(qiáng)的 Vendor lock-in 風(fēng)險(xiǎn),一個(gè)項(xiàng)目要從 DeepKit 中遷移到另一個(gè)技術(shù)相當(dāng)困難。

團(tuán)隊(duì)需要下很大的決心才敢押注 DeepKit 選型,以 DeepKit 目前的完成度和流行度,還無(wú)法支撐我們做出這個(gè)決定。

5.4 自研 RPC-BFF

如前所述,我們深入分析了 RPC BFF 的優(yōu)勢(shì),以及考察了多個(gè)不同的技術(shù)選型。有的過(guò)于龐大、過(guò)分復(fù)雜,有的則過(guò)于簡(jiǎn)單、過(guò)于局限。

但這些項(xiàng)目也啟發(fā)了我們的自研方向,幫助我們從簡(jiǎn)單到復(fù)雜的光譜中,根據(jù)自身實(shí)際需求找到一個(gè)平衡點(diǎn),可以用盡可能低的研發(fā)成本、盡可能小的侵入性、盡可能少的項(xiàng)目結(jié)構(gòu)要求,實(shí)現(xiàn) RPC-BFF 模式。

也就是說(shuō),我們既不要像 DeepKit 和 Relay 那樣,從完整的編譯器乃至編程語(yǔ)言層面切入到代碼生成和運(yùn)行時(shí),它們或許有更大的目標(biāo)和野心,配得上如此高昂的技術(shù)成本和實(shí)現(xiàn)難度,但對(duì)純粹的 BFF 場(chǎng)景而言可能過(guò)猶不及。同時(shí)也不要像 tRPC 那樣完全沒(méi)有代碼生成,而是選擇一個(gè)最小化代碼生成的路線(xiàn),滿(mǎn)足 RPC-BFF 這一聚焦場(chǎng)景的需求。

六、自研 RPC-BFF 的設(shè)計(jì)與實(shí)現(xiàn)

6.1 RPC-BFF 的設(shè)計(jì)思路

RPC-BFF 可以看作樸素 BFF 的拓展增強(qiáng)版。在樸素 BFF 中,后端在最簡(jiǎn)陋的情況下只為前端提供了數(shù)據(jù)。

而 RPC-BFF 則需要做更多:提供數(shù)據(jù)、提供類(lèi)型以及提供代碼。

圖片

如上圖所示,RPC-BFF 既是數(shù)據(jù)提供者(Data Provider),也是類(lèi)型提供者(Type Provider),還是代碼提供者(Code Provider)。前端不必重復(fù)定義 BFF 響應(yīng)的數(shù)據(jù)類(lèi)型,也不必親自構(gòu)造 Http 請(qǐng)求處理通訊問(wèn)題。

要做到這些功能,同時(shí)又不能損害易用性,需要極致地挖掘 TypeScript 的類(lèi)型表達(dá)能力。

圖片

上圖為 RPC-BFF 架構(gòu)圖示意,其中存在四種顏色,分別的含義如下:

  • 服務(wù)端運(yùn)行時(shí)(server-runtime)為粉色,服務(wù)器代碼在此運(yùn)行
  • 類(lèi)型編譯時(shí)(compile-time)為藍(lán)色,類(lèi)型檢查在此進(jìn)行
  • CLI 運(yùn)行時(shí)(cli-runtime)為黃色,本地開(kāi)發(fā)階段使用的命令行工具
  • 客戶(hù)端運(yùn)行時(shí)(client-runtime)為紫色,前端代碼在此運(yùn)行

該 RPC-BFF 架構(gòu)設(shè)計(jì)的核心在于Schema 部分,它是一切的基礎(chǔ)。我們可以看到,Schema 有兩條箭頭,一條為 type infer,一條為 to JSON。也就是說(shuō),Schema 既作用于類(lèi)型(type)所在的編譯時(shí)(compile-time),也作用于值(value)所在的運(yùn)行時(shí)(runtime)。

當(dāng) BFF 端的代碼經(jīng)過(guò)編譯,類(lèi)型信息被編譯器抹除后,我們?nèi)钥梢栽谶\(yùn)行時(shí)訪(fǎng)問(wèn)到 JSON 數(shù)據(jù)結(jié)構(gòu)表示的 Schema。

通過(guò)這種機(jī)制,我們的 RPC-BFF 像 GraphQL 那樣支持內(nèi)省特性(introspection)。在開(kāi)發(fā)階段,前端可以通過(guò)本地 CLI 工具向 RPC-BFF 發(fā)起內(nèi)省請(qǐng)求(Introspection Request)拿到 JSON 形式的 Schema 結(jié)構(gòu),然后通過(guò) Code Generator 生成前端所需的 Client 類(lèi)型和 Client 代碼。

如此,我們既不需要用編譯器去掃描和分析服務(wù)端代碼以提取類(lèi)型,也不需要用編譯器去掃描和分析前端代碼去生成類(lèi)型。對(duì)于 RPC-BFF 來(lái)說(shuō),不需要掌握到圖靈完備的編程語(yǔ)言的所有信息才能工作,只需要掌握RPC 函數(shù)列表及其輸入和輸入類(lèi)型結(jié)構(gòu)即可。

6.2 RPC-BFF Schema 設(shè)計(jì)與實(shí)現(xiàn)

Schema 是一段特殊的代碼,它介于 Type 和 Program 之間,面向特定領(lǐng)域保留其所需的元數(shù)據(jù)性質(zhì)的配置結(jié)構(gòu)。Schema 往往比類(lèi)型復(fù)雜,但比一般意義上的程序簡(jiǎn)單。

不管是 tRPC 還是 GraphQL 都包含 Schema 性質(zhì)的要素。然而,對(duì)于 RPC-BFF 的場(chǎng)景來(lái)說(shuō),它們分別都有能力的缺失。

tRPC 中的 Schema 是只起到了 Validator 和 Type-infer 的作用,而沒(méi)有 Introspection 機(jī)制。

圖片

如上所示,tRPC 自身沒(méi)有實(shí)現(xiàn) schema 部分,但可以從開(kāi)源社區(qū)的多個(gè) schema-based validator 庫(kù)中選擇一個(gè)它目前支持的(如 zod),從而得到在 server runtime 對(duì)客戶(hù)端傳遞進(jìn)來(lái)的參數(shù)驗(yàn)證,以及在開(kāi)發(fā)階段提供 type-infer 功能。

而 GraphQL 的情況則復(fù)雜一些,它分為 Schema-first 和 Code-first 兩類(lèi)實(shí)踐方式。

圖片

Schema-first GraphQL 實(shí)踐如上圖所示,GraphQL Schema 以字符串的形式出現(xiàn)在 TypeScript/JavaScript 代碼中,它是 DSL 形態(tài),要復(fù)用 host language(如 TypeScript)的類(lèi)型系統(tǒng)相當(dāng)困難。

圖片

Code-first/Code-only GraphQL 則如上圖所示,它是 eDSL 形態(tài),即嵌入式領(lǐng)域特定語(yǔ)言(Embedded domain specific language),它可以基于 host language 的API 以程序的方式而非字符串的方式,創(chuàng)建出 GraphQL Schema。因此,這種模式下 GraphQL 相當(dāng)于嵌入到 TypeScript 中,它有機(jī)會(huì)利用 TypeScript 的類(lèi)型推導(dǎo)(type infer)能力,反推出 TypeScript 類(lèi)型;也能夠在運(yùn)行時(shí) stringify 為 DSL 形態(tài)的 GraphQL Schema。

可以說(shuō),這種實(shí)踐方式的 GraphQL 跟 host language 的整合度更高,某種程度上是更靈活的,盡管犧牲了 DSL 那種直觀性。

然而,目前幾乎所有 Code-first/Code-only 的 GraphQL 庫(kù)的 TypeScript 類(lèi)型支持程度都有很大的不足。特別是對(duì) GraphQL 這種類(lèi)型之間遞歸結(jié)構(gòu)特別頻繁的技術(shù)來(lái)說(shuō),其類(lèi)型推導(dǎo)的技術(shù)挑戰(zhàn)遠(yuǎn)大于zod 等樸素 schema-based validator 庫(kù)。

即便是 zod 這類(lèi)更簡(jiǎn)單的場(chǎng)景,對(duì)遞歸類(lèi)型也沒(méi)有充分支持。

You can define a recursive schema in Zod, but because of a limitation of TypeScript, their type can't be statically inferred. Instead you'll need to define the type definition manually, and provide it to Zod as a "type hint".

如 zod 官方文檔所述,當(dāng)我們的 schema 中存在遞歸,type-infer 就受到了限制,需要更繁瑣的方式去自行拼裝出遞歸類(lèi)型。

const baseCategorySchema = z.object({
 name: z.string(),
});

type Category = z.infer<typeof baseCategorySchema> & {
  subcategories: Category[];
};

const categorySchema: z.ZodType<Category> = baseCategorySchema.extend({
  subcategories: z.lazy(() =>categorySchema.array()),
})

如上所示,它需要先用 schema 方式定義遞歸類(lèi)型(Category)中非遞歸的部分,然后用 type-infer 在 type-level 定義遞歸部分的類(lèi)型,最后回到 schema-level 中顯式類(lèi)型標(biāo)注,定義遞歸 schema。

它在 schema-level 和 type-level 中來(lái)回穿梭。每一個(gè)遞歸字段都要求拆成上述 3 個(gè)部分,其工程上的易用性缺乏保障,其代碼也缺乏可讀性。有多少讀者能輕易看出上面的復(fù)雜 schema 是為了定義下面這幾行代碼?

type Category = {
  name: string
    subcategories: Category[]
}

回到 GraphQL,我們現(xiàn)在能夠看到,它在 Validator 和 Introspection 特性上有良好的支持,可以在 server runtime 驗(yàn)證參數(shù)結(jié)構(gòu)和返回值,也可以通過(guò)內(nèi)省請(qǐng)求曝露出schema 結(jié)構(gòu),但在 type-infer 上仍有一些難以攻克的挑戰(zhàn)存在。

RPC-BFF 的 Schema 需要同時(shí)滿(mǎn)足 Validator,Type-infer 和 Introspection 三個(gè)能力,現(xiàn)有方案并不滿(mǎn)足,因此我們通過(guò)自研 Schema方案實(shí)現(xiàn)了它們。

下面是一段定義 User 類(lèi)型的TypeScript 代碼:

type UserType = {
     id: string;
    name: string;
    age: number;
}

const user: UserType = {
    id: 'user_id_01',
    name: 'Jade',
    age: 18
}

經(jīng)過(guò)編譯后,在運(yùn)行時(shí)執(zhí)行的 JavaScript 代碼如下:

"use strict";
const user = {
 id: 'user_id_01',
    name: 'Jade',
    age: 18
};

類(lèi)型信息被抹去,在運(yùn)行時(shí)無(wú)法獲取。通過(guò) RPC-BFF Schema 重新定義 User 結(jié)構(gòu)如下:

圖片

我們用 ObjectType 定義了User Schema,用 TypeOf 推導(dǎo)出UserType,經(jīng)過(guò) TypeScript 編譯后,JavaScript 代碼如下所示:

import { ObjectType } from '@ctrip/rpc-bff';

class User extends ObjectType {
 constructor() {
        super(...arguments);
        this.id = String;
        this.name = String;
        this.age = Number;
    }
}
const user = {
    id: 'user_id_01',
    name: 'Jade',
    age: 18
};

我們可以看到,由 ObjectType 定義的 User Schema 在運(yùn)行時(shí)也得到了保留,因此我們可以基于這些信息,實(shí)現(xiàn)在運(yùn)行時(shí)的 Validation 和 Introspection 功能。

此外,我們的 RPC-BFF Schema 技術(shù)還克服了 zod 等庫(kù)未能解決的遞歸類(lèi)型定義問(wèn)題:

圖片

我們無(wú)需為了支持遞歸而人為拆分類(lèi)型,可以直觀地定義出上述的 Category 結(jié)構(gòu),并支持靜態(tài)類(lèi)型推導(dǎo)。其秘訣在于利用 TypeScript 中 class 聲明具備的獨(dú)特性質(zhì)—— value & type 二象性。

當(dāng)我們用 class 聲明一個(gè)結(jié)構(gòu)時(shí),它同時(shí)也定義了:

  • Constructor 函數(shù)值
  • Instance Type 實(shí)例類(lèi)型

圖片

如上圖所示,左邊箭頭的 Test 是一個(gè)實(shí)例類(lèi)型(Instance Type),右邊箭頭的則是類(lèi)的構(gòu)造器函數(shù)(Constructor)。

目前就我們所知,只在 class 聲明場(chǎng)景下 TypeScript 對(duì)遞歸 Schema 有良好的類(lèi)型推導(dǎo)支持。因此,如果 zod 或者 Code-first GraphQL 庫(kù)想要支持遞歸 Schema 結(jié)構(gòu),它們的 Schema API 可能需要大改,變成我們上面演示的 RPC-BFF Schema 的 API 風(fēng)格。

盡管我們掌握了在 Validator, Type-infer 和 Introspection 能力上更完備的 Schema 技術(shù),滿(mǎn)足了 Code-first GraphQL 的技術(shù)要求,但也僅限于 server 端的情況,在 client 端的類(lèi)型,仍需要引入復(fù)雜的編譯技術(shù)掃描前端代碼庫(kù)里的 GraphQL Query 片段以生成類(lèi)型。這不是我們期望的。

因此,我們目前先將這種技術(shù)應(yīng)用于更簡(jiǎn)單的 RPC-BFF 場(chǎng)景,未來(lái)也不排除支持 GraphQL。

6.3 定義 RPC-BFF 函數(shù)

有了 RPC-BFF Schema 之后,我們就可以用它來(lái)定義 RPC-BFF 函數(shù)了。

在純 TypeScript 的代碼里,定義函數(shù)的 input 和 output 是這樣的:

// 定義 input
type HelloInput = {
  name: string
}

// 定義 output
type HelloOutput = {
  message: string
}

type HelloFunction = (input: HelloInput) => HelloOutput

然后實(shí)現(xiàn)滿(mǎn)足該函數(shù)類(lèi)型的代碼:

const hello: HelloFunction = ({ name })=> {
  return {
    message: `Hello ${name}!`,
  }
}

在 RPC-BFF 里,我們只是換了一種方式去定義 input 和 output。

去掉注釋?zhuān)⑶乙?ObjectType,前面的 hello 函數(shù)就變成了這樣:

import { Api, ObjectType } from '@ctrip/rpc-bff'


class HelloInput extends ObjectType {
  name = String
}

class HelloOutput extends ObjectType {
  message = String
}

export const hello = Api(
  {
    input: HelloInput,
    output: HelloOutput,
  },
  async ({ name }) => {
    return {
      message: `Hello ${name}!`,
    }
  },
)

可以看到,它跟我們純 TypeScript 的結(jié)構(gòu)幾乎是一樣的。

6.4 RPC 函數(shù)和普通函數(shù)的區(qū)別

RPC 函數(shù)和普通函數(shù)的區(qū)別在于,RPC函數(shù)的 input 和 output 都是 value,而普通函數(shù)的 input 和 output 都是 type。

type 會(huì)在編譯時(shí)被擦除,而value 會(huì)在運(yùn)行時(shí)被保留。所以,RPC 函數(shù)的 input和 output 需要是 value,它們?cè)谶\(yùn)行時(shí)是可以被訪(fǎng)問(wèn)到的。這樣可以為 RPC-BFF client 生成類(lèi)型代碼和調(diào)用代碼。

盡管我們使用 value 的方式去定義RPC 函數(shù)的輸入和輸出,但通過(guò) TypeScript 提供的type infer 能力,我們不必為 RPC 函數(shù)的實(shí)現(xiàn)重新寫(xiě)一次類(lèi)型定義,而是可以使用 TypeScript 的類(lèi)型推導(dǎo)能力,讓 TypeScript 自動(dòng)推導(dǎo)出 RPC 函數(shù)的輸入和輸出類(lèi)型。

圖片

可以看到,我們的 hello 函數(shù)實(shí)現(xiàn)是有類(lèi)型的。不僅如此,我們還可以通過(guò) TypeOf 獲取到 Schema API 定義出來(lái)的結(jié)構(gòu)。

圖片

通過(guò)這種方式,我們實(shí)現(xiàn)了 RPC 函數(shù)的輸入和輸出結(jié)構(gòu),具備以下能力:

可以在運(yùn)行時(shí)保留,用以生成代碼或者生成文檔

可以在編譯時(shí)被隱式地推導(dǎo)出來(lái),用以做類(lèi)型檢查

可以通過(guò) TypeOf 工具類(lèi)型顯式地獲取到,用以做類(lèi)型標(biāo)記

現(xiàn)在,讓我們把 RPC-BFF 函數(shù)放到 RPC-BFF App 中:

import { createApp } from '@ctrip/rpc-bff'

import { hello } from './api/hello'

export const app = createApp({
    entries: {
        hello,
    }
})

createApp 將創(chuàng)建一個(gè) RPC-BFF App,其中 options.entries 字段就是我們想要曝露給前端調(diào)用的 RPC-BFF 函數(shù)列表。

啟動(dòng)后,一個(gè) RPC-BFF Server 就運(yùn)行起來(lái)了。

6.5 RPC-BFF 的 Client

在前端項(xiàng)目的開(kāi)發(fā)階段,它將會(huì)新增 rpc.config.js 配置腳本。

rpc.config.js
const { createRpcBffConfig } = require('@ctrip/rpc-bff-cli')

module.exports = createRpcBffConfig({
    client: {
        rootDir: './__generated__/',
        list: [
            {
                src: 'http://localhost:3001/rpc_bff',
                dist: 'my-bff-client.ts',
            }
        ]
    }
})

如上所示,當(dāng)該腳本被 rpc-bff-cli 運(yùn)行時(shí),它會(huì)向 src 發(fā)起 introspection request 并生成代碼到指定目錄下的指定文件(如 my-bff-client.ts)。

就我們前面所演示的 hello 函數(shù)來(lái)說(shuō),其生成的代碼大概如下所示:

./__generated__/my-bff-client.ts

/**
 * This file is auto generated by rpc-bff-client
 * Please do not modify this file manually
 */
 
import { createRpcBffLoader } from '@ctrip/rpc-bff-client'


export type JsonType =
  | number
  | string
  | boolean
  | null
  | undefined
  | JsonType[]
  | { toJSON(): string }
  | { [key: string]: JsonType }


/**
 * @label HelloInput
*/
export type HelloInput = {
  name: string
}


/**
 * @label HelloOutput
*/
export type HelloOutput = {
  message: string
}


export type ApiClientLoaderInput = {
  path: string[]
  input: JsonType
}


declare global {
  interface ApiClientLoaderOptions {}
}


export type ApiClientOptions = {
  loader: (input: ApiClientLoaderInput,
options?: ApiClientLoaderOptions) => Promise<JsonType>
}


export const createApiClient = (options: ApiClientOptions) => {
  return {
    hello: (input: HelloInput,
loaderOptions?: ApiClientLoaderOptions) => {
      return options.loader(
        {
          path: ['hello'],
          input: input as JsonType,
        },
        loaderOptions
      ) as Promise<HelloOutput>
    }
  }
}


export const loader = createRpcBffLoader("http://localhost:3001/rpc_bff")


export default createApiClient({ loader })

我們可以看到,RPC-BFF 的代碼生成結(jié)果主要包含三個(gè)部分:

  • RPC-BFF Server 里的 Schema 變成了前端里的 Type,將在編譯后被擦除,不會(huì)增加前端代碼體積
  • RPC-BFF Server 里的 entries 變成了 createApiClient 函數(shù),包含了跟 BFF 端對(duì)齊的函數(shù)調(diào)用列表及其類(lèi)型信息
  • RPC-BFF Client 被引入和實(shí)例化,它將在前端的運(yùn)行時(shí)接管 RPC 函數(shù)的前后端通訊過(guò)程,對(duì)前端調(diào)用者無(wú)感

通過(guò) Introspection + Code-generator 途徑,一個(gè) RPC-BFF 服務(wù)不必跟它的下游前端項(xiàng)目綁定,而是每個(gè)前端項(xiàng)目通過(guò) rpc.config.js 各自同步它們所需的 RPC-BFF 服務(wù)。如此解耦了前后端的項(xiàng)目依賴(lài),同時(shí)這個(gè)模式在 Monorepo 項(xiàng)目中也能很好地工作,是一種更加靈活的方式。

七、RPC-BFF 特性概覽

至此,我們了解到了 RPC-BFF 的后端和前端分別的開(kāi)發(fā)方式,可以看到對(duì)于 RPC-BFF 服務(wù)的開(kāi)發(fā)者來(lái)說(shuō),并沒(méi)有引入復(fù)雜的 API 或者概念,僅僅是在編寫(xiě)樸素函數(shù)的心智模型的基礎(chǔ)上,將定義函數(shù)輸入和輸出結(jié)構(gòu)的方式,從樸素的 Type 換成了 RPC-BFF Schema。

對(duì)于 RPC-BFF 服務(wù)的調(diào)用方而言,只是增加了 rpc.config.js 配置腳本,在開(kāi)發(fā)階段就能得到 RPC-BFF 的類(lèi)型及其 Client 封裝,用極小的成本獲得極大便利。

但這仍不是 RPC-BFF 的優(yōu)勢(shì)的全部,接下來(lái),我們來(lái)了解一下 RPC-BFF 的幾大特性。

7.1 端到端類(lèi)型安全的函數(shù)調(diào)用

端到端類(lèi)型安全(End to end type-safety)的函數(shù)調(diào)用是 RPC-BFF 的基本功能,前端通過(guò)生成的 RPC-BFF Client 模塊訪(fǎng)問(wèn) RPC-BFF Server 時(shí),像調(diào)用本地異步函數(shù)一樣。

圖片

如上所示,BFF 后端和前端的類(lèi)型對(duì)齊。前端不必關(guān)心底層 HTTP 通訊細(xì)節(jié),可以聚焦 RPC 函數(shù)的 input 和 output 結(jié)構(gòu)。

圖片

當(dāng) RPC 函數(shù)執(zhí)行時(shí),它將發(fā)起 HTTP 請(qǐng)求,將所調(diào)用的 RPC 函數(shù)的路徑(path)和輸入(input)等信息打包發(fā)送給 RPC-BFF Server。

圖片

RPC-BFF Server 接受到 RPC 函數(shù)調(diào)用請(qǐng)求后,將匹配出指定函數(shù)并以 input 參數(shù)調(diào)用它得到其 output 結(jié)果后發(fā)送給前端,前端收到響應(yīng)結(jié)果后,RPC-BFF Client 將其轉(zhuǎn)換為前端 RPC 調(diào)用函數(shù)的返回值。整個(gè) RPC 過(guò)程的前后端流程就完成了。

7.2 直觀的錯(cuò)誤處理

前面介紹的 RPC 調(diào)用只涉及請(qǐng)求正確處理和返回的情況,如果服務(wù)端報(bào)錯(cuò)了,前端如何處理呢?

對(duì)于這個(gè)問(wèn)題,RPC-BFF 也有其優(yōu)勢(shì)。不同于樸素的接口請(qǐng)求錯(cuò)誤處理,需要去判斷 HTTP Status Code 檢查請(qǐng)求狀態(tài)碼是否正確,甚至還得判斷 result.code 檢查業(yè)務(wù)狀態(tài)碼是否正確等等。

RPC-BFF 的錯(cuò)誤處理支持最直觀和自然的 throw 和 try-catch 特性。在 RPC-BFF Client 中可以 catch 到 RPC-BFF Server 里 throw 的錯(cuò)誤。

圖片

如上,改造我們的 hello 函數(shù),當(dāng)它遇到空的 name 參數(shù)時(shí) throw 指定錯(cuò)誤。

圖片

前端則構(gòu)造一個(gè)空的 name 參數(shù),并 try-catch 此次 RPC 調(diào)用,它將能捕獲服務(wù)端拋出的錯(cuò)誤。

圖片

再次執(zhí)行后,從 Chrome Devtools 的 Network 面板中,我們看到了標(biāo)記為錯(cuò)誤的 RPC 響應(yīng)結(jié)果。

圖片

在 Console 面板中,我們則看到了前端 catch 到的 RPC 調(diào)用錯(cuò)誤日志輸出。

7.3 代碼即文檔

RPC-BFF 從 GraphQL-BFF 中學(xué)習(xí)到了很多優(yōu)秀之處。在 GraphQL 中,可以基于其 Schema 的 Introspection 能力,構(gòu)建 GraphQL Playground 平臺(tái),可以在其中查看接口的參數(shù)類(lèi)型、字段描述等信息,還能發(fā)起查詢(xún),相當(dāng)方便。

RPC-BFF 的 Schema 也擁有 Introspection 能力,因此我們可以為前端提供更多內(nèi)容。

export class Todo extends ObjectType {
  id = {
    description: `Todo id`,
    [Type]: Int,
  }


  content = {
    description: 'Todo content',
    [Type]: String,
  }
  
  completed = {
    description: 'Todo status',
    [Type]: Boolean,
  }
}


export class AddTodoInput extends ObjectType {
  content = {
    description: 'a content of todo for creating',
    [Type]: String,
  }
}


export class AddTodoOutput extends ObjectType {
  todos = {
    description: 'Todo list',
    [Type]: TodoList,
  }
}


export const addTodo = Api(
  {
    description: 'add todo',
    input: AddTodoInput,
    output: AddTodoOutput,
  },
  (input) => {
    state.todos.push({
      id: state.uid++,
      content: input.content,
      completed: false,
    })
    return {
      todos: state.todos,
    }
  },
)

如上所示,我們?cè)诙x addTodo 接口的 input schema 和 output schema 時(shí),不僅僅提供了對(duì)應(yīng)的類(lèi)型,還添加了相關(guān)的 description 描述。

經(jīng)過(guò)前端的代碼生成后,將得到如下代碼:

/**
 * @label Todo
*/
export type Todo = {
  /**
  * @remarks Todo id
  */
  id: number,
  /**
  * @remarks Todo content
  */
  content: string,
  /**
  * @remarks Todo status
  */
  completed: boolean
}


/**
 * @label AddTodoInput
*/
export type AddTodoInput = {
  /**
  * @remarks a content of todo for creating
  */
  content: string
}


/**
 * @label AddTodoOutput
*/
export type AddTodoOutput = {
  /**
  * @remarks Todo list
  */
  todos: (Todo)[]
}


export const createApiClient = (options: ApiClientOptions) => {
  return {
    /**
    * @remarks add todo
    */
    addTodo: (input: AddTodoInput, loaderOptions?: ApiClientLoaderOptions) => {
      return options.loader(
        {
          path: ['addTodo'],
          input: input as JsonType,
        },
        loaderOptions
      ) as Promise<AddTodoOutput>
    },
  }
}

相比文本形式的接口契約文檔,RPC-BFF 通過(guò)注釋的方式將接口描述信息呈現(xiàn)出來(lái)。

圖片

如上所示,生成到注釋的方式,比樸素的接口契約更貼近開(kāi)發(fā)者,可以在代碼編輯器里直觀地看到接口描述和類(lèi)型描述,并且基于開(kāi)發(fā)階段的同步機(jī)制,它總是實(shí)時(shí)反映當(dāng)前 RPC-BFF 的最新?tīng)顟B(tài),避免了接口文檔過(guò)時(shí)的問(wèn)題。

當(dāng)我們想要廢棄一個(gè) RPC 函數(shù)時(shí),這項(xiàng)機(jī)制尤為重要。

圖片

如上,我們通過(guò)添加 RPC 函數(shù)的 deprecated 描述,宣布廢棄。

圖片

前端經(jīng)過(guò)代碼生成同步到 RPC-BFF 最新?tīng)顟B(tài)時(shí),將能在代碼編輯器里直觀地看到廢棄的提示信息。如此可以實(shí)現(xiàn)流暢的前后端接口廢棄過(guò)程。

7.4 自由的函數(shù)組合

在 RPC-BFF 中,RPC 函數(shù)跟普通函數(shù)本質(zhì)上是一樣的,只是它通過(guò) Schema 定義額外攜帶了描述自身的元數(shù)據(jù)信息。我們可以像組合普通函數(shù)一樣,組合 RPC 函數(shù)。

假設(shè)我們有 updateTodo 和 removeTodo 兩個(gè) RPC 函數(shù),然后我們希望添加一個(gè)功能:當(dāng) updateTodo 收到的 todo.content 為空時(shí),則 remove 該 todo。

那么,我們不必把 removeTodo 功能分別在 updateTodo 和 removeTodo 中各自實(shí)現(xiàn)一遍,而是在 updateTodo 中根據(jù)條件調(diào)用 removeTodo。

圖片

如上所示,在 updateTodo 中調(diào)用 removeTodo 并沒(méi)有特殊的要求,就像調(diào)用別的異步函數(shù)一樣簡(jiǎn)單,并且對(duì)于前端發(fā)起的 updateTodo RPC 調(diào)用也沒(méi)有額外開(kāi)銷(xiāo),仍是一次對(duì) updateTodo 的遠(yuǎn)程函數(shù)調(diào)用。

7.5 可靠的接口兼容性識(shí)別與版本跟蹤

由于 RPC-BFF 的調(diào)用方在自己的前端項(xiàng)目中,通過(guò) rpc.config.js 同步了 RPC-BFF 當(dāng)前的接口的類(lèi)型,因此它還解決了前后端之間常常遇到的接口兼容性爭(zhēng)議。

在以往樸素的實(shí)踐中,接口兼容往往只是后端對(duì)前端的口頭承諾,缺乏自動(dòng)化的、系統(tǒng)化的方式去發(fā)現(xiàn)和識(shí)別接口兼容性,甚至往往將問(wèn)題暴露在生產(chǎn)環(huán)境。然后前后端開(kāi)始爭(zhēng)執(zhí)判空職責(zé)的前后端邊界劃分問(wèn)題。

而現(xiàn)在,RPC-BFF 提供了自動(dòng)發(fā)現(xiàn)接口兼容性機(jī)制,并且是以系統(tǒng)性的、無(wú)爭(zhēng)議的方式實(shí)現(xiàn)的。

export class AddTodoOutput extends ObjectType {
  todos = {
    description: 'Todo list',
    [Type]: Nullable(TodoList),
  }
}

如上,當(dāng)后端的 addTodo 接口改變了返回值類(lèi)型,從非空的 todos 變成可空的 todos,這是一種不兼容的變更。

前端同步 RPC-BFF 的接口契約后,在代碼編輯器里立即可以看到類(lèi)型系統(tǒng)的 type-check 結(jié)果。

圖片

在不解決這個(gè)類(lèi)型問(wèn)題的情況下,前端項(xiàng)目難以通過(guò)編譯,從而避免將問(wèn)題泄露到生產(chǎn)環(huán)境。

圖片

此外,RPC-BFF 生成的代碼也進(jìn)入了前端項(xiàng)目的版本管理中,可以從 git diff 中,清晰地看到每一次迭代的接口契約變更記錄。更完整無(wú)誤地追溯和跟蹤前后端的接口契約歷史。

7.6 自動(dòng) merge & batch & stream 優(yōu)化

如前文所描述的,RPC-BFF 需要支持自動(dòng)的 merge & batch & stream 功能,以便達(dá)到更少的 HTTP 請(qǐng)求、更快的數(shù)據(jù)響應(yīng)以及更優(yōu)的 SSR 支持。

我們先來(lái)講解一下它們分別的含義。

首先講 batch,它是一種常見(jiàn)的優(yōu)化技術(shù),可以把一組數(shù)據(jù)請(qǐng)求合并為一次,往往用在相同資源的批量化請(qǐng)求上。這需要服務(wù)端接口提供支持,比如 getUser 接口是獲取單個(gè)用戶(hù)信息的,而 getUsers 則是獲取多個(gè)的,后者可以被視為 batch 接口。

而 merge 在這里則偏向前端概念,接口支持 batch 只是說(shuō)我們可以調(diào)用一個(gè) batch 接口獲取更多數(shù)據(jù),但不意味著前端代碼里多個(gè)地方調(diào)用 getUser 接口,會(huì)自動(dòng) merge 到一起去調(diào)用 getUsers 接口。

merge & batch 結(jié)合起來(lái),就可以讓前端里分散的各個(gè)調(diào)用自動(dòng)合并到一次 batch 接口的請(qǐng)求中。

現(xiàn)在我們來(lái)看 stream,它跟 batch 一樣需要服務(wù)端的支持。一次 batch 接口請(qǐng)求的響應(yīng)時(shí)間,往往取決于最慢的數(shù)據(jù),因?yàn)榉?wù)端需要準(zhǔn)備好所有數(shù)據(jù)后才能返回 JSON 結(jié)果;所謂的 stream 支持,則是服務(wù)端能夠提前將已獲得的數(shù)據(jù)一份份發(fā)送給前端。

某種意義上,stream 也需要前端的支持。假設(shè)接口支持 stream 可以一批批返回?cái)?shù)據(jù),前端卻一個(gè) await 等待所有數(shù)據(jù)就位后才開(kāi)始下一步,那么等于沒(méi)有發(fā)揮出接口流式響應(yīng)的優(yōu)勢(shì)。

因此,merge & batch & stream 三者的結(jié)合才能發(fā)揮出更充分的優(yōu)化效果。

  • 通過(guò) merge,前端代碼里各個(gè)地方的接口調(diào)用被 batch 起來(lái)
  • 通過(guò) batch & stream 接口,一份份數(shù)據(jù)從服務(wù)端發(fā)送給前端
  • 通過(guò) RPC-BFF Client 內(nèi)部數(shù)據(jù)分發(fā),指定的數(shù)據(jù)被一份份地發(fā)送到各個(gè)前端調(diào)用點(diǎn)

每個(gè)前端調(diào)用點(diǎn)收到數(shù)據(jù)響應(yīng)后都將第一時(shí)間進(jìn)入后續(xù)的渲染流程,不會(huì)受到其它調(diào)用點(diǎn)的阻塞影響。

我們來(lái)看一個(gè)例子:

圖片

如上,為了演示方便,我用 Promise.all 將 3 次 RPC 函數(shù)調(diào)用的結(jié)果打包到一起返回,但我們?nèi)孕柚?,它們其?shí)是三個(gè)獨(dú)立的 RPC 調(diào)用,被寫(xiě)到一起還是分散在其它地方調(diào)用,不影響結(jié)果。Promise.all 只是在前端匯總 promises 結(jié)果,不包含接口相關(guān)的 merge & batch & stream 等作用。

圖片

從 Network Request 面板中,我們看到了一個(gè)被標(biāo)記為 Stream 的請(qǐng)求,它里面包含了上面 3 個(gè) RPC 調(diào)用的所有信息。

圖片

在 Network Response 面板中,我們則看到了一個(gè) Newline delimited JSON 響應(yīng),即用換行符分隔的streaming JSON 格式。每一個(gè)被 batch & stream 起來(lái)的 RPC 函數(shù)調(diào)用返回?cái)?shù)據(jù)后,都將立即產(chǎn)生一條 JSON 結(jié)果發(fā)送給前端,每一行對(duì)應(yīng)一次 RPC 調(diào)用。

7.7 自動(dòng)緩存和去重

除了 merge & batch & stream 以外,自動(dòng)的緩存和去重(cache & dedup)對(duì)于渲染優(yōu)化也很重要。

在前端界面中,兩個(gè)組件依賴(lài)同一份接口數(shù)據(jù)的情況很常見(jiàn)。傳統(tǒng)方式是,手動(dòng)去重,即兩個(gè)組件都不包含接口調(diào)用,而是 lift up 到公共的祖先級(jí)組件統(tǒng)一處理后通過(guò) props/context 等方式將同一份數(shù)據(jù)傳送給這兩個(gè)組件。

這種方式的缺點(diǎn)是:

  • 兩個(gè)組件都無(wú)法被獨(dú)立使用和復(fù)用,因?yàn)樗鼈兊臄?shù)據(jù)請(qǐng)求邏輯都被挪出去了,內(nèi)聚性被打破
  • 組件優(yōu)化不足。只有枝葉組件各自發(fā)起請(qǐng)求時(shí),Streaming 渲染得到了更優(yōu)條件。越是頂層的組件里懸停,子組件渲染越是受到阻塞

這就是為什么 React 目前致力于接管 data-fetching 層,讓開(kāi)發(fā)者手動(dòng)去管理缺少將系統(tǒng)性?xún)?yōu)化。

對(duì)于 RPC-BFF 而言,支持 cache & dedup 變得重要。

圖片

改造前面的 tryBatch 代碼,使其包含 4 次 RPC 調(diào)用,有 4 個(gè)返回值,但有 2 次的參數(shù)和函數(shù)名是重復(fù)的。

圖片

但我們的請(qǐng)求卻只包含了 3 次 RPC調(diào)用及其響應(yīng),其中 2 次的重復(fù)調(diào)用被合并為一次。

圖片

但并不影響每個(gè) RPC 調(diào)用拿到它對(duì)應(yīng)的 4 個(gè)調(diào)用結(jié)果。

通過(guò)這種 cache & dedup 機(jī)制,每個(gè)組件的數(shù)據(jù)請(qǐng)求都可以?xún)?nèi)聚于組件代碼內(nèi)部,而不必被迫 lift up 到父級(jí)組件做請(qǐng)求托管了。

值得提醒的是,不是所有相同參數(shù)的 RPC 調(diào)用都能被緩存和去重。特別是對(duì)于 mutation 性質(zhì)的請(qǐng)求來(lái)說(shuō),連續(xù)調(diào)用兩次相同參數(shù)的 createTodo,應(yīng)當(dāng)是創(chuàng)建兩條而非一條。

圖片

因此,RPC-BFF 支持在前端傳遞第二個(gè) options 參數(shù)給 RPC 函數(shù),可以關(guān)閉 cache 特性。

圖片

如上圖所示,關(guān)閉 cache 后,即便是相同的 RPC 調(diào)用,也不會(huì)被緩存和去重。

options 選項(xiàng),不僅可以關(guān)閉 cache,還可以關(guān)閉 batch, stream 等特性。

圖片

如上所示,options.batch 是 cache 和 stream 的前提,我們將所有 RPC 調(diào)用的 batch 選項(xiàng)都關(guān)閉。

圖片

其結(jié)果是所有 RPC 函數(shù)調(diào)用退化為樸素形態(tài),每一個(gè) RPC 調(diào)用對(duì)應(yīng)獨(dú)立的一次 HTTP 請(qǐng)求。

通過(guò)靈活的選項(xiàng)配置,我們可以按需決定 RPC-BFF 里的函數(shù)調(diào)用的優(yōu)化策略。

八、總結(jié)

在這篇文章中,我們介紹了 BFF 的起源、模式和技術(shù)選型,并根據(jù)界面渲染優(yōu)化需求,了解到 Streaming BFF 的重要性,同時(shí)也給出了我們當(dāng)前探索的技術(shù)方向——RPC-BFF。

我們對(duì)比了開(kāi)源社區(qū)的一些流行方案,并根據(jù)我們自身的場(chǎng)景做了分析,盡管最終采用了自研的方式,但在調(diào)研和實(shí)驗(yàn)開(kāi)源技術(shù)的過(guò)程中,也讓我們學(xué)習(xí)到了很多知識(shí),使得 RPC-BFF 的設(shè)計(jì)和實(shí)現(xiàn)能夠吸收開(kāi)源社區(qū)的技術(shù)成果。

我們最終達(dá)到了預(yù)期的技術(shù)目標(biāo),實(shí)現(xiàn)了:

  • Validation: BFF 端可以在運(yùn)行時(shí)驗(yàn)證參數(shù)結(jié)構(gòu)和返回值結(jié)構(gòu)的合法性
  • Type-infer: 支持從 Schema 中靜態(tài)類(lèi)型推導(dǎo)出 TypeScript 類(lèi)型,避免重復(fù)定義
  • Introspection: RPC-BFF 服務(wù)可以通過(guò)內(nèi)省請(qǐng)求曝露出其函數(shù)列表的契約結(jié)構(gòu)
  • Code-generation: 支持為調(diào)用方生成類(lèi)型代碼和調(diào)用代碼,解耦項(xiàng)目依賴(lài)
  • Merge: 前端多次 RPC 調(diào)用可以自動(dòng)合并為一次 HTTP 請(qǐng)求
  • Batch: 一次 HTTP 請(qǐng)求支持包含多個(gè) RPC 調(diào)用
  • Stream: 多個(gè) RPC 調(diào)用可以流式響應(yīng),一份份發(fā)給前端,避免阻塞
  • Dedup: 重復(fù)的 RPC 調(diào)用可以被去重合并
  • Cache: 重復(fù)的 RPC 調(diào)用可復(fù)用緩存結(jié)果
  • Type-safe: 前端可復(fù)用和對(duì)齊 BFF 端的類(lèi)型
  • Code as documentation: 代碼即文檔,RPC 接口的文檔描述通過(guò)代碼生成,以代碼注釋的形態(tài)直接作用于前端項(xiàng)目中

目前我們的 RPC-BFF 技術(shù)方案已經(jīng)在內(nèi)部試點(diǎn)項(xiàng)目中落地并上線(xiàn)平穩(wěn)運(yùn)行,接下來(lái)將會(huì)推廣和迭代,并持續(xù)挖掘 RPC-BFF 技術(shù)方向上的優(yōu)化潛力。

以上,希望能給大家?guī)?lái)幫助。

責(zé)任編輯:張燕妮 來(lái)源: 攜程技術(shù)
相關(guān)推薦

2022-06-27 09:36:29

攜程度假GraphQL多端開(kāi)發(fā)

2024-08-28 09:50:51

2022-10-28 12:00:03

前端開(kāi)源

2024-09-10 16:09:58

2024-07-05 15:05:00

2021-11-19 09:29:25

項(xiàng)目技術(shù)開(kāi)發(fā)

2022-05-19 17:50:31

bookie集群延遲消息存儲(chǔ)服務(wù)

2016-09-04 15:14:09

攜程實(shí)時(shí)數(shù)據(jù)數(shù)據(jù)平臺(tái)

2024-03-26 07:35:24

日志索引語(yǔ)言

2023-06-06 11:49:24

2022-08-25 06:27:39

vivoJaCoCo代碼覆蓋率

2024-12-06 08:59:08

2023-08-18 10:49:14

開(kāi)發(fā)攜程

2023-11-06 09:56:10

研究代碼

2022-03-24 09:44:54

TypeScriptSOLID

2023-06-28 14:01:13

攜程實(shí)踐

2024-02-27 07:27:58

云原生推薦系統(tǒng)架構(gòu)云原生技術(shù)棧

2023-06-30 09:46:00

服務(wù)物理機(jī)自動(dòng)化

2023-11-24 09:44:07

數(shù)據(jù)攜程

2020-12-04 14:32:33

AndroidJetpackKotlin
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)