01 什么是BFF
Backend For Frontend,即服務(wù)于前端的后端。
面對(duì)越來(lái)越復(fù)雜的多端應(yīng)用的需求,后端提供的 RESTful 接口形式難以應(yīng)對(duì)多變的頁(yè)面需求,這時(shí)候需要一層專(zhuān)門(mén)的 BFF 層來(lái)彌合這部分差異。
例如同樣一個(gè)商品詳情頁(yè),在 App 端上和 PC 端上,兩者的展示樣式就有很多的不同。以往前后端分離的方式可能有幾種做法。
后端提供完全獨(dú)立的 RESTful API,然后由前端來(lái)進(jìn)行聚合。前端需要負(fù)責(zé)處理多個(gè)數(shù)據(jù)源的聚合和前后數(shù)據(jù)依賴(lài)關(guān)系,并且由于經(jīng)過(guò)了多次的外網(wǎng)請(qǐng)求對(duì)頁(yè)面性能、原生 App 的兼容性上都很不友好。
由網(wǎng)關(guān)層來(lái)進(jìn)行聚合處理。這種方式不太容易靈活的定制一些聚合或者頁(yè)面邏輯的處理。
后端把數(shù)據(jù)聚合處理后,提供一個(gè) API 給到前端。這樣后端的微服務(wù)之間會(huì)存在橫向的調(diào)用,而這是后端微服務(wù)架構(gòu)里一般需要極力避免的做法。
針對(duì)這樣的場(chǎng)景,現(xiàn)在一般會(huì)引入 BFF 這一中間層,讓前端應(yīng)用直接和 BFF 通信,BFF 再和后端 API 進(jìn)行通信,獲取數(shù)據(jù)并且處理完以后返回給前端。這樣就能比較好的滿足前后端各自的需求。其實(shí)從本質(zhì)上來(lái)說(shuō)是前端面向頁(yè)面場(chǎng)景和后端面向業(yè)務(wù)領(lǐng)域之間的矛盾,由 BFF 這層來(lái)解決。
但是 BFF 也只是為了解耦前端和后端間的依賴(lài)而增加的一層,BFF 內(nèi)部還是存在的非常多的問(wèn)題。
02 BFF的主要職責(zé)和問(wèn)題
BFF 最主要是為了針對(duì)前端頁(yè)面進(jìn)行定制化的處理,雖然可以針對(duì)每個(gè)頁(yè)面都開(kāi)發(fā)一個(gè)單獨(dú)的接口,但是實(shí)際上為了開(kāi)發(fā)效率,我們還是會(huì)在很多代碼上做一些復(fù)用。而這些頁(yè)面可能有部分共有的邏輯,又會(huì)有部分差異。對(duì) BFF 進(jìn)行深入的分析我們發(fā)現(xiàn),BFF 面臨最主要的問(wèn)題有三個(gè):
第一個(gè)問(wèn)題是按需取數(shù)
例如同樣一個(gè)商品詳情頁(yè),在 App 端上,完整的獲取數(shù)據(jù)可能需要 100 個(gè)字段,對(duì)應(yīng) 10 個(gè)接口。而在 Mobile Web 上,這個(gè)頁(yè)面可能只需要 50 個(gè)字段,對(duì)應(yīng) 6 個(gè)接口。但是實(shí)際開(kāi)發(fā)的時(shí)候,工程師為了方便很容易寫(xiě)出來(lái)一個(gè)大而全的方法,包含了這 100 個(gè)字段并且調(diào)用 10 個(gè)接口,這樣后期維護(hù)反而會(huì)很困難,而且拖累的部分頁(yè)面的性能。
面對(duì)這樣的場(chǎng)景,如果希望代碼能夠優(yōu)雅的復(fù)用,對(duì)工程師的能力的要求會(huì)特別高,需要設(shè)計(jì)一套非常精巧的代碼框架來(lái)實(shí)現(xiàn)。實(shí)際情況卻是很容易演變成上面例子中描述的樣子。
而 GraphQL 正是這樣一套精巧的框架,可以很方便的按我們需求,選擇性的對(duì)字段和數(shù)據(jù)進(jìn)行獲取。并且對(duì)于不需要獲取的數(shù)據(jù),GraphQL 也不會(huì)調(diào)用對(duì)應(yīng)的數(shù)據(jù)接口,從而提升訪問(wèn)性能。
第二個(gè)問(wèn)題是頁(yè)面差異化兼容
同一個(gè)業(yè)務(wù)針對(duì)不同的端,前端可能也是不同的團(tuán)隊(duì)來(lái)負(fù)責(zé)的,使用的技術(shù)棧也不相同,因此需要的數(shù)據(jù)結(jié)構(gòu)、字段名稱(chēng)可能都不同。比如 Web 端需要完全平鋪的字段結(jié)構(gòu),而 App 上可以接受結(jié)構(gòu)化對(duì)象結(jié)構(gòu),或者前端使用了低代碼平臺(tái)來(lái)實(shí)現(xiàn),字段結(jié)構(gòu)是跟著 UI 組件來(lái)走的。
對(duì)于字段的映射,本質(zhì)上其實(shí)一種 JSON 結(jié)構(gòu)轉(zhuǎn)換成另外一種 JSON 結(jié)構(gòu),我們參考了很多 Node.js 生態(tài)里的解決方案,發(fā)現(xiàn)通過(guò) JSON 模板渲染的方式來(lái)實(shí)現(xiàn) JSON 結(jié)構(gòu)的轉(zhuǎn)換是比較可行的方案。
第三個(gè)問(wèn)題是不同版本的差異化兼容
在原生的 APP 上,BFF 層需要針對(duì)不同的版本做不同的處理。甚至原生的 iOS/Android 兩端,有時(shí)候也要做一些不同的兼容邏輯處理。例如老版本展示 A 樣式,新版本展示 B 樣式?;蛘?iOS 的原生代碼在某個(gè)版本有 bug,只能 BFF 來(lái)兼容。時(shí)間久了以后代碼會(huì)越來(lái)越難以維護(hù),代碼里充斥著各種 if-else 的判斷邏輯。
考慮到絕大部分的情況,App 版本發(fā)布之后,對(duì)應(yīng)的接口一般不會(huì)做大的調(diào)整,特別是兩三個(gè)版本以前的代碼,調(diào)整的概率更低。因此我們引入了路由的能力來(lái)解決這個(gè)問(wèn)題。
不同的版本或者 iOS/Android 端映射到不同的 API 接口上,API 內(nèi)處理 GraphQL 的調(diào)用和 JSON 模板映射
每次需要開(kāi)發(fā)新版本接口的時(shí)候,可以簡(jiǎn)單的復(fù)制以前邏輯到新版本接口上,然后做適當(dāng)?shù)恼{(diào)整
需要處理歷史版本邏輯的時(shí)候,找到對(duì)應(yīng) API,進(jìn)行調(diào)整即可
這樣 BFF 里的邏輯可以始終保持相對(duì)清晰,不同版本的邏輯都可以相互解耦。雖然會(huì)存在一定的代碼拷貝的問(wèn)題,但是長(zhǎng)期的維護(hù)上來(lái)說(shuō)更加清晰了,而且也可以通過(guò)增加字段審計(jì)的能力來(lái)緩解代碼拷貝所帶來(lái)的問(wèn)題。
03 平臺(tái)化構(gòu)建BFF層
針對(duì)上面一節(jié)里提到的三個(gè)問(wèn)題和對(duì)應(yīng)的解決方案,下面分別做詳細(xì)的介紹
數(shù)據(jù)獲?。憾囝I(lǐng)域的按需取數(shù)和數(shù)據(jù)聚合 —— 引入 GraphQL
數(shù)據(jù)轉(zhuǎn)換:一種 JSON 結(jié)構(gòu)轉(zhuǎn)換成另外一種 —— 引入 JSON 模板
請(qǐng)求映射:多版本兼容 —— 引入路由能力
我們先對(duì) GraphQL 做一下簡(jiǎn)單的介紹,關(guān)于 GraphQL 更詳細(xì)的內(nèi)容可以瀏覽官網(wǎng) graphql.org 了解。
GraphQL 從名字上就能看出來(lái)和 SQL 有些類(lèi)似。它首先定義了一套類(lèi)型系統(tǒng)。這里以官方的例子說(shuō)明。
type Query { hero: Character}
type Character { name: String friends: [Character] homeWorld: Planet}
type Planet { name: String climate: String}
官方定義了一套以《星球大戰(zhàn)》背景的兩個(gè)類(lèi)型,角色和角色所屬的星球。這里 type 可以對(duì)應(yīng)到 Java 語(yǔ)言中的 class, 初看起來(lái)和 Java 語(yǔ)言沒(méi)有太大的差別。
{ hero { name friends { name homeWorld { name climate } friends { name homeWorld { name climate } friends { name } } } }}
這是一段 GraphQL 的 query 語(yǔ)句,通過(guò) Query 對(duì)象的入口,就可以開(kāi)始對(duì) GraphQL 對(duì)象進(jìn)行查詢了。它的查詢語(yǔ)句有幾個(gè)特性:
按需取字段,不需要的字段可以不查詢,類(lèi)似于 SQL 里的 select
在類(lèi)型定義的基礎(chǔ)上,可以關(guān)聯(lián)查詢多個(gè)類(lèi)型的數(shù)據(jù),類(lèi)似于 SQL 里的 join(但不完全一樣)
可以遞歸的對(duì)某些字段進(jìn)行理論上無(wú)限深度的查詢(上面例子里的 friends,不過(guò)一般會(huì)限制深度)
而在 GraphQL 的實(shí)現(xiàn)里,是通過(guò)實(shí)現(xiàn) DataFetcher 的接口來(lái)獲取真正的數(shù)據(jù)的,例如調(diào)用 RESTful 接口或者調(diào)用 RPC 接口,都是封裝在這里。DataFetcher 可以綁定在某個(gè) type 的某個(gè)字段上,這樣當(dāng)訪問(wèn)到這個(gè)字段時(shí), GraphQL 會(huì)自動(dòng)調(diào)用這個(gè) DataFetcher 來(lái)獲取數(shù)據(jù),沒(méi)有使用到這個(gè)字段自然也不會(huì)請(qǐng)求。也是因?yàn)榻壎ǖ阶侄蔚脑?,我們?shí)現(xiàn) DataFetcher 的時(shí)候可以聚焦在單一數(shù)據(jù)類(lèi)型的獲取上,而把多類(lèi)型的數(shù)據(jù)關(guān)聯(lián)交給 GraphQL 自己來(lái)完成。
通過(guò) GraphQL 這樣的能力,我們即可以按需選擇需要的數(shù)據(jù)字段,也可以讓 GraphQL 自動(dòng)幫助我們組裝多個(gè)數(shù)據(jù)對(duì)象的數(shù)據(jù)。
從前面的介紹里可以發(fā)現(xiàn) GraphQL 和 SQL 有很多相似之處,而且也很容易對(duì)應(yīng)起來(lái)。所以業(yè)界之前對(duì) GraphQL 也有個(gè)普遍的誤解,需要后端把數(shù)據(jù)庫(kù)直接暴露出來(lái)整合進(jìn) GraphQL,這樣對(duì)后端的架構(gòu)、數(shù)據(jù)庫(kù)的性能都有非常大的侵入性。但是 GraphQL 實(shí)際的使用上,可以很方便的融入現(xiàn)在普遍應(yīng)用的微服務(wù)和 DDD 的架構(gòu)。
我們可以用 GraphQL 服務(wù)來(lái)替換原來(lái)的 BFF 層,這樣后端原有的架構(gòu)體系都不需要進(jìn)行改變,只需要在 GraphQL 中實(shí)現(xiàn) RESTful API 到 GraphQL 的轉(zhuǎn)換功能即可。這也是業(yè)界目前大部分公司使用的方案。
在我們的方案里,為了方便后端同學(xué)更加快速的接入 GraphQL 以及兼容我們內(nèi)部的服務(wù)治理框架,我們提供了一套 Java 注解的方式方便業(yè)務(wù)的同學(xué)快速構(gòu)建出一個(gè) GraphQL 服務(wù)出來(lái)。
針對(duì) GraphQL 里的 Type、Enum、Interface、Union、Query 等,我們定義了對(duì)應(yīng)的注解進(jìn)行轉(zhuǎn)換。
而針對(duì)字段擴(kuò)展,我們單獨(dú)定義了一個(gè)注解來(lái)進(jìn)行處理,可以參考如下形式:
@GraphQLFieldAttach(targetType="Property", sourceFields="communityId", targetFieldName="community", batch=true) public MapResponse getCommunity(@GraphQLQueryKey Set communityId);
我們的房源(Property)和小區(qū)(Community)數(shù)據(jù)是屬于兩個(gè)不同的領(lǐng)域來(lái)對(duì)外提供服務(wù)的。實(shí)際的業(yè)務(wù)場(chǎng)景里,房源屬于某一個(gè)小區(qū),有個(gè)字段(communityId)保存著小區(qū) id,因此需要將這兩個(gè)數(shù)據(jù)對(duì)象進(jìn)行關(guān)聯(lián)。我們提供了一個(gè)查詢小區(qū)的接口(CommunityService),再通過(guò)上面的注解,在 GraphQL 里綁定到房源的對(duì)象的 Property.community 字段上。這樣當(dāng)查詢請(qǐng)求處理到 Property.community 的時(shí)候,會(huì)自動(dòng)請(qǐng)求這個(gè)接口,獲取小區(qū)數(shù)據(jù),返回給調(diào)用方。
同時(shí)為了適配大家已有的微服務(wù)體系,這里以 Spring Cloud 為例,把上面的接口定義打包成類(lèi)似 FeighClient 這樣二方包的形式,集成到 Gateway 中的依賴(lài)?yán)铩H缓髵呙?Jar 包自動(dòng)生成 Schema 和 DataFetcher,在 DataFetcher 里調(diào)用對(duì)應(yīng)的 FeignClient。這樣就可以自動(dòng)構(gòu)建出一個(gè)完整的 GraphQL 服務(wù)。
在 GraphQL 網(wǎng)關(guān)里我們會(huì)解析各個(gè)服務(wù)的二方包,自動(dòng)生成 Schema 和對(duì)應(yīng)的字段解析調(diào)用。當(dāng)某個(gè)業(yè)務(wù)有需求的時(shí)候可以非??焖俚募傻轿覀兊?GraphQL 體系中
目前二方包的依賴(lài)還是靜態(tài)管理的,有更新后需要重新部署網(wǎng)關(guān),后續(xù)迭代中我們會(huì)升級(jí)支持動(dòng)態(tài)更新 Jar 包以實(shí)現(xiàn)動(dòng)態(tài)生成 Schema 的能力。
前端頁(yè)面所需的 JSON 字段的結(jié)構(gòu)和 GraphQL 查詢結(jié)果的 JSON 結(jié)構(gòu)往往不相同,而且頁(yè)面上也存在一些 format、if-else 的判斷邏輯,這部分放在 GraphQL 里的話其實(shí)很難實(shí)現(xiàn)。特別是現(xiàn)在的一些前端低代碼平臺(tái),頁(yè)面的展現(xiàn)模塊可能在很多不同的頁(yè)面復(fù)用,這樣的字段定義和后端的數(shù)據(jù)字段定義是完全不一樣的,一定需要有人參與這部分轉(zhuǎn)換工作。參考 Node.js 生態(tài)的解決方案和以前后端模板的頁(yè)面渲染方式,我們采用 JSON 模板來(lái)對(duì)這兩個(gè)不同的 JSON 結(jié)構(gòu)進(jìn)行映射。
目前我們的平臺(tái)支持 JSLT 模板、Javascript 兩種方式來(lái)進(jìn)行 JSON 結(jié)構(gòu)的映射,下面以 JSLT 為例 (JSLT 是一個(gè)開(kāi)源的 JSON 模板引擎,基于 Java 語(yǔ)言,詳情可以參考 JSLT)。
//GraphQL 的結(jié)果,模板的輸入 JSON{ "data": [ { "id": 10000, "title": "房子 1", "roomNum": 2, "hallNum": 2, "area": 90.12 }, { "id": 10001, "title": "房子 2", "roomNum": 3, "hallNum": 2, "area": 99.34 }, ... ]}//JSLT 模板{ "dataList": [ for( .data) { "id": .id, "title": .title, "label1": "戶型", "text1": .roomNum + "室" + .hallNum + "廳" , "label2": "面積", "text2": .area +"㎡", "link": URLRoute("HousePage", {"id": .id}) } ]}//輸出JSON{ "dataList": [ { "id": 10000, "title": "房子 1", "label1": "戶型", "text1": "2室2廳", "label2": "面積", "text2": "90.12㎡", "link": "https://anjuke.com/house.html?id=10000" }, { "id": 10001, "title": "房子 2", "label1": "戶型", "text1": "3室2廳", "label2": "面積", "text2": "100.34㎡", "link": "https://anjuke.com/house.html?id=10001" } ]
上面這個(gè)例子可以發(fā)現(xiàn),最終輸出的 JSON 結(jié)構(gòu)和字段名稱(chēng)和 GraphQL 請(qǐng)求返回的結(jié)構(gòu)完全不同。通過(guò)這樣的映射處理,可以完全解耦前端頁(yè)面的展示邏輯和后端提供數(shù)據(jù)的取數(shù)邏輯,根據(jù)前端頁(yè)面對(duì)返回?cái)?shù)據(jù)的結(jié)構(gòu)要求,我們可以進(jìn)行各種 JSON 結(jié)構(gòu)的轉(zhuǎn)換來(lái)適配。后期隨著模板越來(lái)越復(fù)雜,也可以引入一些可復(fù)用的子模板方式來(lái)進(jìn)行管理
路由這部分比較簡(jiǎn)單,主要就是根據(jù)不同的端、版本、iOS/Anroid 等參數(shù),映射到對(duì)應(yīng)的 GraphQL 請(qǐng)求和 JSON 模板上即可。
BFF 由前端還是后端開(kāi)發(fā),其實(shí)在各家公司都有不同的實(shí)踐。但不管是誰(shuí)來(lái)做,都會(huì)存在一定的問(wèn)題
BFF 由前端負(fù)責(zé),需要額外關(guān)注服務(wù)器的穩(wěn)定性、性能,以及RPC/HTTP 請(qǐng)求的容錯(cuò)等等,對(duì)前端同學(xué)的能力要求較高
BFF 由后端負(fù)責(zé),由于并不一定能很好的理解前端頁(yè)面的各種數(shù)據(jù)需求,對(duì)后端同學(xué)來(lái)說(shuō)基本上是純工作量。如果是一個(gè)獨(dú)立的后端 BFF 團(tuán)隊(duì),工程師容易覺(jué)得沒(méi)有成長(zhǎng),人員也很難穩(wěn)定
這個(gè)問(wèn)題我們先放放,回到 BFF 本身的開(kāi)發(fā)工作,通過(guò)前面的拆解之后,我們發(fā)現(xiàn) BFF 的開(kāi)發(fā)工作其實(shí)比較模板化
數(shù)據(jù)獲?。壕帉?xiě) GraphQL query,調(diào)用 GraphQL 服務(wù)獲取數(shù)據(jù)
數(shù)據(jù)轉(zhuǎn)換:編寫(xiě) JSON 模板,轉(zhuǎn)換成前端需要的 JSON 結(jié)構(gòu)
請(qǐng)求映射:編寫(xiě)路由邏輯,映射到對(duì)應(yīng)的 GraphQL 請(qǐng)求和 JSON 模板上
基于這樣的項(xiàng)目開(kāi)發(fā)流程,我們把整個(gè) BFF 層構(gòu)建成了一個(gè)平臺(tái)。開(kāi)發(fā)同學(xué)只需要在平臺(tái)里的三個(gè)表單里輸入上面的內(nèi)容,就可以得到想要的 API 接口。
整合完成后,我們的整體架構(gòu)如下
統(tǒng)一請(qǐng)求入口:BFF 平臺(tái)負(fù)責(zé)對(duì)外部統(tǒng)一的 API 接口
請(qǐng)求映射:根據(jù)請(qǐng)求參數(shù)和內(nèi)部配置的路由規(guī)則,把請(qǐng)求映射到不同的配置模板上
獲取模板信息:?jiǎn)蝹€(gè)配置模板里, 保存著 GraphQL 的 query 語(yǔ)句和 JSON 映射模板
數(shù)據(jù)獲?。菏褂?GraphQL query 語(yǔ)句調(diào)用 GraphQL 網(wǎng)關(guān),獲取數(shù)據(jù)結(jié)果
數(shù)據(jù)轉(zhuǎn)換:調(diào)用模板引擎,進(jìn)行 JSON 結(jié)構(gòu)的轉(zhuǎn)換,并將數(shù)據(jù)返回給調(diào)用方
通過(guò)上述幾個(gè)步驟,我們的 BFF 平臺(tái)可以支持非??焖俚膶?shí)現(xiàn)一個(gè) API 來(lái)對(duì)外提供服務(wù)
BFF 平臺(tái)由后端負(fù)責(zé)開(kāi)發(fā)和維護(hù),保證服務(wù)的性能和穩(wěn)定性。前端主要的工作使用 BFF 平臺(tái)寫(xiě) query 和模板,完成頁(yè)面的數(shù)據(jù)拼裝。通過(guò)這樣的方式,前端和后端都能夠最大化的發(fā)揮自己的擅長(zhǎng)的能力,優(yōu)化團(tuán)隊(duì)研發(fā)效率。
04 GraphQL網(wǎng)關(guān)架構(gòu)及微服務(wù)治理
前面的架構(gòu)里可以看到,我們是把 GraphQL 當(dāng)做一個(gè)網(wǎng)關(guān)來(lái)處理,負(fù)責(zé)對(duì)接底層的微服務(wù)。在一些 GraphQL 應(yīng)用的場(chǎng)景里,隨著接入的業(yè)務(wù)越來(lái)越多,GraphQL 的服務(wù)會(huì)逐步的變成一個(gè)非常龐大的單體應(yīng)用,維護(hù)起來(lái)會(huì)越來(lái)越困難。另外所有的業(yè)務(wù)都聚合到這一個(gè) GraphQL 的出口,可能光 Schema 定義就需要上萬(wàn)行。這樣不論是古董維護(hù)還是使用上都很難進(jìn)行下去,而且與現(xiàn)在主流的微服務(wù)架構(gòu)體系相矛盾
業(yè)界目前最主流的解決方案是 Apollo GraphQL 提供的 GraphQL Federation 功能,并且 Netflix 在此基礎(chǔ)上構(gòu)建了一套 DGS (Domain GraphQL Service) 的架構(gòu)來(lái)進(jìn)行治理的。這里做一個(gè)簡(jiǎn)單的介紹:
每個(gè)領(lǐng)域服務(wù)單獨(dú)構(gòu)建一個(gè)對(duì)應(yīng)的GraphQL領(lǐng)域服務(wù)(DGS)
由集中式的 GraphQL Gateway 借助 Federation 的能力來(lái)負(fù)責(zé)聚合多個(gè) DGS,自動(dòng)生成統(tǒng)一 Schema 對(duì)外提供服務(wù)
但是這樣的做法只是解決了 GraphQL 服務(wù)的單體應(yīng)用的問(wèn)題,最終聚合出來(lái)的 GraphQL Schema 還是可能會(huì)非常的龐大,使用起來(lái)還是會(huì)很困難。而且整個(gè)架構(gòu)其實(shí)是做了 2 層的 GraphQL 處理,一層在 DSG 上,一層在 Gateway 上,會(huì)有一定性能的重復(fù)開(kāi)銷(xiāo),服務(wù)穩(wěn)定性上也有更多的挑戰(zhàn)。
針對(duì)這樣的問(wèn)題,結(jié)合前文提到的注解方式構(gòu)建的 GraphQL Gateway,我們?cè)O(shè)計(jì)了如下的架構(gòu)
針對(duì)每個(gè)領(lǐng)域服務(wù),使用我們的 GraphQL 注解定義一套類(lèi)型和接口,然后用類(lèi)似于 FeignClient 的方式提供給網(wǎng)關(guān)和服務(wù)方分別使用
領(lǐng)域服務(wù)實(shí)現(xiàn)這部分接口,提供 RPC 的能力給到 GraphQL Gateway 使用
接口注冊(cè)到 GraphQL Gateway,網(wǎng)關(guān)會(huì)為每個(gè)領(lǐng)域服務(wù)的接口定義生成一個(gè)定義模塊(Module)。同時(shí)針對(duì)每個(gè)模塊,網(wǎng)關(guān)也生成了對(duì)應(yīng)模塊的 RPC 請(qǐng)求的封裝
業(yè)務(wù)方在使用時(shí),定義一個(gè)業(yè)務(wù)應(yīng)用(Application),選擇這個(gè)應(yīng)用所需要的模塊,網(wǎng)關(guān)自動(dòng)聚合所選擇的模塊,生成該應(yīng)用所對(duì)應(yīng)的 GraphQL Schema。在 query 執(zhí)行時(shí),處理到對(duì)應(yīng)的模塊,會(huì)調(diào)用對(duì)應(yīng)的 RPC 接口訪問(wèn)底層服務(wù)獲取數(shù)據(jù)
業(yè)務(wù)方根據(jù)這個(gè)所生成的Application Schema 來(lái)開(kāi)發(fā)
這樣,GraphQL 的使用方只需要選擇自己關(guān)心的模塊來(lái)生成 Schema 即可。比如我們的網(wǎng)關(guān)現(xiàn)在集成了十幾個(gè)領(lǐng)域,而某個(gè)頁(yè)面只使用到了其中的 3 個(gè),只需要選擇這三個(gè)生成自己的 Schema 使用即可。而另外一個(gè)頁(yè)面可能用到了另外 5 個(gè)領(lǐng)域,也可以單獨(dú)生成 Schema。通過(guò)這樣的方式,可以把 Schema 的大小控制在可控的范圍內(nèi),維護(hù)起來(lái)也相對(duì)容易
另外由于在 RPC 的調(diào)用上減少了一層,而且 GraphQL 的處理都還是集中在網(wǎng)關(guān)內(nèi)部一次性進(jìn)行,在服務(wù)的穩(wěn)定性和性能上的提升相對(duì)更容易一些。
06 應(yīng)用場(chǎng)景
目前我們的最主要的應(yīng)用場(chǎng)景是在我們內(nèi)部的前端低代碼平臺(tái)上。
現(xiàn)在市面上的低代碼平臺(tái)大多數(shù)只考慮了前端的頁(yè)面如何快速生成,而對(duì)于后端的接口的實(shí)現(xiàn)上考慮的很少,一般都是生成模板代碼或者僅限于特殊場(chǎng)景的后端代碼生成。極端的情況需要后端針對(duì)每個(gè)頁(yè)面單獨(dú)再開(kāi)發(fā) API 接口進(jìn)行對(duì)接,這樣對(duì)后端來(lái)說(shuō)工作量其實(shí)更多了。
我們將內(nèi)部的低代碼平臺(tái)和這套 BFF 平臺(tái)進(jìn)行了整合,構(gòu)建了一整套低代碼開(kāi)發(fā)流程,幫助需求方能夠快速應(yīng)用上線。
通過(guò)這樣的整合,構(gòu)建了整個(gè)從前端到后端完整的低代碼平臺(tái),來(lái)實(shí)現(xiàn)業(yè)務(wù)需求的快速上線
以我們現(xiàn)在線上的一個(gè)房?jī)r(jià)頁(yè)面為例子
這個(gè)頁(yè)面由于多端上都存在,而且邏輯有部分差異,以前是寫(xiě)了一個(gè)大而全的接口把所有端的邏輯都合到了一起,存在著維護(hù)困難和性能拖慢等問(wèn)題
而在遷移到了 BFF 平臺(tái)之后,近期針對(duì)不同城市對(duì)頁(yè)面的不同需求的項(xiàng)目,開(kāi)發(fā)工作量相比原來(lái)少了 50%。而且由于切換到 GraphQL 之后可以并行的按需取數(shù)據(jù),頁(yè)面的接口性能也從之前的近 100ms 降低到 20ms 左右。
06 總結(jié)
隨著團(tuán)隊(duì)規(guī)模、業(yè)務(wù)復(fù)雜的逐漸上升,傳統(tǒng) BFF 模式實(shí)際上面臨了一個(gè)代碼可維護(hù)性、性能、個(gè)性化頁(yè)面的不可能三角。針對(duì)這樣的場(chǎng)景,我們構(gòu)建了一套平臺(tái)化的 BFF,結(jié)合 GraphQL 、 JSON 模板以及微服務(wù)治理,來(lái)盡可能的解耦各個(gè)需求間的相互依賴(lài),提升團(tuán)隊(duì)研發(fā)效率,更加高效快速的滿足業(yè)務(wù)需求。
未來(lái)我們會(huì)在以下幾個(gè)方面進(jìn)行更進(jìn)一步的迭代以滿足我們的業(yè)務(wù)需求
二方包和 Schema 的動(dòng)態(tài)更新支持,無(wú)需重啟即可更新 Schema
字段使用審計(jì)和調(diào)用量監(jiān)控,對(duì)于長(zhǎng)時(shí)間無(wú)訪問(wèn)的 query、模板和字段可以提示業(yè)務(wù)方做下線處理
GraphQL 內(nèi)部性能優(yōu)化
支持低代碼平臺(tái)以拖拽的方式自動(dòng)綁定數(shù)據(jù)
參考資料:
graphql.org
Reconciling GraphQL and Thrift at Airbnb
How Netflix Scales its API with GraphQL Federation (Part 1)
How Netflix Scales its API with GraphQL Federation (Part 2)
GraphQL及元數(shù)據(jù)驅(qū)動(dòng)架構(gòu)在后端BFF中的實(shí)踐
作者簡(jiǎn)介:
董菲:58 安居客后端架構(gòu)師,負(fù)責(zé)過(guò)安居客 C 端二手房整體業(yè)務(wù)架構(gòu)迭代,目前主要負(fù)責(zé) 58 安居客 B 端業(yè)務(wù)架構(gòu)。
聯(lián)系客服