Module Federation

本章节将介绍如何在 Rslib 中构建 Module Federation 产物。

名词解释

Module Federation 是一种 JavaScript 应用分治的架构模式(类似于服务端的微服务),它允许你在多个 JavaScript 应用程序(或微前端)之间共享代码和资源。

使用场景

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

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

模块联邦可以帮助你:

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

快速接入

首先安装 Module Federation Rsbuild 插件(Rslib 基于 Rsbuild 实现)。

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: [
    // ... other format
    {
      format: 'mf',
      output: {
        distPath: {
          root: './dist/mf',
        },
        // add your assetPrefix here
        assetPrefix: 'http://localhost:3001/mf',
      },
      plugins: [
        pluginModuleFederation({
          name: 'rslib_provider',
          exposes: {
            // add expose here
          },
          // can not add 'remote' here, because you may build 'esm' or 'cjs' assets in one build.
          // if you want the rslib package use remote module, please see below.
          shared: {
            react: {
              singleton: true,
            },
            'react-dom': {
              singleton: true,
            },
          },
        }),
      ],
    },
  ],
  plugins: [pluginReact()],
});

这样我们就完成了 Rslib Module 作为生产者的接入,构建完成后可以看到产物中新增了 mf 目录,消费者可以直接消费这个包。

在上面的例子中,我们设置了一个新的 format: 'mf',这会帮助你生成一份额外的 Module Federation 产物,与此同时,还可以配置 cjsesm 的 format,它们互相不冲突。

如果你希望这个 Rslib Module 同时可以消费其他生产者,请不要使用构建配置 remote 参数,因为在其他 format 中,这可能会导致错误,请参考下方使用 Module Federation 运行时的例子。

消费其他 Module Federation 模块

因为 Rslib 中有多种 format,如果在构建时配置 remote 参数来消费其他模块可能无法在所有 format 中都能正常工作,推荐通过 Module Federation Runtime 进行接入

首先安装运行时依赖

npm
yarn
pnpm
bun
npm add @module-federation/enhanced -D

然后在运行时消费其他 Module Federation 模块,例如

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>
  );
};

这样可以保证在多种 format 中都能按预期进行加载模块。

常见问题

如果 Rslib 生产者是通过 build 构建的,这意味着生产者的 process.env.NODE_ENVproduction。如果这时消费者是 dev 模式启动的, 由于 Module Federation 的 shared 加载策略默认是 version-first,可能会出现加载到不同模式的 react 和 react-dom 的问题(例如 development 模式的 react,production 模式的 react-dom)。 这时可以在消费者处设置 shareStrategy 来解决这个问题,但请确保您充分了解这个配置

pluginModuleFederation({
  // ...
  shareStrategy: 'loaded-first',
}),

示例

Rslib Module Federation Example

  • mf-host: Rsbuild App 消费者
  • mf-react-component: Rslib Module,同时是生产者和消费者,作为生产者提供模块给 mf-host 使用,作为消费者通过运行时消费 mf-remote
  • mf-remote: Rsbuild App 生产者