使用 Angular 打造微前端架構的 ToB 企業級應用


這篇文章其實已經准備了11個月了,因為雖然我們年初就開始使用 Angular 的微前端架構,但是產品一直沒有正式發布,無法通過生產環境實踐驗證可行性,11月16日我們的產品正式灰度發布,所以是時候分享一下我們在使用 Angular 微前端這條路上的心得(踩過的坑)了額,希望和 Angular 社區一起成長一起進步,如果你對微前端有一定的了解並且已經在項目中嘗試了可以忽略前面的章節。

什么是微前端

微前端這個詞這兩年很頻繁的出現在大家的視野中,最早提出這個概念的應該是在 ThoughtWork 的技術雷達,主要是把微服務的概念引入到了前端,讓前端的多個模塊或者應用解耦,做到讓前端的子模塊獨立倉儲,獨立運行,獨立部署。

那么微前端和微服務到底有什么區別呢?

下面這張圖是微服務的示意圖,微服務主要是業務模塊按照一定的規則拆分,獨立開發,獨立部署,部署后通過 Nginx 做路由轉發,微服務的難點是需要考慮多個模塊之間如何調用的問題,以及鑒權,日志,甚至加入網關層

 

image.png

 

對於微服務來說,模塊分開解藕基本就完事了,但是微前端不一樣,前端應用在運行時卻是一個整體,需要聚合,甚至還需要交互,通信。

 

image.png

 

為什么需要微前端(Micro Front-end)

  1. 系統模塊增多,單體應用變得臃腫,開發效率低下,構建速度變慢;
  2. 人員擴大,需要多個前端團隊獨立開發,獨立部署,如果都在一個倉儲中開發會帶來一些列問題;
  3. 解決遺留系統,新模塊需要使用最新的框架和技術,舊系統還繼續使用。

微前端的幾種方案對比

方式 描述 優點 缺點 難度系數
路由轉發 路由轉發嚴格意義上不屬於微前端,多個子模塊之間共享一個導航即可 簡單,易實現 體驗不好,切換應用整個頁面刷新 🌟
嵌套 iframe 每個子應用一個 iframe 嵌套 應用之間自帶沙箱隔離 重復加載腳本和樣式 🌟🌟
構建時組合 獨立倉儲,獨立開發,構建時整體打包,合並應用 方便依賴管理,抽取公共模塊 無法獨立部署,技術棧,依賴版本必須統一 🌟🌟
運行時組合 每個子應用獨立構建,運行時由主應用負責應用管理,加載,啟動,卸載,通信機制 良好的體驗,真正的獨立開發,獨立部署 復雜,需要設計加載,通信機制,無法做到徹底隔離,需要解決依賴沖突,樣式沖突問題 🌟🌟🌟
Web Components 每個子應用需要使用 Web Components 技術編寫組件或者使用框架生成 面向未來 不成熟,需要踩坑 🌟🌟🌟

上述只是簡單列舉了幾種實現方式的對比,當然這些方案也不是互斥的,選擇哪種方案取決你的業務場景是什么,以下幾個前提條件對於技術選型至關重要:

  • 是否為 SPA 單體應用?
  • 技術棧是否統一,需要支持跨框架調用嗎?
  • 是否需要應用間徹底隔離?

我們是做企業級 SaaS 平台的,肯定是 SPA 單體應用,技術棧都是 Angular,應用之間不需要徹底隔離,反而需要共享通用樣式和組件,避免重復加載。

所以選擇的是: 運行時組合  方案。

Worktile 的微前端技術選型之路

目前市面上的微前端解決方案並不多,關注度和成熟度最高的應該就是 single-spa

國內也有很多團隊都有自己的微前端框架,比如開源了的基於 single-spa 的 qiankun - 可能是你見過最完善的微前端解決方案 , 還有 phodal 的 mooa 以及無數內部的解決方案(最近阿里飛冰也開源 了面向大型工作台的微前端解決方案 icestark,只支持 React 和 Vue)

