15 | 跨平臺(tái)架構(gòu):如何設(shè)計(jì) BFF 架構(gòu)系統(tǒng)?

[toc]

前言

本文來自拉勾網(wǎng)課程整理

首先請(qǐng)你想一想:如果沒有一套靈活的可擴(kuò)展的系統(tǒng)架構(gòu),結(jié)果會(huì)怎樣?

這方面我深有感觸,在我們的App 沒有良好的系統(tǒng)架構(gòu)之前,每一個(gè)微小的改動(dòng)都需要“大動(dòng)干戈”。具體來說,由于強(qiáng)耦合性,每次改動(dòng)我們都需要和各個(gè)業(yè)務(wù)部門商討詳細(xì)的技術(shù)方案;功能開發(fā)完畢后,又要協(xié)調(diào)各個(gè)部門進(jìn)行功能回歸測(cè)試。整個(gè)過程下來,不僅耗費(fèi)太多精力和時(shí)間,還容易在跨部門、跨團(tuán)隊(duì)溝通之間生出許多事來。

而一套良好的系統(tǒng)架構(gòu),不僅僅是一款App的基石,也是整套代碼庫的規(guī)范。有了良好的系統(tǒng)架構(gòu),業(yè)務(wù)功能開發(fā)者就能做到有據(jù)可依,團(tuán)隊(duì)之間的溝通變成十分順暢;各個(gè)功能團(tuán)隊(duì)之間也能并行開發(fā),保證彼此快速迭代,提高效率。

因此,我們?cè)谕苿?dòng)工程化實(shí)踐的同時(shí)也需要不斷優(yōu)化系統(tǒng)架構(gòu)。在2017 年,我和公司同事就設(shè)計(jì)與實(shí)現(xiàn)了一套基于原生技術(shù)的跨平臺(tái)系統(tǒng)架構(gòu),能讓所有開發(fā)者同時(shí)在iOSAndroid 平臺(tái)上工作。

如今這套架構(gòu)經(jīng)過不斷改進(jìn),依然在使用。我們現(xiàn)在開發(fā)的 Moments App,它所用的跨平臺(tái)系統(tǒng)架構(gòu),正是我吸取了當(dāng)初的經(jīng)驗(yàn)與教訓(xùn),使用 BFFMVVM 重新架構(gòu)與實(shí)現(xiàn)的。

這一章,我們主要先聊聊如何使用 BFFbackend for frontend,服務(wù)于前端的后端)來設(shè)計(jì)跨平臺(tái)的系統(tǒng)架構(gòu),以提高可重用性,進(jìn)而提升開發(fā)效率。MVVM 的設(shè)計(jì)與實(shí)現(xiàn),我會(huì)在后面幾章詳細(xì)介紹。

為什么使用 BFF ?

我們的 Moments App 是一款類朋友圈的App,隨著功能的不斷完善,目前幾乎所有 App 的數(shù)據(jù)源都由多個(gè)微服務(wù)所支持。在 Moments App 中,后臺(tái)微服務(wù)包括:

  • 用于用戶管理與鑒權(quán)的用戶服務(wù)
  • 用于記錄朋友關(guān)系的朋友關(guān)系服務(wù)
  • 用于拉黑管理的黑名單服務(wù)
  • 用于記錄每條朋友圈信息的信息服務(wù)
  • 用于頭像管理的頭像服務(wù)
  • 用于點(diǎn)贊管理的點(diǎn)贊服務(wù)等等。
ee67d94276c334a68fd7b1d3276f0020

當(dāng)我們需要呈現(xiàn)朋友圈界面時(shí),App 需要給各個(gè)微服務(wù)發(fā)送請(qǐng)求,然后把返回的信息整理,合并和轉(zhuǎn)換成我們所需要的信息進(jìn)行呈現(xiàn)。

這些網(wǎng)絡(luò)請(qǐng)求的順序和邏輯非常復(fù)雜。有些請(qǐng)求需要串行處理,例如只有完成了用戶服務(wù)的請(qǐng)求以后,才能繼續(xù)其他請(qǐng)求;而有些請(qǐng)求卻可以并行發(fā)送,比如在得到信息服務(wù)的返回結(jié)果以后,可以同時(shí)向頭像服務(wù)和點(diǎn)贊服務(wù)發(fā)送請(qǐng)求。

接著,在得到了所有結(jié)果以后,App 需要整理和合并數(shù)據(jù)的邏輯也非常復(fù)雜,如果請(qǐng)求返回結(jié)果的順序不一致,往往會(huì)導(dǎo)致程序出錯(cuò)。于是,為了解決這一系列的問題,我們引入了 BFF 服務(wù)。

