一、引言
自webpack5发布以来,推出了很多全新的特性,其中最主要的就如下三点: ①、可持续性缓存--通过cache配置可实现首次构建后一直保存缓存。 ②、真正意义上的tree-shaking--让你的打包体积更小,去掉无用的代码。 ③、模块联邦(module federation)--本文需要探讨实战的特性;
二、模块联邦(Module Federation)
我们知道webpack可以通过dll实现对同一个项目的公共组件模块,做成代码共享common chunk,但是如果是要实现跨项目针对不同的应用就变得非常的困难不易实现。几乎没办法做到不同应用之间进行插拔式的热更新。那怎么样去实现这种跨应用间的共用模块运用呢?于是乎webpack5内置了一个模块联邦的功能特性,这个功能可以让跨应用间做到模块共享真正的插拔式的便捷使用。比如a应用如果想使用b应用中list的组件,通过模块联邦可以直接在a中进行import('b/list')非常的方便。
三、模块联邦的使用
模块联邦是webpack的内置模块,使用起来也是相当的简单,做好相关配置就可以了,首先要保障项目webpack是5.0及以上。然后在对应的项目的webpack.config.js进行配置,ModuleFederationPlugin有几个重要的参数:
1、name: 当前应用的名称,需要唯一性; 2、exposes: 需要导出的模块,用于提供给外部其他项目进行使用; 3、remotes: 需要依赖的远程模块,用于引入外部其他模块; 4、filename: 入口文件名称,用于对外提供模块时候的入口文件名; 5、shared: 配置共享的组件,一般是对第三方库做共享使用;
四、业务场景
假设公司有个业务集群,公共业务组件库升级了,希望能够尽可能少地影响业务线,仅仅在基础组件库版本升级即可全业务线升级,那么可以考虑使用模块联邦来实现。
他和利用 npm 发包来实现的方案的区别在于,npm 发布的组件库从 1.0.1 升级到 1.0.2 的时候,必须要把业务线项目重新构建,打包,发布才能使用到最新的特性,而模块联邦可以实现实时动态更新而无需打包业务线项目。
大致的原型图如下:
我们看到,project1 的 home 页的 specialItem,project1 的 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 项目下执行 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 页面。
五.联邦模块的原理分析
联邦模块有两个主要概念:Host(消费其他 Remote)和 Remote(被 Host 消费)。每个项目可以是 Host 也可以是 Remote,也可以两个都是。可以通过 webpack 配置来区分,可以参考例子(github.com/module-fede…
- 作为 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 单点登录,页面跳转,埋点,异常捕获等都可以考虑抽象封装成系统内置的方法到里面。 总结 通过这篇文章,我们收获了
- 模块联邦的基础概念。
- 模块联邦常用的配置项。
- 通过简易配置实现雏形项目开发。
- 模块联邦的基本原理。