一、需求以及成果
我所在團(tuán)隊(duì)是做 toB 業(yè)務(wù)的,技術(shù)棧是 Vue,團(tuán)隊(duì)目前有十多個(gè)典型的 toB 業(yè)務(wù)(菜單+內(nèi)容布局),這些業(yè)務(wù)都是服務(wù)于一個(gè)大平臺(tái)的,因?yàn)闅v史原因,每個(gè)業(yè)務(wù)都是獨(dú)立的,都有一個(gè) html 入口,所以當(dāng)用戶在這個(gè)大平臺(tái)上使用這十多個(gè)業(yè)務(wù)的時(shí)候,每當(dāng)切換系統(tǒng)時(shí),頁(yè)面都會(huì)刷新,體驗(yàn)很差;在開(kāi)發(fā)層面,這十多個(gè)業(yè)務(wù)又有太多共同之處,每次修改成本都很高。
最近有一個(gè)很重要的需求 X,內(nèi)容是這樣的:從十多個(gè)項(xiàng)目中,每個(gè)項(xiàng)目抽取若干功能組成一個(gè)新項(xiàng)目,基于現(xiàn)有架構(gòu)的話,每當(dāng)點(diǎn)擊來(lái)自不同系統(tǒng)的功能頁(yè)面就要刷新一次,這是不可接受的。為了新需求 X 重復(fù)開(kāi)發(fā)一遍這些業(yè)務(wù)功能又不現(xiàn)實(shí),所以從技術(shù)角度來(lái)看,架構(gòu)改造不可避免。
經(jīng)過(guò)一番調(diào)研比對(duì),我們決定使用當(dāng)下比較火的 SingleSpahttps://single-spa.js.org/
[1] 來(lái)完成改造(iframe 方案毫無(wú)亮點(diǎn),棄之),目前改造已完成,我們實(shí)現(xiàn)了以下效果:
只有一個(gè)不包含子項(xiàng)目(子項(xiàng)目指的是那十多個(gè)業(yè)務(wù))資源的主項(xiàng)目,主項(xiàng)目只有一個(gè) html 入口,子項(xiàng)目通過(guò)主項(xiàng)目來(lái)按需加載,子系統(tǒng)間切換不再刷新;
菜單欄、登錄、退出等功能都從子項(xiàng)目剝離,寫(xiě)在主項(xiàng)目里,再有相關(guān)改動(dòng)只需修改主項(xiàng)目,包括錯(cuò)誤監(jiān)控、埋點(diǎn)等行為,只需處理一個(gè)主項(xiàng)目,十幾個(gè)子項(xiàng)目不再需要處理;
子項(xiàng)目原本需要加載的公共部分(如 vue、vuex、vue-router、ivew/element、私有 npm 包等),全部由主項(xiàng)目調(diào)度,配合 webpack 的 externals 功能通過(guò)外鏈的方式按需加載,一旦有一個(gè)子項(xiàng)目加載過(guò),下一個(gè)子項(xiàng)目就不需要再加載,這樣一來(lái)每個(gè)子項(xiàng)目的 dist 文件里就只有子項(xiàng)目自己的業(yè)務(wù)代碼(最終子項(xiàng)目包的體積縮小了 80%,只有幾十 k),項(xiàng)目實(shí)際加載速度快了很多,肉眼可見(jiàn);
子項(xiàng)目并沒(méi)有重新開(kāi)發(fā),只是進(jìn)行了一些改造,接入了微前端這套架構(gòu),所以新需求 X 的開(kāi)發(fā)成本也極大的降低了,接入功能同時(shí)可供未來(lái)新增子項(xiàng)目使用;
我們的項(xiàng)目有自己的 tab 系統(tǒng)(類似瀏覽器的 tab 頁(yè)簽),這些 tab 頁(yè)簽通過(guò) keep-alive 和一系列對(duì)緩存的處理,使其體驗(yàn)接近原生瀏覽器 tab。
二、展示以及技術(shù)點(diǎn)
圖 1:項(xiàng)目外觀示意圖:
做微前端改造之前,藍(lán)色系區(qū)域都是用公共包的方式由每個(gè)子項(xiàng)目引入,所以子項(xiàng)目運(yùn)行的時(shí)候展示的藍(lán)色系部分都是相同的,給人一種在使用同一個(gè)系統(tǒng)的錯(cuò)覺(jué),實(shí)際上切換系統(tǒng)的時(shí)候整個(gè)頁(yè)面都要重新載入。
微前端改造后,只有橘色區(qū)域是變化的,頁(yè)面也不再刷新。
圖 2:局部效果動(dòng)圖
圖 2 展示了圖 1 中的 tab 頁(yè)簽區(qū)以及子項(xiàng)目展示區(qū)。信息做了馬賽克處理。
乍一看沒(méi)什么特別的,但如果我說(shuō)這些 tab分別來(lái)自于不同 git 倉(cāng)庫(kù)的獨(dú)立 vue 項(xiàng)目呢?這就是這套微前端架構(gòu)的強(qiáng)大之處,讓不同單頁(yè) vue 項(xiàng)目可以隨意組合成一個(gè)項(xiàng)目,而這些項(xiàng)目自己又是獨(dú)立的 vue 項(xiàng)目。
仔細(xì)看圖 2 中路由的變化,hash 路由的第一級(jí)決定了要加載哪個(gè)子項(xiàng)目(work、sms、tms 是三個(gè)不同的 git 工程),不同子項(xiàng)目間的切換也完全沒(méi)有刷新 ??
為了讓 tab 切換不刷新,這里使用了 keep-alive 去緩存頁(yè)面,考慮到內(nèi)存性能,在關(guān)閉 tab 頁(yè)簽時(shí)通過(guò)一些方法(主要是 keep-alive 的 exclude 屬性)去除了 keep-alive 緩存,同時(shí)為了讓子項(xiàng)目間的 tab 切換也不刷新,對(duì)圖 3 下面提到的包裝器也進(jìn)行了不小的改造。讓 tab 切換不刷新只是為了提升用戶體驗(yàn),這一步不是必要的,有一定的成本。
圖 3:部署架構(gòu)示意圖
實(shí)現(xiàn)一套微前端架構(gòu),可以把其分成四部分(參考:https://alili.tech/archive/11052bf4/[2] )
加載器:也就是微前端架構(gòu)的核心,圖 3 中的“加載器 JS 文件”就是由加載器打包壓縮出來(lái)的,這是原始的加載器:https://github.com/Fantasy9527/lotus-scaffold-micro-frontend-portal
[3] —— 可以把它理解成電源包裝器:有了加載器,我們要把現(xiàn)有的 vue 項(xiàng)目包裝一下,使得加載器可以使用它們,這是原始的包裝器:https://github.com/CanopyTax/single-spa-vue
[4] —— 如果想改造,建議改造這個(gè)部分,它相當(dāng)于電源適配器主項(xiàng)目:一般是包含所有項(xiàng)目公共部分的項(xiàng)目—— 它相當(dāng)于電器底座
子項(xiàng)目:眾多展示在主項(xiàng)目?jī)?nèi)容區(qū)的項(xiàng)目—— 它相當(dāng)于你要使用的電器
所以是這么個(gè)概念:電源(加載器)→ 電源適配器(包裝器)→? 電器底座(主項(xiàng)目)→? 電器(子項(xiàng)目)?
主項(xiàng)目和子項(xiàng)目都需要用包裝器包裝,只不過(guò)主項(xiàng)目的配置寫(xiě)法有不同
加載器和包裝器需要根據(jù)自己的需求做一些二次開(kāi)發(fā)
總的來(lái)說(shuō)是這樣一個(gè)流程:用戶訪問(wèn) index.html 后,瀏覽器運(yùn)行加載器的 js 文件,加載器去讀取圖 4 中的配置文件,然后注冊(cè)配置文件中配置的各個(gè)項(xiàng)目后,首先加載主項(xiàng)目(菜單等),再通過(guò)路由判定,動(dòng)態(tài)遠(yuǎn)程加載子項(xiàng)目。
這里有個(gè)vue 微前端版demo https://github.com/joeldenning/coexisting-vue-microfrontends
[5],包含最基礎(chǔ)的效果與源碼,務(wù)必研究一下這個(gè) demo 再結(jié)合以上理論來(lái)幫助理解 *遠(yuǎn)程加載的子項(xiàng)目資源要在 chrome 的 network 中的 xhr 那一欄才能看到
圖 4:圖 3 中的 apps.config.js
用戶訪問(wèn) index.html 后,js 加載器會(huì)加載 apps.config.js。無(wú)論路由是什么,每次必會(huì)首先加載主項(xiàng)目,再根據(jù)路由來(lái)匹配要加載哪個(gè)子項(xiàng)目。apps.config.js 的生成如圖 3 的綠色部分所示:
在資源服務(wù)器上起一個(gè)監(jiān)聽(tīng)服務(wù)(我使用的是 nodejs 腳本+pm2 守護(hù)),原有子項(xiàng)目的部署方式完全不變(前后端完全分離,資源帶 hash),當(dāng)監(jiān)聽(tīng)服務(wù)檢測(cè)到文件改動(dòng)時(shí),去子項(xiàng)目部署文件夾里找它的 index.html,把入口 js 用如下正則匹配出來(lái),寫(xiě)入 apps.config.js。
// content[i]為子項(xiàng)目文件夾名稱。這段代碼是nodejs腳本片段。const reg = new RegExp(`src="(\/${content[i]}\/index\.\w{8}.js)`) // 對(duì)應(yīng)圖中的 /brain/index.3c4b55cf.js
圖 4 中的 brain 即是主項(xiàng)目,它的 base 屬性為 true,其余子項(xiàng)目的 base 屬性為 false
三、一些技術(shù)細(xì)節(jié)
這里說(shuō)的的項(xiàng)目打包都是基于 webpack。
System.js
它是實(shí)現(xiàn)遠(yuǎn)程加載子項(xiàng)目的核心。我們使用的是 0.21 版本的:https://github.com/systemjs/systemjs/tree/0.21
[6]因?yàn)橐獎(jiǎng)討B(tài)通過(guò) http 引入外部 js,又不影響在開(kāi)發(fā)的時(shí)候使用 import、require 方法,所以找到了 systemjs 來(lái)做這件事。根據(jù) systemjs 文檔說(shuō)明,我們只需要把子項(xiàng)目打成 umd 格式(umd 糅合了 AMD 和 CommonJS)的包即可動(dòng)態(tài)外部加載。
// 每個(gè)子項(xiàng)目的webpack.config.jsoutput: { path: xxx, publicPath: xxx, filename: '[name].[chunkhash:8].js', chunkFilename: 'js/[name].[chunkhash:8].chunk.js', libraryTarget: 'umd', // 這里一定要寫(xiě)成umd,不然打出來(lái)的包system.js無(wú)法讀取 library: xxx, //模塊的名稱},
Webpack Externals
文檔:www.webpackjs.com/configurati…[7]這么多同類型的 vue 項(xiàng)目,一定有大量的重復(fù)代碼、重復(fù)引用,所以這是一塊巨大的性能優(yōu)化點(diǎn),通過(guò)配置 externals 可以極大減小子項(xiàng)目打包出來(lái)的體積。
我并沒(méi)有完全按照文檔說(shuō)明的方式來(lái)從 CDN 引入,原因是這樣的:入口 index.html 只有一個(gè),如果按文檔來(lái)做,一次引入所有 CDN 資源,可能子項(xiàng)目 A 用得到這些,但子項(xiàng)目 B 用不到這些,而我只訪問(wèn)了子項(xiàng)目 B 而已,這樣不就多加載了無(wú)用的資源嗎?經(jīng)過(guò)一番調(diào)研,同樣利用 systemjs 解決了這個(gè)問(wèn)題
// 每個(gè)子項(xiàng)目自己的webpack.config.js,根據(jù)使用情況設(shè)置externals externals: { 'axios': 'axios', 'vue': 'Vue', 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'iview': 'iview', 'moment': 'moment', 'echarts': 'echarts', '@mfb/pc-utils-micro':'@mfb/pc-utils-micro', // 私有公共方法包 '@mfb/pc-components-micro':'@mfb/pc-components-micro', // 私有公共組件包 // '@mfb/pc-components-micro':'@mfb/pc-components-micro-0.2.1', // 如果需要指定版本,則用這一行替換上一行 ...},
// index.html 整個(gè)微前端的唯一入口<script src="system.js"></script><script> SystemJS.config({ map: { Vue: '//xxx.cdn.cn/static/vue/2.5.17/vue.min.js', vue: '//xxx.cdn.cn/static/vue/2.5.17/vue.min.js', // 因?yàn)閕view前置需要vue,是小寫(xiě)的,就又聲明了一次 Vuex: '//xxx.cdn.cn/static/vuex/3.0.1/vuex.min.js', VueRouter: '//xxx.cdn.cn/static/vueRouter/3.0.1/vue-router.min.js', iview: '//xxx.cdn.cn/static/iview/3.3.2/iview.min.js', moment: '//xxx.cdn.cn/static/moment/2.22.2/moment.min.js', axios: '//xxx.cdn.cn/static/axios/0.15.3/axios.min.js', echarts: '//xxx.cdn.cn/static/echarts/4.2.1/echarts.min.js', '@mfb/pc-utils-micro': '//xxx.cdn.cn/static/mfb-pc-utils-micro/mfb-pc-utils-micro-0.0.6.js', '@mfb/pc-components-micro': '//xxx.cdn.cn/static/mfb-pc-components-micro/mfb-pc-components-micro-0.0.42.js', '@mfb/pc-components-micro-0.2.1': '//xxx.cdn.cn/static/mfb-pc-components-micro/mfb-pc-components-micro-0.2.1.js' // 如果需要指定版本 } })</script>
如此一來(lái),systemjs 只是在加載 index.html 時(shí)注冊(cè)了這些 CDN 地址,不會(huì)直接去加載,當(dāng)子項(xiàng)目里用到的時(shí)候,systemjs 會(huì)接管模塊引入,systemjs 會(huì)去上面注冊(cè)的 map 中查找匹配的模塊,就再動(dòng)態(tài)去加載資源。這樣就避免了不同子項(xiàng)目在這套架構(gòu)下產(chǎn)生的多余加載。
按我們的配置,webpack 打包后,externals 配置的模塊不會(huì)打包進(jìn) bundle,會(huì)被摘出來(lái)按 umd 規(guī)范通過(guò) requre/define 方式去加載。
看 systemjs 源碼會(huì)發(fā)現(xiàn)它重新定義了 require 和 define 方法,所以它能接管 externals 的外部引入過(guò)程。
四、總結(jié)體會(huì)
我最直白的感受是實(shí)現(xiàn)了項(xiàng)目級(jí)別的模塊化,把不同項(xiàng)目變成了一個(gè)個(gè)模塊來(lái)拼裝組合,也就是說(shuō)模塊化從項(xiàng)目?jī)?nèi)提升到了項(xiàng)目本身
總結(jié)一下使用這套架構(gòu)收到的好處,分為以下幾點(diǎn):
縮小項(xiàng)目打包體積(平均每個(gè)子項(xiàng)目 bundle 不到 100k),而整合后的公共資源只需加載一次,性能得到很大提升 (技術(shù)角度)
用戶體驗(yàn)更好,用戶感知不到自己在使用多個(gè)不同的項(xiàng)目,更加平順流暢 (產(chǎn)品角度)
不同 git 的項(xiàng)目經(jīng)過(guò)改造后,可以隨意以項(xiàng)目?jī)?nèi)每個(gè)路由頁(yè)面為單元拼裝成一個(gè)新項(xiàng)目,產(chǎn)品靈活性本質(zhì)上得到提升 (產(chǎn)品/技術(shù)角度)
技術(shù)嘗新,使用業(yè)界比較先進(jìn)的微前端理念,幾十個(gè)項(xiàng)目,成千上百個(gè)功能也能很好的分模塊管理。(管理角度)
也是有很多麻煩之處,需要消耗一定成本:
因?yàn)槎鄠€(gè) vue 實(shí)例在同一個(gè) document 里,需要避免全局變量污染、全局監(jiān)聽(tīng)污染、樣式污染等,需要制定接入規(guī)范。
使用了 external 抽離公共模塊(比如 Vue、Vue-router 等)后,構(gòu)造函數(shù)(或者 Class)的污染也需要避免,比如 Vue.mixin、Vue.components、Vue .use 等等都需要做一些額外的工作去避免它們產(chǎn)生沖突。
如果你也想要 tab 切換不刷新(使用 keep-alive),那需要做的工作更多,主要是處理緩存,防止堆內(nèi)存溢出(用 chrome 自帶的 performance monitor 查看),還有項(xiàng)目間切換時(shí)路由鉤子等等的處理。
不過(guò)跟收益比起來(lái),這些成本就不算什么了~
最后要說(shuō)一下,并不是所有場(chǎng)景都適合微前端,尤其是項(xiàng)目規(guī)模小、數(shù)量少的場(chǎng)景不建議使用。什么樣的場(chǎng)景適合這套架構(gòu)呢?一般有以下特征:
項(xiàng)目很多,規(guī)模很大,都是每個(gè)項(xiàng)目獨(dú)立使用git 此類倉(cāng)庫(kù)維護(hù)的、技術(shù)棧為 vue/react/angular 的這類應(yīng)用
需要整合到統(tǒng)一平臺(tái)上,你正在尋找比 iframe 好得多的替代方案
項(xiàng)目 A 有功能 A1、A2、A3,項(xiàng)目 B 有功能 B1、B2、B3,產(chǎn)品經(jīng)理要你把 A2、B1、B3 組合成一個(gè)包含這些功能的新項(xiàng)目
可能你會(huì)問(wèn):為什么不一開(kāi)始就把所有需要整合的功能用一個(gè) git 來(lái)維護(hù)?答:理想是美好的,誰(shuí)也沒(méi)有先知能力,隨著公司業(yè)務(wù)發(fā)展亦或是組織架構(gòu)的改變、人員更迭,以上場(chǎng)景是幾乎不可避免的;我很難想象十多個(gè)項(xiàng)目的好幾百個(gè)功能都在一個(gè) git 里管理起來(lái)有多困難。可能你還會(huì)問(wèn),那我把需要整合的業(yè)務(wù)整合成到一個(gè) git 倉(cāng)庫(kù)呢?答:這當(dāng)然是一個(gè)解決辦法,前提是整合的成本你能接受;并且將來(lái)還有這類需求呢?每次都要手動(dòng)整合業(yè)務(wù)代碼到同一個(gè) git 倉(cāng)庫(kù)嗎?假設(shè)所有人都只維護(hù)這個(gè)整合完的 git 倉(cāng)庫(kù),并行的需求線多了,上線時(shí)間會(huì)不會(huì)擁擠?一個(gè)功能產(chǎn)生了致命錯(cuò)誤,會(huì)不會(huì)所有功能跟著出問(wèn)題?
最后我想說(shuō):
我們做這套框架的初衷是解決眼前的問(wèn)題,然而發(fā)現(xiàn)它附帶的潛力價(jià)值卻比想象的多得多。