多个独立构建应该形成一个单一应用程序。这些独立构建充当容器,可以在构建之间公开和使用代码,从而创建一个单一的、统一的应用程序。
这通常被称为微前端,但并不局限于此。
我们区分本地模块和远程模块。本地模块是当前构建的一部分的常规模块。远程模块是当前构建之外的模块,但在运行时从远程容器加载。
加载远程模块被认为是异步操作。当使用远程模块时,这些异步操作将被放置在远程模块和入口点之间的下一个块加载操作中。如果没有块加载操作,则无法使用远程模块。
块加载操作通常是import()
调用,但旧的结构如require.ensure
或require([...])
也受支持。
容器通过容器条目创建,容器条目提供对特定模块的异步访问。公开的访问分为两个步骤
步骤 1 将在块加载期间完成。步骤 2 将在模块评估期间完成,与其他(本地和远程)模块交织在一起。这样,评估顺序不会受到将模块从本地转换为远程或反之的影响。
可以嵌套容器。容器可以使用来自其他容器的模块。容器之间的循环依赖也是可能的。
每个构建都充当容器,也使用其他构建作为容器。这样,每个构建都可以通过从其容器加载来访问任何其他公开的模块。
共享模块是可以覆盖的模块,并且作为覆盖提供给嵌套容器。它们通常指向每个构建中的同一个模块,例如同一个库。
packageName
选项允许设置包名称以查找requiredVersion
。它默认情况下会自动推断模块请求,将requiredVersion
设置为false
时,应禁用自动推断。
此插件使用指定的公开模块创建额外的容器条目。
此插件将特定引用添加到容器作为外部,并允许从这些容器导入远程模块。它还调用这些容器的override
API 为它们提供覆盖。本地覆盖(通过__webpack_override__
或override
API 当构建也是容器时)和指定的覆盖将提供给所有引用的容器。
ModuleFederationPlugin
结合了 ContainerPlugin
和 ContainerReferencePlugin
。
config.context
解析。requiredVersion
。requiredVersion
。/
结尾的模块请求将匹配所有具有此前缀的模块请求。单页应用程序的每个页面都从容器构建中单独构建。应用程序外壳也是一个单独的构建,它引用所有页面作为远程模块。这样,每个页面都可以单独部署。当路由更新或添加新路由时,应用程序外壳会部署。应用程序外壳将常用库定义为共享模块,以避免在页面构建中重复使用它们。
许多应用程序共享一个通用的组件库,该库可以构建为一个容器,每个组件都暴露出来。每个应用程序都从组件库容器中使用组件。对组件库的更改可以单独部署,而无需重新部署所有应用程序。应用程序会自动使用组件库的最新版本。
容器接口支持get
和init
方法。init
是一个与async
兼容的方法,它使用一个参数调用:共享作用域对象。此对象用作远程容器中的共享作用域,并用来自主机的提供的模块填充。它可以用来在运行时将远程容器动态连接到主机容器。
init.js
(async () => {
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container = window.someContainer; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get('./module');
})();
容器尝试提供共享模块,但如果共享模块已被使用,则会发出警告并忽略提供的共享模块。容器可能仍然使用它作为回退。
这样,您就可以动态加载 A/B 测试,它提供共享模块的不同版本。
示例
init.js
function loadComponent(scope, module) {
return async () => {
// Initializes the shared scope. 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;
};
}
loadComponent('abtests', 'test123');
通常,远程配置使用 URL,例如以下示例
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
}),
],
};
但您也可以将一个 Promise 传递给这个远程,该 Promise 将在运行时解析。您应该使用符合上述get/init
接口的任何模块来解析此 Promise。例如,如果您想通过查询参数传递要使用的联合模块的版本,您可以执行以下操作
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: `promise new Promise(resolve => {
const urlParams = new URLSearchParams(window.location.search)
const version = urlParams.get('app1VersionParam')
// This part depends on how you plan on hosting and versioning your federated modules
const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window.app1.get(request),
init: (arg) => {
try {
return window.app1.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`,
},
// ...
}),
],
};
请注意,使用此 API 时,您必须解析包含 get/init API 的对象。
可以通过从远程模块公开方法来允许主机在运行时设置远程模块的 publicPath。
当您将独立部署的子应用程序安装在主机域的子路径上时,这种方法特别有用。
场景
您有一个托管在https://my-host.com/app/*
上的主机应用程序和一个托管在https://foo-app.com
上的子应用程序。子应用程序也安装在主机域上,因此,https://foo-app.com
预计可以通过https://my-host.com/app/foo-app
访问,并且https://my-host.com/app/foo-app/*
请求通过代理重定向到https://foo-app.com/*
。
示例
webpack.config.js (远程)
module.exports = {
entry: {
remote: './public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // this name needs to match with the entry name
exposes: ['./public-path'],
// ...
}),
],
};
public-path.js (远程)
export function set(value) {
__webpack_public_path__ = value;
}
src/index.js (主机)
const publicPath = await import('remote/public-path');
publicPath.set('/your-public-path');
//bootstrap app e.g. import('./bootstrap.js')
可以从 document.currentScript.src
中的脚本标签推断 publicPath,并在运行时使用 __webpack_public_path__
模块变量设置它。
示例
webpack.config.js (远程)
module.exports = {
entry: {
remote: './setup-public-path',
},
plugins: [
new ModuleFederationPlugin({
name: 'remote', // this name needs to match with the entry name
// ...
}),
],
};
setup-public-path.js (远程)
// derive the publicPath with your own logic and set it with the __webpack_public_path__ API
__webpack_public_path__ = document.currentScript.src + '/../';
Uncaught Error: 共享模块不可用于急切使用
应用程序正在急切地执行一个作为全向主机的应用程序。有多种选择可供选择
您可以在模块联合的高级 API 中将依赖项设置为急切,它不会将模块放入异步块中,而是同步提供它们。这允许我们在初始块中使用这些共享模块。但请注意,所有提供的和回退模块都将始终被下载。建议只在应用程序的一个点提供它,例如外壳。
我们强烈建议使用异步边界。它将拆分出较大块的初始化代码,以避免任何额外的往返行程,并总体上提高性能。
例如,您的入口看起来像这样
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
让我们创建 bootstrap.js
文件并将入口的内容移到其中,并将该引导程序导入到入口中
index.js
+ import('./bootstrap');
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- ReactDOM.render(<App />, document.getElementById('root'));
bootstrap.js
+ import React from 'react';
+ import ReactDOM from 'react-dom';
+ import App from './App';
+ ReactDOM.render(<App />, document.getElementById('root'));
此方法有效,但可能存在限制或缺点。
通过 ModuleFederationPlugin
为依赖项设置 eager: true
webpack.config.js
// ...
new ModuleFederationPlugin({
shared: {
...deps,
react: {
eager: true,
},
},
});
Uncaught Error: 模块 "./Button" 不存在于容器中。
它可能不会显示"./Button"
,但错误消息看起来类似。如果您从 webpack beta.16 升级到 webpack beta.17,通常会看到此问题。
在 ModuleFederationPlugin 中。将暴露项从
new ModuleFederationPlugin({
exposes: {
- 'Button': './src/Button'
+ './Button':'./src/Button'
}
});
Uncaught TypeError: fn is not a function
您可能缺少远程容器,请确保已添加。如果您已加载要使用的远程的容器,但仍然看到此错误,请将主机容器的远程容器文件也添加到 HTML 中。
如果您要从不同的远程加载多个模块,建议为您的远程构建设置 output.uniqueName
选项,以避免多个 webpack 运行时之间的冲突。