微服務平臺下基於 GraphQL 構建 BFF 的思考

fantasticbaby發表於2021-11-30

寫在開頭的部分,本文的契機是最近我們組同事在客戶端實現了一套 Redux,對某個業務域的功能進行重構設計,iOS、Android 都遵循這套規則,即 Redux。為什麼需要客戶端去實現一套 Redux?商品模組業務邏輯非常負責,商品基礎資訊非常多,比如多規格、多單位、價格、庫存等資訊,還有對應的門店、網店模型,還有各種行業能力開關控制,早期的實現有 Rx 的角色,導致程式碼邏輯較為複雜,資料流動比較亂。架構設計、程式碼維護各方面來看都不是很優雅,加上最近有大的業務調整,中臺的同學用 Redux + 單向資料流的方式重構了業務。

另一個下線經常請求閘道器資料,iOS、Android 各自去宣告 DTO Model,然後解析資料,生成客戶端需要的資料,這樣“重複”的行為經常發生,所以索性用 TS + 指令碼,統一做掉了閘道器資料模型自動生成 iOS、Android 模型的能力。但是在討論框架設計的時候回類比 React Redux、Flutter Fish Redux、Vuex 等,還會聊到單向資料流、雙向資料流,但是有些同學的理解就是錯誤的。所以本文第一部分「糾錯題」部分就是講清楚前端幾個關鍵概念。

後面的部分按照邏輯順序講一下:微前端 -> BFF/閘道器 -> 微服務。

一、糾錯題

1. Vue 是雙向資料流嗎?

如果回答“是”,那麼你應該沒有搞清楚“雙向資料流”與“雙向繫結”這2個概念。其實,準確來說兩者不是一個維度的東西,單向資料流也可以實現雙向繫結。

其實,你要是仔細看過 Vue 的官方文件,那麼官方就已經說明 Vue 其實是 One-Way Data Flow。下面這段話來自官方

首先明確下,我們說的資料流就是元件之間的資料流動。Vue 中所有的 props 都使得其父子 props 之間形成一個單向下行繫結:父級 props 的更新迴向下流動到子元件中,但反過來不行,這樣防止從子元件意外更改其父元件的狀態,從而導致你的應用資料流難以理解。

此外,每次父級元件發生變更時,子元件中所有的 props 都將會被重新整理為最新的值,這意味著你不應該在子元件內去修改 prop,假如你這麼做了,瀏覽器會在控制檯中輸出警告。Vue 和 React 修改 props 報錯如下:

2. React 是 MVVM 嗎?

不直接回答這個問題,我們先來聊幾個概念,這幾個概念弄清楚了,問題的答案也就呼之欲出了。

2.1 單向繫結、雙向繫結的區別?

講 MVVM 一定要講 binder 這個角色。也就是單向繫結和雙向繫結。

Vue 支援單向繫結和雙向繫結。單向繫結其實就是 Model 到 View 的關聯,比如 v-bind。雙向繫結就是 Model 的更新會同步到 View,View 上資料的變化會自動同步到 Model,比如 v-model.

v-model 其實是語法糖,因為 Vue 是 tamplate,所以在經過 webpack ast 解析之後,tamplate 會變為 render,v-model 變為 v-bind 和 v-on

可以用單向繫結很簡單地可以實現雙向繫結的效果,就是 one-way binding + auto event binding。如下

其實看了原始碼就會發現(Vue3 Proxy),單向繫結和雙向繫結的區別在於,雙向繫結把資料變更的操作部分,由框架內部實現了,呼叫者無須感知。

2.2 Redux

使用 React Redux 一個典型的流程圖如下

2019-06-26-Redux-Structures.png

假如我們把 action 和 dispatcher 的實現隱藏在框架內部,這個圖可以簡化為

假如進一步,我們將互相手動通知的機制再隱藏起來,可以簡化為

你仔細看,是不是就是 MVVM?那問題的答案很明顯了,React 不是 MVVM。

3. Vue 是 MVVM 嗎

官方說了“雖然沒有完全遵循 MVVM 的思想,但是受到了 MVVM 的啟發。所以 Debug 的時候經常看到 VM” Model 的變動會觸發 View 的更新,這一點體現在 V-bind 上 而 View 的變更即使同步到 Model 上,體現在 V-model 上 上述都遵循 MVVM,但是 Vue 提供了 Ref 屬性,允許使用 ref 拿到 Dom,從而直接操作 Dom 的一些樣式或者屬性,這一點打破了 MVVM 的規範

4. 單向繫結和雙向繫結使用場景

我們再來思考一個問題,為什麼 Vue 要移除 .sync, Angular 要增加 < 來實現單向繫結?

當應用變得越來越大,雙向資料流會帶來很多的不可預期的結果,這讓 debug、除錯、測試都變得複雜。

所以 Vue、Angular 都意識到這一點,所以移除了 .sync,支援單向繫結。

正面聊聊問題:

單向繫結,帶來的單向資料流,帶來的好處是所有狀態變化都可以被記錄、跟蹤,狀態變化必須通過手動呼叫通知,源頭可追溯,沒有“暗箱操作”。同時元件資料只有唯一的入口和出口,使得程式更直觀更容易理解,有利於應用的可維護性。缺點是實現同樣的需求,程式碼量會上升,資料的流轉過程變長。同時由於對應用狀態獨立管理的嚴格要求(單一的全域性store),在處理區域性狀態較多的場景時(如使用者輸入互動較多的“富表單型”應用)會顯得特別繁瑣。

雙向繫結優點是在表單互動較多的場景下,會簡化大量業務無關的程式碼。缺點就是由於都是“暗箱操作”,無法追蹤區域性狀態的變化,潛在的行為太多也增加了出錯時 debug 的難度。同時由於元件資料變化來源入口變得可能不止一個,程式設計師水平參差不齊,寫出的程式碼很容易將資料流轉方向弄得紊亂,整個應用的質量就不可控。

總結:單向繫結跟雙向繫結在功能上基本上是互補的,所以我們可以在合適的場景下使用合適的手段。比如在 UI 控制元件中(通常是類表單操作),可以使用雙向的方式繫結資料;而其他場景則使用單向繫結的方式

二、 微前端

上面提到的 Vue、React、單向資料流、雙向資料流都是針對單個應用去組織和設計工程的,但是當應用很大的時候就會存在一些問題。引出今天的一個主題,“微前端”

客戶端的同學都知道,我們很早就在使用元件化、模組化來拆分程式碼和邏輯。那麼你可以說出到底什麼是元件、什麼是模組?

元件就是把重複的程式碼提取出來合併成為一個個元件,元件強調的是重用,位於系統最底層,其他功能都依賴於元件,獨立性強

模組就是分屬同一功能/業務的程式碼進行隔離成獨立的模組,可以獨立執行,以頁面、功能等維度劃分為不同模組,位於業務架構層。

這些概念在前端也是如此,比如寫過 Vue、React 的都知道,對一個頁面需要進行元件的拆分,一個大頁面由多個 UI 子元件組成。模組也一樣,前端利用自執行函式(IIFE)來實現模組化,成熟的 CommonJS、AMD、CMD、UMD 規範等等。比如 iOS 側利用模組化來實現了商品、庫存、開單等業務域的拆分,也實現了路由庫、網路庫等基礎能力模組。而前端更多的是利用模組化來實現名稱空間和程式碼的可重用性

其實,看的出來,前端、客戶端實行的模組化、元件化的目標都是分治思想,更多是站在程式碼層面進行拆分、組織管理。但是隨著前端工程的越來越重,傳統的前端架構已經很難遵循“敏捷”的思想了。

1. 什麼是微前端

關心技術的人聽到微前端,立馬會想起微服務。微服務是面向服務架構的一種變體,把應用程式設計為一些列鬆耦合的細粒度服務,並通過輕量級的通訊協議組織起來。越老越重的前端工程面臨同樣的問題,自然而然就將微服務的思想借鑑到了前端領域。

言歸正傳,微前端(Micro-Frontends)是一種類似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變為多個小型前端應用聚合為一的應用。各個前端應用還可以獨立執行、獨立開發、獨立部署。微前端不是單純的前端框架或者工具,而是一套架構體系

2. 特點

2.1 技術無關

拋兩個場景,大家思考一下:

  • 你新入職一家公司,老闆扔給你一個 5 年陳的專案,需要你在這個專案上持續迭代加功能。
  • 你們起了一個新專案,老闆看慣了前端的風起雲湧技術更迭,只給了架構上的一個要求:"如何確保這套技術方案在 3~5 年內還葆有生命力,不會在 3、5 年後變成又一個遺產專案?"

第一個場景我們初步一想,可以啊,我只需要把新功能用 React/Vue 開發,反正他們都只是 UI library,給我一個Dom 節點我想怎麼渲染怎麼渲染。但是你有沒有考慮過這只是浮在表層的檢視實現,沉在底下的工程設施呢?我要怎麼把這個用 React 寫的元件打出一個包,並且整合到原來的用 ES5 寫的程式碼裡面?或者我怎麼讓 Webpack 跟 之前的 Grunt 能和諧共存一起友好的產出一個符合預期的 Bundle?