我們在做技術選型的時候首要考慮的就是  single-spa  和  mooa ,  single-spa  成熟度應該最高,示例文檔很完善, mooa  為 Angular 打造的主從結構的微前端框架,和我們的業務和技術符合度最高,研究一段時間后最終我們還是選擇了自研一套符合自己的微前端庫(因為比較簡單,不敢稱之為框架),主要是因為我們的業務有以下幾個需求在以上的框架中不滿足或者說很難滿足, 甚至需要高度定制。

  • 產品是主從結構的,Portal 包含左側導航,消息通知以及子應用管理
  • 需要在多個子應用之間通信,主應用或者某個子應用需要打開其他子應用的詳情頁或者路由跳轉
  • 子應用A的某個頁面中可能會加載子應用B的某個組件
  • 基於以上2個特性,所以需要提供並存模式,即當前顯示的雖然是 B 應用,但是要保證 A 應用正常可以調用,如果銷毀了就無法被其他應用調用
  • 需要提供預加載功能
  • 子應用的樣式也需要獨立加載
  • 路由,不管是在主應用還是子應用,路由體驗要和單體應用一致

我運行了  single-spa  和  mooa  的示例,主要是一些簡單的渲染展示,一旦需要滿足以上一些特性還是需要修改很多東西, mooa  實現應該還是比較全面也比較適合我們的,但是它的示例中路由有一些問題,頁面跳轉了但是路由沒有變,打包已經拋棄了 Angular CLI,代碼層面參考了  single-spa  的很多東西,API 可以再度簡化,既然是為 Angular 定制的,我覺得應該以 Angular 的方式實現更符合,當然不排除作者想要后期支持 React 和 Vue,不可否認的是  phodal  本人對於微前端的理解的確很深,寫的很多不錯的微前端的文章 microfrontends, 甚至出過唯一一本微前端的書《前端架構 - 從入門到微前端》,我在實現微前端的時候也借鑒參考了它的很多思想和實現方式。

使用 Angular 打造微前端應用

使用 Angular 實現微前端其實比 React 和 Vue 更加困難,因為 Angular 包含 AOT 編譯,Module,Zone.js ,Service 共享等等問題,React 和 Vue 直接子應用 JS 加載渲染頁面某個區域即可。

選擇動態加載模塊后編譯還是加載整個應用

在 Angular 單體應用中,必須有一個根模塊 AppModule,然后是每個特性模塊 FeatureModule,每個特性模塊可以有自己的路由,當然可以使用路由的惰性加載這些特性模塊,但是在微前端架構中,每個子模塊都是獨立倉儲的,如何在運行時把子模塊加載到根模塊就是一個技術選擇難點。

  1. 第一種方案就是把每個子模塊當作一個特性模塊,然后在打包的時候隨着主應用一起打包編譯,這樣是最簡單的,但是這個無法做到獨立部署,而且每次部署都是全量更新
  2. 第二種方案還是把子模塊當作一個特性模塊,在主應用通過 SystemJsNgModuleLoader 加載子模塊,然后編譯運行,(注:SystemJsNgModuleLoader 在新版本已經遺棄)
  3. 第三種方案就是每個子模塊是一個獨立的應用,和主應用一樣,有自己的 AppModule, 路由,選擇這種方案就需要處理多個應用路由同步的問題,還有就是 Angular 目前的依賴庫是無法直接運行時使用的,需要每個子應用一起編譯,無法做到公共依賴庫抽取(可能有其他方案)
  4. 第四種方案就是把所有的子模塊編譯成 Web Components 使用,我暫時沒有深入研究過,選擇這種方案直接使用組件肯定沒有問題,但是使用 Web Components 后路由如何處理我不知道。

我們最終選擇了最復雜的第三種方案,因為新的 Ivy 渲染引擎正式發布后會解決第三方依賴庫運行時直接使用的問題,至於 Web Components 沒有深入研究,因為目前第三種方案運行挺好的。

 

