對比微前端方案看 JS 模塊的動態加載

微前端是2019年很火的一個話題,不少公司都分享了他們的微前端解決方案,我的以爲「微前端」這個名字仍是比較貼切的,由於它的目標主要是對標後端的「微服務」,但願前端的巨石工程也可以拆分紅小工程來更好地進行維護。筆者近期也在作微前端的工做,參考了業界的不少方案,有了本身的一些體會,但願經過這篇文章對微前端的一個核心功能——「JS 模塊的動態加載」作一些總結。html

微前端方案分類

目前正經的微前端方案主要是兩種類型:前端

  • 一種是以螞蟻金服 qiankun 爲表明的工程之間技術棧無關型。
  • 另外一種是以美團外賣爲表明的工程之間技術棧統一型。

對於技術棧無關型來講,動態加載子工程主要是將子工程的代碼跑起來便可,可能還涉及到掛載一些生命週期鉤子,而技術棧統一型的目標要大得多,是須要拿到子工程的組件代碼,將其動態嵌入到主工程內完成解析。react

下面咱們來具體看一下幾個方案的實現:

字節跳動

先來看一下字節跳動的實現,從他們文章中的介紹能夠看出來他們也應該屬於技術棧無關型,他們的方案是: 「子模塊(Modules)就是一個個的 CMD 包,我用 new Function 來包起來。」 簡單的兩句話,我猜想這是說用 fetch 或者其餘請求庫直接拿到做爲 cmd 包的子工程模塊,而後用 new Function 傳入自定義的 define 等參數,將子模塊做爲 function 的函數體來執行。可是這裏本能夠跟 requirejs 同樣全局定義好 define 等全局變量,而後用 script 標籤直接引用子工程天然加載執行,爲何要用 fetch + new Function 呢? 多是由於全局的 define 不方便在組件方法內部動態使用吧。jquery

螞蟻金服

qiankun 動態加載
再來看一下典型的技術棧無關型方案螞蟻金服 qiankun 的實現方式,相關的代碼都在它使用的 import-html-entry 倉庫中,一樣是經過 fetch 等請求庫,但拿到的是做爲 umd 包的子工程模塊內容,而後 eval 執行。eval 執行時更改了綁定的 window 對象,這樣作主要是爲了避免把子工程導出值都綁定到 window 上,而是綁定到自定義的 window.proxy 上,作自定義的隔離處理。這裏還用到了相似 Systemjs global loading 的自動尋找新增全局變量的方式來找到子模塊暴露的全局入口,這也是這個方案的一大特色,即不須要對子應用的代碼作特殊改造或者約定,只要webpack 配置成 umd 輸出便可,正如 systemjs global 原本就是給加載 jquery 等傳統庫準備的。這比較適用於子應用還須要獨立部署的情形。

qiankun 推薦的子應用 webpack 配置:webpack

const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};
複製代碼

美團外賣

接下來咱們看一下技術棧統一型的美團外賣方案中模塊的動態加載方法,他們的方案介紹中對於模塊的加載方式沒有細講,可是貼出的代碼裏能夠看到 loadAsyncSubapp 和 subappRoutes,也提到了觸發 jsonp 鉤子window.wmadSubapp,種種跡象顯示他們的方案是經過 jsonp 實現的。能夠經過 webpack 設置 libraryTarget 爲 jsonp,這樣配置的的打包產物在加載時會執行全局的 jsonp 方法,傳入主模塊的 export 值做爲參數。參考 webpack 文檔 other-targets。親測可行:git

子工程配置 webpack configgithub

output: {
  library: `registerSubApp`,
  libraryTarget: 'jsonp',
}
複製代碼

主模塊web

export default App
複製代碼

父工程json

window.registerSubApp = function (lib) {
  ReactDOM.render(
    React.createElement(lib.default),
    document.getElementById('root')
  )
}
// lib = {default: App}
複製代碼

這樣的配置使他們能夠直接拿到子工程的組件,進而能夠將組件動態整合到主工程中。能夠參考他們文章中介紹的結合 react-router 作的動態路由解析。segmentfault

Webpack 拆包和動態加載

咱們知道 webpack 的拆包和動態加載時的模塊加載也是經過 jsonp 實現的,每個拆出來的包,也叫 chunk,都是被包在一個全局 jsonp 方法中的,模塊被加載時 jsonp 方法就會被執行,這個 jsonp 方法會去註冊這個 chunk 和它所依賴的 chunk,在它所依賴的全部 chunk 都加載好以後時候會去觸發該 chunk 的入口模塊(entry module)的執行。熟悉這個過程對咱們排查生產環境中拆包產物的部署問題有很大幫助,圖解以下:

webpack chunk 加載過程

這個 jsonp 函數就是咱們常見的 chunk 頂端的 push 方法:

webpack jsonp
webpack jsonp

Webpack module federation

咱們能夠看到,因爲目前前端工程的主要打包方案是 webpack,微前端的不少動態加載方案都須要藉助 webpack 的能力,後來天然就有人想到讓 webpack 更好更方便地支持不一樣工程之間構建產物的互相加載,這就是 webpack module federation,使用方式多是:

new ModuleFederationPlugin({
    name: 'app_two',
    library: { type: 'global', name: 'app_a' },
    remotes: {
      app_one: 'app_one',
      app_three: 'app_three'
    },
    exposes: {
       AppContainer: './src/App'
    },
    shared: ['react', 'react-dom', 'relay-runtime']
})
----
import('app_one/AppContainer')
複製代碼

目前這項工做還在進行當中,能夠在這裏看到。

小結

總的來講,JS 模塊動態加載的原理主要有兩種,jsonp 方式是用動態 script 標籤直接加載並解析的,而 fetch 請求方式是拿到模塊內容,以後須要用 eval 或者 new Function 這類方法來進行解析。雖然原理並不複雜,可是你們能夠看到,爲了達到環境隔離或者直接使用輸出值等不一樣效果,具體的實現細節變化仍是不少的。

參考:

相關文章
相關標籤/搜索