第二個場景,你如何確保技術棧在 3~5 年都葆有生命力?別說跨框架了,就算都是 React,15 跟 16 都是不相容的,hooks 還不能用於 Class component 呢我說什麼了?還有打包方案,現在還好都預設 Webpack,那 Webpack 版本升級呢,每次都跟進嗎?別忘了還有 Babel、less、typescript 諸如此類呢?別說 3 年,有幾個人敢保證自己的專案一年後把所有依賴包升級到最新還能跑起來?

為什麼舉這兩個場景呢,因為我們去統計一下業界關於”微前端“發過聲的公司,會發現 adopt 微前端的公司,基本上都是做 ToB 軟體服務的,沒有哪家 ToC 公司會有微前端的訴求(有也是內部的中後臺系統),為什麼會這樣?很簡單,因為很少有 ToC 軟體活得過 3 年以上的。而對於 ToB 應用而言,3~5 年太常見了好嗎!去看看阿里雲最早的那些產品的控制檯,去看看那些電信軟體、銀行軟體,哪個不是 10 年+ 的壽命?企業軟體的升級有多痛這個我就不多說了。所以大部分企業應用都會有一個核心的訴求,就是如何確保我的遺產程式碼能平滑的遷移,以及如何確保我在若干年後還能用上時下熱門的技術棧?

如何給遺產專案續命,才是我們對微前端最開始的訴求。很多開發可能感受不深,畢竟那些一年掙不了幾百萬,沒什麼價值的專案要麼是自己死掉了,要麼是扔給外包團隊維護了,但是要知道,對很多做 ToB 領域的中小企業而言,這樣的系統可能是他們安身立命之本,不是能說扔就扔的,他們承擔不了那麼高的試錯成本。

甚至新的業務,每個技術團隊應該可以根據自己團隊成員的技術儲備、根據業務特點選擇合適的技術棧,而不需要特別關心其他團隊的情況,也就是 A 團隊可以用 React,B 團隊可以用 Vue,C 團隊甚至可以用 Angular 去實現。

2.2 簡單、鬆耦合的程式碼庫

比起一整塊的前端工程來說,微前端架構下的程式碼庫更小更容易開發、維護。此外,更重要的是避免模組間不合理的隱式耦合造成的複雜度上升。通過界定清晰的應用邊界來降低意外耦合的可能性,增加子應用間邏輯耦合的成本,促使開發者明確資料和事件在應用程式中的流向

2.3 增量升級

理想的程式碼自然是模組清晰、依賴明確、易於擴充套件、便於維護的……然而,實踐中出於各式各樣的原因:

  • 歷史專案,祖傳程式碼
  • 交付壓力,當時求快
  • 就近就熟,當時求穩……

總存在一些不那麼理想的程式碼:

  • 技術棧落後,甚至強行混用多種技術棧
  • 耦合混亂,不敢動,牽一髮何止動全身
  • 重構不徹底,重構-爛尾,換個姿勢重構-又爛尾……

而要對這些程式碼進行徹底重構的話,最大的問題是很難有充裕的資源去大刀闊斧地一步到位,在逐步重構的同時,既要確保中間版本能夠平滑過渡,同時還要持續交付新特性:

所以,為了實施漸進式重構,我們需要一種增量升級的能力,先讓新舊程式碼和諧共存,再逐步轉化舊程式碼,直到整個重構完成

這種增量升級的能力意味著我們能夠對產品功能進行低風險的區域性替換,包括升級依賴項、更替架構、UI 改版等。另一方面,也帶來了技術選型上的靈活性,有助於新技術、新互動模式的實驗性試錯

2.4 獨立部署

獨立部署的能力在微前端體系中至關重要,能夠縮小變更範圍,進而降低相關風險

因此,每個微前端都應具備有自己的持續交付流水線(包括構建、測試並部署到生產環境),並且要能獨立部署,不必過多考慮其它程式碼庫和交付流水線的當前狀態:

假如 A、B、C 三個 Biz 存在三個系統,三者可以獨立部署,最後由主系統去整合釋出應用。這樣子更加獨立靈活。想想以前的單體應用,比如一個複雜的業務應用,很可能在 B 構建的時候沒有通過 lint 導致整個應用失敗掉,這樣既浪費了構建 A 的時間,又讓整個應用的構建變為序列,低效。

2.5 團隊自治

除了程式碼和釋出週期上的解耦之外,微前端還有助於形成獨立的團隊,由不同的團隊各自負責一塊獨立的產品功能,最好根據 Biz 去劃分,由於程式碼都是獨立的,所以基於 Biz 的前端業務團隊,可以設計思考,提供更加合理的介面和能力。甚至可以抽象更多的基礎能力,按照業務思考提供更加完備的能力,也就是團隊更加自治。

3 如何實現

微前端主要採用的是組合式應用路由方案,該方案的核心思想是“主從”(玩過 Jenkins 的同學是不是很耳熟),即包括一個基座“MainApp “ 應用和若干個微應用(MircroApp),基座大多采用的是一個前端 SPA 專案,主要負責應用註冊、路由對映、訊息下發等,而微應用是獨立的前端專案,這些專案不限於採用的具體技術(React、Vue、Angular、Jquery)等等,每個微應用註冊到基座中,由基座進行管理,即使沒有基座,這些微應用都可以單獨訪問,如下:

一個微應用框架需要解決的問題是:

  • 路由切換的分發問題

    作為微前端的基座應用,是整個應用的入口,負責承載當前微應用的展示和對其他路由微應用的轉發,對於當前微應用的展示,一般是由以下幾步構成:

    1. 作為一個SPA的基座應用,本身是一套純前端專案,要想展示微應用的頁面除了採用iframe之外,要能先拉取到微應用的頁面內容, 這就需要遠端拉取機制
    2. 遠端拉取機制通常會採用fetch API來首先獲取到微應用的HTML內容,然後通過解析將微應用的JavaScript和CSS進行抽離,採用eval方法來執行JavaScript,並將CSS和HTML內容append到基座應用中留給微應用的展示區域,當微應用切換走時,同步解除安裝這些內容,這就構成的當前應用的展示流程。
    3. 當然這個流程裡會涉及到CSS樣式的汙染以及JavaScript對全域性物件的汙染,這個涉及到隔離問題會在後面討論,而目前針對遠端拉取機制這套流程,已有現成的庫來實現,可以參考 import-html-entrysystem.js

    對於路由分發而言,以採用vue-router開發的基座SPA應用來舉例,主要是下面這個流程:

    1. 當瀏覽器的路徑變化後,vue-router會監聽hashchange或者popstate事件,從而獲取到路由切換的時機。
    2. 最先接收到這個變化的是基座的router,通過查詢註冊資訊可以獲取到轉發到那個微應用,經過一些邏輯處理後,採用修改hash方法或者pushState方法來路由資訊推送給微應用的路由,微應用可以是手動監聽hashchange或者popstate事件接收,或者採用React-router,vue-router接管路由,後面的邏輯就由微應用自己控制。
  • 主應用、微應用的隔離問題

    應用隔離問題主要分為主應用和微應用,微應用和微應用之間的JavaScript執行環境隔離,CSS樣式隔離,我們先來說下CSS的隔離。

    CSS隔離:當主應用和微應用同屏渲染時,就可能會有一些樣式會相互汙染,如果要徹底隔離CSS汙染,可以採用CSS Module 或者名稱空間的方式,給每個微應用模組以特定字首,即可保證不會互相干擾,可以採用webpack的postcss外掛,在打包時新增特定的字首。

    而對於微應用與微應用之間的CSS隔離就非常簡單,在每次應用載入時,將該應用所有的link和style 內容進行標記。在應用解除安裝後,同步解除安裝頁面上對應的link和style即可。

    JavaScript隔離:每當微應用的JavaScript被載入並執行時,它的核心實際上是對全域性物件Window的修改以及一些全域性事件的改變,例如jQuery這個js執行後,會在Window上掛載一個window.$物件,對於其他庫React,Vue也不例外。為此,需要在載入和解除安裝每個微應用的同時,儘可能消除這種衝突和影響,最普遍的做法是採用沙箱機制(SandBox)。

    沙箱機制的核心是讓區域性的JavaScript執行時,對外部物件的訪問和修改處在可控的範圍內,即無論內部怎麼執行,都不會影響外部的物件。通常在Node.js端可以採用vm模組,而對於瀏覽器,則需要結合with關鍵字和window.Proxy物件來實現瀏覽器端的沙箱。

  • 通訊問題

    應用間通訊有很多種方式,當然,要讓多個分離的微應用之間要做到通訊,本質上仍離不開中間媒介或者說全域性物件。所以對於訊息訂閱(pub/sub)模式的通訊機制是非常適用的,在基座應用中會定義事件中心Event,每個微應用分別來註冊事件,當被觸發事件時再有事件中心統一分發,這就構成了基本的通訊機制,流程如下圖:

目前開源了很多微前端框架,比如 qiankun ,感興趣的可以去看看原始碼

