公共模块之模块联邦
目录
前言
工作中公共模块通过子仓库在多个项目中使用,其中公共头部,登录,反馈、举报等模块业务与技术栈都和项目耦合很深,在每个项目都会将这些公共模块打包进去,为了减少流量成本,考虑将这些模块打包后放到cdn,对比了webpack中external、 dll、模块联邦方案,最终选择模块联邦。
external 方案 :external的场景是将三方模块当做外链引入, 依赖在公共模块和项目中无法共享;当然如果将依赖都通过external加载,可以解决共享问题,多版本同时存在的问题又无法解决。并且无法做到按需加载。
dll 方案:dll 使用场景是公共模块的处理,问题和external方案差不多
模块联邦概念
Container 容器
ModuleFederationPlugin 处理后打包出来的模块被称为 Container,可以加载其他的 Container,可以被其他的 Container 加载;
Host 宿主容器
消费其它容器的容器可以称为Host,一般动态去加载remote容器。
Remote 远程容器
被其他容器消费的容器可以称为Remote; 主要是导出模块供其他模块消费
因为Host和Remote都是Container,所以两者是相对的,一个Container既可以是Host,也可以是Remote;
Shared 共享
本地和远程可以共享的依赖
使用配置
Remote和Host使用同一个插件,在配置上有些许不同;
Remote例子:
new ModuleFederationPlugin({
name: 'vue2App',
filename: 'remoteEntry.js',
library: { type: 'var', name: 'vue2App' },
exposes: {
'./vue2': './node_modules/vue/dist/vue',
'./Button': './src/components/Button',
},
shared: {
lodash: {
strictVersion: true,
requiredVersion: '^3',
}
}
}),
name字段是用来配置模块容器的名字,当用作被消费的容器的时候(remote) name字段是必需的。
filename 打包后产物的文件名,在host中配置的remotes字段中的文件链接就是这个文件。
exposes 指定需要导出的模块
library 指定打包产物的模块类型,在浏览器中使用一般配置为 { type: 'var', name: '自定义的全局变量' }
shared 指定需要共享的依赖模块,如果两个应用都使用了相同的依赖,则可以使用 shared 来共享依赖,减少资源加载量;支持使用semver规范配置兼容。
shared: [
"lodash/",
{
react: {
import: false,
singleton: true
}
}
]
其中的requiredVersion使用Remote容器中配置的。 Host端可以配置version表示版本。
remotes 指定使用远程模块的别名和地址,地址的格式是固定的,@
前面的名字(比如:vue2App) 需要和模块的library.name保持一致。Host端使用
new ModuleFederationPlugin({
name: "main_app",
remotes: {
vue2App: 'vue2App@http://localhost:3001/remoteEntry.js'
},
shared: ['vue']
})
模块联邦优点
-
配置简单灵活
-
支持独立部署,上线
-
依赖共享机制;
-
容器可以嵌套使用,容器可以使用来自其他容器的模块。容器之间也可以循环依赖。
-
可以同步和异步方式都可以使用
虽然vue2App/Button来自于remote,但是可以用同步的方式使用。
import Vue2Button from 'vue2App/Button'; console.log(Vue2Button)
同样可以异步使用
import('vue2App/Button').then(()=>{})
模块联邦缺点
- 模块联邦对
runtime
运行时做了大量改造,会对我们页面的运行时性能造成一定的负面影响 - 需要额外处理远程模块的版本管理
动态远程模块
联邦模块的remote地址是一个固定的地址,实际开发是需要区分开发、测试、灰度、生产环境的,甚至需要区分版本,这时候怎么能让这个地址成为动态呢?
-
方案一, 通过插件将remotes地址将指定格式的表示式解析为环境依赖的变量,比如下面的
[window.xinyu_version_prod]
在页面执行的时候会取当前页面的window.xinyu_version_prod变量,所以如果能在页面脚本执行前把变量注入,就能实现动态加载远程模块。官方的demo中一个动态remote地址的例子,有人整理成了一个webpack插件
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin'); const DynamicAddress = process.env.NODE_ENV === 'develop' ? 'https://xxxxx/[window.xinyu_version_test]' : 'https://xxxxx/[window.xinyu_version_prod]'; plugins:[ new ModuleFederationPlugin({ name: 'detail2022', remotes: { xinyu: `xinyu@${DynamicAddress}/remoteEntry.js`, }, }), new ExternalTemplateRemotesPlugin(), ]
-
方案二 不通过remotes配置远程模块,而是手动获取远程模块,官方文档链接, github例子链接
通过手动加载remoteEntry.js之后,再通过webpack提供的方法获取模块,这样就可以在代码中动态加载。下面是一个例子
function loadComponent(scope, module) { return async () => { // Initializes the share scope. This fills it with known provided modules from this build and all remotes await __webpack_init_sharing__('default'); const container = window[scope]; // or get the container somewhere else // Initialize the container, it may provide shared modules await container.init(__webpack_share_scopes__.default); const factory = await window[scope].get(module); const Module = factory(); return Module; }; } function loadScript(url){ return new Promise((resolve,reject)=>{ const element = document.createElement('script'); element.src = url; element.type = 'text/javascript'; element.async = true; element.onload = () => { resolve() }; document.body.appendChild(element); }) } components: { Vue2Button: defineAsyncComponent(() => { return new Promise(async(resolve) => { await loadScript('http://localhost:3001/remoteEntry.js'); // 根据环境使用不同的地址,以做到动态获取模块 const Button = await loadComponent('xinyu', './Button')(); const vue2 = await loadComponent('xinyu', './vue2')(); window.Vue2 = vue2; resolve(vue2ToVue3(Button.default, 'vue2Button')) }) }) },
缺点:不能利用webpack对异步的同步处理,代码繁琐。
import add from 'xinyu/add'; // 需要自己load代码,无法这样同步使用了 add()
-
基于promise的动态远程模块,文档
remote容器暴露的全局变量上只有这两个方法,通过代理的方式修改这两个方法,实现动态remote.
该方案和external-remotes-plugin的使用效果差不多,都可以根据环境来动态切换远程模块。