基于Module Federation的模塊化跨棧方案探索
一、背景
公司發(fā)展到一定程度,隨著業(yè)務(wù)分支不斷變多,B端C端的項(xiàng)目也隨之增多,由于歷史原因可能產(chǎn)生新老技術(shù)棧(vue/react)共存的情況,這既不利于組件物料的抽離統(tǒng)一(一類(lèi)通用組件需適配多套技術(shù)棧),也增大了開(kāi)發(fā)者跨項(xiàng)目開(kāi)發(fā)的適應(yīng)成本。因此技術(shù)棧收斂是提升前端平臺(tái)體系開(kāi)發(fā)效率重要的一環(huán)。
提到技術(shù)棧遷移,我們首先想到的是微前端方案,在隔離性上來(lái)說(shuō),微前端確實(shí)很好的方案,但是對(duì)于一些復(fù)雜核心模塊,往往需要較長(zhǎng)的周期遷移,并且伴隨著該模塊的不斷迭代,使得整體項(xiàng)目的遷移進(jìn)度逐步拉長(zhǎng)。最終核心痛點(diǎn)可能還是沒(méi)有完全解決。
基于以上的背景,我們需要解決兩個(gè)問(wèn)題:
更絲滑的技術(shù)棧遷移:不僅是新頁(yè)面,舊有頁(yè)面的需求也能用react開(kāi)發(fā),做到代碼塊級(jí)遷移。
跨技術(shù)棧開(kāi)發(fā):MF組件化開(kāi)發(fā),需要將react組件轉(zhuǎn)化為vue組件以實(shí)現(xiàn)在同一界面嵌入react組件。
二、跨技術(shù)棧開(kāi)發(fā)
vuereact-combined [1]提供了組件轉(zhuǎn)換較為通用的解決方案,利用applyReactInVue在vue指定生命周期渲染React組件,并區(qū)分了update階段與create階段以減少不必要的dom構(gòu)建,同時(shí)監(jiān)聽(tīng)上層的$attrs、$listenters,并通過(guò)reactComponentWrapper中間層拿到的reactInstance透?jìng)飨鄳?yīng)狀態(tài)及方法。
React -> Vue 轉(zhuǎn)化源碼可參考這里[2]
同時(shí)vuereact-combined [3]內(nèi)部還進(jìn)行了vuex、router轉(zhuǎn)化使得react組件可以獲取到全局狀態(tài)以及router實(shí)例以滿(mǎn)足路由跳轉(zhuǎn)以及鑒權(quán)相關(guān)需求。
以下是其支持的轉(zhuǎn)化特性:
同一項(xiàng)目下混合開(kāi)發(fā)?
嗯,看似一切都解決了,但其中有一些無(wú)法避免的問(wèn)題:首先,React與Vue的依賴(lài)以及編譯babel生態(tài)是有區(qū)別的,如果有同一依賴(lài),需要找到兩個(gè)技術(shù)棧同時(shí)適配的版本,并且構(gòu)建成本成倍提升,在本地開(kāi)發(fā)與線(xiàn)上構(gòu)建中需要單獨(dú)拿出精力優(yōu)化,一個(gè)文件夾下同時(shí)存在.vue與.tsx,結(jié)構(gòu)雜亂,不利于維護(hù)。
三、更絲滑的技術(shù)棧遷移
3.1 頁(yè)面級(jí)微前端對(duì)于技術(shù)棧遷移以及提效的局限性
提到技術(shù)棧遷移,我們首先想到的是微前端,例如QianKun、Single SPA、Micro App,他們能做到在平臺(tái)內(nèi)部根據(jù)大的業(yè)務(wù)模塊做項(xiàng)目級(jí)的分拆,并且與技術(shù)棧無(wú)關(guān)。本質(zhì)上解決了項(xiàng)目維護(hù)成本與構(gòu)建優(yōu)化成本隨著項(xiàng)目不斷提升的問(wèn)題。
注意,頁(yè)面級(jí)的微前端雖然能做跨技術(shù)棧開(kāi)發(fā),但是只能做增量改造,新的頁(yè)面我們可以使用內(nèi)部統(tǒng)一的技術(shù)棧,但是在業(yè)務(wù)迭代中有相當(dāng)多的需求是基于舊有頁(yè)面進(jìn)行改造,我們還是需要基于以往的技術(shù)棧開(kāi)發(fā),除非是全量重構(gòu)。那么按照常規(guī)方式是,單獨(dú)抽時(shí)間對(duì)核心模塊做遷移,其中的阻礙可想而知,業(yè)務(wù)在不斷推進(jìn),而重構(gòu)對(duì)于用戶(hù)來(lái)說(shuō)無(wú)感,并且還需要測(cè)試資源回歸,不管對(duì)于業(yè)務(wù)提升以及結(jié)果產(chǎn)出短期來(lái)看都是沒(méi)有明顯正向作用的。所以,結(jié)果只能是舊有模塊維持原狀,技術(shù)棧統(tǒng)一任重道遠(yuǎn)。
3.2 Web components?
Web components 是 chrome推動(dòng)的原生組件API,即不依賴(lài)技術(shù)棧開(kāi)發(fā)組件,實(shí)現(xiàn)組件的高復(fù)用率。在作為組件級(jí)共享上是一個(gè)比較好的方案,但由于是原生API,狀態(tài)管理,組件通信需要開(kāi)發(fā)者自己實(shí)現(xiàn),由于技術(shù)棧遷移這個(gè)場(chǎng)景不管是被遷的還是遷入的都是技術(shù)棧相關(guān),對(duì)于組件轉(zhuǎn)化會(huì)有較高的成本。
3.3 Module Federation的代碼塊級(jí)引入
MF本質(zhì)上是webpack提供的一種能力,可以使得開(kāi)發(fā)者在一個(gè) JavaScript 應(yīng)用中動(dòng)態(tài)加載并運(yùn)行另一個(gè) JavaScript 應(yīng)用的代碼,并實(shí)現(xiàn)應(yīng)用之間的依賴(lài)共享。具體原理可參考Module Federation原理剖析[4]。
這樣我們就能對(duì)于舊有的vue項(xiàng)目在減少重構(gòu)成本的前提下做漸進(jìn)式的遷移。
基于前面微前端與模塊化的對(duì)比,考慮到模塊邏輯的復(fù)雜度與遷移成本,我們決定使用基于Module Federation的模塊化開(kāi)發(fā),這使得復(fù)雜模塊的遷移更加平滑,并且能夠平衡同模塊下技術(shù)遷移與業(yè)務(wù)開(kāi)發(fā)的節(jié)奏,兩者盡量松耦合,做到漸進(jìn)式遷移。下面將介紹實(shí)現(xiàn)模塊化遷移方案的關(guān)鍵點(diǎn)。
四、實(shí)現(xiàn)方案
4.1 組件轉(zhuǎn)換
首先我們需要將開(kāi)發(fā)者的react組件轉(zhuǎn)換為vue組件,在每一次react micro項(xiàng)目變動(dòng)時(shí),我們需要遍歷該項(xiàng)目并找到.jsx/.tsx的文件,并聲明其對(duì)應(yīng)的.vue文件,.vue文件里面做了什么呢?它會(huì)基于vuereact引入react文件,并透?jìng)髯兞颗c方法,這些.vue文件用戶(hù)是沒(méi)有感知的,因此它們會(huì)存放在臨時(shí)目錄中(.mfveat)。
.vue文件的代碼模板如下:
4.2 生成組件expose映射
上節(jié)提到的.vue文件會(huì)生成expose組件地址到文件地址映射,以被Module Federation使用。
4.3 Module Federation動(dòng)態(tài)注入
大家都知道Module Federation是寫(xiě)在構(gòu)建配置文件里的,exposes決定了這個(gè)微應(yīng)用暴露哪些組件,但是在本地開(kāi)發(fā)時(shí)我們業(yè)務(wù)代碼以及導(dǎo)出是變動(dòng)的,如果每次修改expose都得通過(guò)重啟工程的方式效率是很低的。
如何解決動(dòng)態(tài)expose的問(wèn)題呢?這里就要講到Module Federation Plugin的組成,核心是ContainerPlugin(remote端)與ContainerReferencePlugin(host),方案是在基于ContainerPlugin上層封裝一個(gè)插件mf-veact-plugin,支持expose傳入,在微項(xiàng)目構(gòu)建完成后按照以上步驟生成expose.json,然后動(dòng)態(tài)注入mf-veact-plugin。
以上3步就構(gòu)成了跨技術(shù)棧開(kāi)發(fā)的構(gòu)建全鏈路,用戶(hù)只需關(guān)心react業(yè)務(wù)代碼,然后通過(guò)Module Federation Host在主項(xiàng)目中引入即可。
五、開(kāi)發(fā)體驗(yàn)
5.1 刷新監(jiān)聽(tīng)與MF引入
由于MF與主項(xiàng)目是獨(dú)立的,那么用戶(hù)在改了React代碼后如何觸發(fā)主項(xiàng)目的刷新呢?在每一個(gè)子項(xiàng)目編譯完成后,webpack插件會(huì)向主項(xiàng)目寫(xiě)入更新唯一標(biāo)識(shí),主項(xiàng)目循環(huán)監(jiān)聽(tīng)唯一標(biāo)識(shí),變化時(shí)觸發(fā)頁(yè)面刷新,最近加入了熱重載保證子項(xiàng)目與主項(xiàng)目通信順滑。
webpack-dev-server的作者在近期版本中更新了熱重載功能,無(wú)需手動(dòng)監(jiān)聽(tīng)重載。
5.2 UI庫(kù)樣式降級(jí),避免全局污染
MF做到了組件級(jí)微前端,同時(shí)又帶來(lái)了一些問(wèn)題,由于各個(gè)項(xiàng)目可能使用的不同的UI庫(kù),而UI庫(kù)本身會(huì)有全局樣式的改造,不可避免的會(huì)影響其他項(xiàng)目UI庫(kù)的樣式,而MF的組件粒度有很難做到如頁(yè)面微前端一樣的項(xiàng)目樣式隔離。
這里拿Antd舉例,Antd中的global.less會(huì)對(duì)全局樣式做格式化,在社區(qū)中已經(jīng)有很多討論,但直到今天也沒(méi)有進(jìn)展。因?yàn)?Ant-Design 是一套設(shè)計(jì)語(yǔ)言,所以 antd 會(huì)引入一套 fork 自normalize.css[4]的瀏覽器默認(rèn)樣式重置庫(kù)global.less。
因此這里的方案是,「收斂 base.less,并保證外部的全局樣式無(wú)法輕易覆蓋 antd 的樣式」,從編譯角度解決樣式污染問(wèn)題。
1)Antd-vue 樣式污染問(wèn)題
六、總結(jié)
本文簡(jiǎn)要介紹了前端領(lǐng)域在邁出技術(shù)棧統(tǒng)一這一步后經(jīng)歷的痛點(diǎn)以及挑戰(zhàn),分別從遷移粒度以及使用場(chǎng)景兩個(gè)方面針對(duì)微前端以及模塊化這兩類(lèi)開(kāi)發(fā)模式進(jìn)行了對(duì)比,并且從解決用戶(hù)開(kāi)發(fā)痛點(diǎn)出發(fā)闡述架構(gòu)如此設(shè)計(jì)的原因。最后列出了在模塊化遷移或開(kāi)發(fā)過(guò)程中需要注意的問(wèn)題并給出了解決方案。主旨還是希望能夠以更低的成本與更好的開(kāi)發(fā)體驗(yàn)推動(dòng)技術(shù)棧統(tǒng)一與遷移。
模塊化開(kāi)發(fā)是前端領(lǐng)域離不開(kāi)的話(huà)題,解決技術(shù)棧統(tǒng)一問(wèn)題僅是其一個(gè)分支,同時(shí)模塊化代碼隔離與非版本化改動(dòng)也是我們未來(lái)要解決優(yōu)化的方向,組件、平臺(tái)模塊的自動(dòng)化共享一直在被提及,希望本次方案探索能夠給大家?guī)?lái)一些靈感,也歡迎大家在前端平臺(tái)體系組件通用化這一方向上一起交流討論。
參考文章:
[1]https://github.com/devilwjp/vuereact-combined
[2]https://github.com/devilwjp/vuereact-combined/blob/master/src/applyReactInVue.js
[3]https://github.com/devilwjp/vuereact-combined
[4]https://juejin.cn/post/6895324456668495880
[5]《如何優(yōu)雅地徹底解決 antd 全局樣式問(wèn)題》
https://juejin.cn/post/6844904116288749581
[6]《Module Federation原理剖析》https://juejin.cn/post/6895324456668495880