4. 是否採用微前端

為什麼要做微前端?或者說微前端解決了什麼問題?也就回答了是否需要採用微前端。微前端的鼻祖 Single-spa 最初要解決的問題是,在老專案中使用新的技術棧。現階段螞蟻金服微前端框架 Qiankun 所宣告的微前端要解決的另一個主要問題是,巨石工程所面臨的維護困難喝協作開發困難的問題。如果工程面臨這兩方面的問題,我覺得微前端就可以試試了。你們覺得呢?

三、微服務平臺下基於 GraphQL 構建 BFF 的思考

1. 大前端架構演進

我們來講一個故事

1. V1

假設早期2011年一家電商公司完成了單體應用的拆分,後端服務已經 SOA 化,此時應用的形態僅有 Web 端,V1架構圖如下所示:

V1

2. V2

時間來到了2012年初,國內颳起了一陣無線應用的風,各個大廠都開始了自己的無線應用開發。為了應對該需求,架構調整為 V2,如下所示

V1

這個架構存在一些問題:

  • 無線 App 和內部的微服務強耦合,任何一側的變化都可能對另一側造成影響
  • 無線 App 需要知道內部服務細節
  • 無線 App 端需要做大量的聚合裁剪和適配邏輯

    聚合:某一個頁面可能同時需要呼叫好幾個後端 API 進行組合,比如商品詳情頁所需的資料,不能一次呼叫完成

    裁剪:後端提供的基礎服務比較通用,返回的 Payload 比較大,欄位太多,App 需要根據裝置和業務需求,進行欄位裁剪使用者的客戶端是Android還是iOS,是大屏還是小屏,是什麼版本。再比如,業務屬於哪個行業,產品形態是什麼,功能投放在什麼場景,面向的使用者群體是誰等等。這些因素都會帶來面向端的功能邏輯的差異性。後端在微服務和領域驅動設計(不在本文重點講解範疇)的背景下,各個服務提供了當前 Domain 的基礎功能,比如商品域,提供了商品新增、查詢、刪除功能,列表功能、詳情頁功能。

    但是在商品詳情頁的情況下,資料需要呼叫商品域、庫存域、訂單域的能力。客戶端需要做大量的網路介面處理和資料處理。

    適配:一些常見的適配常見就是格式轉換,比如有些後端服務比較老,提供 SOAP/XML 資料,不支援 JSON,這時候就需要做一些適配程式碼

  • 隨著使用型別的增多(iOS Phone/Pad、Android Phone/Pad、Hybrid、小程式),聚合裁剪和適配的邏輯的開發會造成裝置端的大量重複勞動
  • 前端比如 iOS、Android、小程式、H5 各個 biz 都需要業務資料。最早的設計是後端介面直出。這樣的設計會導致出現一個問題,因為 biz1 因為迭代發版,造成後端改介面的實現。 Biz2 的另一個需求又會讓服務端改設計實現,造成介面是面向業務開發的。不禁會問,基礎服務到底是面向業務介面開發的嗎?這和領域驅動的設計相背離

在這樣的背景下誕生了 BFF(Backend For Frontend) 層。各個領域提供基礎的資料模型,各個 biz 存在自己的 BFF 層,按需取查詢(比如商品基礎資料200個,但是小程式列表頁只需要5個),在比如商品詳情頁可能需要商品資料、訂單資料、評價資料、推薦資料、庫存資料等等,最早的設計該詳情頁可能需要請求6個介面,這對於客戶端、網頁來說,體驗很差,有個新的設計,詳情頁的 BFF 去動態組裝呼叫介面,一個 BFF 介面就組裝好了資料,直接返回給業務方,體驗很好

3. V2.1

V2 架構問題太多,沒有開發實施。為解決上述問題,在外部裝置和內部微服務之間引入一個新的角色 Mobile BFF。BFF 也就是 Backend for Frontend 的簡稱,可以認為是一種適配服務,將後端的微服務進行適配(主要包括聚合裁剪和格式適配等邏輯),向無線端裝置暴露友好和統一的 API,方便無線裝置接入訪問後端服務。V2.1 服務架構如下

V1

這個架構的優勢是:

  • 無線 App 和內部服務不耦合,通過引入 BFF 這層簡介,使得兩邊可以獨立變化:

    • 後端如果發生變化,通過 BFF 遮蔽,前端裝置可以做到不受影響
    • 前端如果發生變化,通過 BFF 遮蔽,後端微服務可以暫不變化
    • 當無線端有新的需求的時候,通過 BFF 遮蔽,可以減少前後端的溝通協作開銷,很多需求由前端團隊在 BFF 上就可以自己搞定
  • 無線 App 只需要知道 Mobile BFF 的地址,並且服務介面是統一的,不需要知道內部複雜的微服務地址和細節
  • 聚合裁剪和適配邏輯在 Mobile BFF 上實現,無線 App 端可以簡化瘦身

4. V3

V2.1 架構比較成功,實施後較長一段時間支援了公司早期無線業務的發展,隨著業務量暴增,無線研發團隊不斷增加,V2.1 架構的問題也被暴露了出來:

  • Mobile BFF 中不僅有各個業務線的聚合/裁剪/適配和業務邏輯,還引入了很多橫跨切面的邏輯,比如安全認證,日誌監控,限流熔斷等,隨著時間的推移,程式碼變得越來越複雜,技術債越來越多,開發效率不斷下降,缺陷數量不斷增加
  • Mobile BFF 叢集是個失敗單點,嚴重程式碼缺陷將導致流量洪峰可能引起叢集當機,所有無線應用都不可用

為了解決上述偽命題,決定在外部裝置和 BFF 之間架構一個新的層,即 Api Gateway,V3 架構如下:

V1

新的架構 V3 有如下調整:

  • BFF 按團隊或者業務進行解耦拆分,拆分為若干個 BFF 微服務,每個業務線可以並行開發和交付各自負責的 BFF 微服務
  • 官關(一般由獨立框架團隊負責運維)專注橫跨切面功能,包括:

    • 路由,將來自無線裝置的請求路由到後端的某個微服務 BFF 叢集
    • 認證,對涉及敏感資料的 Api 進行集中認證鑑權
    • 監控,對 Api 呼叫進行效能監控
    • 限流熔斷,當出現流量洪峰,或者後端BFF/微服務出現延遲或故障,閘道器能夠主動進行限流熔斷,保護後端服務,並保持前端使用者體驗可以接受。
    • 安全防爬,收集訪問日誌,通過後臺分析出惡意行為,並阻斷惡意請求。
  • 閘道器在無線裝置和BFF之間又引入了一層間接,讓兩邊可以獨立變化,特別是當後臺BFF在升級或遷移時,可以做到使用者端應用不受影響

在新的 V3 架構中,閘道器承擔了重要的角色,它是解耦和後續升級遷移的利器,在閘道器的配合下,單塊 BFF 實現瞭解耦拆分,各業務團隊可以獨立開發和交付各自的微服務,研發效率大大提升,另外,把橫跨切面邏輯從 BFF 剝離到閘道器後,BFF 的開發人員可以更加專注於業務邏輯交付,實現了架構上的關注分離。

5 V4

業務在不斷髮展,技術架構也需要不斷的迭代和升級,近年來技術團隊又新來了新的業務和技術需求:

  • 開放內部的業務能力,建設開發平臺。藉助第三方社群開發者能力進一步拓寬業務形態
  • 廢棄傳統服務端 Web 應用模式,引入前後端分離架構,前端採用 H5 單頁技術給使用者提供更好的使用者體驗

V4

V4 的思路和 V3 差不多,只是擴充了新的接入渠道:

  • 引入面向第三方開放 Api 的 BFF 層和配套的閘道器層,支援第三方開發者在開放平臺上開發應用
  • 引入面向 H5 應用的 BFF 和配套閘道器,支援前後分離和 H5 單頁應用模式

V4 是一個比較完整的現代微服務架構,從外到內依次是:端使用者體驗層 -> 閘道器層 -> BFF 層 -> 微服務層。整個架構層次清晰、職責分明,是一種靈活的能夠支援業務不斷創新的演進式架構

總結:

  1. 在微服務架構中,BFF(Backend for Frontend)也稱聚合層或者適配層,它主要承接一個適配角色:將內部複雜的微服務,適配成對各種不同使用者體驗(無線/Web/H5/第三方等)友好和統一的API。聚合裁剪適配是BFF的主要職責。
  2. 在微服務架構中,閘道器專注解決跨橫切面邏輯,包括路由、安全、監控和限流熔斷等。閘道器一方面是拆分解耦的利器,同時讓開發人員可以專注業務邏輯的實現,達成架構上的關注分離。
  3. 端使用者體驗層->閘道器層->BFF層->微服務層,是現代微服務架構的典型分層方式,這個架構能夠靈活應對業務需求的變化,是一種支援創新的演化式架構。
  4. 技術和業務都在不斷變化,架構師要不斷調整架構應對這些的變化,BFF和閘道器都是架構演化的產物。

2. BFF & GraphQL