63a6de87bd0fa5ccf7d8e4c289f0df20

BFF 是一個(gè)服務(wù)于不同前端的后臺(tái)服務(wù),所有的前端(比如 iOS, AndroidWeb) 都依賴它。而且 BFF 是一個(gè)整合服務(wù),它負(fù)責(zé)把前端的請(qǐng)求統(tǒng)一分發(fā)到各個(gè)具體的微服務(wù)上,然后把返回?cái)?shù)據(jù)整合在一起統(tǒng)一返回給前端。

可以說,有了 BFF,我們的 App 就不再需要往多個(gè)微服務(wù)發(fā)送請(qǐng)求,也不再需要處理復(fù)雜的并發(fā)請(qǐng)求,這樣就有效減低了復(fù)雜度,避免競(jìng)態(tài)條件等非預(yù)期情況發(fā)生。除此以外, 使用BFF 還有以下好處。

首先,App 僅需依賴一個(gè) BFF 微服務(wù),就能有效地管理 App 對(duì)微服務(wù)的依賴。眾所周知,當(dāng) App 版本發(fā)布以后,我們沒有辦法強(qiáng)迫用戶更新他們?cè)O(shè)備上的 App,如果我們需要變動(dòng)某個(gè)微服務(wù)的地址,原有的 App 將無法訪問新的微服務(wù)地址,但是有了 BFF 以后,我們可以通過 BFF 統(tǒng)一路由到新的微服務(wù)去。

第二,不同的微服務(wù)可能提供不一樣的數(shù)據(jù)傳輸方式,例如有的提供 RESI API,有的提供 gRPC,而有的提供 GraphQL。在沒有 BFF 的情況下,App 端必須實(shí)現(xiàn)各個(gè)技術(shù)棧來訪問各個(gè)微服務(wù)。一旦有了 BFF 以后,App 只需要支持一種傳輸方式,極大減輕移動(dòng)端開發(fā)和維護(hù)成本。

第三,由于 BFF 統(tǒng)一處理所有的數(shù)據(jù),iOSAndroid 兩端都可以得到由 BFF 清理并轉(zhuǎn)換好的數(shù)據(jù),無須在各端重復(fù)開發(fā)一樣的數(shù)據(jù)處理代碼。這極大減少了工作量,讓我們可以把重心放在提高用戶體驗(yàn)上。

ff36c2ac2ca1140afc4f697ee8a29015

第四,BFF 在提升整套系統(tǒng)安全性的同時(shí),提高整體性能。

具體來說,因?yàn)槲覀兊?App 是通過公網(wǎng)連接到后臺(tái)微服務(wù)的,所有微服務(wù)都需要公開給所有外部系統(tǒng)進(jìn)行訪問。這就會(huì)面臨隱私信息暴露等安全問題,比如用戶會(huì)通過 App 獲得本來不應(yīng)該公開的黑名單信息。

但我們引入 BFF 以后,可以為微服務(wù)配置安全規(guī)則(如 AWS 上的 Security Group)只允許 BFF 能訪問,例如上述的黑名單管理服務(wù),就可以設(shè)置除了 BFF 以外不允許任何其他外部系統(tǒng)(包括我們的 App)直接訪問,從而有效保證了隱私信息與公網(wǎng)的隔離。

與此同時(shí), BFF 還可以同步訪問多個(gè)不同的數(shù)據(jù)源,統(tǒng)一管理數(shù)據(jù)緩存,這無疑能有效提升整套系統(tǒng)的性能。

47931f5e06aa3b4051e756c58227bfd6

BFF 的技術(shù)選型——GraphQL

既然 BFF 那么好用,那應(yīng)該怎樣實(shí)現(xiàn)一個(gè) BFF 服務(wù)呢?我經(jīng)過多個(gè)項(xiàng)目的實(shí)踐總結(jié)發(fā)現(xiàn),GraphQL 是目前實(shí)現(xiàn) BFF 架構(gòu)的最優(yōu)方案。
什么是 GraphQL?

具體來說,和 REST API,gRPC 以及 SOAP 相比, GraphQL 架構(gòu)有以下幾大優(yōu)點(diǎn)。

  • GraphQL 允許客戶端按自身的需要通過Query來請(qǐng)求不同數(shù)據(jù)集,而不像 REST APIgRPC 那樣每次都是返回全部數(shù)據(jù),這樣能有效減輕網(wǎng)絡(luò)負(fù)載。
  • GraphQL能減輕為各客戶端開發(fā)單獨(dú) Endpoint 的工作量。比如當(dāng)我們開發(fā) App Clip 的時(shí)候,App Clip 可以在 Query 中以指定子數(shù)據(jù)集的方式來使用和主 App 相同的 Query,而無須重新開發(fā)新 Endpoint。
  • GraphQL 服務(wù)能根據(jù)客戶端的 Query 來按需請(qǐng)求數(shù)據(jù)源,避免無必要的數(shù)據(jù)請(qǐng)求,減輕服務(wù)端的負(fù)載。

