模块联邦

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

使用场景

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

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

模块联邦可以帮助你:

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

快速开始

首先安装 Module Federation Rsbuild Plugin.

npm
yarn
pnpm
bun
npm add @module-federation/rsbuild-plugin -D

然后在 rslib.config.ts 中注册插件:

rslib.config.ts
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rslib/core';

export default defineConfig({
  lib: [
    // ... 其他 format
    {
      format: 'mf',
      output: {
        distPath: {
          root: './dist/mf',
        },
        // production 时, 在这里使用线上 assetPrefix
        assetPrefix: 'http://localhost:3001/mf',
      },
      // Storybook 在 dev 下使用
      dev: {
        assetPrefix: 'http://localhost:3001/mf',
      },
      plugins: [
        pluginModuleFederation(
          {
            name: 'rslib_provider',
            exposes: {
              // 这里添加 expose
            },
            // 此处无法添加 "remote",因为你可能会在一次构建中构建 "esm" 或 "cjs" 产物。
            // 如果你希望 Rslib 包使用远程模块,请参考下面。
            shared: {
              react: {
                singleton: true,
              },
              'react-dom': {
                singleton: true,
              },
            },
          },
          {},
        ),
      ],
    },
  ],
  // Storybook 在 dev 下使用
  server: {
    port: 3001,
  },
  output: {
    target: 'web',
  },
  plugins: [pluginReact()],
});

这样,我们就完成了对 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
import { 
const pluginModuleFederation: (moduleFederationOptions: ModuleFederationOptions, rsbuildOptions?: RSBUILD_PLUGIN_OPTIONS) => RsbuildPlugin
pluginModuleFederation
} from '@module-federation/rsbuild-plugin';
import {
function defineConfig(config: RsbuildConfig): RsbuildConfig (+3 overloads)

This function helps you to autocomplete configuration types. It accepts a Rsbuild config object, or a function that returns a config.

defineConfig
} from '@rsbuild/core';
import {
const pluginReact: (options?: PluginReactOptions) => RsbuildPlugin
pluginReact
} from '@rsbuild/plugin-react';
export default
function defineConfig(config: RsbuildConfig): RsbuildConfig (+3 overloads)

This function helps you to autocomplete configuration types. It accepts a Rsbuild config object, or a function that returns a config.

defineConfig
({
EnvironmentConfig.plugins?: RsbuildPlugins | undefined

Configure Rsbuild plugins.

plugins
: [
function pluginReact(options?: PluginReactOptions): RsbuildPlugin
pluginReact
(),
function pluginModuleFederation(moduleFederationOptions: ModuleFederationOptions, rsbuildOptions?: RSBUILD_PLUGIN_OPTIONS): RsbuildPlugin
pluginModuleFederation
(
{
ModuleFederationPluginOptions.name?: string | undefined

The name of the container.

name
: 'rsbuild_host',
ModuleFederationPluginOptions.remotes?: Remotes | undefined

Container locations and request scopes from which modules should be resolved and loaded at runtime. When provided, property name is used as request scope, otherwise request scope is automatically inferred from container location.

remotes
: {
rslib: string
rslib
: 'rslib@http://localhost:3001/mf/mf-manifest.json',
},
ModuleFederationPluginOptions.shared?: Shared | undefined

Modules that should be shared in the share scope. When provided, property names are used to match requested modules in this compilation.

shared
: {
react: {
    singleton: true;
}
react
: {
SharedConfig.singleton?: boolean | undefined

Allow only a single version of the shared module in share scope (disabled by default).

singleton
: true,
}, 'react-dom': {
SharedConfig.singleton?: boolean | undefined

Allow only a single version of the shared module in share scope (disabled by default).

singleton
: true,
}, }, // 开启这个当 Rslib 产物为 'production' 模式, 但是宿主应用是 'development' 模式。 // 参考链接: https://rslib.rs/guide/advanced/module-federation#faqs
ModuleFederationPluginOptions.shareStrategy?: SharedStrategy | undefined

load shared strategy(defaults to 'version-first').

shareStrategy
: 'loaded-first',
}, {}, ), ], });

然后通过 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
import { dirname, join } from 'node:path';
import type { StorybookConfig } from 'storybook-react-rsbuild';

function getAbsolutePath(value: string): any {
  return dirname(require.resolve(join(value, 'package.json')));
}

const config: StorybookConfig = {
  stories: [
    '../stories/**/*.mdx',
    '../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)',
  ],
  framework: {
    name: getAbsolutePath('storybook-react-rsbuild'),
    options: {},
  },
  addons: [
    {
      name: getAbsolutePath('storybook-addon-rslib'),
      options: {
        rslib: {
          include: ['**/stories/**'],
        },
      },
    },
    {
      name: '@module-federation/storybook-addon/preset',
      options: {
        // 在添加 rslib module manifest 给 storybook dev
        // 我们在上面已经设置了 dev.assetPrefix 和 server.port 到 3001 在 rslib.config.ts
        remotes: {
          'rslib-module':
            //还可以在这里添加 storybook 的 shared
            // shared: {}
            'rslib-module@http://localhost:3001/mf/mf-manifest.json',
        },
      },
    },
  ],
};

export default config;

3. 用远程模块编写 stories

从远程模块引入组件

stories/index.stories.tsx
import React from 'react';
// 在这里加载远程模块,Storybook 相当于宿主应用.
import { Counter } from 'rslib-module';

const Component = () => <Counter />;

export default {
  title: 'App Component',
  component: Component,
};

export 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',
}, {}),

如何让模块联邦产物生成 ES modules 的导出

如果你希望 Rslib 生产者的模块联邦产物生成 ES Modules 的导出,可以额外配置如下:

rslib.config.ts
export default defineConfig({
  lib: [
    {
      format: 'mf',
      // ...
      tools: {
        rspack(config) {
          config.experiments = {
            outputModule: true,
          };
        },
      },
    },
  ],
});

示例

Rslib 模块联邦示例

  • mf-host: Rsbuild App 消费者
  • mf-react-component: Rslib Module, 同时是消费者和生产者, 作为生产者向 mf-host 提供模块, 并消费 mf-remote
  • mf-remote: Rsbuild App 生产者

Rslib 模块联邦 Storybook 示例