網易嚴選企業級微前端解決方案與落地實踐

微前端架構是一種設計方法,其中,前端應用被分解爲多個鬆散而協同工作的半獨立“微應用”。微前端的思想來源於微服務,其名稱也遵循了微服務的命名方式。那麼,微前端到底如何落地呢?來自網易的資深前端開發工程師張浩爲大家解讀了網易嚴選企業級微前端的解決方案與落地實踐。

“微前端”這個詞大家應該並不陌生,甚至可以說近期在前端技術領域算是一個熱門的話題,目前市面上不乏各種微前端的文章與技術方案。

傳統的前端 SPA 開發模式,一方面,隨着系統迭代,發展到一定程度,規模已經非常龐大。通過項目內的模塊化,已經無法解決業務膨脹的問題;另一方面,隨着應用框架的升級、變遷,多框架多版本、同框架多版本共存的狀態無法避免,必須要有一種方案,能對整個業務進行合理拆分、組合,所以微前端的思想應運而生。

而這之中的痛點,以及大家的目標顯然都是一致的。簡而言之,我們希望 項目分離,運營聚合

在網易嚴選,微前端不僅僅是一個框架或者說一個“殼”,而是一整套包括規範、工具、框架、配置中心、應用監控等一系列相關功能在內的前端應用架構體系。當然,不同的企業有不同的需求和應用場景,我們可以針對性地做一些個性化開發和改造,從而真真正正地在業務場景中提高生產力。

如下是網易嚴選微前端的總體架構圖:

網易嚴選微前端的建設背景

挑重點來說,有以下幾點切實而緊迫的實際需求。

1. 技術棧的迭代與升級

嚴選發展至今,內部的技術棧頗多。就當前來說,NEJ 等內部技術棧 /Jquery/AngularJs/ Angular 2.x,4.x,6.x/React 0.x,15.x,16.x/Vue 1.x,2.x,可以說一應俱全。隨着技術的發展,我們也必然會持續不斷地迭代我們的技術體系,同時也要兼容老的項目運行。

2. 巨石應用的維護困難

嚴選內部有非常多的項目經過多年持續不斷的迭代,已經變得非常龐大,動輒 300+ 的頁面數量讓這些系統越來越難以維護,甚至一次編譯部署都要耗費非常久的時間。這其實在許多企業級的項目開發中也非常常見,也就是所謂的“巨石應用”。

3. 新的前端複用模式探索

各系統之間有很多功能需要複用 我們可以通過複製代碼去做模塊複用,當然這種方式不夠高級,而且難以維護。我們還可以通過把功能模塊封裝成包,然後以發佈 npm 包的方式來複用。但其實這種方式,在具體的業務場景開發中也存在諸多問題。

有一個現實案例,嚴選有個業務功能叫 庫存模塊,包含 10 來個頁面。我們把這個模塊整體發佈成了一個 npm 包,在多個項目中引用。本來設想是美好的,只需要和 input、select 那樣的基礎組件去維護就好了。但是,現實擊碎了我們的美好想法。這個業務功能模塊並不像基礎組件一樣穩定,一方面,是因爲頻繁地需求迭代;另一方面,是因爲模塊過大 bug 變多,導致我們需要頻繁修改這個包。所以,每次在開發聯調測試階段都極爲繁瑣,我們需要不斷地在庫存模塊的項目裏調整代碼,發佈 npm,然後再用到庫存模塊的各個項目中,逐個更新依賴、構建、部署。這個過程效率低不說,還容易反覆和遺漏。

跨系統流轉的工單開發模式探索 比如一個採購工單,如圖:

採購工單

需要流轉【採購系統 —> 商品中心 —> 財務系統 】,所以爲了開發一個採購工單,我們需要涉及到 3 個系統的產品、開發、測試人員,流程繁瑣,開發週期長,效率低下。

採購流程開發

4. 跨多團隊合作開發困難

出於某些原因,我們會將一個應用拆分爲多個功能模塊,跨部門多團隊來共同完成開發。比如,其中有一個功能模塊會整體交給外包團隊。當前的流程是:外包團隊先從嚴選 git 倉庫複製一份代碼出去開發,待開發完成後,再將代碼重新合併到嚴選 git 倉庫,然後再編譯後上線。