大前端模式下經常會面臨下面2個問題

  • 頻繁變化的 API 是需要向前相容的
  • BFF 中返回的欄位不全是客戶端需要的

至此 2015 年 GraphQL 被 Facebook 正式開源。它並不是一門語言,而是一種 API 查詢風格。本文著重講解了大前端模式下 BFF 層的設計和演進,需要配合 GraphQL 落地。下面簡單介紹下 GraphQL,具體的可以檢視 Node 或者某個語言對應的解決方案。

1. 使用 GraphQL

服務端描述資料 - 客戶端按需請求 - 服務端返回資料

服務端描述資料

type Project {
  name: String
  tagline: String
  contributors: [User]
}

客戶端按需請求

{
    Project(name: 'GraphQL') {
        tagline
    }
}

服務端返回資料

{
    "project": {
            tagline: "A query language for APIS"
    }
}

2. 特點

1.定義資料模型,按需獲取

就像寫 SQL 一樣,描述好需要查詢的資料即可

2. 資料分層

資料格式清晰,語義化強

3. 強型別,型別校驗

擁有 GraphQL 自己的型別描述,型別檢查

4. 協議⽽非儲存

看上去和 MongoDB 比較像,但是一個是資料持久化能力,一個是介面查詢描述。

5. ⽆須版本化

寫過客戶端程式碼的同學會看到 Apple 給 api 廢棄的時候都會標明原因,推薦用什麼 api 代替等等,這點 GraphQL 也支援,另外廢棄的時候也描述瞭如何解決,如何使用。

3. 什麼時候需要 BFF

  • 後端被諸多客戶端使用,並且每種客戶端對於同一類 Api 存在定製化訴求
  • 後端微服務較多,並且每個微服務都只關注於自己的領域
  • 需要針對前端使用的 Api 做一些個性話的優化

什麼時候不推薦使用 BFF

  • 後端服務僅被一種客戶端使用
  • 後端服務比較簡單,提供為公共服務的機會不多(DDD、微服務)

4. 總結

在微服務架構中,BFF(Backend for Frontend)也稱re聚合層或者適配層,它主要承接一個適配角色:將內部複雜的微服務,適配成對各種不同使用者體驗(無線/Web/H5/第三方等)友好和統一的API。聚合裁剪適配是BFF的主要職責。

在微服務架構中,閘道器專注解決跨橫切面邏輯,包括路由、安全、監控和限流熔斷等。閘道器一方面是拆分解耦的利器,同時讓開發人員可以專注業務邏輯的實現,達成架構上的關注分離。

端使用者體驗層->閘道器層->BFF層->微服務層,是現代微服務架構的典型分層方式,這個架構能夠靈活應對業務需求的變化,是一種支援創新的演化式架構。

技術和業務都在不斷變化,架構師要不斷調整架構應對這些的變化,BFF和閘道器都是架構演化的產物。

四、後端架構演進

1. 一些常見名詞解釋

雲原生( Cloud Native )是一種構建和執行應用程式的方法,是一套技術體系和方法論。 Cloud Native 是一個組合詞,Cloud+Native。 Cloud 是適應範圍為雲平臺,Native 表示應用程式從設計之初即考慮到雲的環境,原生為雲而設計,在雲上以最佳姿勢執行,充分利用和發揮雲平臺的彈性+分散式優勢。

Iaas:基礎設施服務,Infrastructure as a service,如果把軟體開發比作廚師做菜,那麼IaaS就是他人提供了廚房,爐子,鍋等基礎東西, 你自己使用這些東西,自己根據不同需要做出不同的菜。由服務商提供伺服器,一般為雲主機,客戶自行搭建環境部署軟體。例如阿里雲、騰訊雲等就是典型的IaaS服務商。

Paas:平臺服務,Platform as a service,還是做菜比喻,比如我做一個黃燜雞米飯,除了提供基礎東西外,那麼PaaS還給你提供了現成剁好的雞肉,土豆,辣椒, 你只要把這些東西放在一起,加些調料,用個小鍋子在爐子上燜個20分鐘就好了。自己只需關心軟體本身,至於軟體執行的環境由服務商提供。我們常說的雲引擎、雲容器等就是PaaS。

Faas:函式服務,Function as a Service,同樣是做黃燜雞米飯,這次我只提供醬油,色拉油,鹽,醋,味精這些調味料,其他我不提供,你自己根據不同口味是多放點鹽, 還是多放點醋,你自己決定。

Saas:軟體服務,Software as a service,同樣還是做黃燜雞米飯,這次是直接現成搞好的一個一個小鍋的雞,什麼調料都好了,已經是個成品了,你只要貼個牌,直接賣出 去就行了,做多是在爐子上燜個20分鐘。這就是SaaS(Software as a Service,軟體即服務)的概念,直接購買第三方服務商已經開發好的軟體來使用,從而免去了自己去組建一個團隊來開發的麻煩。

Baas:你瞭解到,自己要改的東西,只需要前端改了就可以了,後端部分完全不需要改。這時候你動腦筋,可以招了前端工程師,前端頁面自己做,後端部分還是用服務商的。

這就是BaaS(Backend as a Service,後端即服務),自己只需要開發前端部分,剩下的所有都交給了服務商。經常說的“後端雲”就是BaaS的意思,例如像LeanCloud、Bomb等就是典型的BaaS服務商。

MicroService vs Severless:MicroService是微服務,是一種專注於單一責任與功能的小型服務,Serverless相當於更加細粒度和碎片化的單一責任與功能小型服務,他們都是一種特定的小型服務, 從這個層次來說,Serverless=MicroService。

MicroService vs Service Mesh:在沒有ServiceMesh之前微服務的通訊,資料交換同步也存在,也有比較好的解決方案,如Spring Clould,OSS,Double這些,但他們有個最大的特點就是需要你寫入程式碼中,而且需要深度的寫 很多邏輯操作程式碼,這就是侵入式。而ServiceMesh最大的特點是非侵入式,不需要你寫特定程式碼,只是在雲服務的層面即可享受微服務之間的通訊,資料交換同步等操作, 這裡的代表如,docker+K8s,istio,linkerd等。

2. 架構演進

後端整個的演進可以如上圖所示,經歷了不斷迭代,具體的解釋這裡不做展開。跟 SOA 相提並論的還有一個 ESB(企業服務匯流排),簡單來說ESB就是一根管道,用來連線各個服務節點。為了整合不同系統,不同協議的服務,ESB 可以簡單理解為:它做了訊息的轉化解釋和路由工作,讓不同的服務互聯互通;使用ESB解耦服務間的依賴

SOA 和微服務架構的差別

  • 微服務去中心化,去掉 ESB 企業匯流排。微服務不再強調傳統 SOA 架構裡面比較重的 ESB 企業服務匯流排,同時 SOA 的思想進入到單個業務系統內部實現真正的元件化
  • Docker 容器技術的出現,為微服務提供了更便利的條件,比如更小的部署單元,每個服務可以通過類似 Node 或者 Spring Boot 等技術跑在自己的程式中。
  • SOA 注重的是系統整合方面,而微服務關注的是完全分離

REST API:每個業務邏輯都被分解為一個微服務,微服務之間通過REST API通訊。

API Gateway:負責服務路由、負載均衡、快取、訪問控制和鑑權等任務,向終端使用者或客戶端開發API介面.

前端、BFF、後端一些常見的設計模式

寫在開頭的部分,本文的契機是最近我們組同事在客戶端實現了一套 Redux,對某個業務域的功能進行重構設計,iOS、Android 都遵循這套規則,即 Redux。為什麼需要客戶端去實現一套 Redux?商品模組業務邏輯非常負責,商品基礎資訊非常多,比如多規格、多單位、價格、庫存等資訊,還有對應的門店、網店模型,還有各種行業能力開關控制,早期的實現有 Rx 的角色,導致程式碼邏輯較為複雜,資料流動比較亂。架構設計、程式碼維護各方面來看都不是很優雅,加上最近有大的業務調整,中臺的同學用 Redux + 單向資料流的方式重構了業務。

另一個下線經常請求閘道器資料,iOS、Android 各自去宣告 DTO Model,然後解析資料,生成客戶端需要的資料,這樣“重複”的行為經常發生,所以索性用 TS + 指令碼,統一做掉了閘道器資料模型自動生成 iOS、Android 模型的能力。但是在討論框架設計的時候回類比 React Redux、Flutter Fish Redux、Vuex 等,還會聊到單向資料流、雙向資料流,但是有些同學的理解就是錯誤的。所以本文第一部分「糾錯題」部分就是講清楚前端幾個關鍵概念。

後面的部分按照邏輯順序講一下:微前端 -> BFF/閘道器 -> 微服務。

一、糾錯題

1. Vue 是雙向資料流嗎?

如果回答“是”,那麼你應該沒有搞清楚“雙向資料流”與“雙向繫結”這2個概念。其實,準確來說兩者不是一個維度的東西,單向資料流也可以實現雙向繫結。

其實,你要是仔細看過 Vue 的官方文件,那麼官方就已經說明 Vue 其實是 One-Way Data Flow。下面這段話來自官方

