[第6期 ]每日優鮮供應鏈前端團隊微前端改造

每日優鮮前端團隊 https://juejin.im/post/5d7f702ce51d4561f777e258#heading-10

一、需求以及成果

我所在團隊是做toB業務的,技術棧是Vue,團隊目前有十多個典型的toB業務(菜單+內容布局),這些業務都是服務於一個大平台的,因為歷史原因,每個業務都是獨立的,都有一個html入口,所以當用戶在這個大平台上使用這十多個業務的時候,每當切換系統時,頁面都會刷新,體驗很差;在開發層面,這十多個業務又有太多共同之處,每次修改成本都很高。

最近有一個很重要的需求,內容是這樣的:從十多個項目中,每個項目抽取若干功能組成一個新項目,基於現有架構的話,每當點擊來自不同系統的功能頁面就要刷新一次,這是不可接受的。為了新需求X重複開發一遍這些業務功能又不現實,所以從技術角度來看,架構改造不可避免。

經過一番調研比對,我們決定使用當下比較火的 SingleSpa 來完成改造(iframe方案嘗試過,不太適合我們的場景),目前改造已完成,我們實現了以下效果:

  • 只有一個不包含子項目(子項目指的是那十多個業務)資源的主項目,主項目只有一個html入口,子項目通過主項目來按需載入,子系統間切換不再刷新;
  • 菜單欄、登錄、退出等功能都從子項目剝離,寫在主項目里,再有相關改動只需修改主項目,包括錯誤監控、埋點等行為,只需處理一個主項目,十幾個子項目不再需要處理;
  • 子項目原本需要載入的公共部分(如vue、vuex、vue-router、ivew/element、私有npm包等),全部由主項目調度,配合webpack的externals功能通過外鏈的方式按需載入,一旦有一個子項目載入過,下一個子項目就不需要再載入,這樣一來每個子項目的dist文件里就只有子項目自己的業務程式碼(最終子項目包的體積縮小了80%,只有幾十k),項目實際載入速度快了很多,肉眼可見;
  • 子項目並沒有重新開發,只是進行了一些改造,接入了微前端這套架構,所以新需求X的開發成本也極大的降低了,接入功能同時可供未來新增子項目使用;
  • 我們的項目有自己的tab系統(類似瀏覽器的tab頁簽),這些tab頁簽通過keep-alive和一系列對快取的處理,使其體驗接近原生瀏覽器tab。

二、展示以及技術點

圖1:項目外觀示意圖:

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

微前端改造後,只有橘紅色區域是變化的,頁面也不再刷新。

圖2:局部效果動圖

圖2展示了圖1中的tab頁簽區以及子項目展示區。資訊做了馬賽克處理。

乍一看沒什麼特別的,但如果我說這些tab分別來自於不同git倉庫的獨立vue項目呢?

這就是這套微前端架構的強大之處,讓不同單頁vue項目可以隨意組合成一個項目,而這些項目自己又是獨立的vue項目

仔細看圖2中路由的變化,hash路由的第一級決定了要載入哪個子項目(work、sms、tms是三個不同的git工程),不同子項目間的切換也完全沒有刷新?

為了讓tab切換不刷新,這裡使用了keep-alive去快取頁面,考慮到記憶體性能,在關閉tab頁簽時通過一些方法(主要是keep-alive的exclude屬性)去除了keep-alive快取,同時為了讓子項目間的tab切換也不刷新,對圖3下面提到的包裝器也進行了不小的改造。讓tab切換不刷新只是為了提升用戶體驗,這一步不是必要的,有一定的成本。

圖3:部署架構示意圖

實現一套微前端架構,可以把其分成四部分(參考:alili.tech/archive/110… )

  • 載入器:也就是微前端架構的核心,圖3中的「載入器JS文件」就是由載入器打包壓縮出來的,這是原始的載入器:github.com/Fantasy9527… —— 可以把它理解成電源
  • 包裝器:有了載入器,我們要把現有的vue項目包裝一下,使得載入器可以使用它們,這是原始的包裝器:github.com/CanopyTax/s… —— 如果想改造,建議改造這個部分,它相當於電源適配器
  • 主項目:一般是包含所有項目公共部分的項目—— 它相當於電器底座
  • 子項目:眾多展示在主項目內容區的項目—— 它相當於你要使用的電器

所以是這麼個概念:電源(載入器)→電源適配器(包裝器)→️電器底座(主項目)→️電器(子項目)️

主項目和子項目都需要用包裝器包裝,只不過主項目的配置寫法有不同

載入器和包裝器需要根據自己的需求做一些二次開發

總的來說是這樣一個流程:用戶訪問index.html後,瀏覽器運行載入器的js文件,載入器去讀取圖4中的配置文件,然後註冊配置文件中配置的各個項目後,首先載入主項目(菜單等),再通過路由判定,動態遠程載入子項目。

這裡有個vue微前端版demo,包含最基礎的效果與源碼,務必研究一下這個demo再結合以上理論來幫助理解 *遠程載入的子項目資源要在chrome的network中的xhr那一欄才能看到

圖4:圖3中的apps.config.js

用戶訪問index.html後,js載入器會載入apps.config.js。