image.png

 

應用注冊,加載,銷毀機制

這個是所有微前端應用的基礎和核心,但是我覺得反而是最簡單容易實現的,主要要做的就是:

  • 提供靜態資源動態加載功能
  • 配置好子應用的規則,包含:應用名稱,路由前綴,靜態資源文件

    this.planet.registerApps([
      {
          name: 'app1',
          hostParent: '#app-host-container',
          routerPathPrefix: '/app1',
          selector: 'app1-root',
          scripts: ['/static/app1/main.js'],
          styles: ['/static/app1/styles.css']
      },
      // ...
    ]);
    
  • 應用加載:根據當前頁面的 URL 找到對應的子應用,然后加載應用的靜態資源,調用預定義好的啟動函數直接啟動應用即可,在 Angular 中就是啟動根模塊  platformBrowserDynamic().bootstrapModule(AppModule) 。

  • 應用的預加載:當前應用渲染完畢會預加載其他應用,並啟動,並不會顯示
  • 銷毀應用使用  appModuleRef.destroy(); 

按照上述的步驟處理簡單的場景基本就足夠了,但是如果希望應用共存就不一樣了,我們的做法是把  bootstrapped  狀態隱藏起來,而不是銷毀,只有  Active  狀態的應用才會顯示在當前頁面中。

路由

因為選擇了每個子應用是獨立的 Angular 應用,同時還可以共存多個子應用,那么多個應用的路由同步,跳轉就成了難題,而且還要支持應用之間路由跳轉,應用之間通信,組件渲染等場景。我認為路由是我們在使用微前端架構中遇到的最復雜的問題。

目前我們的做法是主應用的路由中把所有子應用的路由都配置上,組件設置成  EmptyComponent  , 這樣在切換到子應用路由的時候,主應用會匹配空路由狀態,不會報錯,每個子應用需要添加一個通用的空路由  EmptyComponent 

{
        path: '**',
        component: EmptyComponent
}

除此之外還需要在切換路由的時候同步更新其他應用的路由,否則會造成每個應用的當前路由狀態不一致,切換的時候會有跳轉不成功的問題。

  • 主應用路由切換時,找到所有當前啟動的子應用,使用  router.navigateByUrl  同步跳轉
  • 子應用路由切換時,同步主應用路由,同時同步其他啟動狀態的子路由

我看了很多微前端框架包括  single-spa ,基本上路由這一塊沒有處理,完全交給開發者自己去填坑, single-spa  的 Angular 示例基本就是切換就銷毀了 Angular 應用,因為沒有並存,所以也就不需要處理多個應用路由的問題了,當然它作為和框架無關的微前端解決方案,也只能做到這一步了吧。

這個等 Ivy 渲染引擎正式發布后,可以把子應用編譯成直接可以運行的模塊,整個應用如果只有一個路由會簡化很多。

共享全局服務

對於一些全局的數據我們一般會存儲在服務中,然后子應用可以直接共享,比如: 當前登錄用戶 , 多語言服務 等,簡單的數據共享可以直接掛載在 window 上即可,為了讓每個子應用使用全局服務和模塊內服務一致,我們通過在主應用中實例化這些服務,但后在每個子應用的 AppModule 中使用 provide 重新設置主應用的 value,當然這些不需要子應用的業務開發人員自己設置,已經封裝到業務組件庫中全局配置好了。

{
  provide: AppContext,
  useValue: window.portalAppContext
}

應用間通信

應用間通信有很多中方式,我們底層使用瀏覽器的  CustomEvent  ,在這之上封裝了  GlobalEventDispatcher  服務做通信(當然你也可以使用在 window 對象上掛載全局對象實現),場景就是某個子應用要打開另外一個子應用的詳情頁

// App1
globalEventDispatcher.dispatch('open-task-detail', { taskId: 'xxx' });

// App2
globalEventDispatcher.register('open-task-detail').subscribe((payload) => {
    // open dialog of task detail
});

應用間組件互相調用

