Bifrost微前端框架及其在美團閃購中的實踐

Bifrost(英 ['bi:frɔst])原意彩虹橋,北歐神話中是連通天地的一條通道。而在漫威電影《雷神》中,Bifrost是神域——阿斯加德(Asgard)的出入口,神域的人通過它自由穿梭於“九界”(指九個平行的宇宙)之間。借用“彩虹橋”的寓意,我們希望Bifrost可以成爲前端不同SPA(Single Page Application)系統之間的橋樑,使得不同的單頁應用可以用這種方式實現功能的自由聚合/拆分。

項目背景

立項之初,閃購賦能企管平臺(以下簡稱“企管平臺”)僅僅是面向單個商家的CRM管理系統,採用常規的Vue單頁應用方式來實現。隨着項目的推進,它的定位逐漸發生了變化,從一個單一業務的載體逐漸變成了面向多種場景的商家管理平臺。另一方面,由於系統由多個前端團隊共同開發維護,越來越多的問題隨之浮現:

  1. 異地協作時,信息同步不及時引起的代碼衝突以及修改公共組件引入的Bug。
  2. 不同的商家針對同一個頁面存在定製化的需求。
  3. 已經實現的一些功能需要集成到企管平臺中來。

因此,我們希望構建一個更高維度的解耦方案,使我們能夠在開發階段把互不干涉的模塊拆成一個個類似後端微服務架構那樣的子系統,各自迭代,在運行時集成爲一個能夠覆蓋上述各種使用場景的完整系統。

方案選型

首先,我們整理了核心訴求,按優先級排序如下:

  1. 希望異地開發時不同的模塊能夠獨立開發、獨立部署。
  2. 對已在線上運行的項目,希望能夠低成本地接入企管平臺,而不需要對開發、部署流程做大規模的改動。
  3. 各個子系統獨立運行,互不影響,但允許我們在開發階段與其他子系統進行聯調。
  4. 保持單頁應用的體驗。
  5. 由於現有項目都是基於Vue技術棧開發,因此,我們的框架並不需要做到技術棧無關,只要滿足Vue的項目即可。

基於以上這些訴求,我們調研了目前市面上常用的微前端方案,最常見的方案有:

  1. 基於Nginx的路由分發。
  2. 使用Iframe將頁面嵌入。

除此之外,還有美團集團內部的微前端實踐——美團HR系統(用微前端方式搭建類單頁應用)和業界比較知名的微前端框架——SingleSPA。

這些方案的優劣整理如下:

從用戶體驗角度出發,Nginx和Iframe首先被否決;HR系統的方案需要對現有的項目進行改造,把不同團隊目前開發的項目整合到同一個單頁應用中,在項目快速迭代的過程中,成本過高,所以也被否掉。SingleSPA看起來完美,但它沒有照顧到實際生產環境中的開發、部署的差異性,並不是Product-Ready。綜合多種因素考慮,我們最終決定採用自研的方式來實現微前端化Bifrost。

核心架構

Bifrost框架在設計的時候參考了SingleSPA的思路,將系統了分爲主系統和子系統。

主系統是用來控制子系統的調度中心,職責包括:

  • 維護子系統的註冊表。
  • 管理各個子系統的生命週期。
  • 傳遞路由信息。
  • 加載子項目的入口資源。
  • 爲子系統的實例提供掛載點。

子系統只負責業務邏輯的實現。如果進一步細分的話,子系統可以分爲業務子系統、實現公共菜單子系統、導航佈局子系統,其中佈局子系統會先於業務子系統加載。

Bifrost採用路由消息分發的方式來控制子系統的加載和跳轉。主系統維護了一條路由消息總線,當路由發生變化時,子系統會將路由事件推送給路由總線,然後由路由總線決定加載/跳轉的目標子系統。如果路由不需要切換子系統,則交由當前子系統進行處理。

如果子系統發生切換,主系統會在DOM中添加對應子系統的掛載節點,並異步加載系統的靜態資源。由於子系統都是完整的Vue實例,當子系統的代碼加載並執行之後,子系統就會自動在其對應的掛載節點上渲染相應的內容。

整個系統的生命週期如下圖所示:

具體實現

基於Bifrost實現的項目架構如下圖所示:

這裏,我們主要關注主系統、業務子系統和佈局子系統的實現。

主系統

主系統的邏輯比較簡單,主要是實例化Bifrost中定義的Platform對象,並註冊各個子系統。子系統的註冊信息包括:

  • AppName:子系統名,與系統的路由前綴保持對應,同時也會作爲子系統在DOM中掛載節點的ID。
  • Domain:非必填,如果出現多個路由前綴都對應同一個子系統,可以通過Domain進行映射。
  • ConfigPath:對應子系統配置文件的URL。

一個簡單的主系統實現如下:

import { Platform } from '@sfe/bifrost'

new Platform({
  layoutFrame: {
    render () {
      // render layout
    }
  },
  appRegister: [
    { appName: 'app1', configPath: '/path/to/app1/config.js' }
  ]
}).start()

業務子系統

在設計方案時,我們始終保持一個理念,就是保證對業務代碼的零侵入,因此業務系統改造的工作量很小。代碼層面,只需要把原本子系統的初始化流程放到AppContainer對象的Mounted回調函數裏即可:

import { AppContainer } from '@sfe/bifrost'
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
const router = new VueRouter({})
new AppContainer({
  appName: 'app1',
  router,
  mounted () {
    return new Vue({
      router,
      components: { App },
      template: '<App/>'
    }).$mount()
  }
}).start()

另外,還需要修改子系統的構建流程,構建完成之後生成一個包含子系統入口資源信息的配置文件。一個典型的配置文件如下:

((callback) => callback({
  scripts: [
    '/js/chunk-vendors.dee65310.js','/js/home.b822227c.js'
  ],
  styles: [
    '/css/chunk-vendors.e7f4dbac.css','/css/home.285dac42.css'
  ]
}))(configLoadedCb.crm)

此處我們實現了@sfe/bifrost-config-plugin插件,在Webpack構建腳本中引入該插件就可以自動生成項目對應的配置文件。配置文件是一個立即執行函數,主系統可以通過JSONP的方式讀取配置文件中的內容。

在實際生產環境中,我們可以將子系統發佈到任意CDN,只要能夠保證配置文件的URL始終不變,那麼無需依賴任何服務,主系統就可以感知到子系統的發佈。

佈局子系統

佈局子系統是用來實現菜單和導航欄的Vue工程,本質上和一般的業務子系統沒有區別。只需要注意,佈局子系統使用的是LayoutContainer而非AppContainer進行包裝。

import { LayoutContainer } from '@sfe/bifrost'
import Vue from 'vue'

import App from './app'

new LayoutContainer({
  appName: 'layout',
  router,
  onInit ({ appSlot, callback }) {
    Vue.config.productionTip = false
    const app = new Vue({
      el: '#app',
      router,
      store,
      render: (h) => (
        <App appSlot={appSlot} />
      )
    })
    callback()
  }
}).start()

佈局子系統作爲主系統的一部分,既可以放在主系統中去實現,也可以像其他子系統一樣通過異步的方式去加載。在我們的項目中,結合了上面兩種方式(佈局子系統既可以爲作爲常規的Vue項目構建,也可以發佈成NPM包),每次發佈時,會同時發佈佈局的靜態資源和NPM包。主系統通過NPM包的方式引入佈局子系統,將它打包到項目中,避免線上運行時,額外加載佈局子系統的資源,減小項目體積,加快渲染速度。

本地開發時,我們則會通過Bifrost定義的MockPlatform異步加載佈局子系統的靜態資源,保證線上/線下運行效果的一致性,方便本地聯調。

工程實踐

代碼層面的改動雖然不多,但要在實際的生產環境中落地,還需要解決一系列老生常談的問題,包括:

  • 本地開發時,如何保證與線上實際運行效果的一致性?
  • 如何實現全局狀態管理和子系統之間的通信?
  • 如何對公共依賴和公共模塊進行管理?
  • 發佈部署流程需要怎樣調整?

根據閃購業務實踐,我們總結了一套適用於Bifrost的解決方案。

本地聯調

採用微前端的方式意味着子系統的完全隔離,這給我們的開發帶來了一系列困擾:

  • 本地開發時,無法看到當前開發的功能在主系統中實際運行的效果。
  • 子系統之間有時會存在跳轉關係,在開發階段難以驗證這種跳轉邏輯的正確性。

爲了解決這些問題,Bifrost定義了MockPlatform。MockPlatform的思路很簡單:既然主系統可以動態加載線上的子系統,那麼我們只需要在開發時,模擬主系統的運行方式,去加載其他子系統的線上資源,之後就可以像調用後端API一樣同各個子系統進行聯調了。這也就解釋了爲什麼佈局子系統在輸出NPM包的同時還維護了一份靜態資源。

MockPlatform的API同Platform對象的API是一致的,開發時,我們只需要按照主系統的方式引用佈局或業務子系統的配置文件URL即可:

// ...others...

new AppContainer({
  // ...others...
  runDevPlatform: process.env.NODE_ENV === 'development', // 只在開發環境下啓動mock platform
  devPlatformConfig: {
    layoutFrame: {
      mode: 'remote',
      configPath: 'path/to/layout/config.js'
    },
    appRegister: [{
      appName: 'app2',
      mode: 'remote',
      configPath: 'path/to/app2/config.js'
    }]
  },
  // ...others...
}).start()

藉助MockPlatform,我們項目在開發階段的感受和開發普通的單頁應用沒有任何差異,如果某個我們依賴的子系統更新了功能,只需要讓對應的RD發佈一下,就可以在本地看到它的最新效果。

全局狀態

除了本地聯調,全局通信也是微前端項目中繞不開的一個話題。由於我們所有項目採用的都是Vue技術棧,所以會選擇基於Vuex來實現全局通信。Bifrost的主系統會維護一個全局的Vuex Store,用於保存全局狀態。

當子系統希望監聽全局狀態時,子系統並不是直接訂閱全局Store(Vue的依賴收集機制也決定了子系統無法響應全局Store的變化),而是藉助Bifrost提供的syncGlobalStore函數來訂閱全局Store。調用該函數後,任何全局狀態的變化都會被同步到本地Store的Global命名空間下。之後,就可以像普通的單頁應用那樣,調用Vuex的mapState方法實現和全局狀態的雙向綁定。

import { AppContainer, syncGlobalStore } from '@sfe/bifrost'
import Vue from 'vue'
import Vuex from 'vuex'
// ...others...
Vue.use(Vuex)
const store = new Vuex.Store({})
new AppContainer({
  mounted () {
    // 同步全局store狀態
    syncGlobalStore(store)

    // ...others...
    return new Vue({
      store,
      router,
      components: { App },
      template: '<App/>'
    }).$mount()
  }
}).start()

如果子系統自身的狀態需要共享,Bifrost還會提供installGlobalModule函數。該函數會將當前子系統需要共享的狀態掛載到全局Store下,其他子系統可以通過前面提到的方式來同步這些狀態。雖然Bifrost提供了子系統通信的能力,但在實際拆分子系統時,應該儘量避免這種情況發生。如果兩個子系統之間需要頻繁通信,那就應該考慮把他們劃分到同一個子系統。

公共依賴

由於各個子系統都需要集成到企管平臺,爲了保證體驗的一致性,大家都是基於同樣的組件庫進行開發。幾乎所有項目都會依賴lodash、Moment等基礎庫,因此如果不對公共依賴進行管理,項目會加載大量冗餘代碼。

針對這個問題,我們採用的是Webpack External方式來解決。構建時,各個子系統會將公共依賴排除,主系統會打包一份包含所有這些公共依賴的DLL文件。子系統在運行時,直接從全局引用對應的依賴。如果子系統希望使用某些庫的特定版本,也可以選擇不排除這些依賴項。這在子系統希望升級某些依賴庫的時候顯得極爲有用:通過子系統的局部升級,可以限制依賴庫升級的影響範圍,避免造成全局影響。

DLL文件會包含大部分公共依賴,但有一個例外——我們不會將Vue打到DLL文件中。因爲在實際開發中,很多庫都喜歡向Vue的原型鏈上掛載方法和屬性。如果不同團隊開發時掛載的內容恰好用到同一個字段,就會帶來不可預知的影響。

模塊複用

除了底層的依賴,我們還需要考慮對公共的業務模塊和工具函數進行復用。在企管平臺,我們爲公共業務組件庫和公共函數庫創建獨立的Git工程,然後將所有的子系統和公共模塊通過Git Submodule的方式引入到主系統的工程中。主系統採用Lerna的方式組織代碼,各個子系統在開發時,可以通過軟鏈直接引用到本地公共模塊的代碼,實現公共模塊的複用。當公共模塊發生更新,直接調用Lerna Publish就可以同時更新所有子系統package.json中依賴版本。

發佈及部署流程

前面提到,主系統採用的是JSONP方式加載子系統的配置文件,整個發佈過程都只需要發佈靜態資源,因此,Talos(美團內部自研的持續集成平臺)提供的前端靜態資源發佈的能力就可以滿足我們的需求。每次發佈時,只需要構建有更新的項目,並將打包後的靜態資源上傳到CDN即可。

版本控制

採用微前端架構還有一個額外的好處:在Nginx和實際的業務層之間,多了一層主系統,我們可以像客戶端一樣,動態決定需要加載的子系統版本。基於此,我們實現了子系統的版本控制和定向灰度功能。發佈時,我們通過參數確定本次發佈是否是灰度版本。在發佈成功後,會記錄本次發佈的灰度信息、版本和配置文件URL等信息。

主系統每次啓動時,首先會調用接口確定當前用戶所處的鏈路(全量/灰度),再根據鏈路信息加載相應的子系統。我們記錄了每次發佈的資源URL,所以也支持子系統的版本切換。只需要在版本服務中修改各條鏈路上需要激活的子系統版本,就可以輕鬆實現子系統版本切換。

埋點及錯誤上報

這裏我們主要討論Bifrost框架的埋點方案。在Bifrost項目中,可以藉助主系統提供的一系列鉤子函數實現針對子系統的埋點,包括:onAppLoading、onAppLoaded、onAppRouting、onError。每當子系統發生切換都會調用onAppRouting函數,因此我們可以在這裏記錄子系統加載的次數(PV)。onAppLoading和onAppLoaded則會在子系統初次加載時調用,通過計算Loading和Loaded成功率的比值,我們可以得到子系統加載的成功率。子系統加載失敗時,會調用onError函數,幫助排查子系統加載失敗的原因。

收益

今年年初,我們對企管平臺進行了微前端改造,目前系統已經在線上平穩運行半年時間,支持上百個零售商品牌,上千家門店業務的運轉。採用微前端架構,給我們項目帶來的好處是顯而易見的:

  1. 實現了異地合作開發時的完全解耦。採用微前端架構之後,兩地團隊在開發過程中再也沒有遇到代碼衝突的問題。
  2. 避免了單頁應用發展成“巨石”應用。目前,企管平臺總共實現了上百個頁面,採用微前端的方式進行劃分後,每個子系統包含的頁面都不超過三十個,子系統的可維護性得到大大提高。
  3. 今年企管平臺經歷了兩次大的組件庫版本升級。第一次升級時,項目還是單頁應用,我們在暫停業務開發的基礎上,耗費了大約一週的時間對所有的頁面進行迴歸驗證、完成升級。第二次升級時,我們已經完成了項目的微前端改造,可以通過增量的方式,先升級不常用的子系統,驗證通過後再升級其他子系統。這樣既不用中斷正常的業務開發,也保證了依賴庫升級時的影響範圍和風險可控。

不是“銀彈”

當然,同所有的架構方案一樣,微前端這種模式也存在一些折衷和妥協。在獲得低耦合和靈活性的同時,也引入了額外的複雜度。在微前端項目中,我們需要考慮多個工程的規範和代碼質量的統一,需要引入更多的自動化工具來管理項目的發佈部署流程,還需要處理多個前端工程運行在同一個域名下引起的Cookie覆蓋等問題。因此,在採用微前端架構之前,建議大家要謹慎的評估自己的項目是否真的適合採用微前端的方式,避免盲目引入微前端導致項目難以維護,得不償失。我們認爲,如果項目中存在以下兩個場景,比較適合採用微前端架構:

  1. 功能模塊較多,且各個功能模塊相對較爲獨立的中後臺系統。
  2. 項目存在大量歷史遺留問題,希望在保留已有功能的基礎上,開發新的功能模塊。

其他大部分項目都可以通過調整代碼結構,構建單頁應用,甚至採用最傳統的多頁應用等方式來進行優化、調整,從來達到降低耦合的目的。微前端並不是“銀彈”。

期許

從去年12月立項至今,Bifrost經歷了近一年的迭代,發佈了2個大版本和38個小版本。誕生之初,Bifrost僅僅是針對企管平臺這個特定業務場景的微前端方案。如今,已進化爲面向Vue技術棧的通用微前端框架。期間,我們圍繞Bifrost,逐步完善了整個微前端技術體系的建設,實現了Bifrost主/子系統的腳手架工程和命令行工具、子系統的管理平臺、灰度發佈功能等一系列平臺和工具,完成了Bifrost微前端生態的雛形。

當然,Bifrost依然還有很多可以提升的地方。未來,我們將會從以下幾個方面進一步完善Bifrost:

  • 提供更加完善的前端微服務治理工具。
  • 實現JS和CSS沙盒。
  • 支持更多的技術棧。

結語

隨着前端工程的日益複雜,我們對可擴展的前端架構的訴求也變得更加強烈。微前端作爲一種前端解耦的方案,自然更加頻繁地被大家所提及和應用。另一方面,雖然網上已經有了很多關於微前端的討論,但依然缺乏真正落地到生產環境的案例。因此,我們希望通過對閃購團隊近半年在微前端方案上的實踐分享,幫助大家對微前端從概念到應用有一個更加清晰的認識,也期待與大家一起交流,碰撞出更多的火花。

作者介紹

雨甫,美團閃購前端研發工程師。

本文轉載自公衆號美團技術團隊(ID:meituantech)。

原文鏈接

https://mp.weixin.qq.com/s/GgVo5KyZPlEsEeICcPyuLA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章