微前端實踐

在 toB 的前端開發工做中,咱們每每就會遇到以下困境:css

  1. 工程愈來愈大,打包愈來愈慢
  2. 團隊人員多,產品功能複雜,代碼衝突頻繁、影響面大
  3. 心裏想作 SaaS 產品,但客戶老是要作定製化

不一樣的團隊可能有不一樣的方法去解決這些問題。在前端開發突飛猛進、前端工程化蓬勃發展的今天,我想給你們介紹下另外一種嘗試——微前端。html

微前端是什麼

那什麼是微前端?微前端主要是借鑑後端微服務的概念。簡單地說,就是將一個巨無霸(Monolith)的前端工程拆分紅一個一個的小工程。別小看這些小工程,它們也是「麻雀雖小,五臟俱全」,徹底具有獨立的開發、運行能力。整個系統就將由這些小工程協同合做,實現全部頁面的展現與交互。前端

能夠跟微服務這麼對比着去理解:vue

微服務 微前端
一個微服務就是由一組接口構成,接口地址通常是 URL。當微服務收到一個接口的請求時,會進行路由找到相應的邏輯,輸出響應內容。 一個微前端則是由一組頁面構成,頁面地址也是 URL。當微前端收到一個頁面 URL 的請求時,會進行路由找到相應的組件,渲染頁面內容。
後端微服務會有一個網關,做爲單一入口接收全部的客戶端接口請求,根據接口 URL 與服務的匹配關係,路由到對應的服務。 微前端則會有一個加載器,做爲單一入口接收全部頁面 URL 的訪問,根據頁面 URL 與微前端的匹配關係,選擇加載對應的微前端,由該微前端進行進行路由響應 URL。

這裏要注意跟 iframe 實現頁面嵌入機制的區別。微前端沒有用到 iframe,它很純粹地利用 JavaScript、MVVM 等技術來實現頁面加載。後面咱們將介紹相關的技術實現。webpack

爲何要用微前端

在介紹具體的改造方式以前,我想跟你們先說明下咱們當時面臨的問題,以及改造後的對比,以便你們以此爲對照,評判或決定使用。主要包括打包速度、頁面加載速度、多人多地協做、SaaS 產品定製化、產品拆分這幾個角度。web

首先是打包速度。在 6 個月前,咱們的 B 端工程那會兒仍是一個 Monolith。當時已經有 20 多個依賴、60 多個公共組件、200 多個頁面,對接 700 多個接口。咱們使用了 Webpack 2,並啓用 DLL Plugin、HappyPack 4。在個人我的主機上使用 4 線程編譯,大概要 5 分鐘。而若是不拆分,算下來如今咱們已經有近 400 個頁面,對接1000 多個接口。
這個時間意味着什麼?它不只會耽誤咱們開發人員的時間,還會影響整個團隊的效率。上線時,在 Docker、CI 等環境下,耗時還會被延長。若是部署後出幾個 Bug,要線上當即修復,那就不知道要熬到幾點了。
在使用微前端改造後,目前咱們已經有 26 個微前端工程,平均打包時間在 30-45 秒之間(注意,這裏尚未應用 DLL + HappyPack)。vue-router

頁面加載速度其實影響到並非很大,由於通過 CDN、gzip 後,資源的大小還能接受。這裏只是給你們看一些直觀的數據變化。6 個月前,打包生成的 app.js 有 5MB(gzip 後 1MB),vendor.js 有 2MB(gzip 後 700KB),app.css 有 1.5MB(gzip 後 250KB)。這樣首屏大概要傳輸 2MB 的內容。拆分後,目前首屏只須要傳輸 800KB 左右。bootstrap

在協做上,咱們在全國有三個地方的前端團隊,這麼多人在同一個工程裏開發,遭遇代碼衝突的機率會很頻繁,並且衝突的影響面比較大。若是代碼中出現問題,致使 CI 失敗,全部其餘人的代碼提交與更新也都會被阻塞。使用微前端後,這樣的風險就平攤到各個工程上去了。windows