無論路由是什麼,每次必會首先載入主項目,再根據路由來匹配要載入哪個子項目。 apps.config.js的生成如圖3的綠色部分所示:

在資源伺服器上起一個監聽服務(我使用的是nodejs腳本+pm2守護),原有子項目的部署方式完全不變(前後端完全分離,資源帶hash),當監聽服務檢測到文件改動時,去子項目部署文件夾里找它的index.html,把入口js用如下正則匹配出來,寫入apps.config.js。


// content[i]為子項目文件夾名稱。這段程式碼是nodejs腳本片段。  const reg = new RegExp(`src="(\/${content[i]}\/index\.\w{8}.js)`)  // 對應圖中的 /brain/index.3c4b55cf.js  

圖4中的brain即是主項目,它的base屬性為true,其餘子項目的base屬性為false

三、一些技術細節

這裡說的的項目打包都是基於webpack。

System.js

它是實現遠程載入子項目的核心。

我們使用的是0.21版本的:github.com/systemjs/sy…

因為要動態通過http引入外部js,又不影響在開發的時候使用import、require方法,所以找到了systemjs來做這件事。

根據systemjs文檔說明,我們只需要把子項目打成umd格式(umd糅合了AMD和CommonJS)的包即可動態外部載入。

// 每個子項目的webpack.config.js  output: {      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…

這麼多同類型的vue項目,一定有大量的重複程式碼、重複引用,所以這是一塊巨大的性能優化點,通過配置externals可以極大減小子項目打包出來的體積。

我並沒有完全按照文檔說明的方式來從CDN引入,原因是這樣的:入口index.html只有一個,如果按文檔來做,一次引入所有CDN資源,可能子項目A用得到這些,但子項目B用不到這些,而我只訪問了子項目B而已,這樣不就多載入了無用的資源嗎?

經過一番調研,同樣利用systemjs解決了這個問題

// 每個子項目自己的webpack.config.js,根據使用情況設置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中查找匹配的模組,就再動態去載入資源。這樣就避免了不同子項目在這套架構下產生的多餘載入。

按我們的配置,webpack打包後,externals配置的模組不會打包進bundle,會被摘出來按umd規範通過requre/define方式去載入。

看systemjs源碼會發現它重新定義了require和define方法,所以它能接管externals的外部引入過程。

四、總結體會

我最直白的感受是實現了項目級別的模組化,把不同項目變成了一個個模組來拼裝組合,也就是說模組化從項目內提升到了項目本身。

總結一下使用這套架構收到的好處,分為以下幾點:

  • 縮小項目打包體積(平均每個子項目bundle不到100k),而整合後的公共資源只需載入一次,性能得到很大提升 (技術角度)
  • 用戶體驗更好,用戶感知不到自己在使用多個不同的項目,更加平順流暢 (產品角度)
  • 不同git的項目經過改造後,可以隨意以項目內每個路由頁面為單元拼裝成一個新項目,產品靈活性本質上得到提升 (產品/技術角度)
  • 技術嘗新,使用業界比較先進的微前端理念,幾十個項目,成千上百個功能也能很好的分模組管理。 (管理角度)

也是有很多麻煩之處,需要消耗一定成本:

  • 因為多個vue實例在同一個document里,需要避免全局變數污染、全局監聽污染、樣式污染等,需要制定接入規範。
  • 使用了external抽離公共模組(比如Vue、Vue-router等)後,構造函數(或者Class)的污染也需要避免,比如Vue.mixin、Vue.components、Vue .use等等都需要做一些額外的工作去避免它們產生衝突。
  • 如果你也想要tab切換不刷新(使用keep-alive),那需要做的工作更多,主要是處理快取,防止堆記憶體溢出(用chrome自帶的performance monitor查看),還有項目間切換時路由鉤子等等的處理。

不過跟收益比起來,這些成本就不算什麼了~

最後要說一下,並不是所有場景都適合微前端,尤其是項目規模小、數量少的場景不建議使用。

什麼樣的場景適合這套架構呢?一般有以下特徵:

  • 項目很,規模很,都是每個項目獨立使用git此類倉庫維護的、技術棧為vue/react/angular的這類應用
  • 需要整合到統一平台上,你正在尋找可能比iframe更合適的替代方案
  • 項目A有功能A1、A2、A3,項目B有功能B1、B2、B3,產品經理要你把A2、B1、B3組合成一個包含這些功能的新項目

可能你會問:為什麼不一開始就把所有需要整合的功能用一個git來維護?

答:理想是美好的,誰也沒有先知能力,隨著公司業務發展亦或是組織架構的改變、人員更迭,以上場景是幾乎不可避免的;我很難想像十多個項目的好幾百個功能都在一個git里管理起來有多困難。

可能你還會問,那我把需要整合的業務整合成到一個git倉庫呢?

答:這當然是一個解決辦法,前提是整合的成本你能接受;並且將來還有這類需求呢?每次都要手動整合業務程式碼到同一個git倉庫嗎?假設所有人都只維護這個整合完的git倉庫,並行的需求線多了,上線時間會不會擁擠?一個功能產生了致命錯誤,會不會所有功能跟著出問題?

最後我想說:

我們做這套框架的初衷是解決眼前的問題,然而發現它附帶的潛力價值卻比想像的多得多。