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

這篇文章其實已經準備了11個月了,由於雖然咱們年初就開始使用 Angular 的微前端架構,可是產品一直沒有正式發佈,沒法經過生產環境實踐驗證可行性,11月16日咱們的產品正式灰度發佈,因此是時候分享一下咱們在使用 Angular 微前端這條路上的心得(踩過的坑)了額,但願和 Angular 社區一塊兒成長一塊兒進步,若是你對微前端有必定的瞭解而且已經在項目中嘗試了能夠忽略前面的章節。css

什麼是微前端

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

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

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

image.png

對於微服務來講,模塊分開解藕基本就完事了,可是微前端不同,前端應用在運行時倒是一個總體,須要聚合,甚至還須要交互,通訊。github

image.png

爲何須要微前端(Micro Front-end)

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

微前端的幾種方案對比

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

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

  • 是否爲 SPA 單體應用?
  • 技術棧是否統一,須要支持跨框架調用嗎?
  • 是否須要應用間完全隔離?

咱們是作企業級 SaaS 平臺的,確定是 SPA 單體應用,技術棧都是 Angular,應用之間不須要完全隔離,反而須要共享通用樣式和組件,避免重複加載。json

因此選擇的是:運行時組合 方案。bootstrap

Worktile 的微前端技術選型之路

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

國內也有不少團隊都有本身的微前端框架,好比開源了的基於 single-spa 的 qiankun - 多是你見過最完善的微前端解決方案 , 還有 phodal 的 mooa 以及無數內部的解決方案(最近阿里飛冰也開源 了面向大型工做臺的微前端解決方案 icestark,只支持 React 和 Vue)bash

咱們在作技術選型的時候首要考慮的就是 single-spamooa, single-spa 成熟度應該最高,示例文檔很完善,mooa 爲 Angular 打造的主從結構的微前端框架,和咱們的業務和技術符合度最高,研究一段時間後最終咱們仍是選擇了自研一套符合本身的微前端庫(由於比較簡單,不敢稱之爲框架),主要是由於咱們的業務有如下幾個需求在以上的框架中不知足或者說很難知足, 甚至須要高度定製。

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

我運行了 single-spamooa 的示例,主要是一些簡單的渲染展現,一旦須要知足以上一些特性仍是須要修改不少東西,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:planet.ngnice.com

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

image.png

但願 Angular 社區能夠多一些微前端的解決方案,一塊兒進步,咱們的方案確定也存在不少問題,也歡迎你們提出改進的建議和吐槽,咱們也將繼續在 Angular 微前端的路上繼續深耕下去,若是你正在尋找 Angular 的微前端類庫,不妨試試 ngx-planet。

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


本文做者:Worktile 高級工程師 徐海峯

文章來源:Worktile技術博客

歡迎訪問交流更多關於技術及協做的問題。

文章轉載請註明出處。

相關文章
相關標籤/搜索