首先明確下,我們說的資料流就是元件之間的資料流動。Vue 中所有的 props 都使得其父子 props 之間形成一個單向下行繫結:父級 props 的更新迴向下流動到子元件中,但反過來不行,這樣防止從子元件意外更改其父元件的狀態,從而導致你的應用資料流難以理解。

此外,每次父級元件發生變更時,子元件中所有的 props 都將會被重新整理為最新的值,這意味著你不應該在子元件內去修改 prop,假如你這麼做了,瀏覽器會在控制檯中輸出警告。Vue 和 React 修改 props 報錯如下:

2. React 是 MVVM 嗎?

不直接回答這個問題,我們先來聊幾個概念,這幾個概念弄清楚了,問題的答案也就呼之欲出了。

2.1 單向繫結、雙向繫結的區別?

講 MVVM 一定要講 binder 這個角色。也就是單向繫結和雙向繫結。

Vue 支援單向繫結和雙向繫結。單向繫結其實就是 Model 到 View 的關聯,比如 v-bind。雙向繫結就是 Model 的更新會同步到 View,View 上資料的變化會自動同步到 Model,比如 v-model.

v-model 其實是語法糖,因為 Vue 是 tamplate,所以在經過 webpack ast 解析之後,tamplate 會變為 render,v-model 變為 v-bind 和 v-on

可以用單向繫結很簡單地可以實現雙向繫結的效果,就是 one-way binding + auto event binding。如下

其實看了原始碼就會發現(Vue3 Proxy),單向繫結和雙向繫結的區別在於,雙向繫結把資料變更的操作部分,由框架內部實現了,呼叫者無須感知。

2.2 Redux

使用 React Redux 一個典型的流程圖如下

React-Redux

假如我們把 action 和 dispatcher 的實現隱藏在框架內部,這個圖可以簡化為

假如進一步,我們將互相手動通知的機制再隱藏起來,可以簡化為

你仔細看,是不是就是 MVVM?那問題的答案很明顯了,React 不是 MVVM。

3. Vue 是 MVVM 嗎

官方說了“雖然沒有完全遵循 MVVM 的思想,但是受到了 MVVM 的啟發。所以 Debug 的時候經常看到 VM” Model 的變動會觸發 View 的更新,這一點體現在 V-bind 上 而 View 的變更即使同步到 Model 上,體現在 V-model 上 上述都遵循 MVVM,但是 Vue 提供了 Ref 屬性,允許使用 ref 拿到 Dom,從而直接操作 Dom 的一些樣式或者屬性,這一點打破了 MVVM 的規範

4. 單向繫結和雙向繫結使用場景

我們再來思考一個問題,為什麼 Vue 要移除 .sync, Angular 要增加 < 來實現單向繫結?

當應用變得越來越大,雙向資料流會帶來很多的不可預期的結果,這讓 debug、除錯、測試都變得複雜。

所以 Vue、Angular 都意識到這一點,所以移除了 .sync,支援單向繫結。

正面聊聊問題:

單向繫結,帶來的單向資料流,帶來的好處是所有狀態變化都可以被記錄、跟蹤,狀態變化必須通過手動呼叫通知,源頭可追溯,沒有“暗箱操作”。同時元件資料只有唯一的入口和出口,使得程式更直觀更容易理解,有利於應用的可維護性。缺點是實現同樣的需求,程式碼量會上升,資料的流轉過程變長。同時由於對應用狀態獨立管理的嚴格要求(單一的全域性store),在處理區域性狀態較多的場景時(如使用者輸入互動較多的“富表單型”應用)會顯得特別繁瑣。

雙向繫結優點是在表單互動較多的場景下,會簡化大量業務無關的程式碼。缺點就是由於都是“暗箱操作”,無法追蹤區域性狀態的變化,潛在的行為太多也增加了出錯時 debug 的難度。同時由於元件資料變化來源入口變得可能不止一個,程式設計師水平參差不齊,寫出的程式碼很容易將資料流轉方向弄得紊亂,整個應用的質量就不可控。

總結:單向繫結跟雙向繫結在功能上基本上是互補的,所以我們可以在合適的場景下使用合適的手段。比如在 UI 控制元件中(通常是類表單操作),可以使用雙向的方式繫結資料;而其他場景則使用單向繫結的方式

二、 微前端

上面提到的 Vue、React、單向資料流、雙向資料流都是針對單個應用去組織和設計工程的,但是當應用很大的時候就會存在一些問題。引出今天的一個主題,“微前端”

客戶端的同學都知道,我們很早就在使用元件化、模組化來拆分程式碼和邏輯。那麼你可以說出到底什麼是元件、什麼是模組?

元件就是把重複的程式碼提取出來合併成為一個個元件,元件強調的是重用,位於系統最底層,其他功能都依賴於元件,獨立性強

模組就是分屬同一功能/業務的程式碼進行隔離成獨立的模組,可以獨立執行,以頁面、功能等維度劃分為不同模組,位於業務架構層。

這些概念在前端也是如此,比如寫過 Vue、React 的都知道,對一個頁面需要進行元件的拆分,一個大頁面由多個 UI 子元件組成。模組也一樣,前端利用自執行函式(IIFE)來實現模組化,成熟的 CommonJS、AMD、CMD、UMD 規範等等。比如 iOS 側利用模組化來實現了商品、庫存、開單等業務域的拆分,也實現了路由庫、網路庫等基礎能力模組。而前端更多的是利用模組化來實現名稱空間和程式碼的可重用性

其實,看的出來,前端、客戶端實行的模組化、元件化的目標都是分治思想,更多是站在程式碼層面進行拆分、組織管理。但是隨著前端工程的越來越重,傳統的前端架構已經很難遵循“敏捷”的思想了。

1. 什麼是微前端

關心技術的人聽到微前端,立馬會想起微服務。微服務是面向服務架構的一種變體,把應用程式設計為一些列鬆耦合的細粒度服務,並通過輕量級的通訊協議組織起來。越老越重的前端工程面臨同樣的問題,自然而然就將微服務的思想借鑑到了前端領域。

言歸正傳,微前端(Micro-Frontends)是一種類似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變為多個小型前端應用聚合為一的應用。各個前端應用還可以獨立執行、獨立開發、獨立部署。微前端不是單純的前端框架或者工具,而是一套架構體系

2. 特點

2.1 技術無關

拋兩個場景,大家思考一下:

  • 你新入職一家公司,老闆扔給你一個 5 年陳的專案,需要你在這個專案上持續迭代加功能。
  • 你們起了一個新專案,老闆看慣了前端的風起雲湧技術更迭,只給了架構上的一個要求:"如何確保這套技術方案在 3~5 年內還葆有生命力,不會在 3、5 年後變成又一個遺產專案?"

第一個場景我們初步一想,可以啊,我只需要把新功能用 React/Vue 開發,反正他們都只是 UI library,給我一個Dom 節點我想怎麼渲染怎麼渲染。但是你有沒有考慮過這只是浮在表層的檢視實現,沉在底下的工程設施呢?我要怎麼把這個用 React 寫的元件打出一個包,並且整合到原來的用 ES5 寫的程式碼裡面?或者我怎麼讓 Webpack 跟 之前的 Grunt 能和諧共存一起友好的產出一個符合預期的 Bundle?

第二個場景,你如何確保技術棧在 3~5 年都葆有生命力?別說跨框架了,就算都是 React,15 跟 16 都是不相容的,hooks 還不能用於 Class component 呢我說什麼了?還有打包方案,現在還好都預設 Webpack,那 Webpack 版本升級呢,每次都跟進嗎?別忘了還有 Babel、less、typescript 諸如此類呢?別說 3 年,有幾個人敢保證自己的專案一年後把所有依賴包升級到最新還能跑起來?

為什麼舉這兩個場景呢,因為我們去統計一下業界關於”微前端“發過聲的公司,會發現 adopt 微前端的公司,基本上都是做 ToB 軟體服務的,沒有哪家 ToC 公司會有微前端的訴求(有也是內部的中後臺系統),為什麼會這樣?很簡單,因為很少有 ToC 軟體活得過 3 年以上的。而對於 ToB 應用而言,3~5 年太常見了好嗎!去看看阿里雲最早的那些產品的控制檯,去看看那些電信軟體、銀行軟體,哪個不是 10 年+ 的壽命?企業軟體的升級有多痛這個我就不多說了。所以大部分企業應用都會有一個核心的訴求,就是如何確保我的遺產程式碼能平滑的遷移,以及如何確保我在若干年後還能用上時下熱門的技術棧?

如何給遺產專案續命,才是我們對微前端最開始的訴求。很多開發可能感受不深,畢竟那些一年掙不了幾百萬,沒什麼價值的專案要麼是自己死掉了,要麼是扔給外包團隊維護了,但是要知道,對很多做 ToB 領域的中小企業而言,這樣的系統可能是他們安身立命之本,不是能說扔就扔的,他們承擔不了那麼高的試錯成本。