這種多團隊合作的問題不言而喻:

(1) 容易引起代碼衝突;

(2) 各團隊開發的模塊之間沒有隔離,容易互相影響,從而遇到各種不可預知的問題。

所以,每次在合併代碼後,仍然需要花費不少的時間進行迴歸測試。

鑑於以上的場景,我們從 2019 年初就開始了對微前端方案的調研,期間也借鑑了許多外部的優秀方案與設計。經過長時間的探索、開發、實踐,以及實際業務場景的多個項目落地,形成了現在的嚴選微前端應用架構體系。

微前端建設方案要點分析

正如前面所說,微前端其實是一個完整的技術應用架構體系。 該體系涉及到的技術點比較廣,從具體的應用開發,到主框架的實現原理,再到我們相關的配套設施(配置中心、應用監控等),恐怕不是短短一篇文章所能闡述。這裏簡單闡述幾點。

1. 應用開發鏈路閉環

嚴選的項目基於 @sharkr/cli 腳手架創建,底層基於 webpack。我們通過約定、插件、配置等手段,讓每個應用在開發、構建、部署時,不需要用戶再去做一絲一毫的改動,從而都能符合我們微前端所需的規範。說白了,我們做到了“零成本接入”。舉兩個例子:

(1)假設我們有兩個 react 應用,都基於 webpack 構建,爲了讓它們能在同一個頁面上跑起來,我們就需要對 webpackJsonp 進行配置,修改全局暴露的 window.webpackJsonp 名稱。而這個過程就是自動會在我們的工具中完成修改,無需手動地進行每個應用修改。

(2)嚴選的一個前端應用包含前端和 Node(用於承擔 BFF 層和 SSR 等功能)兩塊。我們構建後的應用,會自動在 Node 端生成一個接口 /xhr/config/get.json 用來獲取該應用的配置信息(包括 JS、CSS 靜態資源路徑等),用於後續的應用加載。(具體在下面圖中會說到)

當然,我們做的不止這些。簡單來說,就是我們可以通過一些工具和規範約定,同時與每個企業自身的 devops 平臺無縫對接,形成完整的開發鏈路閉環。

2. 主框架的關鍵技術點

在說這個之前,我們不妨先來簡單瞭解下當前幾種主流的微應用架構。

MPA + 路由分發

這種方式就是在多個獨立的 SPA 應用之間跳轉,通過把界面、導航、皮膚做成類似的樣子,讓用戶感覺像是同一個應用。

優點:

a. 框架無關;
b. 獨立開發、部署、運行;
c. 應用之間 100% 隔離。

缺點:

a. 應用之間的徹底割裂導致複用困難。(比如,每個應用左側和頂部都帶有導航,那麼, 當我要把該應用在其他系統中複用時,需要對該子應用的導航做較爲複雜的改動) ;
b. 每個獨立的 SPA 應用加載時間較長,容易出現白屏,影響用戶體驗;
c. 後續如果要做同屏多應用,不便於擴展。

類 Single-SPA

這裏我把與 Single-SPA 原理類似的,或基於 Single-SPA 開發的一些解決方案都歸爲一類,他們可能在實現上有所差異,但本質趨同。

主應用的代碼一般非常簡單,僅作爲加載容器,管理子應用的生命週期。主應用捕獲全局的路由事件,基於判斷當前路由需要加載哪個子應用,然後 load 它。比如路由爲 /vue,我們就加載 /vue 子應用;路由爲 /react,我們就加載 /react 子應用。當然,在路由切走後,也要卸載該應用。

優點:

a. 框架無關;
b. 獨立開發、部署、運行;
c. 項目自由切割,應用可以自由組合,方便複用;
d. 便於自由擴展功能。

缺點:

a. 子應用需要實現 mount、unmount 等鉤子,侵入式的代碼開發體驗並不友好;
b. 全局污染和資源競爭。

基座式 SPA,主從應用設計

