今天来聊聊webpack 5.0 模块联邦
引言
在上篇《阿里云微前端原理剖析》一文中,我们了解到:阿里云的微前端子应用和子应用、子应用和主应用之间是完全独立部署、独立打包的,主应用通过 manifest.json 这份配置项动态引入子应用构建好的js、css、图片等资源,再结合闭包、iframe容器隔离等手段,从而实现微应用的解决方案。在文末,我们也提到了 webpack5 的新特性——模块联邦。接下来,我就带大家一起了解下这个新特性,看看他究竟解决什么问题?
什么是模块联邦
先引用一下其他人的总结: 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。模块联邦是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享。 它支持直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。npm 包方式和模块联邦区别如下图:
如何使用
联邦模块有两个主要概念:Host(消费其他 Remote)和 Remote(被 Host 消费), 每个项目可以是 Host 也可以是 Remote。模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:
- name 当前应用名称,需要全局唯一,必填。
- remotes 可以将 Remote 中的 exposes 配置的模块映射到当前项目中,作为 Host 时必填。
- exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用,作为 Remote 时必填。
- shared 主要是用来避免项目出现多个公共依赖,若是配置了这个属性,webpack在加载的时候会先判断本地应用是否存在对应的包,若是不存在,则加载远程应用的依赖包。
接下来以一个简单的例子看下在项目中如何配置,参考了部分官方例子 。
Host(主应用) **webpack.config.js **部分配置:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "main_app",
remotes: {
'component-app': 'component_app@http://localhost:3001/remoteEntry.js',
},
shared: ["react", "react-dom", "react-router-dom"]
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["main"]
})
]
};
Remote(子应用)**webpack.config.js **部分配置:
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'component_app',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.jsx',
'./Dialog': './src/Dialog.jsx',
'./Logo': './src/Logo.jsx',
'./ToolTip': './src/ToolTip.jsx',
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
]
};
在Host(主应用)代码中就可以直接利用以下方式直接从Remote(子应用)调用模块:
import Button from 'component-app/Button';
import Dialog from 'component-app/Dialog';
import ToolTip from 'component-app/ToolTip';
原理
示例看完了,接下来我们简单看一下 ModuleFederationPlugin 插件的原理:
- 如果配置了 exposes,即为 Remote,开启 ContainerPlugin
- 如果配置了 remotes,即为 Host,开启 ContainerReferencePlugin
- 如果配置了 shared,即存在共享依赖,开启 SharePlugin
ContainerPlugin ContainerPlugin 在构建时,会根据 exposes 的配置,为每个 expose 创建一个 AsyncDepnedenciesBlock(Block 在 Webpack 中主要用于创建异步加载,Block 会编译成一个独立的chunk),并在这个 Block 下添加对应 expose 的 ContainerExposedDependency,这个依赖对应常规的 NormalModule,后续流程也就是递归构建这些普通模块即可。最终编译出一个 entry chunk,N 个 expose chunk。
remoteEntry.js 结构如下,同时对外暴露了get、init方法:
// 定义全局变量, 这个全局变量是在 ModuleFederationPlugin 插件中定义
var app2;
app2 = (() => {
// 这个函数的内容,与主文件类似, 定义一部分变量
var __webpack_modules__ = {
677: () => {
// ... 还有其他代码
__webpack_require__.d(exports, {
// get 方法是用于根据组件名称获取相关组件内容
get: () => get,
// init 方法是用于初始化当前分离打包组件需要依赖包的版本这部分
init: () => init
});
}
}
var __webpack_module_cache__ = {}
function __webpack_require__() {}
// 后续就不详细展开,也有定义 jsonp 的回调函数等
function webpackJsonpCallback() {}
// 与主文件不一致的地方, 给全局变量返回对应的数据
return __webpack_require__(677);
})()
在使用Remote的模块时候,通过 init 将自身 shared 写入 Remote 中,再通过 get获取 Remote 中 expose 的组件,而作为 Remote 时,判断 Host 中是否有可用的共享依赖,若有,则加载 Host 的这部分依赖;若无,则加载自身依赖。
ContainerReferencePlugin ContainerReferencePlugin 读取 remotes,并干预模块的生成流程,NormalModuleFactory 在模块生成时如果发现对应模块是 remote 格式,那么就不再生成 NormalModule(本地模块),而是生成一个 RemoteModule(远程模块)。 加载 RemoteModule(远程模块)被认为是异步操作,当使用远程模块时,这些异步操作将被放置在远程模块和入口之间的下一个 chunk 的加载操作中。如果没有 chunk 加载操作,就不能使用远程模块。 chunk 的加载操作通常是通过调用 import() 实现的,但也支持像 require.ensure 或 require([...]) 之类的旧语法。所以 RemoteModule 的加载原理和异步模块的加载几乎是一样的。
SharePlugin 当设定了 shared 时,SharePlugin 会开启 ConsumeSharePlugin 和 ProviedSharePlugin,分别用于消费和生成共享依赖,这里就不详细展开了。
最后总结一下模块联邦中模块的加载流程:
- __.f.consumes 执行,对module federation插件配置的 share 相关包进行版本注册(通常是一些公共基础包,例如 react, react-dom 等)。配置 share,可以减少重复加载基础包。
- 加载对应的 remoteEntry.js 文件,根据插件配置的全局变量,获取到 remoteEntry 暴露的数据。
- 调用 remoteEntry 暴露的 init 方法,把主应用的公共基础包与 remoteEntry 基础包的版本进行对比,根据x.y.z版本号的方式,看双方版本是否适配。如果适配,加载同一份公共基础包,否则,各自加载。
- 当应用中,有需要用到 remoteEntry 中的组件,则会调用这一步, __.f.remotes
- __.f.remotes 加载对应远端组件, 调用 remoteEntry 暴露的 get 方法,根据组件名称,获取到对应的组件,挂载到 webpack_modules 变量下,后续会被 webpack_require 方法所使用。
发展现状
webpack 5.0 正式发布是在 2020.10,距今已经过去快2年的时间了,我们来看一下模块联邦在社区的影响力如何,通过谷歌趋势搜索关键词 “webpack 5 module federation” ,趋势图如下:
再结合 github module-federation-examples 项目关注度来看,模块联邦这个新特性在社区的流行程度一般,我想应该还是受限于它的使用场景,目前主要还是运用在微应用方面。 当然在阿里内部,仍然有不少团队尝试使用它作为跨项目的模块共享解决方案。不过在qiankun 和 Alfa 等流行微应用解决方案里面还暂时没有看到它的身影。
参考文档: webpack.js.org/concepts/mo… module-federation.github.io/
转载自:https://juejin.cn/post/7119298510247165965