微前端架構設計之基於 Vue.js 的微前端實現


Situation

19年之前團隊內部前端編寫模式是:原始項目 A 使用 Vue-CLI2 創建,現在需求方提交新模塊 B 的需求給到產品。當產品交付原型圖后,復制項目 A 改配置成新項目 B。項目 B 基於項目 A 的代碼修修改改,待開發完之后打包到后端的 Java SpringBOOT 項目內部,通過 Jenkins 部署后端應用后通過 Eureka 注冊微服務到 SpringCould,然后使用 nginx 反向代理來實現不同項目之間不同 domain 的分發。

由於在我們的項目中存在許多不同的 domain,比如 notify , workflow , construction , appMgt , form , auth , print , pan , market , hr , asset, contract, material, commerce, ... 等等。隨着項目越來越多,前端開發暴露的問題如下:

  • 不同項目之間相同功能存在不同的實現
  • 不同項目之間相同依賴存在版本不兼容
  • 不同項目之間相同依賴的修改,需要排查每個項目再手動去修改、復制粘貼,然后一一測試、部署、發版,浪費時間和開發效率,屬於重復勞動
  • 不同項目之間相同依賴被重復構建打包以及被客戶端重復的加載和執行
  • 被復制項目越來越臃腫
  • 整體的維護成本隨着時間長度和項目個數與其復雜度逞正相關

Task

2018 年底項目經理 Jimmy 通過 infoQ 了解到 Micro-FrontEnds 概念后,替前端團隊重新划定了架構方向,於是我們便嘗試在項目中實現微前端架構來解決上述問題。

於是,開始在社區調研。當時寫微前端的文章並不多,實踐的團隊也比較少,留下印象最深刻的是這篇 phodal/microfrontends 文章,文中提到了微前端的各種實現方式、實現成本、工程成本等問題,比較全面。

真正意義上的微前端應該是框架無關的,現在社區中首推 Single-SPA,以及螞蟻前端團隊基於此框架封裝的 qiankun

但在 18 年底時,Single-SPA 還未 Production Ready,於是我們決定實現我們團隊內部的微前端框架。加之團隊技術棧統一使用 Vuejs,所以無需做到框架無關。於是我們最終的選擇是 微前端:微應用化

不但解決了上述問題,同時實施成本低技術難度小維護成本低

Action

微應用化

在各個 domain 時通過 nginx 反向代理的情況下,維護公共代碼很痛苦。所以將各個業務模塊拆為微應用后,公共部分則被組合成獨立應用。社區稱之為基座應用,或者主應用。

將所有 domain 即子應用的公共部分封裝到主應用中單獨維護,同時發布到內部 npm 私倉,以供子應用在開發環境中將其作為依賴安裝。在需要修改公共代碼或公共服務的時候,只需要修改、測試、構建、和部署主應用即可,解決了前文提到的關於公共依賴的幾大痛點。

然后將各個子模塊拆成獨立的業務模塊,使其都在主運行時中 運行。從而實現子應用的獨立開發、獨立維護、獨立部署。

基礎架構示意圖

基礎流程

我們拆分了主應用 App(即基座應用)和各個 domain 子應用 SubApp。使用 Vue-MFE 為 router 做了功能增強,並通過 AppConfig 的 resources 配置從 Package Server 動態加載子應用代碼以實現子應用的加載、執行和渲染。

執行流程示意圖

主應用 App

App 是一個獨立的完整的 Vuejs 應用,獨立運行、開發和部署。我們的項目中,主應用除了包含了下列公共內容之外,還包含了 VueMfe 和 PackageServer (后文介紹).