這裏的“基座”也就是主應用,會包含應用依賴的絕大多數環境,包括基礎框架、基礎組件與第三方依賴包,而子應用只會包含自己的一些業務代碼(以及一些主應用可能不包含的 third party)。當我們的主應用啓動之後,基本就有了全套的運行時環境。同樣的,主應用捕獲全局的路由事件,基於判斷當前路由需要加載哪個子應用,然後 load 它。路由這裏有點不同,在類 Single-SPA 方案中,子應用在加載後,一般會由子應用去接管系統路由。而在基座式的方式中,子應用一般會把自己的路由註冊到主應用中,並不接管系統路由。子應用更像是主應用的一個“路由模塊”。

優點:

顯而易見,這種模式打包出來的子應用只包含了業務代碼,體積小、加載快、用戶體驗好。

缺點:

缺點也很明顯,首先基座就決定了它是框架強相關的,哪怕是基座的版本升級迭代,也會非常容易造成子應用 break change。

這個方案需要對自定義構建的依賴頗多,基本上需要自己定義一套方案,來解決公共資源的問題(當然這是難點不是缺點)。我見過有類似方案做的不夠完善的,未能實現獨立構建,而是必須依賴於主應用構建,也就是所謂的 構建時組合。同時,這種方式對規範的依賴最強,我們必須遵照一定的規範來開發項目,從 dev 到 build,都需要建設自己的開發體系來實現上述效果。

傳統 SPA + 組件化(比如 Web Components) + 私有 npm 源

這個概念比較好懂,簡而言之,就是把通用的一些業務功能發佈成組件,通過私有 npm 的方式去維護和管理。其中,跟框架無關又比較有代表性的方案就屬 Web Components 了。當然,這種模式更像是業務組件,或者說業務模塊,而不是應用。

優點:

對現有項目漸進式增強,逐步改進;

缺點:

隨着業務中組件數量的爆發式增加,組件粒度通信、組件的維護成本都急劇增加;並不能做到真正的獨立開發、測試、部署。

總結來說,如下圖:

總體分析下來,類 Single-SPA 是相對較好的。

嚴選的微前端方案,在 Single-SPA 的思想上進行了大刀闊斧的改革和創新,同時藉助 Node 層(數據層、服務端渲染、靜態資源處理)來作爲支撐,可以說形成了一個較有特色的微前端應用體系。

a. 獨立開發、獨立部署、獨立運行;

b. 技術棧無關;

c. 應用可以自由拆解和組合,子應用複用性強;

d. 配套的 Node 層設計,使用前端 + 服務端作爲整體方案,子應用既可以獨立提供服務,亦可以作爲主應用的一個子模塊;

e. 業務代碼無侵入設計,子應用免主動式申明生命週期;

f. 子應用間實現絕對隔離;

g. 優秀的用戶體驗;

h. 項目改造成本低,老項目能快速落地。

2.1 微前端應用抽象

通常來說,我們的中後臺應用一般長這樣。

左側和頂部是我們的應用導航區,右邊的區域是我們的內容展示區。

在嚴選的微前端模式中,我們乾脆把這種結構進一步抽象和統一化處理。由主應用負責應用加載與管理的同時來承載左側和頂部導航欄。(當然這種抽象是否必須,需要由具體的業務場景決定,或許你並不需要把導航功能集成到主應用中,但這並不影響我們的設計)。而不同的子應用,則展示在右側區域。如下圖所示:

這裏我們只討論同屏單應用的情況,就目前嚴選實際的業務落地場景來看,同屏單應用 + 業務組件的方式基本覆蓋了所有需求(100+ 的中後臺應用)。當然,理論上同屏多應用只需合理劃分頁面區域即可,原理同樣適用。

2.2 運行時加載與路由策略

假設我們有一個主應用叫 main,有一個子應用叫 s1,我們來看一下應用的加載流程。

簡單梳理一下:

(1) 主應用是微前端框架的承載體,主要包含:

a. 頁面主體框架的渲染,比如一些通用的導航;

b. 監聽捕獲全局的路由變化,加載 / 卸載子應用,active 標籤等;

c. 應用隔離、應用通信、數據共享等全局方法的載體。

