模組聯邦淺析

語言: CN / TW / HK

引言

作為前端打包工具的重要工具人--webpack,相信大家在專案中並不陌生。 前段時間 webpack5 新出了個特性: 模組聯邦。大家可能雖然聽說過,但還沒在專案中使用,今天就帶大家通過一個小實戰來熟悉一下它的用法。

業務場景

假設公司有個業務叢集,公共業務元件庫升級了,希望能夠儘可能少得影響業務線,僅僅在基礎元件庫版本升級即可全業務線升級,那麼可以考慮使用模組聯邦來實現。

他和利用 npm 發包來實現的方案的區別在於,npm 釋出的元件庫從 1.0.1 升級到 1.0.2 的時候,必須要把業務線專案重新構建,打包,釋出才能使用到最新的特性,而模組聯邦可以實現實時動態更新而無需打包業務線專案。

大致的原型圖如下:

我們看到,project1 的 home 頁的 specialItem,project2 的 about 頁的 searchItem 元件被用於 project2 的 home 中, project2 的 about 直接用的 project1 的 about 頁。

總體上的原始碼來自於模組聯邦的 示例程式碼 ,稍作改動。

以下只列出改動的關鍵部分目錄結構,冗餘檔案已省略。 戳我 檢視本專案程式碼示例地址。

├── README.md
├── app-exposes
│   ├── babel.config.js
│   ├── src
│   │   ├── App.vue
│   │   ├── assets
│   │   ├── components
│   │   │   ├── SearchItem.vue  ---搜尋元件
│   │   │   └── SpecialItem.vue  ---自定義業務元件
│   │   ├── index.ts
│   │   ├── main.ts
│   │   ├── router
│   │   │   └── index.ts
│   │   └── views
│   │       ├── AboutView.vue   ---關於頁
│   │       └── HomeView.vue  ---首頁
│   ├── tsconfig.json
│   └── vue.config.js
├── app-general
│   ├── babel.config.js
│   ├── src
│   │   ├── router
│   │   │   └── index.ts
│   │   └── views
│   │       └── HomeView.vue
│   ├── tsconfig.json
│   └── vue.config.js

利用腳手架分別建立 app-exposes 與 app-general 的 vue3 專案,此部分大家應該都輕車熟路在此就略過了。嫌麻煩的可以直接用我提供的 demo 樣本。

首先克隆本專案程式碼地址後,分別在 app-exposes 與 app-general 專案下執行 npm i 安裝依賴,然後分別執行 npm run serve 執行程式碼。 此時能夠看到本地起了兩個服務,埠號分別為 8083 與 8081,其中 app-exposes 為 8083,app-general 為 8081。

專案執行示意效果圖如下

然後我們看看兩個專案的配置檔案如何配置的。

app-exposes 的 vue.config.js 配置:

app-general 的 vue.config.js 配置:

可以看到,總體上我們用到了 webpack 原生的外掛 ModuleFederationPlugin 來實現模組聯邦的效果的。

在首頁中,我們非同步引用的 app-exposes 提供的 SearchItem 以及 SpecialItem 元件。

在 about 頁面的路由配置中,我們直接引入的遠端連線的 AboutView 頁面。

如果想檢視更多關於 聯邦模組 的案例,可以訪問 官方倉庫

二.聯邦模組外掛的結構及其常見的呼叫方式(Module Federation Plugin)

上面我們大概瞭解了下模組聯邦外掛的大致使用方法。不過知其然也要知其所以然,所以我接下來從個人角度簡單聊一聊他的實現原理。

webpack 的整體流程上來說大體分為三個主要階段

  • 初始化階段
  • 構建階段
  • 生成階段

在這三大階段時擁有極其龐大的外掛庫在各個階段以及節點中發揮各自的作用,而模組聯邦外掛就是其中之一。

模組聯邦作為一個 webpack5 時期新出的外掛,形態上看通常是一個帶有 apply 方法的類。

class ModuleFederationPlugin {
  apply(compiler) {}
}

引數 compiler 是 webpack 上下文,可以呼叫 hook 物件註冊各種鉤子回撥。

如下文中的 compiler.hooks.thisCompilation.tap,表明呼叫 afterPlugins 這個鉤子的 tap 方法,傳入外掛名稱與回撥函式,執行我們指定的邏輯,webpack 通過這種方式來構建其龐大繁雜的外掛體系。

class ModuleFederationPlugin {
    apply(compiler) {
      compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
        ...
      }
    }
}

鉤子的核心邏輯定義在 Tapable 倉庫,內部定義瞭如下型別的鉤子。

const {
        SyncHook,
        SyncBailHook,
        SyncWaterfallHook,
        SyncLoopHook,
        AsyncParallelHook,
        AsyncParallelBailHook,
        AsyncSeriesHook,
        AsyncSeriesBailHook,
        AsyncSeriesWaterfallHook
 } = require("tapable");