ibuild-portal-lte 基礎架構圖

  • 公共資源,比如:樣式、字體、圖標、圖片、Theme 等

  • 公共數據,比如:Auth, Config, Message 等 Vuex modules,我們通過 Vuex 實現全局 Store 共享,借助其 dynamic-module-registration 的能力,實現子應用之間共享數據的注冊和銷毀。

  • 公共路由,比如: /index , /error/401 , /error/404

  • 公共布局,比如: <DefaultContainer /> , <DetailContrainer />

  • 公共插件,比如:ProgressBar, MicroFrontend, LazyLoad, Vuex, VueRouter, Element-UI 等

  • 公共服務,比如:Utils, Http, Socket, Storage 等

  • 公共組件,比如: <ContentBlock /> , <FilePreview />

  • 公共依賴:在我們的項目中直接在 publlic/index.html 手動引入了構建好的公共依賴,同時維護了一份公共的 webpack externals 配置,以避免主應用和各個子應用在打包時重復構建公共依賴。
    publlic/index.html:
    publlic/index.html
    external.config.js:
    公共依賴

  • 公共配置,比如:vue.config.js,.prettierrc,.eslintrc, .babelrc, .stylelintrc 等配置

  • 鑒權和校驗,比如:路由權限校驗 Router before/after Hook 等公用狀態校驗

Vue-MFE

Vue-MFE 的核心由兩部分組成 EnhanceRouter + MicroAppLoader. EnhanceRouter 提供了圍繞路由的核心功能:支持添加嵌套路由支持動態安裝路由監聽未匹配路由。MicroAppLoader 提供了圍繞微前端的核心功能:創建主應用創建子應用懶加載應用

EnhanceRouter:

Vue.js 官方提供的 Vue-Router 雖然提供了 router.addRoutes(routes: Array ) 這個接口,但有一個致命的缺點,就是不支持嵌套路由。可以參考這個 ISSUE 的討論,Dynamically add child routes to an existing route,根據 vuejs/rfcs 的 Dynamic routing ,官方團隊也正在征集社區意見決定是否實現這個功能。

在實際業務中,子應用通常是某個 Layout 下的嵌套子路由。子應用 SubApp 也通常繼承主應用 App 的布局 layout。所以在 vue-mfe 內部重寫了 router.addRoutes 方法以實現支持嵌套路由的目的。

  1. 在 Vue-MFE 內部維護了獨立的 pathListpathMap ,雖然增加了內存開銷的成本,但好處是不會對 VueRouter 本身功能造成任何影響。
  2. 當調用 router.addRoutes(routes: RouteConfig[], parentPath: string) 時,深度優先找到 parentPath 所在的舊路由 oldRoute,並將其 children 與新的 routes 合並后生成新路由的參數 options: newRouterOptions
  3. 再使用 newRouterOptions 重新實例化 new VueRouter(options: newRouterOptions) 拿到新的 router.matcher 並替換 app 的原 matcher app.$router.matcher 便達到了支持動態嵌套路由、動態更新應用路由注冊表(動態安裝路由)的目的。

監聽未匹配路由則是通過注冊 beforeEach 鈎子,攔截路由 to 是否已存在於當前路由表中,若不存在則認為這可能是一個需要被動態加載的子應用。

MicroAppLoader:

VueMfe 的 createApp 和 createSubApp 的核心目的是注冊微前端配置項。懶加載應用則是和 EnhanceRouter 的功能配合使用。

VueMfe 完整流程如下:

  1. 使用 VueMfe.createApp(AppConfig) 注冊 主應用 App,初始化 Router,刷新 VueMfe 內部路由注冊表 pathListpathMap
  2. 注冊 beforeEach 鈎子,以攔截路由 to 是否已存在於當前路由中,若不存在則認為這是一個需要被動態加載的子應用。
  3. 攔截到未匹配路由后根據路徑獲取 prefix 前綴 getAppPrefix(to)
  4. 然后通過 MicroAppLoader 動態加載和執行 resources[prefix] 的資源,如果獲取不到則會拋出無法找到 prefix 資源的異常。
  5. 獲取到 SubApp 資源后,廣播加載開始 LOAD_START 事件,開始安裝 SubApp 的靜態資源和路由,執行 SubApp 的 init 初始化方法,並將執行結果 result 返回的 routes 動態安裝到 parentPath 下,加載成功后廣播加載成功 LOAD_SUCCESS 事件。
  6. 執行 next(to) 跳轉到用戶訪問的路由 prefix 實現完整閉環。