甚至新的業務,每個技術團隊應該可以根據自己團隊成員的技術儲備、根據業務特點選擇合適的技術棧,而不需要特別關心其他團隊的情況,也就是 A 團隊可以用 React,B 團隊可以用 Vue,C 團隊甚至可以用 Angular 去實現。

2.2 簡單、鬆耦合的程式碼庫

比起一整塊的前端工程來說,微前端架構下的程式碼庫更小更容易開發、維護。此外,更重要的是避免模組間不合理的隱式耦合造成的複雜度上升。通過界定清晰的應用邊界來降低意外耦合的可能性,增加子應用間邏輯耦合的成本,促使開發者明確資料和事件在應用程式中的流向

2.3 增量升級

理想的程式碼自然是模組清晰、依賴明確、易於擴充套件、便於維護的……然而,實踐中出於各式各樣的原因:

  • 歷史專案,祖傳程式碼
  • 交付壓力,當時求快
  • 就近就熟,當時求穩……

總存在一些不那麼理想的程式碼:

  • 技術棧落後,甚至強行混用多種技術棧
  • 耦合混亂,不敢動,牽一髮何止動全身
  • 重構不徹底,重構-爛尾,換個姿勢重構-又爛尾……

而要對這些程式碼進行徹底重構的話,最大的問題是很難有充裕的資源去大刀闊斧地一步到位,在逐步重構的同時,既要確保中間版本能夠平滑過渡,同時還要持續交付新特性:

所以,為了實施漸進式重構,我們需要一種增量升級的能力,先讓新舊程式碼和諧共存,再逐步轉化舊程式碼,直到整個重構完成

這種增量升級的能力意味著我們能夠對產品功能進行低風險的區域性替換,包括升級依賴項、更替架構、UI 改版等。另一方面,也帶來了技術選型上的靈活性,有助於新技術、新互動模式的實驗性試錯

2.4 獨立部署

獨立部署的能力在微前端體系中至關重要,能夠縮小變更範圍,進而降低相關風險

因此,每個微前端都應具備有自己的持續交付流水線(包括構建、測試並部署到生產環境),並且要能獨立部署,不必過多考慮其它程式碼庫和交付流水線的當前狀態:

假如 A、B、C 三個 Biz 存在三個系統,三者可以獨立部署,最後由主系統去整合釋出應用。這樣子更加獨立靈活。想想以前的單體應用,比如一個複雜的業務應用,很可能在 B 構建的時候沒有通過 lint 導致整個應用失敗掉,這樣既浪費了構建 A 的時間,又讓整個應用的構建變為序列,低效。

2.5 團隊自治

除了程式碼和釋出週期上的解耦之外,微前端還有助於形成獨立的團隊,由不同的團隊各自負責一塊獨立的產品功能,最好根據 Biz 去劃分,由於程式碼都是獨立的,所以基於 Biz 的前端業務團隊,可以設計思考,提供更加合理的介面和能力。甚至可以抽象更多的基礎能力,按照業務思考提供更加完備的能力,也就是團隊更加自治。

3 如何實現

微前端主要採用的是組合式應用路由方案,該方案的核心思想是“主從”(玩過 Jenkins 的同學是不是很耳熟),即包括一個基座“MainApp “ 應用和若干個微應用(MircroApp),基座大多采用的是一個前端 SPA 專案,主要負責應用註冊、路由對映、訊息下發等,而微應用是獨立的前端專案,這些專案不限於採用的具體技術(React、Vue、Angular、Jquery)等等,每個微應用註冊到基座中,由基座進行管理,即使沒有基座,這些微應用都可以單獨訪問,如下:

一個微應用框架需要解決的問題是:

  • 路由切換的分發問題

    作為微前端的基座應用,是整個應用的入口,負責承載當前微應用的展示和對其他路由微應用的轉發,對於當前微應用的展示,一般是由以下幾步構成:

    1. 作為一個SPA的基座應用,本身是一套純前端專案,要想展示微應用的頁面除了採用iframe之外,要能先拉取到微應用的頁面內容, 這就需要遠端拉取機制
    2. 遠端拉取機制通常會採用fetch API來首先獲取到微應用的HTML內容,然後通過解析將微應用的JavaScript和CSS進行抽離,採用eval方法來執行JavaScript,並將CSS和HTML內容append到基座應用中留給微應用的展示區域,當微應用切換走時,同步解除安裝這些內容,這就構成的當前應用的展示流程。
    3. 當然這個流程裡會涉及到CSS樣式的汙染以及JavaScript對全域性物件的汙染,這個涉及到隔離問題會在後面討論,而目前針對遠端拉取機制這套流程,已有現成的庫來實現,可以參考 import-html-entrysystem.js

    對於路由分發而言,以採用vue-router開發的基座SPA應用來舉例,主要是下面這個流程:

    1. 當瀏覽器的路徑變化後,vue-router會監聽hashchange或者popstate事件,從而獲取到路由切換的時機。
    2. 最先接收到這個變化的是基座的router,通過查詢註冊資訊可以獲取到轉發到那個微應用,經過一些邏輯處理後,採用修改hash方法或者pushState方法來路由資訊推送給微應用的路由,微應用可以是手動監聽hashchange或者popstate事件接收,或者採用React-router,vue-router接管路由,後面的邏輯就由微應用自己控制。
  • 主應用、微應用的隔離問題

    應用隔離問題主要分為主應用和微應用,微應用和微應用之間的JavaScript執行環境隔離,CSS樣式隔離,我們先來說下CSS的隔離。

    CSS隔離:當主應用和微應用同屏渲染時,就可能會有一些樣式會相互汙染,如果要徹底隔離CSS汙染,可以採用CSS Module 或者名稱空間的方式,給每個微應用模組以特定字首,即可保證不會互相干擾,可以採用webpack的postcss外掛,在打包時新增特定的字首。

    而對於微應用與微應用之間的CSS隔離就非常簡單,在每次應用載入時,將該應用所有的link和style 內容進行標記。在應用解除安裝後,同步解除安裝頁面上對應的link和style即可。

    JavaScript隔離:每當微應用的JavaScript被載入並執行時,它的核心實際上是對全域性物件Window的修改以及一些全域性事件的改變,例如jQuery這個js執行後,會在Window上掛載一個window.$物件,對於其他庫React,Vue也不例外。為此,需要在載入和解除安裝每個微應用的同時,儘可能消除這種衝突和影響,最普遍的做法是採用沙箱機制(SandBox)。

    沙箱機制的核心是讓區域性的JavaScript執行時,對外部物件的訪問和修改處在可控的範圍內,即無論內部怎麼執行,都不會影響外部的物件。通常在Node.js端可以採用vm模組,而對於瀏覽器,則需要結合with關鍵字和window.Proxy物件來實現瀏覽器端的沙箱。

  • 通訊問題

    應用間通訊有很多種方式,當然,要讓多個分離的微應用之間要做到通訊,本質上仍離不開中間媒介或者說全域性物件。所以對於訊息訂閱(pub/sub)模式的通訊機制是非常適用的,在基座應用中會定義事件中心Event,每個微應用分別來註冊事件,當被觸發事件時再有事件中心統一分發,這就構成了基本的通訊機制,流程如下圖:

目前開源了很多微前端框架,比如 qiankun ,感興趣的可以去看看原始碼

4. 是否採用微前端

為什麼要做微前端?或者說微前端解決了什麼問題?也就回答了是否需要採用微前端。微前端的鼻祖 Single-spa 最初要解決的問題是,在老專案中使用新的技術棧。現階段螞蟻金服微前端框架 Qiankun 所宣告的微前端要解決的另一個主要問題是,巨石工程所面臨的維護困難喝協作開發困難的問題。如果工程面臨這兩方面的問題,我覺得微前端就可以試試了。你們覺得呢?

三、微服務平臺下基於 GraphQL 構建 BFF 的思考

1. 大前端架構演進

我們來講一個故事

1. V1

假設早期2011年一家電商公司完成了單體應用的拆分,後端服務已經 SOA 化,此時應用的形態僅有 Web 端,V1架構圖如下所示:

V1

2. V2

時間來到了2012年初,國內颳起了一陣無線應用的風,各個大廠都開始了自己的無線應用開發。為了應對該需求,架構調整為 V2,如下所示

V1