再者就是定製化了。咱們作的額是一款 toB 的產品,作成 SaaS 標準版產品大概是全部從業者的願望。但總體市場環境與產品功能所限,常常要面臨一些客戶要求作本地化與定製化的要求。本地化就會有代碼安全方面的考量,最好是不給客戶源代碼,最差則是隻給客戶購買功能的源代碼。而定製化從易到難則能夠分爲獨立新模塊、改造現有模塊、替換現有模塊。
經過微前端技術,咱們能夠很容易達到本地化代碼安全的下限——只給客戶他所購買的模塊的前端源碼。定製化裏最簡單的獨立新模塊也變得簡單:交付團隊增長一個新的微前端工程便可,不須要揉進現有研發工程中,不佔用研發團隊資源。而定製化中的改造現有模塊也能夠比較好地實現:好比說某個標準版的頁面中須要增長一個面板,則能夠經過一個新的微前端工程,一樣響應該頁面的 URL(固然要控制好順序),在頁面的恰當位置插入一個新的 DOM 節點便可。後端

最後就是產品拆分方面的考量了。咱們的產品比較大,有幾塊功能比較獨立、有特點。若是說未來須要獨立成一個子產品,有微前端拆分做爲鋪墊,騰挪組合也會變得更加容易些。

其餘目標

有了以上的一些緣由與訴求,在決定進行微前端改造前,還須要設定一些額外的小目標:

  • 不能對現有的前端開發方式帶來太大變化,至少要有平滑過渡的機制。
  • 每一個爲前端工程都要求能夠獨立運行,至少在本地開發時要能作到。
  • 微前端在加載時,要實現預加載,並能夠自由調整預加載順序,甚至是根據用戶的偏好來實現智能化、個性化的加載順序。

如何改造現有工程

「Talk is cheap,show me the code「。下面就讓咱們一塊兒來看看具體的改造吧!咱們的微前端工程能夠劃分爲 portal 工程、業務工程、common 工程這幾類。

portal 工程

portal,顧名思義,就是入口。這也就是上面所說的微前端加載器。當用戶打開瀏覽器,首次進入咱們的頁面時,不論是什麼 URL,首先加載的就是 portal。portal 裏會配置全部業務工程的地址、匹配哪些 URL、須要加載哪些資源。如:

// 業務工程的名稱
customer: {
    // URL 匹配模式
    matchUrlHash: ['^/customer'],
    // 微前端地址
    target: 'http://localhost:8101/mfe-customer/index.html',
    // 資源匹配模式
    resourcePatterns: ['/app.*.css$', '/vendor.*.css$', '/manifest.*.js$', '/vendor.*.js$', '/app.*.js$'],
}
複製代碼

portal 會定時、異步、併發地下載業務工程的資源,並將它們進行註冊,此時並不會加載這些業務工程。這裏之因此要業務工程的地址(target)、資源(resourcePatterns),是爲了加載時肯定地知道其所包含的 app.js、vendor.js、app.css 等資源的路徑。由於業務工程每次有變動,app.js 等資源路徑上都會帶有新的文件內容哈希值(Hash),致使路徑不可預測。而它的 index.html 的路徑是固定的。咱們讀取該 HTML,解析其內容,經過正則就能匹配到 app.js 等資源的路徑。