三.聯邦模組的原理分析

聯邦模組有兩個主要概念: Host (消費其他 Remote)和 Remote (被 Host 消費)。 每個專案可以是 Host 也可以是 Remote,也可以兩個都是。可以通過 webpack 配置來區分,可以參考 例子

  • 作為 Host 需要配置 remote 列表和 shared 模組。
  • 作為 Remote 需要配置專案名(name),打包方式(library),打包後的檔名(filename),提供的模組(exposes),和 Host 共享的模組(shared)。

webpack 打包原理

webpack4 對於非同步模組載入步驟

  • import(chunkId) => webpack_require .e(chunkId) 將相關的請求回撥存入 installedChunks。
  • 發起 JSONP 請求。
  • 將下載的模組錄入 modules。
  • 執行 chunk 請求回撥。
  • 載入 module。
  • 執行使用者回撥。

聯邦模組是基於 webpack 做的優化,所以在深入聯邦模組之前我們首先得知道 webpack 是怎麼做的打包工作。 webpack 每次打包都會將資源全部包裹在一個 立即執行函式 裡面,這樣雖然避免了全域性環境的汙染,但也使得外部不能訪問內部模組。 在這個立即執行函式裡面,webpack 使用 webpack_modules 物件儲存所有的模組程式碼,然後用內部定義的 webpack_require 方法從 webpack_modules 中載入模組。並且在非同步載入和檔案拆分兩種情況下向全域性暴露一個 webpackChunk 陣列用於溝通多個 webpack 資源,這個陣列通過被 webpack 重寫 push 方法,會在其他資源向 webpackChunk 陣列中新增內容時同步新增到 webpack_modules 中從而實現模組整合。

聯邦模組就是基於這個機制,修改了 webpack_require 的部分實現,在 require 的時候從遠端載入資源, 快取到全域性物件 window["webpackChunk"+appName] 中,然後合併到 webpack_modules 中。

ModuleFederationPlugin 的原理

原始碼中 ModuleFederationPlugin 主流程 主要做了三件事:

  • 通過引數是否配置 shared 來判斷是否使用共享依賴 SharePlugin 模組。
  • 通過引數是否配置 exposes 來判斷是否使用公開 ContainerPlugin 模組。
  • 通過引數是否配置 remotes 來判斷是否使用 ContainerReferencePlugin 引用模組。

下面是專案原始碼,部分程式碼以及判斷條件已省略。

// 原始碼目錄lib/container/ModuleFederationPlugin
class ModuleFederationPlugin {
  ...
    apply(compiler) {
        if (library && ...) {
            compiler.options.output.enabledLibraryTypes.push(library.type);
        }
        compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
            if (options.exposes && ...) {
                new ContainerPlugin({
                    ...
                }).apply(compiler);
            }
            if (options.remotes && ...) {
                new ContainerReferencePlugin({
                    remoteType,
                    remotes: options.remotes
                }).apply(compiler);
            }
            if (options.shared) {
                new SharePlugin({
                    shared: options.shared,
                    shareScope: options.shareScope
                }).apply(compiler);
            }
        });
    }
}

module.exports = ModuleFederationPlugin;

webpack5 模組聯邦對非同步模組載入的處理

  • 下載並執行 remoteEntry.js,掛載入口點物件到 window.app-exposes,他有兩個函式屬性,init 和 get。init 方法用於初始化作用域物件 initScope,get 方法用於下載 moduleMap 中匯出的遠端模組。
  • 載入 app-exposes 到本地模組。
  • 建立 app-exposes.init 的執行環境,收集依賴到共享作用域物件 shareScope。
  • 執行 app-exposes.init,初始化 initScope。
  • 使用者 import 遠端模組時呼叫 app-exposes.get(moduleName) 通過 Jsonp 懶載入遠端模組,然後快取在全域性物件 window['webpackChunk' + appName]。
  • 通過 webpack_require 讀取快取中的模組,執行使用者回撥。

四.使用場景

目前模組聯邦已經在微前端領域發揮了巨大的作用,也起到 webpack 能夠越來越強大。

利用模組聯邦強大的跨應用級模組共享能力,我們可以搭建一個非業務的中臺搭建系統,實現 app 級別的低程式碼搭建平臺,這與市場上常見頁面級低程式碼搭建不同,能夠實現系統級能力複用的同時降低維護成本。後續比如說 sso 單點登入,頁面跳轉,埋點,異常捕獲等都可以考慮抽象封裝成系統內建的方法到裡面。

總結通過這篇文章,我們收穫了

  • 模組聯邦的基礎概念。
  • 模組聯邦常用的配置項。
  • 通過簡易配置實現雛形專案開發。
  • 模組聯邦的基本原理。