在我們的 敏捷開發 子產品中,一個用戶故事的詳情頁,需要顯示 測試管理 應用的關聯的測試用例和測試執行情況,那么這個測試用例列表組件放在  測試管理  子應用是最合適的,那么用戶故事詳情頁肯定在 敏捷開發 應用中,如何加載 測試管理 應用的某個組件就是一個問題。

這一塊使用了  Angular CDK 中的 DomPortalOutlet  動態創建組件,並指定渲染在某個容器中,這樣保證了這個動態組件的創建還是  測試管理  模塊的,只是渲染在了其他應用中而已。

const portalOutlet = new DomPortalOutlet(container, componentFactoryResolver, appRef, injector);
const testCasesPortalComponent = new ComponentPortal(TestCasesComponent, null);
portalOutlet.attachComponentPortal(testCasesPortalComponent);

工程化

使用微前端開發應用不僅僅要解決 Angular 的技術問題,還有一些開發,協作,部署等工程化的問題需要解決,比如:

  • 公共依賴庫抽取
  • 本地如何啟動開發
  • 如何打包部署,生成的 hash 資源文件如何通知主應用

應用公共依賴庫抽取避免類庫重復打包,減少打包體積,這就需要自定義 Webpack Config 實現,起初我們是完全自定義 Webpack 打包 Angular 應用,一旦這么做就會失去很多 CLI 提供的方便功能,偶爾發現了一個類庫 angular-builders ,他的作用其實就是在 Angular CLI 生成的 Webpack Config 中合並自定義的 Webpack Config,這樣就做到了只需要寫少量的自定義配置,其余的還是完全使用 CLI 的打包功能,差一點就要自己寫一個類似的工具了。
在主應用中把需要公共依賴包放入  scripts  中,然后在子應用中配置  externals ,比如: moment   lodash   rxjs  這樣的類庫。

const webpackExtraConfig = {
    optimization: {
        runtimeChunk: false // 子應用一定要設置 false,否則會報錯
    },
    externals: {
        moment: 'moment',
        lodash: '_',
        rxjs: 'rxjs',
       'rxjs/operators': 'rxjs.operators',
        highcharts: 'Highcharts'
    },
    devtool: options.isDev ? 'eval-source-map' : '',
    plugins: [new WebpackAssetsManifest()]
};
return webpackExtraConfig;

WebpackAssetsManifest 主要作用是生成  manifest.json  文件,目的就是讓生成的 Hash 文文件的對應關系,讓主應用加載正確的資源文件。

本地開發配置  proxy.conf.js  代理訪問每個子應用的資源文件,同時包括 API 調用。

基於 Angular 的微前端庫 ngx-planet

以上是我們在使用 Angular 打造微前端應用遇到的一些技術難點和我們的解決方案,調研后最終選擇自研一套符合我們業務場景的,同時只為 Angular 量身打造的微前端庫。

Github 倉儲地址:ngx-planet
在線 Demo:http://planet.ngnice.com

不敢說 “你見過最完善的微前端解決方案” ,但至少是 Angular 社區目前我見過完全可用於生產環境的方案,API 符合 Angular Style ,國內很多大廠做微前端方案基本都忽略了 Angular 這個框架的存在,Worktile 四個研發子產品完全基於  ngx-planet  打造開發,經過接近一年的踩坑和實踐,基本完全可用。

 

image.png

 

希望 Angular 社區可以多一些微前端的解決方案,一起進步,我們的方案肯定也存在很多問題,也歡迎大家提出改進的建議和吐槽,我們也將繼續在 Angular 微前端的路上繼續深耕下去,如果你正在尋找 Angular 的微前端類庫,不妨試試 ngx-planet。

將來會調研在 Ivy 渲染引擎下的優化和改進方案。

 

 

Worktile官網:www.worktile.com 

本文作者:Worktile 高級工程師 徐海峰

文章首發於「Worktile官方博客」,轉載請注明來源。

 


免責聲明!

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



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