(2)子引用在被主應用啓動後,會接管系統路由,與一個獨立運行的應用沒有本質區別。

(3)在獲取子應用的配置信息時,我們完全可以按照約定 path 的規則,避免像類 Single-SPA 技術方案那種 router 對應 entry js/html 的繁瑣配置。不只是這裏的 router 規則,在整套微前端應用架構體系中,我們也在許多地方遵循着 [約定優於配置] 的遊戲規則。對於約定規則,你可以理解爲是一種規範,當大家都按照同一標準做事的時候,許多事情就變得簡單了。諸如此類設計思想的框架很多,比如 egg.js、springBoot 就是其中比較出名的。當然如果你確實更喜歡做配置的話,我們也支持。

2.3 應用隔離

微前端架構中,應用隔離可以說是不得不提的一環。大名鼎鼎的 Single-SPA 雖然做了完善的應用加載邏輯,卻把應用隔離的問題拋給了用戶,讓一衆想要在生產環境實踐的同學不得不望而卻步。在這一點上,螞蟻的 qiankun 框架不得不說是在衆多方案中走在前面的一個,路由系統基於 Single-SPA 實現,在應用的加載和管理層引入了 jsSandBox,雖然目前仍存在一些問題,但並不妨礙它是我目前爲止在市面上見過的考慮比較完善的方案之一。

迴歸主題,嚴選微前端做應用隔離時考慮了兩方面:子應用與子應用隔離,以及主應用與子應用隔離。

2.3.1 子應用與子應用隔離

js 隔離

一個子應用從加載到運行,再到卸載,有可能會對全局產生一些污染。這些污染包括但不限於:添加 / 刪除 / 修改全局變量(比如:window.$ = jQuery)、綁定全局事件(比如:window.addEventListener(‘popstate’,cb) )、修改原生方法或對象(比如:Promise)、修改原生方法或對象的原型鏈(比如:XMLHttpRequest.prototype.open)。而所有這些子應用造成的影響都可能引起潛在的全局衝突。爲此,我們需要在加載和卸載一個應用的同時,儘可能消除這種影響。

這個子應用的加載引擎方案,我們內部稱之爲 Loader Engine 。在我們的設計中,Loader Engine 是一套規範定義,只要按照規則實現,理論上可以替換爲任意的 Loader Engine。當前我們實現了兩種方式。

硬隔離

簡單來說,就是在每個子應用加載之前,都進行一次 window reload,這樣我們可以保證每個子應用在渲染時都是一個全新的環境,哪怕是上一個子應用把 window 上的屬性改了個遍,也絲毫沒有影響。

當然,光有 window reload 不行,頁面的刷新會帶來用戶體驗上的下降。所以配合該方案,我們還需要進行以下一些手段優化:

a. 前端 snapshot + resume,快速恢復應用界面。當前已應用於生產環境。

b. 主應用使用 SSR 局部直出,使頁面在視覺效果上無刷新。當前還沒有應用於生產環境。

實際使用中,以上兩種方案帶來的體驗差別不大,選擇其一即可。當然,目前我們也在探索通過 service worker 來替代 Node 渲染的工作,理論上 service worker 不比 Node 端渲染的執行環境差,並且,這麼做的好處是無需再考慮 Node 端渲染的性能問題,同時又有 Node 端渲染的效果。當然缺點是做起來還是比較繁瑣,技術上存在一些改造的難點,具體實踐方式仍在探索中。

軟隔離

簡單來說,就是在應用加載之前做一次全局快照,在應用卸載之後,按快照恢復全局屬性。考慮到主應用和子應用會同時運行在 Window 中,所以,我們必須區分哪些修改是由子應用引起而需要恢復的。爲此,我們創建了一個 sandbox。在子應用加載前後,我們需要做這些事。

a. 記住對全局變量的修改,解除應用時恢復原有值;

b. 記住全局事件的修改,比如 window/document.addEventListener,卸載應用時 remove 事件;

c. 記住 setTimeout 和 setIntervald 的修改,卸載應用時解除;

