模块联邦
本章介绍如何在 Rslib 中构建 模块联邦 产物。
使用场景
模块联邦有一些典型的使用场景,包括:
- 允许独立应用程序(微前端架构中称为“微前端”)共享模块,而无需重新编译整个应用。
- 不同的团队处理同一应用程序的不同部分,而无需重新编译整个应用程序。
- 运行时中在应用间动态加载和共享代码。
模块联邦可以帮助你:
- 减少代码重复
- 提高代码可维护性
- 减小应用程序的整体大小
- 提高应用性能
快速开始
首先安装 Module Federation Rsbuild Plugin.
npm add @module-federation/rsbuild-plugin -D
然后在 rslib.config.ts
中注册插件:
rslib.config.ts
1import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
2import { pluginReact } from '@rsbuild/plugin-react';
3import { defineConfig } from '@rslib/core';
4
5export default defineConfig({
6 lib: [
7 // ... 其他 format
8 {
9 format: 'mf',
10 output: {
11 distPath: {
12 root: './dist/mf',
13 },
14 // production 时, 在这里使用线上 assetPrefix
15 assetPrefix: 'http://localhost:3001/mf',
16 },
17 // Storybook 在 dev 下使用
18 dev: {
19 assetPrefix: 'http://localhost:3001/mf',
20 },
21 plugins: [
22 pluginModuleFederation({
23 name: 'rslib_provider',
24 exposes: {
25 // 这里添加 expose
26 },
27 // 此处无法添加 "remote",因为你可能会在一次构建中构建 "esm" 或 "cjs" 产物。
28 // 如果你希望 Rslib 包使用远程模块,请参考下面。
29 shared: {
30 react: {
31 singleton: true,
32 },
33 'react-dom': {
34 singleton: true,
35 },
36 },
37 }),
38 ],
39 },
40 ],
41 // Storybook 在 dev 下使用
42 server: {
43 port: 3001,
44 },
45 output: {
46 target: 'web',
47 },
48 plugins: [pluginReact()],
49});
这样,我们就完成了对 Rslib Module 生产者的集成。构建完成后,我们可以看到产物中已经添加了 mf 目录,消费者可以直接消费这个包。
在上面的例子中,我们添加了一个新的 format: 'mf'
,它将添加一个额外的模块联邦产物,同时还配置了 cjs
和 esm
的格式,它们是不冲突的。
但是,如果你希望此 Rslib 模块同时消费其他生产者,请不要使用构建配置 remote
参数,因为在其他格式下,这可能会导致错误,请参考下面使用 Module Federation 运行时的示例
开发 MF 远程模块
使用宿主应用
Rslib 支持宿主应用和 Rslib 模块联邦项目同时开发。
1. 启动库的 rslib mf-dev
命令
添加 dev
命令在 package.json
文件:
package.json
{
"scripts": {
"dev": "rslib mf-dev"
}
}
然后运行 dev
命令即可启动模块联邦开发模式,可被宿主应用消费,
同时具有模块热更新(HMR)功能。
2. 启动宿主应用
设置宿主应用消费 Rslib 的模块联邦库。查看@module-federation/rsbuild-plugin
获取更多信息。
rsbuild.config.ts
1import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
2import { defineConfig } from '@rsbuild/core';
3import { pluginReact } from '@rsbuild/plugin-react';
4
5export default defineConfig({
6 plugins: [
7 pluginReact(),
8 pluginModuleFederation({
9 name: 'rsbuild_host',
10 remotes: {
11 rslib: 'rslib@http://localhost:3001/mf/mf-manifest.json',
12 },
13 shared: {
14 react: {
15 singleton: true,
16 },
17 'react-dom': {
18 singleton: true,
19 },
20 },
21 // 开启这个当 Rslib 产物为 'production' 模式, 但是宿主应用是 'development' 模式。
22 // 参考链接: https://lib.rsbuild.dev/guide/advanced/module-federation#faqs
23 shareStrategy: 'loaded-first',
24 }),
25 ],
26});
然后通过 rsbuild dev
启动宿主应用。
使用 Storybook
Rslib 支持使用 Storybook 开发 Rslib 模块联邦项目。
1. 启动库的 rslib mf-dev
命令
添加 dev
命令在 package.json
文件:
package.json
{
"scripts": {
"dev": "rslib mf-dev"
}
}
然后运行 dev
命令即可启动模块联邦开发模式,可被 Storybook 消费,
同时具有模块热更新(HMR)功能。
2. 创建 Storybook 配置
首先,在 Rslib 项目中配置 Storybook。你可以参考 Storybook 章节来了解如何执行此操作。在本章中,我们将使用 React 框架作为示例。
-
安装以下 Storybook addon,让 Storybook 与 Rslib 模块联邦一起使用:
npm add storybook-addon-rslib @module-federation/storybook-addon -D
-
然后创建 Storybook 配置文件 .storybook/main.ts
,指定 stories 和 addons,并设置 framework 和相应的 framework 集成。
.storybook/main.ts
1import { dirname, join } from 'node:path';
2import type { StorybookConfig } from 'storybook-react-rsbuild';
3
4function getAbsolutePath(value: string): any {
5 return dirname(require.resolve(join(value, 'package.json')));
6}
7
8const config: StorybookConfig = {
9 stories: [
10 '../stories/**/*.mdx',
11 '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
12 ],
13 framework: {
14 name: getAbsolutePath('storybook-react-rsbuild'),
15 options: {},
16 },
17 addons: [
18 {
19 name: getAbsolutePath('storybook-addon-rslib'),
20 options: {
21 rslib: {
22 include: ['**/stories/**'],
23 },
24 },
25 },
26 {
27 name: '@module-federation/storybook-addon/preset',
28 options: {
29 // 在添加 rslib module manifest 给 storybook dev
30 // 我们在上面已经设置了 dev.assetPrefix 和 server.port 到 3001 在 rslib.config.ts
31 remotes: {
32 'rslib-module':
33 //还可以在这里添加 storybook 的 shared
34 // shared: {}
35 'rslib-module@http://localhost:3001/mf/mf-manifest.json',
36 },
37 },
38 },
39 ],
40};
41
42export default config;
3. 用远程模块编写 stories
从远程模块引入组件
stories/index.stories.tsx
1import React from 'react';
2// 在这里加载远程模块,Storybook 相当于宿主应用.
3import { Counter } from 'rslib-module';
4
5const Component = () => <Counter />;
6
7export default {
8 title: 'App Component',
9 component: Component,
10};
11
12export const Primary = {};
4. 在 tsconfig.json
中添加模块联邦类型和 stories 文件
tsconfig.json
{
"compilerOptions": {
// ...
"paths": {
"*": ["./@mf-types/*"]
}
},
"include": ["src/**/*", ".storybook/**/*", "stories/**/*"]
}
5. 启动 Storybook app
大功告成,启动 Storybook npx storybook dev
。
使用其他模块联合模块
由于 Rslib 中有多种格式,如果在构建时配置 remote
参数来消耗其他模块,则可能无法在所有格式下正常工作。建议通过以下方式访问 Module Federation Runtime
首先安装运行时依赖
npm add @module-federation/enhanced -D
然后在运行时使用其他模块联邦模块,例如
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import { Suspense, createElement, lazy } from 'react';
init({
name: 'rslib_provider',
remotes: [
{
name: 'mf_remote',
entry: 'http://localhost:3002/mf-manifest.json',
},
],
});
export const Counter: React.FC = () => {
return (
<div>
<Suspense fallback={<div>loading</div>}>
{createElement(
lazy(
() =>
loadRemote('mf_remote') as Promise<{
default: React.FC;
}>,
),
)}
</Suspense>
</div>
);
};
这确保了模块可以按预期以多种格式加载。
FAQs
如果 Rslib 生产者是用 build 构建的, 这意味着生产者中的 process.env.NODE_ENV
是 production
。如果这时消费者是使用的开发模式启动,由于模块联邦默认使用共享的加载策略,可能会有 react 和 react-dom 加载模式不一致的问题 (比如 react 在 development mode, react-dom 在 production mode)。
你可以在消费者设置 shareStrategy 来解决这个问题,这需要你确保已经完全理解了这个配置。
pluginModuleFederation({
// ...
shareStrategy: 'loaded-first',
}),
示例
Rslib 模块联邦示例
mf-host
: Rsbuild App 消费者
mf-react-component
: Rslib Module, 同时是消费者和生产者, 作为生产者向 mf-host
提供模块, 并消费 mf-remote
mf-remote
: Rsbuild App 生产者
Rslib 模块联邦 Storybook 示例