這個架構存在一些問題:

  • 無線 App 和內部的微服務強耦合,任何一側的變化都可能對另一側造成影響
  • 無線 App 需要知道內部服務細節
  • 無線 App 端需要做大量的聚合裁剪和適配邏輯

    聚合:某一個頁面可能同時需要呼叫好幾個後端 API 進行組合,比如商品詳情頁所需的資料,不能一次呼叫完成

    裁剪:後端提供的基礎服務比較通用,返回的 Payload 比較大,欄位太多,App 需要根據裝置和業務需求,進行欄位裁剪使用者的客戶端是Android還是iOS,是大屏還是小屏,是什麼版本。再比如,業務屬於哪個行業,產品形態是什麼,功能投放在什麼場景,面向的使用者群體是誰等等。這些因素都會帶來面向端的功能邏輯的差異性。後端在微服務和領域驅動設計(不在本文重點講解範疇)的背景下,各個服務提供了當前 Domain 的基礎功能,比如商品域,提供了商品新增、查詢、刪除功能,列表功能、詳情頁功能。

    但是在商品詳情頁的情況下,資料需要呼叫商品域、庫存域、訂單域的能力。客戶端需要做大量的網路介面處理和資料處理。

    適配:一些常見的適配常見就是格式轉換,比如有些後端服務比較老,提供 SOAP/XML 資料,不支援 JSON,這時候就需要做一些適配程式碼

  • 隨著使用型別的增多(iOS Phone/Pad、Android Phone/Pad、Hybrid、小程式),聚合裁剪和適配的邏輯的開發會造成裝置端的大量重複勞動
  • 前端比如 iOS、Android、小程式、H5 各個 biz 都需要業務資料。最早的設計是後端介面直出。這樣的設計會導致出現一個問題,因為 biz1 因為迭代發版,造成後端改介面的實現。 Biz2 的另一個需求又會讓服務端改設計實現,造成介面是面向業務開發的。不禁會問,基礎服務到底是面向業務介面開發的嗎?這和領域驅動的設計相背離

在這樣的背景下誕生了 BFF(Backend For Frontend) 層。各個領域提供基礎的資料模型,各個 biz 存在自己的 BFF 層,按需取查詢(比如商品基礎資料200個,但是小程式列表頁只需要5個),在比如商品詳情頁可能需要商品資料、訂單資料、評價資料、推薦資料、庫存資料等等,最早的設計該詳情頁可能需要請求6個介面,這對於客戶端、網頁來說,體驗很差,有個新的設計,詳情頁的 BFF 去動態組裝呼叫介面,一個 BFF 介面就組裝好了資料,直接返回給業務方,體驗很好

3. V2.1

V2 架構問題太多,沒有開發實施。為解決上述問題,在外部裝置和內部微服務之間引入一個新的角色 Mobile BFF。BFF 也就是 Backend for Frontend 的簡稱,可以認為是一種適配服務,將後端的微服務進行適配(主要包括聚合裁剪和格式適配等邏輯),向無線端裝置暴露友好和統一的 API,方便無線裝置接入訪問後端服務。V2.1 服務架構如下

V1

這個架構的優勢是:

  • 無線 App 和內部服務不耦合,通過引入 BFF 這層簡介,使得兩邊可以獨立變化:

    • 後端如果發生變化,通過 BFF 遮蔽,前端裝置可以做到不受影響
    • 前端如果發生變化,通過 BFF 遮蔽,後端微服務可以暫不變化
    • 當無線端有新的需求的時候,通過 BFF 遮蔽,可以減少前後端的溝通協作開銷,很多需求由前端團隊在 BFF 上就可以自己搞定
  • 無線 App 只需要知道 Mobile BFF 的地址,並且服務介面是統一的,不需要知道內部複雜的微服務地址和細節
  • 聚合裁剪和適配邏輯在 Mobile BFF 上實現,無線 App 端可以簡化瘦身

4. V3

V2.1 架構比較成功,實施後較長一段時間支援了公司早期無線業務的發展,隨著業務量暴增,無線研發團隊不斷增加,V2.1 架構的問題也被暴露了出來:

  • Mobile BFF 中不僅有各個業務線的聚合/裁剪/適配和業務邏輯,還引入了很多橫跨切面的邏輯,比如安全認證,日誌監控,限流熔斷等,隨著時間的推移,程式碼變得越來越複雜,技術債越來越多,開發效率不斷下降,缺陷數量不斷增加
  • Mobile BFF 叢集是個失敗單點,嚴重程式碼缺陷將導致流量洪峰可能引起叢集當機,所有無線應用都不可用

為了解決上述偽命題,決定在外部裝置和 BFF 之間架構一個新的層,即 Api Gateway,V3 架構如下:

V1

新的架構 V3 有如下調整:

  • BFF 按團隊或者業務進行解耦拆分,拆分為若干個 BFF 微服務,每個業務線可以並行開發和交付各自負責的 BFF 微服務
  • 官關(一般由獨立框架團隊負責運維)專注橫跨切面功能,包括:

    • 路由,將來自無線裝置的請求路由到後端的某個微服務 BFF 叢集
    • 認證,對涉及敏感資料的 Api 進行集中認證鑑權
    • 監控,對 Api 呼叫進行效能監控
    • 限流熔斷,當出現流量洪峰,或者後端BFF/微服務出現延遲或故障,閘道器能夠主動進行限流熔斷,保護後端服務,並保持前端使用者體驗可以接受。
    • 安全防爬,收集訪問日誌,通過後臺分析出惡意行為,並阻斷惡意請求。
  • 閘道器在無線裝置和BFF之間又引入了一層間接,讓兩邊可以獨立變化,特別是當後臺BFF在升級或遷移時,可以做到使用者端應用不受影響

在新的 V3 架構中,閘道器承擔了重要的角色,它是解耦和後續升級遷移的利器,在閘道器的配合下,單塊 BFF 實現瞭解耦拆分,各業務團隊可以獨立開發和交付各自的微服務,研發效率大大提升,另外,把橫跨切面邏輯從 BFF 剝離到閘道器後,BFF 的開發人員可以更加專注於業務邏輯交付,實現了架構上的關注分離。

5 V4

業務在不斷髮展,技術架構也需要不斷的迭代和升級,近年來技術團隊又新來了新的業務和技術需求:

  • 開放內部的業務能力,建設開發平臺。藉助第三方社群開發者能力進一步拓寬業務形態
  • 廢棄傳統服務端 Web 應用模式,引入前後端分離架構,前端採用 H5 單頁技術給使用者提供更好的使用者體驗

V4

V4 的思路和 V3 差不多,只是擴充了新的接入渠道:

  • 引入面向第三方開放 Api 的 BFF 層和配套的閘道器層,支援第三方開發者在開放平臺上開發應用
  • 引入面向 H5 應用的 BFF 和配套閘道器,支援前後分離和 H5 單頁應用模式

V4 是一個比較完整的現代微服務架構,從外到內依次是:端使用者體驗層 -> 閘道器層 -> BFF 層 -> 微服務層。整個架構層次清晰、職責分明,是一種靈活的能夠支援業務不斷創新的演進式架構

總結:

  1. 在微服務架構中,BFF(Backend for Frontend)也稱聚合層或者適配層,它主要承接一個適配角色:將內部複雜的微服務,適配成對各種不同使用者體驗(無線/Web/H5/第三方等)友好和統一的API。聚合裁剪適配是BFF的主要職責。
  2. 在微服務架構中,閘道器專注解決跨橫切面邏輯,包括路由、安全、監控和限流熔斷等。閘道器一方面是拆分解耦的利器,同時讓開發人員可以專注業務邏輯的實現,達成架構上的關注分離。
  3. 端使用者體驗層->閘道器層->BFF層->微服務層,是現代微服務架構的典型分層方式,這個架構能夠靈活應對業務需求的變化,是一種支援創新的演化式架構。
  4. 技術和業務都在不斷變化,架構師要不斷調整架構應對這些的變化,BFF和閘道器都是架構演化的產物。

2. BFF & GraphQL

大前端模式下經常會面臨下面2個問題

  • 頻繁變化的 API 是需要向前相容的
  • BFF 中返回的欄位不全是客戶端需要的

至此 2015 年 GraphQL 被 Facebook 正式開源。它並不是一門語言,而是一種 API 查詢風格

1. 使用 GraphQL

服務端描述資料 - 客戶端按需請求 - 服務端返回資料

服務端描述資料

type Project {
  name: String
  tagline: String
  contributors: [User]
}

客戶端按需請求

{
    Project(name: 'GraphQL') {
        tagline
    }
}

服務端返回資料

{
    "project": {
            tagline: "A query language for APIS"
    }
}

2. 特點

1.定義資料模型,按需獲取

就像寫 SQL 一樣,描述好需要查詢的資料即可

2. 資料分層

資料格式清晰,語義化強

3. 強型別,型別校驗

擁有 GraphQL 自己的型別描述,型別檢查

4. 協議⽽非儲存

看上去和 MongoDB 比較像,但是一個是資料持久化能力,一個是介面查詢描述。

5. ⽆須版本化

寫過客戶端程式碼的同學會看到 Apple 給 api 廢棄的時候都會標明原因,推薦用什麼 api 代替等等,這點 GraphQL 也支援,另外廢棄的時候也描述瞭如何解決,如何使用。

3. 什麼時候需要 BFF

  • 後端被諸多客戶端使用,並且每種客戶端對於同一類 Api 存在定製化訴求
  • 後端微服務較多,並且每個微服務都只關注於自己的領域
  • 需要針對前端使用的 Api 做一些個性話的優化

什麼時候不推薦使用 BFF

  • 後端服務僅被一種客戶端使用
  • 後端服務比較簡單,提供為公共服務的機會不多(DDD、微服務)

4. 總結