d. 此外,sandbox 並不能監聽到對全局方法(對象)和它們的原型鏈修改(比如:XMLHttpRequest.prototype.open,Promise,Promise.prototype.then),因此我們還需要在加載子應用前創建一份 window snapshot ,卸載應用後按 snapshot 恢復全局方法(對象)和它們的原型鏈。這些對象和原型鏈包括但不限於:Promise、fetch、setTimeout、clearTimeout、setInterval、clearInterval、requestAnimationFrame、cancelAnimationFrame、MutationObserver、IntersectionObserver、FileReader、XMLHttpRequest、XMLHttpRequestEventTarget、Document、Element、HTMLElement、HTMLMediaElement、HTMLFrameSetElement、HTMLBodyElement、HTMLFrameElement、HTMLIFrameElement、WebSocket、Object。

至此,我們可以讓每個子應用在運行時都有一份乾淨的運行環境。

就當前前端技術手段而言,任何的軟隔離方案(或者說模擬 sandbox)都是存在漏洞的,除非哪天 Window 原生給我們支持了類似於 copy window 的方法。但是就具體實踐來說,我們可以儘可能去覆蓋更多情況來滿足我們的業務場景。或許可能仍然會有漏洞,那就打補丁吧!

css 隔離

子應用與子應用之間的 css 隔離非常簡單,我們只需要在子應用加載時,標記該子應用所有的 link 和 style 文件。在子應用卸載後,同步卸載所有的 link 和 style 即可。

2.3.2 主應用與子應用隔離

主應用和子應用的隔離相對沒有那麼麻煩,因爲主應用的功能是可收斂的,需要包含的功能基本可預見,在相對可控的情況下,隔離起來並沒有那麼麻煩。(子應用與主應用不同,你不能約束一個子應用會用什麼第三方包,使用什麼樣的框架,乃至於使用一些 hack 的方法。)

js 隔離

基於 webpack 模塊化的打包方式,應用之間天然就可以避免絕大多數的全局衝突,因爲大多數都被編譯成了閉包、內部變量和方法。而在主應用功能可收斂的情況下,餘下的一小部分基本可以通過約束業務代碼開發規範的形式,避免產品全局衝突和污染。這些規範包括但不限於:儘量不掛載會引起副作用的全局變量;建立模塊池,收斂不可控第三方包等等。

而針對當前已知的會引起衝突的包(比如之前所說的 webpack 的全局 webpackJsonp 變量),則全部會在 cli 工具中 cover 掉。

css 隔離

當主應用和子應用同屏渲染時,要徹底隔離 css 污染,當前有兩種方法,iframe 和 shadow dom。iframe 缺點頗多,此處不再贅述。Shadow DOM 擁有類似 iframe 的獨立作用域,我們可以將應用渲染到 shadow dom 中,乍一看有一個光明的前景。但在當前的環境下,就算我們自己可以在業務代碼上遵從規範,避免把 dom 渲染到全局的 document 上,也不能避免我們子應用的某些第三方庫不會這麼幹。而一旦把 css/html 寫到全局,那麼勢必就會破壞 css 隔離。

綜上所述,對於全局共享的基礎樣式,其實我們並不能做到絕對隔離。也就是說,我們對於全局基礎框架樣式的隔離,不得不退一步:採用同一套 rebase 方案,保證樣式不會互相覆蓋。對於具體業務代碼的樣式,我們的做法要簡單得多,可以採用 CSS Module 或者命名空間的方式,給每個業務模塊以特定前綴,即可保證不會互相干擾。

2.4 應用通信

應用間通信有很多種方式,當然,要讓兩個分離的應用之間要做到通信,本質上仍離不開中間媒介或者說全局對象。爲此,我們封裝了 Event 來進行跨應用的通信。Event 對象初始化後掛載在 Window 下,在全局以單例模式運行。簡單的使用範例如下:

import { Event } from '@sharkr/utils';
Event.on('customEvent', (data)=>{
    console.log(data);
});
Event.dispatch('customEvent', someData);

應用之間共享數據的思路與之類似,具體方式這裏不再贅述。

3. 配置中心等相關配套設施