Vue-MFE 完整流程示意圖

vue-cli-plugin-mfe

基於微應用架構設計的 CLI 插件,由 subapp generator + cli commands 組成。

vue-cli-plugin-mfe 架構圖

使用 vue add mfe --registry={proviteNpmRegistryLink} 添加 mfe 插件同時生成 SubApp Template。並在 vue-cli-service runtime 為 SubApp 注冊 package, upload, publish 三個命令,分別負責 SubApp 的打包、上傳和發布。

vue-cli-plugin-mfe commands usage

SubApp 在 package 階段做了特殊處理,其被 webpack 打包成 umd 格式的包。因為不同的 App 由不同的 webpack build context 構建,無法共享 chunkId 和 moduleId。

因此 build 的入口 必須是執行 VueMfe.createSubApp({}: SubAppConfig) 的文件,而執行該方法返回的 SubAppConfig 配置項在打包后被作為全局變量供 VueMfe 的 loader 動態加載、執行和安裝。而后續其他資源的控制權則交還給 _webpack_require_ 控制,無論是 code-splitting 還是 VueMfe.Lazy 均被 Webpack 按需加載。

而 19 年末 webpack5 提供的 module-federation,正是為了解決這個問題提出,但當時還是 beta 版本。

出現了新曙光,而且業界很多大佬已經開始了探索。后續,會酌情決定是否跟上 Webpack5 的升級。

Package Server

插件執行 package 打包完成后執行 upload 命令上傳其靜態資源 js/css/img 到 package server。它可以是一個接口,也可以是 oss 服務器,也可以是 CDN。只要能在網絡中被正常訪問即可。

准確來說,package server 是一個抽象層,可以做復雜實現,可以做簡單實現,也可以不用實現。

但在 ibuild-portal-lte 中,package server 的實現是后端的 RPC(Remote Procedure Call)接口。上傳文件使用的是 /api/mfe/uplaod,查詢所有 SubApp 資源使用的是 /api/mfe/resources 接口,發布的實現是 /api/mfe/publish 接口。

每次發版,vue-cli-mfe-plugin 會根據時間戳生成對應的版本號和包名 {subAppName}@{timeStamp}。后端會記錄當前版本號及其對應的資源入口文件,用於實現版本的歷史記錄、當前版本、回滾版本等操作。

子應用 SubApp

在提出了所有公共代碼之后,子應用變成了純業務代碼的容器被主應用在運行時加載執行。因此在啟動子應用之前需要先啟動主應用,以擁有主應用運行時的能力。

在開發環境下,將子應用的入口設置為主應用,將 devServer 的 contentBase 也設置為主運行時的 public 目錄,以保證 主應用App/子應用SubApp 在開發和生產環境下的一致性。

修改子應用的構建入口 construction/frontend/vue.config.js

SubApp/src/vue.config.js

模式對比

不通模式對比

Result

1968年,計算機學家梅爾文·E·康威發表了一篇著名論文,后來被稱為康威定律(Conway's law)。

"軟件系統的架構,反映了公司的組織結構。"

我們團隊也是,團隊內部成員根據不同 domain 被分為各個子團隊。現行的開發模式是公共代碼由前端同學一起維護,各個 SubApp 的代碼則由各個子團隊獨立開發和維護。如果需要暴露組件和模塊給其他團隊,則在 SubApp 內部單獨暴露即可,除非其能被通用才會集成到 App 中暴露。

截止目前為止,團隊內已使用微前端之微應用架構快 2 年了。由 Jimmy 指導架構的設計,我來編碼實現的微前端架構體系,雖不算完美,或者一些概念在日新月異的前端領域已經過時。但是這套方案,在團隊內部切切實實解決了開篇提到的種種問題,也有效的提升了團隊之間及團隊成員的溝通協同和開發效率。

謝謝團隊同事對我工作的幫助、支持和配合。

Refs


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM