微前端改造初探

在寫這篇文章的一個多月前,本坑還不知道微前端是什麼,大概從字面上的含義是比較小的前端項目。css

本坑開始實踐它,是因爲工做要求。改造一個運行多年,前端用jsp寫的服務平臺項目(如下簡稱該平臺)。改造它是改造它的前端架構。改造它的緣由是比較多人反饋,其頁面加載和渲染顯得吃力,頁面切換後首屏等待時間長的問題,交互體驗溫馨度不可避免的降低了,特別是在老式電腦面前。html

該平臺業務比較多,因此組長但願前端框架組能把平臺中的前端部分分離出來,最好用當下滿大街的Vue、可以按照各個一級菜單分紅若干前端子項目,用戶訪問依然是總體的項目,同時這一改造實施過程不須要重作一個、而是整個500多個頁面從局部開始、是逐步、兼容的,新舊同時運行,直至總體被替換。(ps:不重作?這......科學嗎?)前端

看看,慢在哪裏

其實大概知道慢在哪裏,可是不知道究竟慢在具體哪一個部分。和其餘一以貫之的相似管理平臺佈局並沒有不一樣。左邊導航欄,上面頂欄,右側內容欄,總體頁面是一個index.jsp。上面提到的內容欄是一個iframe,裏面經過切換src來切換頁面。更多的業務造就更多頁面,更多頁面帶來更多的加載。加上長時間沒有作好資源加載的管理,致使渲染一次頁面須要加載大量js,css或者屢次加載同一個文件的狀況。該平臺大量的配置頁面生成,是經過easyui的來作的。經過數據來創造整個頁面dom節點,也拖累了內容完整呈現的時間。vue

咱們使用谷歌瀏覽器performance能夠最終追查到這個系統在哪些方面,哪一個方法存在着哪些延遲。結論是:node

一、混亂的項目資源管理致使大量的資源請求。jquery

二、easyui和項目中很多的dom操做帶來大量的重排和重繪。ios

三、埋點,插件使用不當以及其餘。web

系統截圖

微前端

它是什麼呢?vue-router

微前端的概念來自於以前流行的微服務。它的來源很大程度是來自於這篇文章 。微服務系統使得後臺服務架構可以比較好地規避愈來愈臃腫的體積帶來的性能降低。根據業務合理拆分紅一個個的服務,儘可能避免一個子服務影響整個項目運行的優點,有效的進行隔離。vuex

那麼,前端也有一樣的需求嗎?答案是確定的。

今天,日益更新的前端技術,已經可以把一個個頁面各個小元素打包成組件庫,功能包,在多個項目中引入使用。此外,咱們不用再使用難受的iframe來聚合不一樣的項目,而是導出一個個web component,只須要import 到頁面就可使用。把一個個子項目打包成一個個web component,聚合在入口項目以內。這也許就是微前端如今比較時髦的樣子。 若是一個大項目有如下特色,微前端能夠在這些項目中運用: 一、大項目有統一的入口,子項目頁面須要無刷新下切換,但是各個子項目在業務上和開發團隊上是不一樣的。

二、項目過大,打包、運行、部署效率出現顯著降低的問題。這時但願能根據業務拆分打包,部署。

方案與實施

回到本文開頭,一開始面對這樣的需求仍是有些想辭職的衝動,由於以爲需求有點不是符合實際,實際上要實施改版也是須要過程的。

不過靜下來想一想,搜搜,翻了翻當前項目的前端結構,隱隱約約彷佛浮現一些需求可行性的線索。

由於項目的最終目的是把整個jsp頁面改爲vue來寫。而這一要求是逐步替換的過程,因此在改造過程當中,同時要保證項目兼容jsp的頁面。咱們繼續沿用了原來就有的iframe,藉此把jsp融入整個微前端框架,而已經改造的micro則不須要iframe. 咱們的開發團隊,分框架組和各個業務組。其中每一個業務組有3到8我的,他們大多數是後端背景,主要作的也是後端開發。框架組有前端和後端。爲了應付龐大的業務開發需求。大部分後端人員都須要使用jsp,js等前端技術進行開發。 框架組爲了減小他們的前端開發門檻,前端框架組會封裝好easyui組件,提供業務組使用。因此,正如前文提到,後端人員是經過數據,結合框架組提供的組件來完成頁面的開發的。從某種角度來講,數據配置的頁面對接下來的改造工做有必定的幫助,由於大部分頁面能夠同時改寫。

咱們對整個項目進行了大體的分類。

一、portal 項目:該項目是整個微前端項目的入口。裏面含有loader,用以加載各個項目模塊。它也嵌入到子項目中,使得單獨運行子項目和portal項目同樣的界面要求。 二、permission 項目:該項目包含菜單組件,登陸頁面,頂欄組件,權限控制等。在任何環境下,它都必須首先加載,爲子項目模塊掛載提供錨點。 三、common項目:該項目包含公共業務組件。好比封裝好的頁面,能夠直接給不太可以掌握vue項目的後端人員更加友好的去使用。 四、業務項目:就是指業務組各個模塊開發的前端項目。什麼樣的業務分爲一個項目,這點由產品和技術人員一塊兒來決定。相對於portal項目,業務項目至關於它的子項目。

前端框架組必須提供一套統一的業務項目的前端模板,能夠在確認新建的子項目後迅速的加入到整個項目中,進行開發和部署,而這一過程不能影響其餘項目的部署和運行。

除了上述方案浮出水面,還會在改造過程當中遇到一個個細節問題。 不過在大方向,框架組成上,前端結構上作好了,細節問題也會隨耐心和時間被解決。

分別闡述

本坑根據以上的分類,大體進行說明其實現。這其中結合了很多前輩之經驗,在文章結尾處鳴謝。

Portal:

portal 項目是整個項目部署的入口,它的核心來自於single-spa 在整個項目結構中它將集成到每個子項目。集成的方式很粗暴簡單,就是外聯加載。 portal負責根據不一樣的環境來對應的組件和app,同時也安裝各個app,卸載各個app等,它負責app在single-spa的生命週期。好比集成模式下根據環境和路由加載對應的app,而在子項目運行時只加載公共組件和不一樣業務的app。

那麼protal是如何加載的呢?

protal維護了一個json裏面包含了各個子項目的index.html的信息,經過匹配index.html裏面的src 、link,加載各項資源。

module.exports = {
  common: {
    webName:'common',
    globalVarName: 'mfe:common',
    componentsTarget: '/common/release/components/web.html',
    resourcePatterns: ['/components.[0-9a-z]{8}.js/g'],
    loadType:'before'
  },
  permission: {
    webName:'permission',
    globalVarName: 'mfe:permission',
    // URL 匹配模式
    matchUrlHash: '',
    // 微前端地址
    componentsTarget: '/permission/release/components/web.html',
    webTarget:'/permission/release/web/web.html',
    // 資源匹配模式
    resourcePatterns: ['/common.[0-9a-z]{8}.css/g','/store.[0-9a-z]{8}.js/g', '/publicPath.[0-9a-z]{8}.js/g','/singleSpaEntry.[0-9a-z]{8}.js/g','/components.[0-9a-z]{8}.js/g'],
    //是否要在項目啓動前加載,before爲提早加載,after爲hash變化後加載
    loadType:'before'
  },
  app4vue:{
    webName:'repair-order',
    globalVarName: 'mfe:app4vue',
    matchUrlHash: '/layout/repair-order',
    webTarget: '/app4/release/web/web.html',
    resourcePatterns: ['/common.[0-9a-z]{8}.css/g','/store.[0-9a-z]{8}.js/g', '/singleSpaEntry.[0-9a-z]{8}.js/g'],
    loadType:'after'
  }
}
複製代碼
async  gatherResource () {
    const self = this
    // const spaEntry = 'portal'
    const web = self._webName
    //若是是微前端聚合模式
    if (window._IS_SIGLE_PORTAL) {
      if (web !== 'mfe-permission') {
        await self.loadComponents(micros.common)
        await self.loadApp(micros.permission)
      }
    }
    else {
      if (web === 'mfe-permission') {
        await self.loadComponents(micros.common)
      } else {
        if (web !== 'mfe-common') {
          await self.loadComponents(micros.common)
        }
        await self.loadApp(micros.permission)
      }
    }
    // return new Promise(resolve => resolve('loader:all Finish!'))
  }
複製代碼
permisson:

permission負責登陸頁,layout中的菜單欄,頂欄。全部的子項目app都必須掛載到permission項目中的顯示區塊裏。也就是說permssion會提供錨點給子項目掛載。

因此permission負責路由的控制,這裏的路由有整體路由和app內路由切換。若是app切換的路由控制涉及到singer-spa,app的切換會觸發single-spa:routing-event事件,portal監聽該事件 unmounted和mounted app,若是app內部的路由切換,須要觸發app內部路由切換。

本坑嘗試監聽permission的 hash,因爲vue新版本,hash實際是監聽不了的,因此監聽hash是沒辦法的。

permission監控路由發出事件,app內部監聽,從而觸發路由

permission監控路由發出事件,app內部監聽,從而觸發路由

