一、需求以及成果
我所在團隊是做 toB 業(yè)務(wù)的,技術(shù)棧是 Vue村缸,團隊目前有十多個典型的 toB 業(yè)務(wù)(菜單+內(nèi)容布局),這些業(yè)務(wù)都是服務(wù)于一個大平臺的,因為歷史原因涛舍,每個業(yè)務(wù)都是獨立的,都有一個 html 入口唆途,所以當用戶在這個大平臺上使用這十多個業(yè)務(wù)的時候富雅,每當切換系統(tǒng)時掸驱,頁面都會刷新,體驗很差没佑;在開發(fā)層面毕贼,這十多個業(yè)務(wù)又有太多共同之處,每次修改成本都很高蛤奢。
最近有一個很重要的需求 X鬼癣,內(nèi)容是這樣的:從十多個項目中,每個項目抽取若干功能組成一個新項目啤贩,基于現(xiàn)有架構(gòu)的話待秃,每當點擊來自不同系統(tǒng)的功能頁面就要刷新一次,這是不可接受的痹屹。為了新需求 X 重復(fù)開發(fā)一遍這些業(yè)務(wù)功能又不現(xiàn)實章郁,所以從技術(shù)角度來看,架構(gòu)改造不可避免志衍。
經(jīng)過一番調(diào)研比對暖庄,我們決定使用當下比較火的 SingleSpahttps://single-spa.js.org/
[1] 來完成改造(iframe 方案毫無亮點,棄之)楼肪,目前改造已完成培廓,我們實現(xiàn)了以下效果:
只有一個不包含子項目(子項目指的是那十多個業(yè)務(wù))資源的主項目,主項目只有一個 html 入口春叫,子項目通過主項目來按需加載肩钠,子系統(tǒng)間切換不再刷新;
菜單欄暂殖、登錄蔬将、退出等功能都從子項目剝離,寫在主項目里央星,再有相關(guān)改動只需修改主項目霞怀,包括錯誤監(jiān)控、埋點等行為莉给,只需處理一個主項目毙石,十幾個子項目不再需要處理;
子項目原本需要加載的公共部分(如 vue颓遏、vuex徐矩、vue-router、ivew/element叁幢、私有 npm 包等)滤灯,全部由主項目調(diào)度,配合 webpack 的 externals 功能通過外鏈的方式按需加載,一旦有一個子項目加載過鳞骤,下一個子項目就不需要再加載窒百,這樣一來每個子項目的 dist 文件里就只有子項目自己的業(yè)務(wù)代碼(最終子項目包的體積縮小了 80%,只有幾十 k)豫尽,項目實際加載速度快了很多篙梢,肉眼可見;
子項目并沒有重新開發(fā)美旧,只是進行了一些改造渤滞,接入了微前端這套架構(gòu),所以新需求 X 的開發(fā)成本也極大的降低了榴嗅,接入功能同時可供未來新增子項目使用妄呕;
我們的項目有自己的 tab 系統(tǒng)(類似瀏覽器的 tab 頁簽),這些 tab 頁簽通過 keep-alive 和一系列對緩存的處理嗽测,使其體驗接近原生瀏覽器 tab绪励。
二、展示以及技術(shù)點
圖 1:項目外觀示意圖:
做微前端改造之前论咏,藍色系區(qū)域都是用公共包的方式由每個子項目引入,所以子項目運行的時候展示的藍色系部分都是相同的颁井,給人一種在使用同一個系統(tǒng)的錯覺厅贪,實際上切換系統(tǒng)的時候整個頁面都要重新載入。
微前端改造后雅宾,只有橘色區(qū)域是變化的养涮,頁面也不再刷新。
圖 2:局部效果動圖
圖 2 展示了圖 1 中的 tab 頁簽區(qū)以及子項目展示區(qū)眉抬。信息做了馬賽克處理贯吓。
乍一看沒什么特別的,但如果我說這些 tab分別來自于不同 git 倉庫的獨立 vue 項目呢蜀变?這就是這套微前端架構(gòu)的強大之處悄谐,讓不同單頁 vue 項目可以隨意組合成一個項目,而這些項目自己又是獨立的 vue 項目库北。
仔細看圖 2 中路由的變化爬舰,hash 路由的第一級決定了要加載哪個子項目(work、sms寒瓦、tms 是三個不同的 git 工程)情屹,不同子項目間的切換也完全沒有刷新 ??
為了讓 tab 切換不刷新,這里使用了 keep-alive 去緩存頁面杂腰,考慮到內(nèi)存性能垃你,在關(guān)閉 tab 頁簽時通過一些方法(主要是 keep-alive 的 exclude 屬性)去除了 keep-alive 緩存,同時為了讓子項目間的 tab 切換也不刷新,對圖 3 下面提到的包裝器也進行了不小的改造惜颇。讓 tab 切換不刷新只是為了提升用戶體驗皆刺,這一步不是必要的,有一定的成本官还。
圖 3:部署架構(gòu)示意圖
實現(xiàn)一套微前端架構(gòu)芹橡,可以把其分成四部分(參考:https://alili.tech/archive/11052bf4/[2] )
加載器:也就是微前端架構(gòu)的核心,圖 3 中的“加載器 JS 文件”就是由加載器打包壓縮出來的望伦,這是原始的加載器:https://github.com/Fantasy9527/lotus-scaffold-micro-frontend-portal
[3] —— 可以把它理解成電源包裝器:有了加載器林说,我們要把現(xiàn)有的 vue 項目包裝一下,使得加載器可以使用它們屯伞,這是原始的包裝器:https://github.com/CanopyTax/single-spa-vue
[4] —— 如果想改造腿箩,建議改造這個部分,它相當于電源適配器主項目:一般是包含所有項目公共部分的項目—— 它相當于電器底座
子項目:眾多展示在主項目內(nèi)容區(qū)的項目—— 它相當于你要使用的電器
所以是這么個概念:電源(加載器)→ 電源適配器(包裝器)→? 電器底座(主項目)→? 電器(子項目)?
主項目和子項目都需要用包裝器包裝劣摇,只不過主項目的配置寫法有不同
加載器和包裝器需要根據(jù)自己的需求做一些二次開發(fā)
總的來說是這樣一個流程:用戶訪問 index.html 后珠移,瀏覽器運行加載器的 js 文件,加載器去讀取圖 4 中的配置文件末融,然后注冊配置文件中配置的各個項目后钧惧,首先加載主項目(菜單等),再通過路由判定勾习,動態(tài)遠程加載子項目浓瞪。
這里有個vue 微前端版demo https://github.com/joeldenning/coexisting-vue-microfrontends
[5],包含最基礎(chǔ)的效果與源碼巧婶,務(wù)必研究一下這個 demo 再結(jié)合以上理論來幫助理解 *遠程加載的子項目資源要在 chrome 的 network 中的 xhr 那一欄才能看到
圖 4:圖 3 中的 apps.config.js
用戶訪問 index.html 后乾颁,js 加載器會加載 apps.config.js。無論路由是什么艺栈,每次必會首先加載主項目英岭,再根據(jù)路由來匹配要加載哪個子項目。apps.config.js 的生成如圖 3 的綠色部分所示:
在資源服務(wù)器上起一個監(jiān)聽服務(wù)(我使用的是 nodejs 腳本+pm2 守護)湿右,原有子項目的部署方式完全不變(前后端完全分離诅妹,資源帶 hash),當監(jiān)聽服務(wù)檢測到文件改動時毅人,去子項目部署文件夾里找它的 index.html漾唉,把入口 js 用如下正則匹配出來,寫入 apps.config.js堰塌。
// content[i]為子項目文件夾名稱赵刑。這段代碼是nodejs腳本片段。const reg = new RegExp(`src="(\/${content[i]}\/index\.\w{8}.js)`) // 對應(yīng)圖中的 /brain/index.3c4b55cf.js
圖 4 中的 brain 即是主項目场刑,它的 base 屬性為 true般此,其余子項目的 base 屬性為 false
三蚪战、一些技術(shù)細節(jié)
這里說的的項目打包都是基于 webpack。
System.js
它是實現(xiàn)遠程加載子項目的核心铐懊。我們使用的是 0.21 版本的:https://github.com/systemjs/systemjs/tree/0.21
[6]因為要動態(tài)通過 http 引入外部 js邀桑,又不影響在開發(fā)的時候使用 import、require 方法科乎,所以找到了 systemjs 來做這件事壁畸。根據(jù) systemjs 文檔說明,我們只需要把子項目打成 umd 格式(umd 糅合了 AMD 和 CommonJS)的包即可動態(tài)外部加載茅茂。
// 每個子項目的webpack.config.jsoutput: { path: xxx, publicPath: xxx, filename: '[name].[chunkhash:8].js', chunkFilename: 'js/[name].[chunkhash:8].chunk.js', libraryTarget: 'umd', // 這里一定要寫成umd捏萍,不然打出來的包system.js無法讀取 library: xxx, //模塊的名稱},
Webpack Externals
文檔:www.webpackjs.com/configurati…[7]這么多同類型的 vue 項目,一定有大量的重復(fù)代碼空闲、重復(fù)引用令杈,所以這是一塊巨大的性能優(yōu)化點,通過配置 externals 可以極大減小子項目打包出來的體積碴倾。
我并沒有完全按照文檔說明的方式來從 CDN 引入逗噩,原因是這樣的:入口 index.html 只有一個,如果按文檔來做跌榔,一次引入所有 CDN 資源异雁,可能子項目 A 用得到這些,但子項目 B 用不到這些僧须,而我只訪問了子項目 B 而已纲刀,這樣不就多加載了無用的資源嗎?經(jīng)過一番調(diào)研皆辽,同樣利用 systemjs 解決了這個問題
// 每個子項目自己的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 整個微前端的唯一入口<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', // 因為iview前置需要vue芥挣,是小寫的驱闷,就又聲明了一次 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>
如此一來,systemjs 只是在加載 index.html 時注冊了這些 CDN 地址空免,不會直接去加載空另,當子項目里用到的時候,systemjs 會接管模塊引入蹋砚,systemjs 會去上面注冊的 map 中查找匹配的模塊扼菠,就再動態(tài)去加載資源。這樣就避免了不同子項目在這套架構(gòu)下產(chǎn)生的多余加載坝咐。
按我們的配置循榆,webpack 打包后,externals 配置的模塊不會打包進 bundle墨坚,會被摘出來按 umd 規(guī)范通過 requre/define 方式去加載秧饮。
看 systemjs 源碼會發(fā)現(xiàn)它重新定義了 require 和 define 方法,所以它能接管 externals 的外部引入過程。
四盗尸、總結(jié)體會
我最直白的感受是實現(xiàn)了項目級別的模塊化柑船,把不同項目變成了一個個模塊來拼裝組合,也就是說模塊化從項目內(nèi)提升到了項目本身
總結(jié)一下使用這套架構(gòu)收到的好處泼各,分為以下幾點:
縮小項目打包體積(平均每個子項目 bundle 不到 100k)鞍时,而整合后的公共資源只需加載一次,性能得到很大提升 (技術(shù)角度)
用戶體驗更好扣蜻,用戶感知不到自己在使用多個不同的項目逆巍,更加平順流暢 (產(chǎn)品角度)
不同 git 的項目經(jīng)過改造后,可以隨意以項目內(nèi)每個路由頁面為單元拼裝成一個新項目弱贼,產(chǎn)品靈活性本質(zhì)上得到提升 (產(chǎn)品/技術(shù)角度)
技術(shù)嘗新蒸苇,使用業(yè)界比較先進的微前端理念,幾十個項目吮旅,成千上百個功能也能很好的分模塊管理溪烤。(管理角度)
也是有很多麻煩之處,需要消耗一定成本:
因為多個 vue 實例在同一個 document 里庇勃,需要避免全局變量污染檬嘀、全局監(jiān)聽污染、樣式污染等责嚷,需要制定接入規(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 查看),還有項目間切換時路由鉤子等等的處理枢舶。
不過跟收益比起來懦胞,這些成本就不算什么了~
最后要說一下,并不是所有場景都適合微前端凉泄,尤其是項目規(guī)模小躏尉、數(shù)量少的場景不建議使用。什么樣的場景適合這套架構(gòu)呢后众?一般有以下特征:
項目很多胀糜,規(guī)模很大稼锅,都是每個項目獨立使用git 此類倉庫維護的、技術(shù)棧為 vue/react/angular 的這類應(yīng)用
需要整合到統(tǒng)一平臺上僚纷,你正在尋找比 iframe 好得多的替代方案
項目 A 有功能 A1矩距、A2、A3,項目 B 有功能 B1怖竭、B2锥债、B3,產(chǎn)品經(jīng)理要你把 A2痊臭、B1哮肚、B3 組合成一個包含這些功能的新項目
可能你會問:為什么不一開始就把所有需要整合的功能用一個 git 來維護?答:理想是美好的广匙,誰也沒有先知能力允趟,隨著公司業(yè)務(wù)發(fā)展亦或是組織架構(gòu)的改變、人員更迭鸦致,以上場景是幾乎不可避免的潮剪;我很難想象十多個項目的好幾百個功能都在一個 git 里管理起來有多困難。可能你還會問分唾,那我把需要整合的業(yè)務(wù)整合成到一個 git 倉庫呢抗碰?答:這當然是一個解決辦法,前提是整合的成本你能接受绽乔;并且將來還有這類需求呢弧蝇?每次都要手動整合業(yè)務(wù)代碼到同一個 git 倉庫嗎?假設(shè)所有人都只維護這個整合完的 git 倉庫折砸,并行的需求線多了看疗,上線時間會不會擁擠?一個功能產(chǎn)生了致命錯誤睦授,會不會所有功能跟著出問題两芳?
最后我想說:
我們做這套框架的初衷是解決眼前的問題,然而發(fā)現(xiàn)它附帶的潛力價值卻比想象的多得多睹逃。