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

[toc]

前言

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

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

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

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

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

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

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

為什么使用 BFF ?

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

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

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

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

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

63a6de87bd0fa5ccf7d8e4c289f0df20

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

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

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

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

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

ff36c2ac2ca1140afc4f697ee8a29015

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

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

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

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

47931f5e06aa3b4051e756c58227bfd6

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

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

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

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

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

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

6c1f4f54f77c32efe58adb07581a446a

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

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

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

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

既然我們確定了 GraphQL,那需要選擇一個服務(wù)框架來幫我們實現(xiàn)。具體怎么實現(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 正常運行,因為比較熟悉,所以我就選了它。

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

第一步,使用 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!
}

如果你想要查看完整定義,可以點擊倉庫中查看。

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

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

當(dāng)數(shù)據(jù)類型后面有!時,表示該屬性不能為null。這其中需要注意一點,那就是!在數(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ù)提供一個名叫getMomentsDetailsByUserIDQuery,該Query接受userID作為入口參數(shù),并返回MomentsDetails。

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

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

其實 Mutation 是一個會更新狀態(tài)的Query,因為在更新后還是可以返回數(shù)據(jù)的。例如上例中updateMomentLike接受了momentIDuserIDisLiked作為入口參數(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屬性來表示用戶是否點贊。在 Moments AppBFF中,我們維護(hù)了一個內(nèi)存數(shù)據(jù)庫,而在真實生產(chǎn)環(huán)境中,可以訪問 MySQL、MongoDB 來直接存儲數(shù)據(jù),或者通過其他微服務(wù)來橋接數(shù)據(jù)庫的訪問。

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

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

總結(jié)

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

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

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

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