模塊聯(lián)邦淺析

本文首發(fā)于政采云前端團(tuán)隊(duì)博客:模塊聯(lián)邦淺析

https://www.zoo.team/article/webpack-modular

引言
作為前端打包工具的重要工具人--webpack,相信大家在項(xiàng)目中并不陌生。前段時(shí)間 webpack5 新出了個(gè)特性: 模塊聯(lián)邦。大家可能雖然聽(tīng)說(shuō)過(guò),但還沒(méi)在項(xiàng)目中使用,今天就帶大家通過(guò)一個(gè)小實(shí)戰(zhàn)來(lái)熟悉一下它的用法。

業(yè)務(wù)場(chǎng)景
假設(shè)公司有個(gè)業(yè)務(wù)集群,公共業(yè)務(wù)組件庫(kù)升級(jí)了,希望能夠盡可能少地影響業(yè)務(wù)線,僅僅在基礎(chǔ)組件庫(kù)版本升級(jí)即可全業(yè)務(wù)線升級(jí),那么可以考慮使用模塊聯(lián)邦來(lái)實(shí)現(xiàn)。

他和利用 npm 發(fā)包來(lái)實(shí)現(xiàn)的方案的區(qū)別在于,npm 發(fā)布的組件庫(kù)從 1.0.1 升級(jí)到 1.0.2 的時(shí)候,必須要把業(yè)務(wù)線項(xiàng)目重新構(gòu)建,打包,發(fā)布才能使用到最新的特性,而模塊聯(lián)邦可以實(shí)現(xiàn)實(shí)時(shí)動(dòng)態(tài)更新而無(wú)需打包業(yè)務(wù)線項(xiàng)目。

大致的原型圖如下:



我們看到,project1 的 home 頁(yè)的 specialItem,project2 的 about 頁(yè)的 searchItem 組件被用于 project2 的 home 中, project2 的 about 直接用的 project1 的 about 頁(yè)。

