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