模块联邦

本章介绍如何在 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 { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'rsbuild_host',
      remotes: {
        rslib: 'rslib@http://localhost:3001/mf/mf-manifest.json',
      },
      shared: {
        react: {
          singleton: true,
        },
        'react-dom': {
          singleton: true,
        },
      },
      // 开启这个当 Rslib 产物为 'production' 模式, 但是宿主应用是 'development' 模式。
      // 参考链接: https://lib.rsbuild.dev/guide/advanced/module-federation#faqs
      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',
}),

示例

Rslib 模块联邦示例

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

Rslib 模块联邦 Storybook 示例