總體上的源代碼來(lái)自于模塊聯(lián)邦的示例代碼(https://github.com/module-federation/module-federation-examples/tree/master/vue3-cli-demo),稍作改動(dòng)。

以下只列出改動(dòng)的關(guān)鍵部分目錄結(jié)構(gòu),冗余文件已省略。戳我(https://github.com/AshesOfHistory/vue3-cli-module-federation-demo)查看本項(xiàng)目代碼示例地址。

├── README.md
├── app-exposes
│   ├── babel.config.js
│   ├── src
│   │   ├── App.vue
│   │   ├── assets
│   │   ├── components
│   │   │   ├── SearchItem.vue  ---搜索組件
│   │   │   └── SpecialItem.vue  ---自定義業(yè)務(wù)組件
│   │   ├── index.ts
│   │   ├── main.ts
│   │   ├── router
│   │   │   └── index.ts
│   │   └── views
│   │       ├── AboutView.vue   ---關(guān)于頁(yè)
│   │       └── HomeView.vue  ---首頁(yè)
│   ├── tsconfig.json
│   └── vue.config.js
├── app-general
│   ├── babel.config.js
│   ├── src
│   │   ├── router
│   │   │   └── index.ts
│   │   └── views
│   │       └── HomeView.vue
│   ├── tsconfig.json
│   └── vue.config.js
利用腳手架分別創(chuàng)建 app-exposes 與 app-general 的 vue3 項(xiàng)目,此部分大家應(yīng)該都輕車(chē)熟路在此就略過(guò)了。嫌麻煩的可以直接用我提供的 demo 樣本。

首先克隆本項(xiàng)目代碼地址后,分別在 app-exposes 與 app-general 項(xiàng)目下執(zhí)行 npm i 安裝依賴,然后分別執(zhí)行 npm run serve 運(yùn)行代碼。此時(shí)能夠看到本地起了兩個(gè)服務(wù),端口號(hào)分別為 8083 與 8081,其中 app-exposes 為 8083,app-general 為 8081。

項(xiàng)目運(yùn)行示意效果圖如下



然后我們看看兩個(gè)項(xiàng)目的配置文件如何配置的。

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



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



可以看到,總體上我們用到了 webpack 原生的插件 ModuleFederationPlugin 來(lái)實(shí)現(xiàn)模塊聯(lián)邦的效果的。

在首頁(yè)中,我們異步引用的 app-exposes 提供的 SearchItem 以及 SpecialItem 組件。








在 about 頁(yè)面的路由配置中,我們直接引入的遠(yuǎn)程連接的 AboutView 頁(yè)面。



如果想查看更多關(guān)于聯(lián)邦模塊的案例,可以訪問(wèn)官方倉(cāng)庫(kù)(https://github.com/module-federation/module-federation-examples)。

二.聯(lián)邦模塊插件的結(jié)構(gòu)及其常見(jiàn)的調(diào)用方式(Module Federation Plugin)
上面我們大概了解了下模塊聯(lián)邦插件的大致使用方法。不過(guò)知其然也要知其所以然,所以我接下來(lái)從個(gè)人角度簡(jiǎn)單聊一聊他的實(shí)現(xiàn)原理。

webpack 的整體流程上來(lái)說(shuō)大體分為三個(gè)主要階段

初始化階段
構(gòu)建階段
生成階段
在這三大階段時(shí)擁有極其龐大的插件庫(kù)在各個(gè)階段以及節(jié)點(diǎn)中發(fā)揮各自的作用,而模塊聯(lián)邦插件就是其中之一。

模塊聯(lián)邦作為一個(gè) webpack5 時(shí)期新出的插件,形態(tài)上看通常是一個(gè)帶有 apply 方法的類(lèi)。

class ModuleFederationPlugin {
  apply(compiler) {}
}
參數(shù) compiler 是 webpack 上下文,可以調(diào)用 hook 對(duì)象注冊(cè)各種鉤子回調(diào)。

如下文中的 compiler.hooks.thisCompilation.tap,表明調(diào)用 afterPlugins 這個(gè)鉤子的 tap 方法,傳入插件名稱與回調(diào)函數(shù),執(zhí)行我們指定的邏輯,webpack 通過(guò)這種方式來(lái)構(gòu)建其龐大繁雜的插件體系。

class ModuleFederationPlugin {
  apply(compiler) {
    compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
  ...
    }
  }
}
鉤子的核心邏輯定義在 Tapable(https://github.com/webpack/tapable) 倉(cāng)庫(kù),內(nèi)部定義了如下類(lèi)型的鉤子。

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
 } = require("tapable");
三.聯(lián)邦模塊的原理分析
聯(lián)邦模塊有兩個(gè)主要概念:Host(消費(fèi)其他 Remote)和 Remote(被 Host 消費(fèi))。每個(gè)項(xiàng)目可以是 Host 也可以是 Remote,也可以兩個(gè)都是??梢酝ㄟ^(guò) webpack 配置來(lái)區(qū)分,可以參考例子(https://github.com/module-federation/module-federation-examples/tree/master/bi-directional)。

作為 Host 需要配置 remote 列表和 shared 模塊。
作為 Remote 需要配置項(xiàng)目名(name),打包方式(library),打包后的文件名(filename),提供的模塊(exposes),和 Host 共享的模塊(shared)。
webpack 打包原理
webpack4 對(duì)于異步模塊加載步驟

import(chunkId) => webpack_require.e(chunkId) 將相關(guān)的請(qǐng)求回調(diào)存入 installedChunks。
發(fā)起 JSONP 請(qǐng)求。
將下載的模塊錄入 modules。
執(zhí)行 chunk 請(qǐng)求回調(diào)。
加載 module。
執(zhí)行用戶回調(diào)。
聯(lián)邦模塊是基于 webpack 做的優(yōu)化,所以在深入聯(lián)邦模塊之前我們首先得知道 webpack 是怎么做的打包工作。webpack 每次打包都會(huì)將資源全部包裹在一個(gè)立即執(zhí)行函數(shù)里面,這樣雖然避免了全局環(huán)境的污染,但也使得外部不能訪問(wèn)內(nèi)部模塊。在這個(gè)立即執(zhí)行函數(shù)里面,webpack 使用 webpack_modules 對(duì)象保存所有的模塊代碼,然后用內(nèi)部定義的 webpack_require 方法從 webpack_modules 中加載模塊。并且在異步加載和文件拆分兩種情況下向全局暴露一個(gè) webpackChunk 數(shù)組用于溝通多個(gè) webpack 資源,這個(gè)數(shù)組通過(guò)被 webpack 重寫(xiě) push 方法,會(huì)在其他資源向 webpackChunk 數(shù)組中新增內(nèi)容時(shí)同步添加到 webpack_modules 中從而實(shí)現(xiàn)模塊整合。

聯(lián)邦模塊就是基于這個(gè)機(jī)制,修改了 webpack_require 的部分實(shí)現(xiàn),在 require 的時(shí)候從遠(yuǎn)程加載資源,緩存到全局對(duì)象 window["webpackChunk"+appName] 中,然后合并到 webpack_modules 中。

ModuleFederationPlugin 的原理
源碼中 ModuleFederationPlugin 主流程 主要做了三件事:

通過(guò)參數(shù)是否配置 shared 來(lái)判斷是否使用共享依賴 SharePlugin 模塊。
通過(guò)參數(shù)是否配置 exposes 來(lái)判斷是否使用公開(kāi) ContainerPlugin 模塊。
通過(guò)參數(shù)是否配置 remotes 來(lái)判斷是否使用 ContainerReferencePlugin 引用模塊。
下面是項(xiàng)目源碼,部分代碼以及判斷條件已省略。

// 源碼目錄 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 模塊聯(lián)邦對(duì)異步模塊加載的處理

下載并執(zhí)行 remoteEntry.js,掛載入口點(diǎn)對(duì)象到 window.app-exposes,他有兩個(gè)函數(shù)屬性,init 和 get。init 方法用于初始化作用域?qū)ο?initScope,get 方法用于下載 moduleMap 中導(dǎo)出的遠(yuǎn)程模塊。
加載 app-exposes 到本地模塊。
創(chuàng)建 app-exposes.init 的執(zhí)行環(huán)境,收集依賴到共享作用域?qū)ο?shareScope。
執(zhí)行 app-exposes.init,初始化 initScope。
用戶 import 遠(yuǎn)程模塊時(shí)調(diào)用 app-exposes.get(moduleName) 通過(guò) Jsonp 懶加載遠(yuǎn)程模塊,然后緩存在全局對(duì)象 window['webpackChunk' + appName]。
通過(guò) webpack_require 讀取緩存中的模塊,執(zhí)行用戶回調(diào)。
四.使用場(chǎng)景
目前模塊聯(lián)邦已經(jīng)在微前端領(lǐng)域發(fā)揮了巨大的作用,也起到 webpack 能夠越來(lái)越強(qiáng)大。

利用模塊聯(lián)邦強(qiáng)大的跨應(yīng)用級(jí)模塊共享能力,我們可以搭建一個(gè)非業(yè)務(wù)的中臺(tái)搭建系統(tǒng),實(shí)現(xiàn) app 級(jí)別的低代碼搭建平臺(tái),這與市場(chǎng)上常見(jiàn)頁(yè)面級(jí)低代碼搭建不同,能夠?qū)崿F(xiàn)系統(tǒng)級(jí)能力復(fù)用的同時(shí)降低維護(hù)成本。后續(xù)比如說(shuō) sso 單點(diǎn)登錄,頁(yè)面跳轉(zhuǎn),埋點(diǎn),異常捕獲等都可以考慮抽象封裝成系統(tǒng)內(nèi)置的方法到里面。