一個主應用由多個子應用構成,但主應用如何手動維護或去代碼化自動生成?主應用如何知道自己關聯了哪些子應用從而控制訪問權限?子應用如何支持多套環境?怎麼監控子應用的狀態?諸如此類的問題,我們不僅要用框架把應用跑起來,還要讓應用跑得好,跑得穩。

總而言之,爲了讓開發者便於使用微前端的開發模式,我們勢必要建立一套成熟的,與之配套的管理平臺——我們稱之爲配置中心,它是組成微前端應用架構體系不可或缺的一部分。我們需要在上面維護應用的基本信息、應用關聯信息、應用代理層配置、應用訪問權限控制等。如果說主框架讓我們從技術上實現了微前端,那麼 相關配套設施則是在具體的業務場景中落地的點睛之筆

以下是當前嚴選配置中心的部分功能截圖:

微前端給網易嚴選帶來的實際價值

1. 巨石應用的“化整爲零”

這個很好理解,我們不再持續開發一個超大的應用,而是可以適當地去做一些拆解和分離。

2. 模塊複用能力的提升

我們可以把通用能力封裝成子應用,借用一句話: “write once,run any where”。回到剛纔的微前端配置中心,我們綠色方框所圈出來的“權限管理子應用”,就是一個通用型子應用,可以無縫集成到任何其他項目中運行。

3. 工單開發模式的革新

回到前面所說的工單開發問題。此時,開發一個採購工單,我們不再需要去召集採購系統、商品中心、財務系統等 3 個系統的產品、開發、測試人員,而是只需要專門一個團隊開發一個採購工單子應用,然後在不同的系統中接入即可。當然,爲了能更便捷地進行工單開發,我們還配套做了“流程平臺”,只不過這已經不屬於微前端的討論範疇了。

4. 多團隊合作模式的優化

其他團隊拿到開發任務後,無需再去拿源碼 clone,也無需關注其他團隊的開發狀態,直接開發一個子應用即可。如果規範允許,甚至連技術棧都可以讓團隊自由選擇,只要符合我們的規範即可。最後,等待他們開發的子應用上線,我們只需要在應用配置中心把他們開發的子應用配置到主應用裏。

新一代前端開發模式的繼任者

網易嚴選微前端方案的內部代號是 wolf。一匹狼並不兇猛,就比如一個子應用也並不能解決什麼問題。但當我們的微應用成體系、成規模時,我們希望它如狼羣一樣能緊密、有序協作,發揮 1+1>2 的效果。

我們會持續觀察微前端開發模式在嚴選業務中的落地,儘可能多地解決在具體使用中的痛點與難點,同時把相關配套做到極致。(在我們的設想的美好藍圖中,如果子應用做得足夠豐富,主應用能夠做到足夠的配置化。那麼當有新需求來時,通過配置化來實現大部分業務場景是不是也未嘗不可呢?就好比搭積木一般,只不過這裏的積木是各種子應用罷了。)

以後可能會開源與業務無關的主框架代碼以及相關配套,也就是類似於 qiankun 或 Single-SPA 那樣的“構建主應用的殼”和相關工具。當然,正如我反覆提到的,微前端是一個完整的技術應用架構體系,我們能提供的“殼”僅僅是一部分,或許我們能做的更多的是提供一些探索思路和實踐案例,爲前端社區做一點微小的貢獻是我們始終的追求。

作者介紹

張浩,網易資深前端開發工程師,嚴選數據產品前端負責人。先後負責過網易企業郵箱、網易有錢、網易嚴選等大型項目的前端架構設計及開發。當前致力於大前端與通用能力建設、工程化與效率工具、企業級應用架構等領域研究。

活動推薦

大前端場景越來越複雜,要把大前端方方面面涉及的技術細節都瞭解到,不是一件容易的事。通過網易嚴選的微前端架構在實際場景中的落地,我們看到了網易嚴選在大前端工程化的探索進度。本次GMTC北京2020,我們將邀請騰訊、美團、字節跳動、bilibili的技術專家來分享他們自身的實踐案例,談談他們目前在大前端工程化的最新進展,希望能給大家在技術方向選擇和方案選型上,提供一些參考。詳情請點擊GMTC北京2020官網

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