下面我們以一個(gè)例子來看看GraphQL 是怎樣處理不同的 Query 的。

假設(shè)我們要開發(fā)一個(gè)顯示某大 V朋友圈的 App Clip,當(dāng)用戶使用 App Clip 時(shí)不需要鑒權(quán),不必查看黑名單,就直接可以看到該大 V 的朋友圈信息,那么我們?cè)谠L問GraphQL 的流程會(huì)就簡(jiǎn)化了(如下圖所示)。

6c1f4f54f77c32efe58adb07581a446a

和我們的主App請(qǐng)求相比,App Clip 不需要顯示點(diǎn)贊信息,返回的結(jié)果就可以精簡(jiǎn)了。而且由于不需要進(jìn)行鑒權(quán),也不需要查詢朋友關(guān)系、黑名單和點(diǎn)贊等信息,BFF 也無須向這些微服務(wù)發(fā)起請(qǐng)求,從而有效減輕了 BFF 服務(wù)的負(fù)載。

另外一方面,和 REST API 相比,GraphQL 的數(shù)據(jù)交換都由 Schema 統(tǒng)一管理,能有效減少由于數(shù)據(jù)類型和可空類型不匹配所導(dǎo)致的問題。

除此之外,GraphQL 還能減輕版本管理的工作量。因?yàn)?GraphQL 能支持返回不同數(shù)據(jù)集,從而無須像 REST API 那樣為每個(gè)新功能不斷地更新 Endpoint 的版本號(hào)。

如何使用 GraphQL 實(shí)現(xiàn) BFF

既然我們確定了 GraphQL,那需要選擇一個(gè)服務(wù)框架來幫我們實(shí)現(xiàn)。具體怎么實(shí)現(xiàn)呢?為了方便演示,我選擇了 Apollo Serve。

Apollo Serve 是基于 Node.jsGraphQL 服務(wù)器,目前非常流行。使用它,可以很方便地結(jié)合 ExpressWeb 服務(wù),而且還可以部署到亞馬遜Lambda,微軟 Azure FunctionsServerless 服務(wù)上。

再加上 Apollo Serve 在我們公司的生產(chǎn)環(huán)境上使用多年,一直穩(wěn)定地支撐著 App 正常運(yùn)行,因?yàn)楸容^熟悉,所以我就選了它。

下面一起看看具體怎么做。

第一步,使用 GraphQL,我們先要為前后端傳遞的數(shù)據(jù)定義 schema。 在這里我寫了 Moment 類型的部分 Schema 定義。比如在 Moment 類型里,我定義了 id,type,titleuser details 等屬性,其中 user details 屬性的類型是 User Details,它定義了 nameavatar 等屬性。其的代碼示例如下所示。

enum MomentType {
  URL
  PHOTOS
}
type Moment {
  id: ID!
  userDetails: UserDetails!
  type: MomentType!
  title: String # nullable
  photos: [String!]! # non-nullable but can be empty
}
type UserDetails {
  id: ID!
  name: String!
  avatar: String!
  backgroundImage: String!
}

如果你想要查看完整定義,可以點(diǎn)擊倉(cāng)庫中查看。

GraphQL 支持枚舉類型,比如上面的MomentType就是一個(gè)枚舉類型,它只有兩個(gè)值URLPHOTOS,在數(shù)據(jù)傳輸過程中,它們是通過字符串傳送給前端的。

Moment是一個(gè)類型定義,在 Swift 中可以對(duì)應(yīng)成struct,而在 Kotlin 中則對(duì)應(yīng)為data class。這個(gè)類型有id、userDetails等屬性。這些屬性可以是基礎(chǔ)數(shù)據(jù)類型,如String、ID、Int等;也可以是自定義類型,如自定義的UserDetails

當(dāng)數(shù)據(jù)類型后面有!時(shí),表示該屬性不能為null。這其中需要注意一點(diǎn),那就是!在數(shù)組定義里面的使用。比如photos: [String!]!,表示該數(shù)組不能為null,而且不能存放值為null的數(shù)據(jù)。而photos: [String!]則表示photos數(shù)組自身可能為null,但還是不能存放值為null的數(shù)據(jù) 。再來看photos: [String]!,這表示photos數(shù)組自己不可以為null, 但是可以放值為null的數(shù)據(jù)。

