每日優鮮供應鏈前端團隊微前端改造

文 掘金號 Terry豆 @每日優鮮html

著做權歸做者全部。商業轉載請聯繫本帳號得到受權,非商業轉載請註明每日優鮮大前端團隊以及原文地址。前端

1、需求以及成果

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

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

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

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

2、展現以及技術點

圖1:項目外觀示意圖:

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

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

圖2:局部效果動圖

圖2展現了圖1中的tab頁籤區以及子項目展現區。信息作了馬賽克處理。
git

乍一看沒什麼特別的,但若是我說這些tab分別來自於不一樣git倉庫的獨立vue項目呢?
這就是這套微前端架構的強大之處,讓不一樣單頁vue項目能夠隨意組合成一個項目,而這些項目本身又是獨立的vue項目
github

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

爲了讓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

3、一些技術細節

這裏說的的項目打包都是基於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的外部引入過程。

4、總結體會

我最直白的感覺是實現了項目級別的模塊化,把不一樣項目變成了一個個模塊來拼裝組合,也就是說模塊化從項目內提高到了項目自己

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

  • 縮小項目打包體積(平均每一個子項目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有功能A一、A二、A3,項目B有功能B一、B二、B3,產品經理要你把A二、B一、B3組合成一個包含這些功能的新項目

可能你會問:爲何不一開始就把全部須要整合的功能用一個git來維護?
答:理想是美好的,誰也沒有先知能力,隨着公司業務發展亦或是組織架構的改變、人員更迭,以上場景是幾乎不可避免的;我很難想象十多個項目的好幾百個功能都在一個git裏管理起來有多困難。
可能你還會問,那我把須要整合的業務整合成到一個git倉庫呢?
答:這固然是一個解決辦法,前提是整合的成本你能接受;而且未來還有這類需求呢?每次都要手動整合業務代碼到同一個git倉庫嗎?假設全部人都只維護這個整合完的git倉庫,並行的需求線多了,上線時間會不會擁擠?一個功能產生了致命錯誤,會不會全部功能跟着出問題?

最後我想說:

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

招賢納士

招前端賢士,隸屬每日優鮮前端團隊,更多小夥伴期待與你一塊兒浪。若是你覺的一技之長沒地方施展,若是你以爲想玩更潮的技術,若是你厭倦了一成不變,若是你覺的到了瓶頸沒法突破,若是你以爲幹了這麼多年前端突然迷茫了,若是你有股創業勁卻沒機會,若是若是,那是時候做出改變和嘗試了,期待你跟咱們聊聊,寫點什麼發給咱們。大前端TL郵箱:mazhuang@missfresh.cn 座標:北京

相關文章
相關標籤/搜索