模块联邦

本章介绍如何在 Rslib 中构建 模块联邦 产物。

使用场景

模块联邦有一些典型的使用场景,包括:

  • 允许独立应用程序(微前端架构中称为“微前端”)共享模块,而无需重新编译整个应用。
  • 不同的团队处理同一应用程序的不同部分,而无需重新编译整个应用程序。
  • 运行时中在应用间动态加载和共享代码。

模块联邦可以帮助你:

  • 减少代码重复
  • 提高代码可维护性
  • 减小应用程序的整体大小
  • 提高应用性能

快速开始

首先安装 Module Federation Rsbuild Plugin.

npm
yarn
pnpm
bun
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' ,它将添加一个额外的模块联邦产物,同时还配置了 cjsesm 的格式,它们是不冲突的。

但是,如果你希望此 Rslib 模块同时消费其他生产者,请不要使用构建配置 remote 参数,因为在其他格式下,这可能会导致错误,请参考下面使用 Module Federation 运行时的示例

开发 MF 远程模块

使用宿主应用

Rslib 支持宿主应用和 Rslib 模块联邦项目同时开发。

1. 启动库的 rslib mf-dev 命令

添加 dev 命令在 package.json 文件:

package.json
{
  "scripts": {
    "dev": "rslib mf-dev"
  }
}

然后运行 dev 命令即可启动模块联邦开发模式,可被宿主应用消费, 同时具有模块热更新(HMR)功能。

npm
yarn
pnpm
bun
npm run dev

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)功能。

npm
yarn
pnpm
bun
npm run dev

2. 创建 Storybook 配置

首先,在 Rslib 项目中配置 Storybook。你可以参考 Storybook 章节来了解如何执行此操作。在本章中,我们将使用 React 框架作为示例。

  1. 安装以下 Storybook addon,让 Storybook 与 Rslib 模块联邦一起使用:

    npm
    yarn
    pnpm
    bun
    npm add storybook-addon-rslib @module-federation/storybook-addon -D
  2. 然后创建 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
yarn
pnpm
bun
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_ENVproduction 。如果这时消费者是使用的开发模式启动,由于模块联邦默认使用共享的加载策略,可能会有 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 示例