第二步,有了 Schema 的定義以后,接下來我們可以定義 Query 和 Mutation,以便為 App 提供查詢和更新的接口。

type Query {
  getMomentsDetailsByUserID(userID: ID!): MomentsDetails!
}

這表示該 GraphQL 服務(wù)提供一個(gè)名叫getMomentsDetailsByUserIDQuery,該Query接受userID作為入口參數(shù),并返回MomentsDetails。

一般 Query只能用于查詢,如果要更新,則需要使用Mutation,下面是一個(gè) Mutation 的定義

type Mutation {
  updateMomentLike(momentID: ID!, userID: ID!, isLiked: Boolean!): MomentsDetails!
}

其實(shí) Mutation 是一個(gè)會(huì)更新狀態(tài)的Query,因?yàn)樵诟潞筮€是可以返回?cái)?shù)據(jù)的。例如上例中updateMomentLike接受了momentID、userIDisLiked作為入口參數(shù),在更新狀態(tài)后也可以返回MomentsDetails。

第三步,有了以上的定義以后,我們可以借助 resolver 來查詢或者更新數(shù)據(jù)。

const resolvers = {
  Query: {
    getMomentsDetailsByUserID: (_, {userID}) => momentsDetails,
  },
  Mutation: {
    updateMomentLike: (_, {momentID, userID, isLiked}) => {
      for (const i in momentsDetails.moments) {
        if (momentsDetails.moments[i].id === momentID) {
          if (momentsDetails.moments[i].isLiked === isLiked) {
            break
          }
          momentsDetails.moments[i].isLiked = isLiked;
          if (isLiked) {
            const likedUserDetails = getUserDetailsByID(userID)
            momentsDetails.moments[i].likes.push(likedUserDetails);
          } else {
            // remove the item for that user
            momentsDetails.moments[i].likes = momentsDetails.moments[i].likes.filter((item) => item.id !== userID);
          }
          break;
        }
      }
      return momentsDetails;
    }
  }
};


resolvers的大致邏輯是,在 get Moments Details By User ID 查詢里面,直接把momentsDetails的數(shù)據(jù)返回。在 update moment like 更新里面,我們更新了momentsDetails 的 is Liked屬性來表示用戶是否點(diǎn)贊。在 Moments AppBFF中,我們維護(hù)了一個(gè)內(nèi)存數(shù)據(jù)庫,而在真實(shí)生產(chǎn)環(huán)境中,可以訪問 MySQL、MongoDB 來直接存儲(chǔ)數(shù)據(jù),或者通過其他微服務(wù)來橋接數(shù)據(jù)庫的訪問。

到此為止,我們就通過GraphQL實(shí)現(xiàn)了一個(gè) BFF。 注意,這只是一個(gè)例子,并不是每個(gè) BFF 都必須通過 Apollo Server 以及 Node.js來實(shí)現(xiàn)。你可以根據(jù)所做團(tuán)隊(duì)成員的技能來挑選適合你們的技術(shù)棧。

比如,Kotlin 是一個(gè)不錯(cuò)的選擇,因?yàn)榇蟛糠?Android開發(fā)者都熟悉Kotlin語言,而且 Kotlin 還可以完美兼容JVM。特別JVM生態(tài)非常發(fā)達(dá),我們可以利用Kotlin 和基于JVM的開源庫構(gòu)建穩(wěn)定的BFF 方案。

總結(jié)

這一章我介紹了如何使用 BFF 來設(shè)計(jì)跨平臺(tái)的系統(tǒng)架構(gòu),以及如何使用 GraphQL實(shí)現(xiàn) BFF。雖然GraphQL 有眾多優(yōu)點(diǎn),但并非十全十美,甚至可以說,世界上并沒有完美的技術(shù)。所以,在使用 GraphQL過程中,我們需要注意以下兩點(diǎn)。

  • 在定義 Schema 的過程中,需要前后臺(tái)開發(fā)者共同協(xié)商溝通,特別要注意nullable類型的處理,如果前端定義有誤,很容易引起 App的崩潰。
  • GraphQL 通常使用 HTTP POST請(qǐng)求,但有些 CDN (content delivery network,內(nèi)容分發(fā)網(wǎng)絡(luò))對(duì) POST 緩存支持不好,當(dāng)我們把 GraphQL 的請(qǐng)求換成 GET 時(shí),整個(gè) Query 會(huì)變成 JSON-encoded字符串并放在Query String里面進(jìn)行發(fā)送。此時(shí),要特別注意該 Query String 的長(zhǎng)度不要超過 CDN所支持的長(zhǎng)度限制(比如Akamai支持最長(zhǎng)的 URL 是 8892 字節(jié)),否則請(qǐng)求將會(huì)失敗。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容