教程 - 企业模块联合指南
更新:2022年4月17日
请参阅我的企业模块联合系列的第 2 部分,了解比下面描述的方法更简单的实现多环境设置的方法。
更新:2021年9月11日
可以完全避免硬编码 URL 和环境变量。请参阅下方 Zack Jackson 的评论,其中阐明了如何使用 Promise new Promise 在运行时推断远程位置。
企业模块联合方法
本指南适用于哪些人?
如果您所在的组织有以下要求,那么本指南可能会引起您的兴趣:
- 多种开发环境(、、、
local
等)dev
staging
prod
- 跨多个域(URL)共享的多个应用程序
介绍
优势
模块联合是 Webpack 5 中一个令人兴奋的新功能。正如其创建者 Zack Jackson 所描述的:
模块联合允许 JavaScript 应用程序从另一个应用程序动态加载代码,并在此过程中共享依赖项。
这种强大的编排微前端架构将使组织更容易解耦其应用程序并在团队之间共享。
限制
尽管模块联合具有诸多优势,但当将其应用于环境要求更复杂的组织时,我们仍会看到其局限性。
让我们看下面的例子:
webpack.dev.js
new ModuleFederationPlugin({
remotes: {
FormApp: "FormApp@http://localhost:9000/remoteEntry.js",
Header: "Header@http://localhost:9001/remoteEntry.js",
Footer: "Footer@http://localhost:9002/remoteEntry.js",
},
...
}),
webpack.prod.js
new ModuleFederationPlugin({
remotes: {
FormApp: "FormApp@http://www.formapp.com/remoteEntry.js",
Header: "Header@http://www.header.com/remoteEntry.js",
Footer: "Footer@http://www.footer.com/remoteEntry.js",
},
...
}),
您可能首先注意到的是,URL 在 Webpack 配置中是硬编码的。虽然这种设置可以正常工作,但如果有多个应用程序分布在多个环境中,则扩展性会很差。
另一个需要考虑的因素是代码部署。如果远程应用 URL 发生变化,团队必须记住同时更改远程应用和主机应用的配置。需要对不同项目中的多个文件进行更改,这会增加生产环境中出现错误和代码崩溃的可能性。
结论
我们需要一种方法来动态地为 local 和 remote 分配适当的环境上下文entrypoints
。然而,抽象出分配环境上下文的逻辑将导致模块联合无法containers
在 Webpackbuild
过程中知道在何处以及如何加载 remote;因为 Webpack 配置中将不再存在绝对 URL 路径。我们需要能够在环境上下文建立后动态加载 remote 应用。
高层概述
该存储库采用了几种已记录技术的修改来支持完全动态的多环境设置。
MutateRuntimePlugin.js
Module Federation Author 开发的这个插件Zack Jackson
允许利用 WebpackMutateRuntime
编译钩子进行publicPath
动态变异。
此代码片段是通过在期间初始化的变量赋值来拦截和变异的devonChurch
实现。MutateRuntimePlugin.js
publicPath
runtime
多环境架构
该讨论线程和代码示例概述了通过上述方法通过突变注入本地和远程devonChurch
的方法。entrypoints
runtime
publicPath
该方法还采用了配置文件,其中包含所有本地和远程URL 以及当前环境.json
的全局映射。entrypoint
动态远程容器
此代码片段通过 Webpack 文档描述了在运行时动态初始化远程的公开方法containers
。
Webpack 配置
在实施上述技术的过程中,我gotchyas
在设置更高级的 Webpack 配置时遇到了一些问题。我记录了这些问题及其修复方法,以便您可以避免这些陷阱。
项目设置
在深入研究项目代码之前,让我们简单讨论一下项目的结构和底层配置。
| dynamic-container-path-webpack-plugin (dcp)
| -----------
| Shared Configs
| -----------
| map.config.json
| bootstrap-entries.js
| Host / Remote
| -----------
| chunks.config.json
| * environment.config.json
| webpack.common.js
| webpack.dev.js
| webpack.prod.js
| index.html
| Host
| -----------
| bootstrap.js
| load-component.js
| Remote
| -----------
| bootstrap.js
动态容器路径 webpack 插件
我的修改版本MutateRuntimePlugin.js
在. 可以从 进行安装publicPath
,并可用作插件,并在您的 Webpack 配置中进行自定义。runtime
npm
共享配置
map.config.json
包含本地和远程端点 URL 的全局对象。
bootstrap-entries.js
chunks
根据当前环境使用正确的 URL引导 Webpack 。
主机/远程
chunks.config.json
entrypoints
是应用程序初始化所需的 Webpack 和供使用的远程应用程序命名空间的数组。
environment.config.json
bootstrap-entries.js
是一个键/值对,指示当前环境。这可以通过构建管道设置。但为了简单起见,我们将在本教程中设置环境。
Webpack 配置文件的使用,webpack-merge
使我们能够减少 Webpack 样板代码(加载器、常见的 Webpack 环境配置等)。为了简化跨应用程序的配置,我们推荐使用这种架构。
index.html
将包含一个脚本引用,bootstrap-entries.js
以便它可以引导 Webpack chunks
,从而runtime
可以加载我们的联合模块。
主持人
bootstrap.js
充当本地和远程代码的异步屏障。这是 Module Federation 正常工作所必需的文件。您可以在此处阅读更多相关信息。我们还将在此处设置逻辑以延迟加载我们的远程应用程序。
load-component.js
代码直接摘自本指南中引用的Dynamic Remote Containers
Webpack 文档。此文件将动态加载远程应用的共享库,并与主机协商。
偏僻的
与 类似Host
,bootstrap.js
它作为我们本地和远程代码的异步屏障。
通过全局变量赋值改变 publicPath
publicPath
关于作业选项的讨论
我们的第一步是确定一种动态变异的方法publicPath
。在回顾解决方案之前,让我们先通过Webpack 文档来简要讨论一下我们的选择。
我们可以使用DefinePlugin
设置环境变量来修改publicPath
,但是,我们将无法轻松地扩展到具有多个环境的多个远程。
一个可行的方案是利用 Webpack 的publicPath
:功能auto
,根据上下文自动确定值(例如: )。我们甚至可以在的动态远程示例仓库document.currentScript
中实际操作。Zack Jackson
虽然此选项确实满足了我们从 webpack 配置中移除硬编码 URL的需求,但遗憾的是,现在我们需要通过 在主机中定义远程路径App.js
,从而违背了将硬编码 URL 排除在代码之外的初衷。另一个缺点是style-loader
它依赖于静态方法publicPath
在 HTML 中内联嵌入样式,导致我们无法使用它。请参阅此 GitHub 问题线程。
剩下的最后一个选项就是publicPath
动态修改。下一节我们将讨论如何利用 Webpack 的复杂钩子之一,并编写一个支持publicPath
运行时自定义修改的 Webpack 插件。
外包逻辑可以runtime
减少硬编码的 Webpack 构建配置,减少维护,并提高配置的可重用性。
高层概述
我们可以publicPath
通过引用和修改 Module Federation Author 的自定义 Webpack 插件来进行变异Zack Jackson
,该插件使用MutateRuntime
编译钩子进行publicPath
动态变异。
首先我们来看一下完成的插件的API:
const DynamicContainerPathPlugin =
require('dynamic-container-path-webpack-plugin');
const setPublicPath =
require('dynamic-container-path-webpack-plugin/set-path');
new DynamicContainerPathPlugin({
iife: setPublicPath,
entry: 'host',
}),
DynamicContainerPathPlugin
接受两个参数。iife
是一个立即调用的函数表达式,它将作为entry
它的参数。
当iife
在插件内部执行时,它将用作查找正确环境的entry
参数。当返回时,将结果值赋给 Webpack 的内部变量。key
iife
DynamicContainerPathPlugin
publicPath
利用PublicPathRuntimeModule
让我们深入了解一下dynamic-container-path-plugin 的工作原理。
注意:本指南假设您已了解 Webpack 插件的基本工作原理。如需了解更多信息,请参阅此处的Webpack 文档。
首先我们调用apply(compiler)
来访问Webpack的编译生命周期:
apply(compiler) {
};
接下来,我们需要一种方法来在完成编译之前拦截 Webpack。我们可以使用make
钩子来实现:
compiler.hooks.make.tap('MutateRuntime', compilation => {});
在这个make
钩子中,我们可以访问 Webpack 的编译钩子,从而创建一个新的构建。我们可以使用该runtimeModule
钩子直接进行publicPath
赋值,并调用自定义方法changePublicPath
进行动态publicPath
重新赋值:
compilation.hooks.runtimeModule.tap('MutateRuntime', (module, chunk) => {
module.constructor.name === 'PublicPathRuntimeModule'
? this.changePublicPath(module, chunk)
: false;
});
});
changePublicPath
方法
changePublicPath
调用两个方法。第一个方法使用 Webpack 在构建时设置的内部全局变量来getInternalPublicPathVariable
剥离值,并仅返回内部变量。publicPath's
__webpack_require__.p
getInternalPublicPathVariable(module) {
const [publicPath] = module.getGeneratedCode().split('=');
return [publicPath];
}
第二种方法接受派生的setNewPublicPathValueFromRuntime
内部publicPath
变量作为参数。该变量使用提供给 Webpack 插件的自定义逻辑重新赋值。__webpack_require__.p
getInternalPublicPathVariable
然后在构建时publicPath
将新值分配给module._cachedGeneratedCode
等于__webpack_require__.p
我们的内部 Webpack变量。publicPath
setNewPublicPathValueFromRuntime(module, publicPath) {
module._cachedGeneratedCode =
`${publicPath}=${this.options.iife}('${this.options.entry}');`;
return module;
}
iife
和entry
在上一节中,我们介绍了该方法如何setNewPublicPathValueFromRuntime
分配新publicPath
值。在本节中,我们将介绍 中包含的逻辑iffe
:
`${publicPath}=${this.options.iife}('${this.options.entry}');`;
让我们再次缩小到使用我们的原始 API 设置DynamicContainerPathPlugin
。
const DynamicContainerPathPlugin =
require('dynamic-container-path-webpack-plugin');
const setPublicPath =
require('dynamic-container-path-webpack-plugin/set-path');
new DynamicContainerPathPlugin({
iife: setPublicPath,
entry: 'host',
}),
DynamicContainerPathPlugin
带有通过分配的逻辑publicPath
,setPublicPath
但您可以修改以满足您自己的需要。
dynamic-container-path-webpack-plugin/set-path
包含以下代码:
module.exports = function (entry) {
const { __MAP__, __ENVIRONMENT__ } = window;
const { href } = __MAP__[entry][__ENVIRONMENT__];
const publicPath = href + '/';
return publicPath;
};
__MAP__
和__ENVIRONMENT__
,稍后会介绍,是我们将在运行时设置的全局变量。这些全局变量的值将被赋值给我们从json
URL 映射(下文会介绍)中获取的数据。
entry
entrypoint
用作在 中查找当前的键__MAP__
。是从 中提取并分配给 的href
结果值,然后将其分配给 Webpack 的内部变量,正如我们在上一节中所述。__MAP__
publicPath
publicPath
创建端点的全局映射
如前所述,模块联合的一个缺点是依赖于硬编码的 URL,而这些 URL 在更复杂的组织需求下扩展性较差。我们将定义一个json
对象,其中包含主机和远程 URL 的全局引用,entrypoint
这些 URL 将被存储库引用。
{
"Host": {
"localhost": {
"href": "http://localhost:8000"
},
"production": {
"href": "https://dynamic-host-module-federation.netlify.app"
}
},
"RemoteFormApp": {
"localhost": {
"href": "http://localhost:8001"
},
"production": {
"href": "https://dynamic-remote-module-federation.netlify.app"
}
}
}
Host
并RemoteFormApp
引用entrypoint
我们稍后将在存储库中定义的 Webpack 名称。
每个都entrypoints
包含环境 URL;key
引用环境名称并property
href
包含硬编码的 URL。
编写脚本来引导 Chunks
支持多环境设置的关键是根据运行时的当前环境动态分配适当的端点 URL。
我们将创建一个名为的文件bootstrap-entries.js
,其任务如下:
dynamic-container-path-webpack-plugin
获取配置文件并将其分配给全局变量以供变异使用publicPath
- 配置文件和新定义将在页面上
publicPath
注入本地和远程。chunks
初始设置
首先,我们将定义一个iife
,以便它将立即执行index.html
:
(async () => {
// our script goes here
})();
接下来我们将设置逻辑来确定当前环境:
注意:请参阅部分中的代码片段A Quick Note on environment.config.js
以了解构建管道配置。
const environment = () =>
location.host.indexOf('localhost') > -1 ? 'localhost' : 'production';
由于我们将引用相对于各个存储库的配置文件,因此我们有一个小函数来获取适当的基本路径:
const getBasePath = environment() == 'localhost' ? './' : '/';
接下来,我们将获取一个名为的文件assets-mainfest.json
。
对于production
构建,资产通常通过使用 Webpack 的contentHash
功能进行缓存破坏。此文件将由生成webpack-assets-manifest
,并允许我们获取,而chunks
无需知道contentHash
每次production
构建时分配的动态生成值:
const getManifest = await fetch('./assets-manifest.json').then(response =>
response.json()
);
接下来,我们将定义一个const
配置文件数组:
const configs = [
`https://cdn.jsdelivr.net/gh/waldronmatt/
dynamic-module-federation-assets/dist/map.config.json`,
`${getBasePath}chunks.config.json`,
];
第一个配置引用了我们之前定义的端点的全局映射。
注意:我使用jsdeliver
服务map.config.json
,以便bootstrap-entries.js
所有存储库都能从一处引用。对于关键任务应用程序,请研究更强大的云替代方案。
第二个配置是应用程序初始化所需的数组entrypoints
,以及用于消费的远程应用程序命名空间。每个存储库的配置都是唯一的,稍后会介绍。
获取配置并分配给全局变量
现在我们的实用函数和配置文件引用已经定义,下一步是获取我们的配置并将它们分配给全局定义的变量。
首先,我们将并行获取配置文件。我们希望确保在变量赋值之前获取所有配置:
const [map, chunks] = await Promise.all(
configs.map(config => fetch(config).then(response => response.json()))
);
接下来,我们将environment
和赋值map
给全局变量。这一步至关重要,因为它将用于dynamic-container-path-webpack-plugin
重新赋值 的值publicPath
。
window.__ENVIRONMENT__ = environment();
window.__MAP__ = map;
entrypoints
从页面获取 JavaScript并注入
最后,我们将循环遍历每个chunk
定义chunks.config.js
并返回代码:
注意:正如我们稍后将在本节中看到的,chunks.config.js
包含两个数组,其中包含对本地和远程 Webpack 的名称引用chunks
。
首先,我们获取所有本地数据chunks
并返回代码。由于webpack-assets-manifest
没有生成remoteEntry.js
(模块联邦用于引导远程文件)的条目,我们仅通过名称获取。
注意: 在存储库中remoteEntry.js
被视为。local chunk
remote
...chunks.entrypoints.map(chunk => {
return chunk !== 'remoteEntry'
? fetch(`./${getManifest[`${chunk}.js`]}`)
.then(response => response.text())
: fetch(`${chunk}.js`).then(response => response.text());
}),
接下来,我们将获取所有远程信息chunks
并返回代码。首先,我们chunk
根据当前环境为每个远程信息获取相应的端点。
然后我们使用派生的端点值并将其分配给,remoteEntry.js
以便我们可以正确地获取远程。
...chunks.remotes.map(chunk => {
const { href } = map[chunk][environment()];
return fetch(`${href}/remoteEntry.js`).then(response => response.text());
}),
最后,chunk
我们为每个标签创建一个script
标签,将返回的代码分配给它,并将其附加到页面以供执行。
.then(scripts =>
scripts.forEach(script => {
const element = document.createElement('script');
element.text = script;
document.querySelector('body').appendChild(element);
})
);
总的来说,我们的代码应该如下所示:
(async () => {
const environment = () =>
location.host.indexOf('localhost') > -1 ? 'localhost' : 'production';
const getBasePath = environment() == 'localhost' ? './' : '/';
const getManifest = await fetch('./assets-manifest.json').then(response =>
response.json()
);
const configs = [
`https://cdn.jsdelivr.net/gh/waldronmatt/
dynamic-module-federation-assets/dist/map.config.json`,
`${getBasePath}chunks.config.json`,
];
const [map, chunks] = await Promise.all(
configs.map(config => fetch(config).then(response => response.json()))
);
window.__ENVIRONMENT__ = environment();
window.__MAP__ = map;
await Promise.all([
...chunks.entrypoints.map(chunk => {
console.log(`Getting '${chunk}' entry point`);
return chunk !== 'remoteEntry'
? fetch(`./${getManifest[`${chunk}.js`]}`).then(response =>
response.text()
)
: fetch(`${chunk}.js`).then(response => response.text());
}),
...chunks.remotes.map(chunk => {
const { href } = map[chunk][environment()];
return fetch(`${href}/remoteEntry.js`).then(response => response.text());
}),
]).then(scripts =>
scripts.forEach(script => {
const element = document.createElement('script');
element.text = script;
document.querySelector('body').appendChild(element);
})
);
})();
稍后,我们将介绍如何在我们的存储库中实现代码。
关于environment.config.js
为简单起见,我们将在本教程中定义用于确定环境的逻辑bootstrap-entries.js
。但是,您可能更喜欢根据构建管道来定义它。如果您是这种情况,下面是一些代码片段,您可以用来代替我们将在后续章节中介绍的环境逻辑:
environment.config.js
- (将按存储库创建)
{
"environment": "localhost"
}
bootstrap-entries.js
const configs = [
`${getBasePath}environment.config.json`,
...
]
...
const [{ environment }, ... ] = await Promise.all(
configs.map(config => fetch(config).then(response => response.json()))
);
...
window.__ENVIRONMENT__ = environment;
项目设置
现在是时候把学到的知识付诸实践了。由于我们讲解了具体的文件和配置,您可以参考此处的仓库。我们只讲解重要的文件和配置。
config/
目录
chunks.config.json
我们将在项目根目录下的文件夹中创建一个名为 的文件config
。该文件包含本地和远程入口点的列表。
{
"entrypoints": ["Host"],
"remotes": ["RemoteFormApp"]
}
注意A Quick Note on environment.config.js
:您可以选择在此目录中定义使用构建管道的环境配置文件集。有关更多信息,请参阅 部分。
environment.config.js
- (将按存储库创建)
{
"environment": "localhost"
}
bootstrap.js
如果您在项目中的任何地方使用静态导入,则需要设置异步边界以确保模块联合正常工作。您可以通过设置一个名为 的文件并动态导入应用程序的bootstrap.js
主文件来实现。.js
import('./app.js');
注意:要进一步了解该主题,请参考以下链接:
动态延迟加载远程容器
load-component.js
在 下创建一个名为 的文件。我们将复制/粘贴Webpack 文档中关于动态远程容器 的/src/
代码。此代码允许动态加载远程容器。
const loadComponent = (scope, module) => {
return async () => {
await __webpack_init_sharing__('default');
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
};
export default () => loadComponent;
接下来,我们将复制/粘贴Webpack 文档中有关延迟加载bootstrap.js
的更多代码。我们将在文件中动态导入的下方修改并实现此代码app.js
。
const lazyLoadDynamicRemoteApp = () => {
const getHeader = document.getElementById('click-me');
getHeader.onclick = () => {
import(/* webpackChunkName: "RemoteFormApp" */ './load-component')
.then(module => {
const loadComponent = module.default();
const formApp = loadComponent('FormApp', './initContactForm');
formApp();
})
.catch(() => `An error occurred while loading ${module}.`);
};
};
lazyLoadDynamicRemoteApp();
之所以不需要硬编码 URL 就可以工作,是因为我们publicPath
在运行时动态分配,获取适当的入口点,并将代码注入到页面上。
由于这包括remoteEntry.js
,反过来,它又加载到我们的远程,我们可以自动访问远程范围FormApp
,现在我们能够仅使用./initContactForm
位于远程存储库中的相对路径成功加载它。
注意:如果您不想延迟加载应用程序并正常动态导入它们,请将上面的代码替换为以下内容bootstrap.js
:
import('./load-component').then(module => {
const loadComponent = module.default();
const formApp = loadComponent('FormApp', './initContactForm');
formApp();
});
引用bootstrap-entries.js
文件
之前,我们设置了自定义代码来在运行时引导 Webpack 块。现在是时候在我们的代码库中引用它了,index.html
正如我们在本节中介绍的那样Reference for Use in Repositories
(更多信息请参阅此处)。我们将对所有仓库重复此过程。
<script
preload
src="https://unpkg.com/regenerator-runtime@0.13.1/runtime.js"
></script>
<script preload <!-- reference the bootstrap-entries.js link above -->
src=`...`>
</script>
我们提供的文件bootstrap-entries.js
是脚本的转换和缩小版本,以支持旧版浏览器并提高性能。
注意: regenerator-runtime
需要提供支持async/await
。
注意:我们可以使用preload
这些脚本来提高页面性能。
注意:我们之前设置的硬编码 URL 的全局映射也位于dynamic-module-federation-assets
仓库中(即bootstrap-entries.js
所在位置)。原因是这个文件在我们所有的仓库中都是通用的。如果我们需要添加、删除或更改 URL,只需在一个位置进行一次即可。
Webpack 配置
Webpack 合并
主机和远程仓库使用 Webpack Merge 来复用通用配置,并减少需要安装的依赖项数量。在本教程中,我使用我自己的可共享配置(可在此处找到)。
开发配置
我们至少需要一个开发服务器和热重载设置以及来自 Webpack 合并配置的配置默认值。
我们正在向开发服务器标头添加一个配置以忽略CORS
。您可以添加可选的 linters 和任何其他所需的配置。主机和远程存储库的最终代码webpack.dev.js
如下:
const commonConfig = require('./webpack.common.js');
const extendWebpackBaseConfig = require('@waldronmatt/webpack-config');
const path = require('path');
const webpack = require('webpack');
const developmentConfig = {
devServer: {
contentBase: path.resolve(__dirname, './dist'),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers':
'X-Requested-With, content-type, Authorization',
},
index: 'index.html',
port: 8000,
},
plugins: [new webpack.HotModuleReplacementPlugin()],
};
module.exports = extendWebpackBaseConfig(commonConfig, developmentConfig);
生产配置
我们可以利用 Webpack 的splitchunks
功能来分割代码以及动态加载的远程代码和本地代码。
由于我们的远程FormApp
需要额外的依赖项,我们可以告诉 Webpack 将属于库的代码拆分到单独的文件中。
cacheGroups: {
vendor: {
name: `Vendors-${mainEntry}`,
chunks: 'async',
test: /node_modules/,
},
},
注意:代码块的名称非常重要。它必须是唯一的,以避免与远程服务器发生命名空间冲突。使用主入口点的名称以及描述代码拆分性质的命名系统(vendors
在我们的例子中)可能是保持名称唯一性的好方法。
注意:如果您还记得之前的内容,为了使模块联合能够正常工作,我们需要设置一个异步边界,以便支持静态导入。现在我们所有的代码都是异步的,这意味着我们还需要将其设置chunks
为async
我们的配置。
我们可以重复此过程来拆分入口点之间共享的代码。主机和远程存储库的最终代码如下所示:
const commonConfig = require('./webpack.common.js');
const extendWebpackBaseConfig = require('@waldronmatt/webpack-config');
const chunks = require('./config/chunks.config.json');
const mainEntry = chunks.entrypoints[0];
const productionConfig = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: `Vendors-${mainEntry}`,
chunks: 'async',
test: /node_modules/,
priority: 20,
},
common: {
name: `Common-${mainEntry}`,
minChunks: 2,
chunks: 'async',
priority: 10,
reuseExistingChunk: true,
enforce: true,
},
},
},
},
};
module.exports = extendWebpackBaseConfig(commonConfig, productionConfig);
通用配置
最后,我们将设置 Webpack 和 Module Federation 正常运行所需的核心配置。
主机模块联合配置
主机将包含远程依赖项版本共享契约。我们通过声明该shared
属性来实现这一点。为了方便起见,我们使用了一个可选插件,automatic-vendor-federation
以便更轻松地获取版本数据并从协商过程中排除库。
const ModuleFederationConfiguration = () => {
const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
const packageJson = require('./package.json');
const exclude = ['express', 'serverless-http'];
return new ModuleFederationPlugin({
shared: AutomaticVendorFederation({
exclude,
packageJson,
shareFrom: ['dependencies'],
jquery: {
eager: true,
},
}),
});
};
远程模块联合配置
远程配置将包含范围name
、module
在存储库中公开的相对路径,以及最后用于引导远程的远程入口点的默认名称:
const ModuleFederationConfiguration = () => {
return new ModuleFederationPlugin({
name: 'FormApp',
filename: 'remoteEntry.js',
exposes: {
'./initContactForm': './src/form/init-contact-form',
},
});
};
DynamicContainerPathPlugin
接下来我们配置DynamicContainerPathPlugin
设置publicPath
为runtime
:
const DynamicContainerPathPlugin =
require('dynamic-container-path-webpack-plugin');
const setPublicPath =
require('dynamic-container-path-webpack-plugin/set-path');
new DynamicContainerPathPlugin({
iife: setPublicPath,
entry: mainEntry,
}),
基本配置
下一步是配置入口点、输出配置和剩余的插件。首先,我们将设置主入口点。引用的文件应该是bootstrap.js
,它是静态导入的异步边界。
target: 'web',
entry: {
[mainEntry]: ['./src/bootstrap.js'],
},
输出配置的publicPath
默认值为/
。可以忽略该值,因为DynamicContainerPathPlugin
运行时会修改该值。
output: {
publicPath: '/',
path: path.resolve(__dirname, './dist'),
},
runtimeChunk: single
这些存储库中使用的 Webpack 合并配置已runtimeChunk
:single
设置为优化默认值,以便在所有生成的块之间共享运行时文件。
在撰写本文时,模块联合存在一个问题,即此设置不会清空联合容器运行时;从而导致构建中断。我们通过设置runtimeChunk
为 来覆盖此问题false
。
optimization: {
runtimeChunk: false,
},
HtmlWebpackPlugin
此插件用于生成html
。我们不希望我们的js
资源被复制,HtmlWebpackPlugin
因为我们已经在运行时动态注入了入口点,不再需要在编译时引导它们。我们将使用它excludeChunks
来实现这一点:
new HtmlWebpackPlugin({
filename: 'index.html',
title: `${mainEntry}`,
description: `${mainEntry} of Module Federation`,
template: 'src/index.html',
excludeChunks: [...chunks.entrypoints],
}),
其他插件
我们正在添加ProvidePlugin
定义 jQuery(我们主要使用这个库来测试模块联合库协商过程)。
我们还将添加包含块映射的目录的CopyPlugin
复制并生成缓存破坏资产的映射。config/
WebpackAssetManifest
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
new CopyPlugin({
patterns: [{ from: 'config', to: '' }],
}),
new WebpackAssetsManifest({}),
整个代码应该如下所示:
webpack.common.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const { ModuleFederationPlugin } = require('webpack').container;
const DynamicContainerPathPlugin = require('dynamic-container-path-webpack-plugin');
const setPublicPath = require('dynamic-container-path-webpack-plugin/set-path');
const chunks = require('./config/chunks.config.json');
const mainEntry = chunks.entrypoints[0];
const commonConfig = isProduction => {
// HOST M.F. Configuration
const ModuleFederationConfiguration = () => {
const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
const packageJson = require('./package.json');
const exclude = ['express', 'serverless-http'];
return new ModuleFederationPlugin({
shared: AutomaticVendorFederation({
exclude,
packageJson,
shareFrom: ['dependencies'],
jquery: {
eager: true,
},
}),
});
// REMOTE M.F. Configuration
const ModuleFederationConfiguration = () => {
return new ModuleFederationPlugin({
name: 'FormApp',
filename: 'remoteEntry.js',
exposes: {
'./initContactForm': './src/form/init-contact-form',
},
});
};
};
return {
target: 'web',
entry: {
[mainEntry]: ['./src/bootstrap.js'],
},
output: {
publicPath: '/',
path: path.resolve(__dirname, './dist'),
},
optimization: {
runtimeChunk: false,
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
new CopyPlugin({
patterns: [{ from: 'config', to: '' }],
}),
new WebpackAssetsManifest({}),
new HtmlWebpackPlugin({
filename: 'index.html',
title: `${mainEntry}`,
description: `${mainEntry} of Module Federation`,
template: 'src/index.html',
excludeChunks: [...chunks.entrypoints],
}),
new DynamicContainerPathPlugin({
iife: setPublicPath,
entry: mainEntry,
}),
].concat(ModuleFederationConfiguration),
};
};
module.exports = commonConfig;
结论
如果你已经读到这里,谢谢你,恭喜你!你可以在以下代码库中找到所有代码:
虽然有很多内容需要介绍,但最终结果是一个支持完全动态、多环境配置的解决方案。
总结一下,本指南涵盖了以下内容:
- 模块联合的高层概述及其优点和缺点。
- 问题和期望的技术成果的摘要。
- 已确定的各种解决方案和项目结构的概述。
- 如何
publicPath
动态地改变和引导块。 - 核心项目文件和 Webpack 配置概述。
最后,我们将回顾使用此方法的优点和缺点,以便您做出明智的决定,确定这是否适合您:
优点:
- 更轻松地支持多个测试环境,而无需增加捆绑配置的复杂性(硬编码 URL)
- URL 仅需在一个位置更新一次(
map.config.js
)。 - 环境上下文设置可以推迟到构建管道。
- 尽管远程和主机容器在运行时初始化,您仍然可以利用模块联合的所有当前功能(库协商等)
- 大多数配置代码(包括 Webpack 配置)都可以捆绑并重用为其他项目的脚手架。
- 继续利用高级 Webpack 功能以及模块联合,包括代码拆分、延迟加载、缓存清除、webpack 合并支持等。
缺点
- 存储库依赖于单个全局 URL 映射文件。需要仔细规划以确保将停机时间降至最低。
- 重命名入口点需要在项目级别 (
chunks.config.js
) 和全局级别 (map.config.json
) 进行更新。任何引用远程的主机应用程序也需要chunks.config.js
更新其引用。 - 所涵盖的配置增加了相当大的复杂性,并且需要团队对 Webpack 有更深层次的了解。
替代方法
旨在提供与上述功能类似的功能的替代方法可以在以下存储库中找到:
补充阅读
我想分享一些有助于巩固我对模块联合的理解的参考资料:
执照
麻省理工学院
鏂囩珷鏉ユ簮锛�https://dev.to/waldronmatt/tutorial-a-guide-to-module-federation-for-enterprise-n5