生產(chǎn)可用:是時(shí)候來(lái)一個(gè)微前端架構(gòu)了!
隨著前端越來(lái)越復(fù)雜,微前端的概念也越來(lái)越熱,那么什么是微前端?如何應(yīng)用微前端來(lái)改進(jìn)現(xiàn)有的前端架構(gòu)?有沒(méi)有哪些成功的案例和實(shí)踐經(jīng)驗(yàn)?
本文將分享微前端的場(chǎng)景域在螞蟻落地時(shí)遇到的問(wèn)題,通過(guò)實(shí)施一個(gè)標(biāo)準(zhǔn)的微前端架構(gòu),提出面臨的技術(shù)決策以及需要處理的技術(shù)細(xì)節(jié),真正意義上幫助你構(gòu)建一個(gè)生產(chǎn)可用的微前端架構(gòu)系統(tǒng)。
微前端的場(chǎng)景域
在選擇一個(gè)微前端方案之前,常常需要思考這樣一個(gè)問(wèn)題,我們?yōu)槭裁葱枰⑶岸恕Mǔ?duì)微前端的訴求有兩個(gè)方面,一是工程上的價(jià)值,二是產(chǎn)品上的價(jià)值。
對(duì)于工程上的價(jià)值,可以從一個(gè)三年陳的項(xiàng)目來(lái)看,如下所示, commit 的記錄顯示,第一次提交是 2016 年 8 月。
依賴(lài)樹(shù) dependencies:
打包體積:
雖然這個(gè)三年陳的項(xiàng)目看上去版本比較低,但仍然是相對(duì)主流的全家桶方案。這樣一個(gè)樂(lè)觀的項(xiàng)目,在真實(shí)的場(chǎng)景中經(jīng)過(guò)三年的時(shí)間,也不實(shí)用了。因?yàn)殚_(kāi)發(fā)的時(shí)間比較長(zhǎng),并且人員流動(dòng)也比較大,會(huì)導(dǎo)致一些祖?zhèn)鞯拇a出現(xiàn),其次,在技術(shù)上不能及時(shí)的升級(jí),導(dǎo)致開(kāi)發(fā)體驗(yàn)變得很差,例如打包的時(shí)間就超過(guò)三分鐘。也有可能在不經(jīng)意間依賴(lài)一些不兼容的框架,導(dǎo)致項(xiàng)目無(wú)法升級(jí)。種種原因,最后很有可能變成一個(gè)遺產(chǎn)項(xiàng)目。
對(duì)于產(chǎn)品體驗(yàn)上的問(wèn)題,例如下圖所示,要完成一個(gè)跳多個(gè)控制臺(tái)任務(wù),在過(guò)程中發(fā)現(xiàn)每個(gè)控制臺(tái)視覺(jué)不統(tǒng)一、流程出現(xiàn)斷點(diǎn)以及重復(fù)加載或認(rèn)證的問(wèn)題導(dǎo)致整個(gè)產(chǎn)品的體驗(yàn)較差。
微前端的定義
Techniques, strategies and recipes for building a modern web app with multiple teams using different JavaScript frameworks.
—— Micro Frontends。
以上是 Micro Frontends 網(wǎng)站對(duì)微前端的定義。意思是所謂微前端就是一種多個(gè)團(tuán)隊(duì)之間可以使用不同技術(shù)構(gòu)建一個(gè)現(xiàn)代化 web 的技術(shù)手段以及方法策略。其中的關(guān)鍵字是多團(tuán)隊(duì)、采用不同的技術(shù)棧以及現(xiàn)代化的 web。微前端的思路繼承自微服務(wù)的思想。
微前端的架構(gòu)圖
如圖,其中上層為統(tǒng)一共享的拼接層,主要做一些基礎(chǔ)信息的加載,和對(duì)來(lái)自不同團(tuán)隊(duì)不同技術(shù)棧的客戶(hù)端在運(yùn)行時(shí)動(dòng)態(tài)組成一個(gè)完整的 SPA 應(yīng)用,以及生命周期的調(diào)度和事件的管理??傊?,微前端是將微服務(wù)概念做了一個(gè)很好的延伸和實(shí)現(xiàn)。
在具體實(shí)踐中,衡量一個(gè)微前端方案是否是可利用的,需要滿足以下幾個(gè)條件:
- 技術(shù)棧無(wú)關(guān)性,不僅指子應(yīng)用之間使用多個(gè)不同的框架,也指在使用同一個(gè)框架時(shí),有可能在一個(gè)長(zhǎng)的時(shí)間跨度下,由于框架的不兼容的升級(jí),導(dǎo)致應(yīng)用被鎖死的情況。
- 開(kāi)發(fā)、發(fā)布及部署獨(dú)立,要求子應(yīng)用和主應(yīng)用做到工程上的解耦和獨(dú)立。
- 應(yīng)用隔離的能力,是指需要考慮如何不干擾到原來(lái)子應(yīng)用的開(kāi)發(fā)模式和部署模式的情況下,做好運(yùn)行時(shí)的樣式隔離、JS 隔離以及異常隔離等。
以上幾點(diǎn)是基于工程價(jià)值方面考慮的。此外,也需要?jiǎng)討B(tài)組合的能力,是基于產(chǎn)品價(jià)值方面考慮的。
落地的關(guān)鍵問(wèn)題
微前端架構(gòu)中的技術(shù)選擇
按架構(gòu)類(lèi)型區(qū)分,常規(guī) web 應(yīng)用的架構(gòu)類(lèi)型分為兩種,一種是 MPA,另一種是 SPA。如上圖所示為 2017 年各云產(chǎn)品控制臺(tái)架構(gòu)調(diào)研,除了 google cloud 之外,大部分的云廠商都使用 MPA 架構(gòu)。MPA 的優(yōu)點(diǎn)在于部署簡(jiǎn)單,具備獨(dú)立開(kāi)發(fā)和獨(dú)立部署的特性。但是,它的缺點(diǎn)是完成一個(gè)任務(wù)要跳到多個(gè)控制臺(tái),并且每個(gè)控制臺(tái)又是重復(fù)刷新的。而 SPA 能極大保證多個(gè)任務(wù)之間串聯(lián)的流暢性,但問(wèn)題是通常一個(gè) SPA 是一個(gè)技術(shù)棧的應(yīng)用,很難共存多個(gè)技術(shù)棧方案的選型。SPA 和 MPA 都是微前端方案的基礎(chǔ)選型,但是也都存在各自的問(wèn)題。
單實(shí)例,一個(gè)運(yùn)行時(shí)只有一個(gè) APP Actived
多實(shí)例,一個(gè)運(yùn)行同時(shí)有多個(gè) APP Actived
按運(yùn)行時(shí)特性區(qū)分,微前端包含兩個(gè)類(lèi)別,一類(lèi)是單實(shí)例,另一類(lèi)是多實(shí)例。單實(shí)例場(chǎng)景如上圖中左側(cè),通常是一個(gè)頁(yè)面級(jí)別的組合,例如一個(gè)運(yùn)行時(shí)只有一個(gè) App 被激活。多實(shí)例場(chǎng)景如上圖右側(cè),像一個(gè)組件或者是容器級(jí)別的應(yīng)用,運(yùn)行時(shí)可以做到多個(gè)應(yīng)用被同時(shí)激活。這兩種模式都有自己適應(yīng)的場(chǎng)景和優(yōu)勢(shì)。微前端架構(gòu)的核心訴求是實(shí)現(xiàn)能支持自由組合的微前端架構(gòu),將其他的 SPA 應(yīng)用以及其他組件級(jí)別的應(yīng)用自由的組合到平臺(tái)中。那么,如何選擇 SPA 和 MPA 以及單實(shí)例和多實(shí)例是一個(gè)問(wèn)題,我們是否能探索出一種方案,將 SPA 和 MPA 工程上的特點(diǎn)結(jié)合起來(lái),同時(shí)兼顧多實(shí)例和單實(shí)例運(yùn)行時(shí)的場(chǎng)景來(lái)實(shí)現(xiàn)。
技術(shù)細(xì)節(jié)上的決策
為了實(shí)現(xiàn)上述的方案,在技術(shù)細(xì)節(jié)上的決策需要注意以下問(wèn)題:
- 如何做到子應(yīng)用之間的技術(shù)無(wú)關(guān)。
- 如何設(shè)計(jì)路由和應(yīng)用導(dǎo)入。
- 如何做到應(yīng)用隔離。
- 基礎(chǔ)應(yīng)用之間資源的處理以及跨應(yīng)用間通信的選擇。
對(duì)于如何做到子應(yīng)用之間的技術(shù)無(wú)關(guān)問(wèn)題,我們是通過(guò)協(xié)議來(lái)解決的。如下代碼所示的方式,就可以完成子應(yīng)用的導(dǎo)入。如果子應(yīng)用接入時(shí)做了一些框架上的耦合或者依賴(lài)一個(gè)具體實(shí)現(xiàn)庫(kù)的機(jī)制,就一定會(huì)存在與實(shí)現(xiàn)庫(kù)版本耦合的可能,不利于整個(gè)微前端生態(tài)的統(tǒng)一和融合。
- export async function bootstrap() {
- console.log('react app bootstraped') ;
- }
- export async function mount(props) {
- console.log(props) ;
- ReactDOM.render(<App/>, document.getElementById('react15Root'));
- }
- export async function unmount() {
- ReactDOM.unmountComponentAtNode(document.getElementById('react15Root') ) ;
- }
如下所示是一個(gè)與具體框架實(shí)現(xiàn)相耦合的例子(反例):
- //主應(yīng)用
- import React from 'react' ;
- import ReactDOM from 'react-dom';
- import MicroFrontend from 'micro-frontend';
- ReactDOM.render(<MicroFrontend base="/app1" entry="//localhost/a.js">);
- //子應(yīng)用
- window.microFrontends = {
- app:{...} ,
- reduxStore:{...} ,
- globals:(...) ,
- }
對(duì)于路由的問(wèn)題,如下圖所示。這樣一條訪問(wèn)鏈路后,刷新當(dāng)前 URL 通常情況下會(huì)發(fā)生什么?
正常訪問(wèn)一個(gè)站點(diǎn),經(jīng)過(guò)一番操作之后,進(jìn)入到站點(diǎn)的列表頁(yè),路由會(huì)變大很復(fù)雜,但如果是一個(gè)微前端用戶(hù),刷新一下頁(yè)面會(huì)出現(xiàn) 404 的情況。解決思路是將 404 路由 fallback 到一個(gè)異步注冊(cè)的子應(yīng)用路由機(jī)制上。
對(duì)于應(yīng)用導(dǎo)入方式的選擇,比較常見(jiàn)的方案是 Config Entry。通過(guò)在主應(yīng)用中注冊(cè)子應(yīng)用依賴(lài)哪些 JS。這種方案一目了然,但是最大的問(wèn)題是 ConfigEntry 的方式很難描述出一個(gè)子應(yīng)用真實(shí)的應(yīng)用數(shù)據(jù)信息。真實(shí)的子應(yīng)用會(huì)有一些 title 信息,依賴(lài)容器 ID 節(jié)點(diǎn)信息,渲染時(shí)會(huì)依賴(lài)節(jié)點(diǎn)做渲染,如果只配 JS 和 CSS,那么很多信息是會(huì)丟失的,有可能會(huì)導(dǎo)致間接上的依賴(lài)。
- <html>
- <head>
- <title>sub app</title>
- <link rel="stylesheet" href="//localhost/app.css">
- </head>
- <body>
- <main id="root"></main>
- <script src="//localhost/base. js">
- </body>
- </html>
- <script>
- import React from 'react'
- import ReactDOM from 'react-dom'
- ReactDOM.render(<App/>, document.getElementById('root') )
- </script>
另外一種方案是 HTML Entry,直接配 html,因?yàn)?html 本身就是一個(gè)完整的應(yīng)用的 manifest,包含依賴(lài)的信息。HTML Entry 的優(yōu)點(diǎn)是接入應(yīng)用的信息可以得到完整的保留,接入應(yīng)用地址只需配一次,子應(yīng)用的原始開(kāi)發(fā)模式得到完整保留,因?yàn)樽討?yīng)用接入只需要告知主應(yīng)用 html 在哪,包括在不接入主應(yīng)用時(shí)獨(dú)立的打開(kāi)。它的缺點(diǎn)是將解析的消耗留給了運(yùn)行時(shí)。而 Config Entry 相較于 HTML Entry 減少了運(yùn)行時(shí)的解析消耗。Config Entry 的缺點(diǎn)是主應(yīng)用需配置完整的子應(yīng)用信息,包含初始 DOM 信息、js/css 資源地址等。
- registerMicroApps([
- {
- name: 'react app'
- // index.html 本身就是一個(gè)完整的應(yīng)用的 manifest
- entry: '//localhost: 8080/index.html',
- render,
- activeRule: '/react'
- }
- ]) ;
對(duì)于樣式隔離問(wèn)題,例如 BEM,每個(gè)子應(yīng)用在寫(xiě)樣式之前要加一些前綴,做一些隔離,但是這個(gè)做法并不推薦。相對(duì)而言,CSS Module 更簡(jiǎn)單高效,也更智能化,是比較推薦的方式,但是也存在著問(wèn)題。而 Web Components 看上去很不錯(cuò),但在實(shí)踐過(guò)程中也會(huì)發(fā)生一些問(wèn)題。
例如在 Web Components 渲染的流程中出現(xiàn)了問(wèn)題,如下圖所示。
在 antd 中提供了全局的 API,可以提前設(shè)置好所有的彈框的 container,但是也不是每個(gè)組件庫(kù)都能像 antd 一樣完成度那么高。
螞蟻所采用的解決方案是做動(dòng)態(tài)的加載和卸載樣式表,如下圖所示,這種方案是很有效的。
對(duì)于 JS 隔離,螞蟻提出了 JS Sandbox 機(jī)制,如上圖所示,其中 bootstrap、mount及 unmount 生命周期是子應(yīng)用需要暴露出來(lái)的,因?yàn)樽討?yīng)用的整個(gè)生命周期都是被主應(yīng)用所管理的,所以可以在主應(yīng)用中給子應(yīng)用插入各種攔截的機(jī)制,也可以捕獲到子應(yīng)用在加載期間做了哪些全局上的修改。在 unmount 時(shí),可以將全局上的副作用全部手動(dòng)移除掉,同時(shí)也可以實(shí)現(xiàn)在重新進(jìn)來(lái)時(shí),將上次忘記卸載的副作用重建一遍,因?yàn)樾枰WC下次進(jìn)來(lái)時(shí)能完整回復(fù)到與上次一致的上下文。
對(duì)于資源加載問(wèn)題,在微前端方案中存在一個(gè)典型的問(wèn)題,如果子應(yīng)用比較多,就會(huì)存在之間重復(fù)依賴(lài)的場(chǎng)景。解決方案是在主應(yīng)用中主動(dòng)的依賴(lài)基礎(chǔ)框架,然后子應(yīng)用保守的將基礎(chǔ)的依賴(lài)處理掉,但是,這個(gè)機(jī)制里存在一個(gè)問(wèn)題,如果子應(yīng)用中既有 react 15 又有 react 16,這時(shí)主應(yīng)用該如何做?螞蟻的方案是在主應(yīng)用中維護(hù)一個(gè)語(yǔ)義化版本的映射表,在運(yùn)行時(shí)分析當(dāng)前的子應(yīng)用,最后可以決定真實(shí)運(yùn)行時(shí)真正的消費(fèi)到哪一個(gè)基礎(chǔ)框架的版本,可以實(shí)現(xiàn)真正運(yùn)行時(shí)的依賴(lài)系統(tǒng),也能解決子應(yīng)用多版本共存時(shí)依賴(lài)去從的問(wèn)題,能確保最大程度的依賴(lài)復(fù)用。
基于 props 以單向數(shù)據(jù)流的方式傳遞給子應(yīng)用:
- export function mount(props) {
- ReactDOM.render(
- <App {...props}/>,
- container
- )
- }
基于瀏覽器原生事件做通信:
- //主應(yīng)用
- window.dispathEvent(
- new CustomEvent('master:collapse-menu'),
- {detail: {collapsed:true} }
- )
- //子應(yīng)用
- window.addEventLister(
- 'master:collapse-menu',
- event => console.log(event.detail.collapsed)
- )
對(duì)于應(yīng)用之間數(shù)據(jù)共享及通信的問(wèn)題,螞蟻提出了兩個(gè)原則,第一個(gè)原則是基于 props 以單向數(shù)據(jù)流的方式傳遞給子應(yīng)用。第二個(gè)原則是基于瀏覽器原生事件做跨業(yè)務(wù)之間的通信。
在真實(shí)的生產(chǎn)實(shí)踐中,螞蟻總結(jié)出了幾點(diǎn)經(jīng)驗(yàn)及建議:兄弟節(jié)點(diǎn)間通信以主應(yīng)用作為消息總線,不建議自己封裝的 Pub/Sub 機(jī)制,也不推薦直接基于某一狀態(tài)管理庫(kù)做數(shù)據(jù)通信。
螞蟻在實(shí)踐中做的性能優(yōu)化,包括異步樣式導(dǎo)致閃爍問(wèn)題的解決以及預(yù)加載問(wèn)題的解決。
異步樣式導(dǎo)致的閃爍問(wèn)題:
預(yù)加載:
- export function prefetch(entry: Entry, fetch?: Fetch) {
- const requestIdleCallback = window.requestIdleCallback || noop;
- requestIdleCallback(async () => {
- const { getExternalScripts, getExternalStyleSheets }
- = await importEntry(entry, { fetch } );
- requestIdleCallback(getExternalStyleSheets) ;
- requestIdleCallback(getExternalScripts) ;
- }) ;
- }
如圖所示為微前端方案涉及到的技術(shù)點(diǎn),本文分享了圖中三分之二的內(nèi)容。
在螞蟻金服做了大量關(guān)于微前端方案之后,總結(jié)了衡量一個(gè)微前端方案是否友好的兩個(gè)標(biāo)準(zhǔn),第一個(gè)標(biāo)準(zhǔn)是技術(shù)無(wú)關(guān),也是微前端最核心的特性,不論是子應(yīng)用還是主應(yīng)用都應(yīng)該做到框架不感知。第二個(gè)標(biāo)準(zhǔn)是接入友好,子應(yīng)用接入應(yīng)該像接入一個(gè) iframe 一樣輕松自然。
螞蟻的微前端落地的實(shí)踐成果
螞蟻內(nèi)部基于微前端基礎(chǔ)架構(gòu)提出了一體化上云解決方案,稱(chēng)為 OneX,是一個(gè)基礎(chǔ)的平臺(tái),它可以將各種流程和工具串聯(lián),其價(jià)值體現(xiàn)在品牌、產(chǎn)品和技術(shù)方面。品牌價(jià)值指的是統(tǒng)一的界面框架、UI、交互形成了螞蟻金服科技品牌心智。
OneNav + OneConsole + TechUI + OneAPI + Bigfish
下圖所示為螞蟻的一個(gè)真實(shí)應(yīng)用的例子,除了中間接入的產(chǎn)品是自己控制之外,其他內(nèi)容都是由平臺(tái)提供,這樣,如論是一個(gè)三年陳項(xiàng)目還是新做的項(xiàng)目,在基本的視覺(jué)上可以做到統(tǒng)一。
產(chǎn)品價(jià)值指的是產(chǎn)品具有自由組合能力。之前的產(chǎn)品是多個(gè)產(chǎn)品、多個(gè)站點(diǎn)的控制臺(tái),而現(xiàn)在只需要一個(gè)控制臺(tái),將多個(gè)產(chǎn)品自由的組合,這樣,可以在商業(yè)上有更多的相應(yīng)空間以及更多自由的搭配。基于這樣的系統(tǒng)也可以做一些全局性的事情,例如埋點(diǎn)、用戶(hù)的轉(zhuǎn)化跟蹤業(yè)務(wù)。
技術(shù)價(jià)值指的是研發(fā)上的提效。經(jīng)過(guò)微前端的改造后,螞蟻可以將大型的系統(tǒng)解耦成可以獨(dú)立開(kāi)發(fā)的并行的小型的系統(tǒng),這些小型系統(tǒng)可以交給別的團(tuán)隊(duì)或者使用可視化的系統(tǒng)去實(shí)現(xiàn),最后在運(yùn)行時(shí)只需要將他們集成起來(lái)。
在技術(shù)價(jià)值方面也可以實(shí)現(xiàn)交付上的提效,只需要在某一個(gè)環(huán)境的任意一個(gè)環(huán)境中做平臺(tái)上的接入,應(yīng)用就可以做到在多余的環(huán)境中不改代碼,直接運(yùn)行。
下圖為阿里云剛上市的一個(gè)產(chǎn)品例子,其中包括 15 個(gè)來(lái)自不同團(tuán)隊(duì)的應(yīng)用進(jìn)行維護(hù),它的特點(diǎn)是并不是單獨(dú)為阿里云而設(shè)計(jì)的,之前在螞蟻也有運(yùn)行,只不過(guò)在阿里云中做了動(dòng)態(tài)的組合。OneTour 微應(yīng)用組件主要解決的是在多個(gè)產(chǎn)品控制臺(tái)之間自由切換導(dǎo)致流程割裂的問(wèn)題。
螞蟻微前端的落地成果包括:有 70+ 線上應(yīng)用接入(阿里云 + 螞蟻云 + 專(zhuān)有云),最復(fù)雜一個(gè)控制臺(tái)同時(shí)集成 15 個(gè)應(yīng)用,并且有 4+ 不同技術(shù)棧,以及開(kāi)發(fā)到發(fā)布上線全鏈路的自動(dòng)化支持,一云入駐多云運(yùn)行。
基于以上技術(shù)上的成果,螞蟻沉淀了自己的微前端方案并開(kāi)源。
基于以上技術(shù)上的成果,螞蟻沉淀了自己的微前端方案并開(kāi)源。qiankun 是框架無(wú)關(guān)的微前端內(nèi)核,umi-plugin-qiankun 是基于 umi 應(yīng)用的 qiankun 插件,方便 umi 應(yīng)用通過(guò)修改配置的方式變身成為一個(gè)微前端系統(tǒng)。基于上述實(shí)踐的檢驗(yàn)和內(nèi)部落地結(jié)果來(lái)看,在大規(guī)模中后臺(tái)應(yīng)用場(chǎng)景下,微前端架構(gòu)是一個(gè)值得嘗試的方案。