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

[toc]

前言

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

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

這方面我深有感觸,在我們的App 沒有良好的系統(tǒng)架構(gòu)之前魁衙,每一個微小的改動都需要“大動干戈”报腔。具體來說,由于強(qiáng)耦合性剖淀,每次改動我們都需要和各個業(yè)務(wù)部門商討詳細(xì)的技術(shù)方案纯蛾;功能開發(fā)完畢后,又要協(xié)調(diào)各個部門進(jìn)行功能回歸測試纵隔。整個過程下來翻诉,不僅耗費(fèi)太多精力和時間炮姨,還容易在跨部門、跨團(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ù)
  • 用于點(diǎn)贊管理的點(diǎn)贊服務(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ù)和點(diǎn)贊服務(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ā)布以后,我們沒有辦法強(qiáng)迫用戶更新他們設(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)點(diǎn)。

  • GraphQL 允許客戶端按自身的需要通過Query來請求不同數(shù)據(jù)集俩莽,而不像 REST APIgRPC 那樣每次都是返回全部數(shù)據(jù)旺坠,這樣能有效減輕網(wǎng)絡(luò)負(fù)載。
  • GraphQL能減輕為各客戶端開發(fā)單獨(dú) 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 不需要顯示點(diǎn)贊信息,返回的結(jié)果就可以精簡了句灌。而且由于不需要進(jìn)行鑒權(quán)夷陋,也不需要查詢朋友關(guān)系、黑名單和點(diǎ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 正常運(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)擊倉庫中查看蹂午。

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。這其中需要注意一點(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ù)提供一個名叫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接受了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ù)了一個內(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)點(diǎn)摩桶,但并非十全十美,甚至可以說帽揪,世界上并沒有完美的技術(shù)硝清。所以,在使用 GraphQL過程中转晰,我們需要注意以下兩點(diǎn)芦拿。

  • 在定義 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)系作者
  • 序言:七十年代末门躯,一起剝皮案震驚了整個濱河市淆党,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖染乌,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件山孔,死亡現(xiàn)場離奇詭異,居然都是意外死亡荷憋,警方通過查閱死者的電腦和手機(jī)台颠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勒庄,“玉大人串前,你說我怎么就攤上這事∈当危” “怎么了荡碾?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長局装。 經(jīng)常有香客問我坛吁,道長,這世上最難降的妖魔是什么贼邓? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任阶冈,我火速辦了婚禮,結(jié)果婚禮上塑径,老公的妹妹穿的比我還像新娘女坑。我一直安慰自己,他們只是感情好统舀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布匆骗。 她就那樣靜靜地躺著,像睡著了一般誉简。 火紅的嫁衣襯著肌膚如雪碉就。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天闷串,我揣著相機(jī)與錄音瓮钥,去河邊找鬼。 笑死烹吵,一個胖子當(dāng)著我的面吹牛碉熄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肋拔,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锈津,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凉蜂?” 一聲冷哼從身側(cè)響起幢妄,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豁鲤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體错览,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年蛉顽,在試婚紗的時候發(fā)現(xiàn)自己被綠了蝗砾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡携冤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出闲勺,到底是詐尸還是另有隱情曾棕,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布菜循,位于F島的核電站翘地,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏癌幕。R本人自食惡果不足惜衙耕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勺远。 院中可真熱鬧橙喘,春花似錦、人聲如沸胶逢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽初坠。三九已至和簸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碟刺,已是汗流浹背锁保。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留半沽,地道東北人爽柒。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像抄囚,于是被迫代替她去往敵國和親霉赡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容