總結(jié) 通過(guò)這篇文章,我們收獲了

模塊聯(lián)邦的基礎(chǔ)概念。
模塊聯(lián)邦常用的配置項(xiàng)。
通過(guò)簡(jiǎn)易配置實(shí)現(xiàn)雛形項(xiàng)目開(kāi)發(fā)。
模塊聯(lián)邦的基本原理。
參考文章
webpack 5 官方文檔(https://webpack.docschina.org/concepts/module-federation/)
Webpack5 跨應(yīng)用代碼共享 - Module Federation(https://segmentfault.com/a/1190000024449390)
嘗試 webpack5 Module Federation(https://zhuanlan.zhihu.com/p/141390589)
探索 webpack5 新特性 Module federation 引發(fā)的 javascript 共享模塊變革(https://blog.csdn.net/yingyangxing/article/details/109653116)
三大應(yīng)用場(chǎng)景調(diào)研,Webpack 新功能 Module Federation 深入解析(https://developer.aliyun.com/article/755252)
利用聯(lián)邦模塊實(shí)現(xiàn)跨應(yīng)用的代碼共享(https://juejin.cn/post/6961678963680739359)
[萬(wàn)字總結(jié)] 一文吃透 Webpack 核心原理 (https://xie.infoq.cn/article/ddca4caa394241447fa0aa3c0)
Webpack 源碼解讀:理清編譯主流程(https://juejin.cn/post/6844903987129352206)

作者:滄瀾


歡迎關(guān)注微信公眾號(hào) :政采云前端團(tuán)隊(duì)