在微服務架構中,BFF(Backend for Frontend)也稱re聚合層或者適配層,它主要承接一個適配角色:將內部複雜的微服務,適配成對各種不同使用者體驗(無線/Web/H5/第三方等)友好和統一的API。聚合裁剪適配是BFF的主要職責。

在微服務架構中,閘道器專注解決跨橫切面邏輯,包括路由、安全、監控和限流熔斷等。閘道器一方面是拆分解耦的利器,同時讓開發人員可以專注業務邏輯的實現,達成架構上的關注分離。

端使用者體驗層->閘道器層->BFF層->微服務層,是現代微服務架構的典型分層方式,這個架構能夠靈活應對業務需求的變化,是一種支援創新的演化式架構。

技術和業務都在不斷變化,架構師要不斷調整架構應對這些的變化,BFF和閘道器都是架構演化的產物。

四、後端架構演進

1. 一些常見名詞解釋

雲原生( Cloud Native )是一種構建和執行應用程式的方法,是一套技術體系和方法論。 Cloud Native 是一個組合詞,Cloud+Native。 Cloud 是適應範圍為雲平臺,Native 表示應用程式從設計之初即考慮到雲的環境,原生為雲而設計,在雲上以最佳姿勢執行,充分利用和發揮雲平臺的彈性+分散式優勢。

Iaas:基礎設施服務,Infrastructure as a service,如果把軟體開發比作廚師做菜,那麼IaaS就是他人提供了廚房,爐子,鍋等基礎東西, 你自己使用這些東西,自己根據不同需要做出不同的菜。由服務商提供伺服器,一般為雲主機,客戶自行搭建環境部署軟體。例如阿里雲、騰訊雲等就是典型的IaaS服務商。

Paas:平臺服務,Platform as a service,還是做菜比喻,比如我做一個黃燜雞米飯,除了提供基礎東西外,那麼PaaS還給你提供了現成剁好的雞肉,土豆,辣椒, 你只要把這些東西放在一起,加些調料,用個小鍋子在爐子上燜個20分鐘就好了。自己只需關心軟體本身,至於軟體執行的環境由服務商提供。我們常說的雲引擎、雲容器等就是PaaS。

Faas:函式服務,Function as a Service,同樣是做黃燜雞米飯,這次我只提供醬油,色拉油,鹽,醋,味精這些調味料,其他我不提供,你自己根據不同口味是多放點鹽, 還是多放點醋,你自己決定。

Saas:軟體服務,Software as a service,同樣還是做黃燜雞米飯,這次是直接現成搞好的一個一個小鍋的雞,什麼調料都好了,已經是個成品了,你只要貼個牌,直接賣出 去就行了,做多是在爐子上燜個20分鐘。這就是SaaS(Software as a Service,軟體即服務)的概念,直接購買第三方服務商已經開發好的軟體來使用,從而免去了自己去組建一個團隊來開發的麻煩。

Baas:你瞭解到,自己要改的東西,只需要前端改了就可以了,後端部分完全不需要改。這時候你動腦筋,可以招了前端工程師,前端頁面自己做,後端部分還是用服務商的。

這就是BaaS(Backend as a Service,後端即服務),自己只需要開發前端部分,剩下的所有都交給了服務商。經常說的“後端雲”就是BaaS的意思,例如像LeanCloud、Bomb等就是典型的BaaS服務商。

MicroService vs Severless:MicroService是微服務,是一種專注於單一責任與功能的小型服務,Serverless相當於更加細粒度和碎片化的單一責任與功能小型服務,他們都是一種特定的小型服務, 從這個層次來說,Serverless=MicroService。

MicroService vs Service Mesh:在沒有ServiceMesh之前微服務的通訊,資料交換同步也存在,也有比較好的解決方案,如Spring Clould,OSS,Double這些,但他們有個最大的特點就是需要你寫入程式碼中,而且需要深度的寫 很多邏輯操作程式碼,這就是侵入式。而ServiceMesh最大的特點是非侵入式,不需要你寫特定程式碼,只是在雲服務的層面即可享受微服務之間的通訊,資料交換同步等操作, 這裡的代表如,docker+K8s,istio,linkerd 等。

2. 架構演進

1. 演進圖

後端整個的演進可以如上圖所示,經歷了不斷迭代,目前到了微服務、Service Mesh 的時代。具體的解釋這裡不做展開,感興趣的可以去自行谷歌。跟 SOA 相提並論的還有一個 ESB(企業服務匯流排),簡單來說 ESB 就是一根管道,用來連線各個服務節點,為了整合不同系統,不同協議的服務。

ESB 可以簡單理解為:它做了訊息的轉化解釋和路由工作,讓不同的服務互聯互通,使用ESB解耦服務間的依賴。

2. SOA 和微服務架構的差別

微服務去中心化,去掉 ESB 企業匯流排。微服務不再強調傳統 SOA 架構裡面比較重的 ESB 企業服務匯流排,同時 SOA 的思想進入到單個業務系統內部實現真正的元件化

Docker 容器技術的出現,為微服務提供了更便利的條件,比如更小的部署單元,每個服務可以通過類似 Node 或者 Spring Boot 等技術跑在自己的程式中。

SOA 注重的是系統整合方面,而微服務關注的是完全分離

3. 早期微服務架構

REST API:每個業務邏輯都被分解為一個微服務,微服務之間通過REST API通訊。

API Gateway:負責服務路由、負載均衡、快取、訪問控制和鑑權等任務,向終端使用者或客戶端開發API介面

每個業務邏輯都被分解為一個微服務,微服務之間通過 REST API 通訊。一些微服務也會向終端使用者或客戶端開發API介面。但通常情況下,這些客戶端並不能直接訪問後臺微服務,而是通過 API Gateway 來傳遞請求。API Gateway 一般負責服務路由、負載均衡、快取、訪問控制和鑑權等任務。

4. 斷路器

斷路器背後的基本思想非常簡單。您將受保護的函式呼叫包裝在斷路器物件中,該物件監視故障。一旦故障達到某個閾值,斷路器就會跳

閘,並且所有對斷路器的進一步呼叫都會返回錯誤,而根本不會進行受保護的呼叫。通常,如果斷路器跳閘,您還需要某種監視器警報。

通常,斷路器和服務發現等基礎實現獨立接入。

6. 早期微服務架構的問題及解決方案

  • 框架/SDK太多,後續升級維護困難
  • 服務治理邏輯嵌入業務應用,佔有業務服務資源
  • 服務治理策略難以統一
  • 額外的服務治理元件(中介軟體)的維護成本
  • 多語言:隨著Node、Java以及其他後端服務的興起,可能需要開發多套基礎元件來配合主應用接入,SDK維護成本高

隨機引入了 Sidecar 模式:

將這些基礎服務和應用程式繫結在一起,但使用獨立的程式或容器部署,這能為跨語言的平臺服務提供同構介面。SideCare 很多人會很懵逼,這個詞怎麼理解,下面的配圖,旁邊那一小坨就比較形象了。

Sidecar模式的優勢

  • 在執行時環境和程式語言方面獨立於它的主應用程式,不需要為每種語言開發一個 Sidecar
  • Sidecar可以訪問與主應用程式相同的資源
  • 因為它靠近主應用程式(部署在一起),所以在它們之間通訊時沒有明顯的延遲
  • 即使對於不提供可擴充套件性機制的應用程式,也可以、將sidecar作為自己的程式附加到主應用程式所在的主機或子容器中進行擴充套件

7. Service Mesh 的形成

每個服務都將有一個配套的代理 sidecar,鑑於服務僅通過 sidecar 代理相互通訊,我們最終會得到類似於下圖的部署:

服務網格是用於處理服務到服務通訊的專用基礎設施層。它負責通過構成現代雲原生應用程式的複雜服務拓撲來可靠地交付請求。在實踐中,服務網格通常被實現為一系列輕量級網路代理,這些代理與應用程式程式碼一起部署,應用程式不需要知道。

網格 = 容器網格 + 服務

典型的 Service Mesh 架構

控制層:

  • 不直接解析資料包
  • 與控制平面中的代理通訊,下發策略和配置
  • 負責網路行為的視覺化
  • 通常提供 API 或者命令列工具可用於配置版本化管理,便於持續整合和部署

資料層:

  • 通常是按照無狀態目標設計的,但實際上為了提高流量轉發效能,需要快取一些資料,因此無狀態也是有爭議的
  • 直接處理入站和出站資料包,轉發、路由、健康檢查、負載均衡、認證、鑑權、產生監控資料等
  • 對應用來說透明,即可以做到無感知部署

Istio 是一個典型主流的 Service Mesh 框架。有關 Istio 的詳細介紹可以看這裡的電子書

五、寫在最後

為什麼客戶端工程師需要看這些,看上去和日常工作沒啥關係,但是知識都是積累才有用的,你先儲備才有機會發揮出來,知識是會複利的,某個領域的知識和解決方案可以為你在解決其他問題的時候提供思路和眼界。另外大前端不可能只和客戶端、前端打交道,後端的解決方案、技術趨勢也要了解,有助於你站在更巨集觀的角度解決某個問題,清楚設計。

相關文章