這看上去會干涉到子項目的代碼,若是哪位大神有好方法能夠在評論區貼上你的見解。

業務項目

業務項目的獨立運行只會發生在開發模式之下,在生產或者測試環境並不會獨立運行。

集成環境下輸出成三個週期,提供給single-spa

export var global = {};
export const bootstrap = () => { return Promise.resolve(); }
export function mount (props) {
    Vue.mixin({
        data: function () {
            return {
                props
            }
        }
    })
    return Promise.resolve().then(() => {
        createDomElement();
        global.instance = new Vue({
            el: '#app4',
            router,
            render: h => h(App)
        })
    })
}
export function unmount () {
    return Promise.resolve().then(() => {
        global.instance.$destroy();
        global.instance.$el.innerHTML = '';
        delete global.instance
    })
}
function createDomElement () {
    // Make sure there is a div for us to render into
    let node = document.getElementById('main-content');
    let el = document.getElementById('app4');
    if (!el) {
        el = document.createElement('div');
        el.id = 'app4';
        node.appendChild(el);
    }
    return el;

}
複製代碼

開發模式獨立運行

const init = async () => {
  //啓動single-spa
  const loader = new Loader(process.env)
  await loader.startSingleSpa()
  Vue.mixin({
    data () {
      return {
        loader
      }
    }
  })
  Vue.config.productionTip = false;
  //permission渲染後再掛載本身上去
  window.addEventListener('single-spa:main-content-mount', evt => {
    if (!window.vim) {
      window.vm = new Vue({
        el: loader.createHookEle('app4'),//掛載本身
        // store,
        router,
        render: h => h(App)
      })
    }
  })
}
init()
複製代碼
common

common 相似插件的打包,不贅述。

import './styles/vars.scss'
import MButton from './components/button'
const components = [MButton];

const install = function (Vue) {
  if (install.installed) return;
  components.map(component => {
    Vue.use(component);
  });
};
// 全局引用可自動安裝
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}
export default {
  install,
  MButton
}
複製代碼

幾個主要源碼採用外聯形式

<link rel="stylesheet" href="/micro-frontend/common/release/components/common.css">
	<script rel="preload" src="/micro-frontend/base/vue/2.6.10/vue.min.js" as="script"></script>
	<script rel="preload" src="/micro-frontend/base/element-ui/2.7.2/lib/index.js" as="script"></script>
	<script rel="preload" src="/micro-frontend/base/vue-router/3.0.6/vue-router.min.js" as="script"></script>
	<!-- <script rel="preload" src="/micro-frontend/base/redux/4.0.1/redux.min.js" as="script"></script> -->
	<script rel="preload" src="/micro-frontend/base/vuex/2.2.1/vuex.min.js" as="script"></script>
	<script rel="preload" src="/micro-frontend/base/axios/0.15.3/axios.min.js" as="script"></script>
	<script rel="preload" src="/micro-frontend/base/jquery/3.1.0/jquery.min.js" as="script"></script>
複製代碼
Single-spa

single-spa是怎麼聚合各個獨立的vue app的呢?本坑嘗試理解它

Single-spa 把app聚合成三個週期 bootstrap mounted unmounted,這三個週期是須要本身去配置改寫的,其實single-spa還有其餘週期,不須要改寫。

也就是對single-spa來講app只有這三個東西須要特別的關心,app的卸載和加載。其餘都是app本身的事。mounted、unmonuted和vue app 獨立運行的mounted和destroy本質上沒有區別,只是single-spa作了一層代理。代理完成app的掛載和銷燬。

single-spa內部也保存了一個數組,負責維護內部註冊的app.註冊完後代理完成app的掛載。卸載後銷燬之。

總結

若是親愛的客官,你也遇到這種問題,用這種改法是沒有保證的。

本坑實踐它很大的理由也是用本身的方法初探微前端實踐方法的可行性。

這種大跨度的改變會帶來很多不可預測的底層衝突,先後端的衝突。

第二呢,這種大跨度的改變幾乎等同於重構。

第三呢,微前端方案也有自身的侷限性,好比對庫版本的管理,app樣式的隔離沒有作到很好等等。仍是要根據實際來衡量。

針對太過古老的系統好比jsp,能夠先嚐試把jsp轉爲html,在前端性能上多改進,再另行考慮綜合性改版。 若是您的項目已是結構化工程vue,angular,是能夠考慮這種改法的。

估計不少地方搞的很差,代碼或者信息錯誤,歡迎各位指導。

鳴謝

single-spa官網

微前端實踐

前端微服務化解決方案

相關文章
相關標籤/搜索