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

blank

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

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

什麼是微前端

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

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

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

blank

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

blank

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

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

微前端的幾種方案對比

blank

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

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

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

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

Worktile 的微前端技術選型之路

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

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

我們在做技術選型的時候首要考慮的就是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本人對於微前端的理解的確很深,寫的很多不錯的微前端的文章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 沒有深入研究,因為目前第三種方案運行挺好的。

blank

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

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

  • 提供靜態資源動態加載功能
  • 配置好子應用的規則,包含:應用名稱,路由前綴,靜態資源文件
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 ,這樣在切換到子應用路由的時候,主應用會匹配空路由狀態,不會報錯,每個子應用需要添加一個通用的空路由

{ 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 });

這裡需要注意的是:如果直接調用其他子應用的代碼需要觸發應用的髒檢查,否則會不起作用,使用ngZone.run(()=> { })或者applicationRef.tick() ,當然我們做的GlobalEventDispatcher已經處理好了,不需要開發者自己處理,包括上面說的路由同步問題。

可以直接看代碼實現: global-event-dispatcher.ts

跨應用組件渲染

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

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

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

因為我們內部的組件庫業務組件庫都是基於CDK 開發的,所以也就直接使用了,如果將來有其他用戶使用不想引入CDK ,也可以單獨把這個邏輯抽取出來。

工程化

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

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

應用公共依賴庫抽取避免類庫重複打包,減少打包體積,這就需要自定義Webpack Config 實現,起初我們是完全自定義Webpack 打包Angular 應用,一旦這麼做就會失去很多CLI 提供的方便功能,偶爾發現了一個類庫just-jeb/angular-builders ,他的作用其實就是在Angular CLI生成的Webpack Config中合併自定義的Webpack Config,這樣就做到了只需要寫少量的自定義配置,其餘的還是完全使用CLI 的打包功能,差一點就要自己寫一個類似的工具了。

在主應用中把需要公共依賴包放入scripts中,然後在子應用中配置externals ,比如: moment、 lodashrxjs這樣的類庫。

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 倉儲地址

在線Demo: Micro Front-end for Angular

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

blank

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

最後附上一張使用微前端打造的研發產品截圖

blank

文章來源於Worktile部落格解讀,想了解更多關於企業協作內容可前往查看

知乎的編輯器,真是寫一次吐一次,如果想看Markdown文件直接移動到 github.com/worktile/ngx

What do you think?

Written by marketer

blank

知源· 致遠- AntV 11.22 年度發布

blank

9102 年,螞蟻金服前端是怎麼寫圖表的?