portal 在運行時,會監聽 URL 變化。目前咱們只支持 URL Hash(如 #/customer)。當 Hash 發生變動時,匹配到業務工程,而後執行卸載、加載的工做。這個機制主要是利用 single-spa 來實現,但原理就是這麼簡單。

import { registerApplication } from 'single-spa';
registerApplication('customer', 
    // 下載微前端工程,獲取三個函數鉤子:bootstrap、mount、unmount
    () => {
        const html = fetch(mfeConfig.target);
        const {cssUrls, jsUrls} = match(html, mfeConfig.resourcePatterns);
        loadCss(cssUrls);
        loadJs(jsUrls);
        return windows['mfe:customer'];
    },
    // 對當前瀏覽器 URL Hash 進行匹配,若是匹配(返回 true),則加載該微前端(調用 mount);不然卸載(調用 unmount)
    () => {
        return match(window.location.hash, mfeConfig.matchUrlHash);
    },
    mfeConfig.customProps
);
複製代碼

業務工程

業務工程就是普通的微前端工程,通常一個模塊一個工程。業務工程要扮演兩個角色,一個是可獨立運行的前端工程,一個是受 portal 控制的運行時。前者主要用於咱們本地開發,後者則是線上集成時使用。在獨立運行時,它跟原來的前端工程沒有什麼區別。以 Vue 工程爲例,照樣使用 new Vue({el: '#app'}) 來啓動、渲染頁面。

new Vue({
    el: '#app',
    i18n,
    router,
    store,
    template: '<App/>',
    components: { App }
});
複製代碼

而當受控運行時,則是利用 UMD 方式輸出幾個鉤子函數,包括初始化、加載、卸載。

if(!window.IS_IN_MFE){ // 獨立運行時
    new Vue({...})
} else { // 受控運行時
    module.exports = {
        bootstrap(){ // 註冊時執行
        },
        mount(customProps){ // 加載時執行
            return Promise.resolve().then(()=>{
            instance = new Vue({...})
            })
        },
        unmount(){ // 卸載時執行
            return Promise.resolve().then(()=>{
            instance.$destroy()
            })
        }
    }
}
複製代碼

線上環境的 Webpack 配置:

output: {
    libraryTarget: "umd",
    library: 'mfe:customer'
}
複製代碼

而區分是否受控,則能夠經過判斷一個全局變量來實現。如 window.IS_IN_MFE,portal 工程在運行時會將其設置爲 true

爲了支持本地多個工程同時開發,咱們須要爲每一個微前端工程指定一個肯定的、獨佔的端口號。好比從 8100 開始,逐一遞增。同時,爲了支持線上部署,咱們還須要給每一個微前端工程指定一個肯定的、獨佔的基礎路徑(前綴)。這樣相同域名下能夠用不一樣路徑進行獨立訪問。路徑統一以 /mfe- 開頭,如 /mfe-customer。這也就是上面 portal 裏業務工程的配置示例裏所展示的那樣。

特殊業務工程:mfe-navs

咱們產品的頁面結構分爲頂部欄、側邊欄、中間內容區三大塊。頂部欄和側邊欄在頁面跳轉過程當中,基本上保持不變。因此咱們也將它們剝離出來做爲一個獨立的微前端業務工程,叫作 mfe-navs。它會匹配全部的 URL,也就是說訪問任意 URL 時,都會加載它,並且還要保證先加載它。當它加載完畢後,會在頁面內提供一箇中間內容區的錨點 DOM(#app:),供其餘業務工程加載時掛載。

Common 工程

上面能夠看到,每個業務工程都是一個獨立的前端工程,因此裏面會有一些相同的依賴,如 Vue、moment、lodash 等。若是將這些內容都打包到各自的 vendor.js 裏,則勢必會致使代碼冗餘太多,瀏覽器運行內存壓力增大。咱們把這些公共依賴、公共組件、CSS、Fonts 等都放到一個工程裏,由該工程進行打包,將依賴、組件 export,並以 UMD 的方式注入到全局。

main.js:

import Vue from 'vue'; // 公共依賴
import VueRouter from 'vue-router';
import VueI18n from 'vue-i18n';
import '@/css/icon-font/iconfont.css';
import ContentSelector from '@/components/ContentSelector'; // 公共組件

Vue.use(VueI18n); // 你們都要這麼作,咱們就代勞吧!

module.exports = {
    'vue': Vue,
    'vue-router': VueRouter,
    'content-selector': ContentSelector,
};
複製代碼

Webpack 配置:

output: {
    libraryTarget: "umd",
    library: 'mfe:common'
}
複製代碼

業務工程則經過 Webpack 外部依賴(external)的方式引入到工程中。這樣業務工程打包時就不會包含這些公共代碼了。

var externalModules = ['vue', 'vue-router', 'content-selector'];

module.exports = { // webpack 配置項
    // ...
    externals: (context, request, callback)=>{
        if(externalModules.includes(request)){
            callback(null, 'root window["mfe:common"]["'+request+'"]')
        } else {
            callback();
        }
    },
}
複製代碼

結語

以上就是咱們微前端改造與實踐方面的一些經驗。前路漫漫,這裏面還存在不少待完善的地方,如 History 模式支持、i18n 更好地集成、各個業務工程的加載順序優化及個性化等。除了這些純粹技術上的探索,在擁有微前端、微服務這些架構的基礎上,團隊也能夠考慮進行垂直拆分:一個小組獨立負責一塊業務,它有本身的微前端工程和微服務工程。從技術管理到人員管理,將它們糅合在一塊兒統一考慮,這也是咱們軟件工程的探索方向。期待這些可以對你們帶來一些思考和幫助!

相關文章
相關標籤/搜索