每日優(yōu)鮮供應(yīng)鏈前端團隊微前端改造(轉(zhuǎn))

原文鏈接:https://juejin.im/post/5d7f702ce51d4561f777e258

一、需求以及成果

我所在團隊是做 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:項目外觀示意圖:

項目外觀示意圖.jpg

做微前端改造之前论咏,藍色系區(qū)域都是用公共包的方式由每個子項目引入,所以子項目運行的時候展示的藍色系部分都是相同的颁井,給人一種在使用同一個系統(tǒng)的錯覺厅贪,實際上切換系統(tǒng)的時候整個頁面都要重新載入。

微前端改造后雅宾,只有橘色區(qū)域是變化的养涮,頁面也不再刷新。

圖 2:局部效果動圖

局部效果動圖.gif

圖 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)示意圖

部署架構(gòu)示意圖.jpg

實現(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

圖 3 中的 apps.config.js.jpg

用戶訪問 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)它附帶的潛力價值卻比想象的多得多睹逃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盗扇,一起剝皮案震驚了整個濱河市祷肯,隨后出現(xiàn)的幾起案子沉填,更是在濱河造成了極大的恐慌,老刑警劉巖佑笋,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翼闹,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒋纬,警方通過查閱死者的電腦和手機猎荠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門坚弱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人关摇,你說我怎么就攤上這事荒叶。” “怎么了输虱?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵些楣,是天一觀的道長。 經(jīng)常有香客問我宪睹,道長愁茁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任亭病,我火速辦了婚禮鹅很,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘罪帖。我一直安慰自己促煮,他們只是感情好,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布整袁。 她就那樣靜靜地躺著污茵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葬项。 梳的紋絲不亂的頭發(fā)上泞当,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機與錄音民珍,去河邊找鬼襟士。 笑死,一個胖子當著我的面吹牛嚷量,可吹牛的內(nèi)容都是我干的陋桂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼蝶溶,長吁一口氣:“原來是場噩夢啊……” “哼嗜历!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抖所,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤梨州,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后田轧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暴匠,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年傻粘,在試婚紗的時候發(fā)現(xiàn)自己被綠了每窖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帮掉。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖窒典,靈堂內(nèi)的尸體忽然破棺而出蟆炊,到底是詐尸還是另有隱情,我是刑警寧澤瀑志,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布盅称,位于F島的核電站,受9級特大地震影響后室,放射性物質(zhì)發(fā)生泄漏缩膝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一岸霹、第九天 我趴在偏房一處隱蔽的房頂上張望疾层。 院中可真熱鬧,春花似錦贡避、人聲如沸痛黎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湖饱。三九已至,卻和暖如春杀捻,著一層夾襖步出監(jiān)牢的瞬間井厌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工致讥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留仅仆,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓垢袱,卻偏偏與公主長得像墓拜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子请契,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348