开始使用

在 Modern.js 中使用 Module Federation 我们推荐使用官方插件 @module-federation/modern-js

本章节将会介绍如何通过官方插件搭含生产者应用与消费者应用,我们首先根据 Modern.js 快速上手 创建两个应用。

安装插件

完成应用创建后,我们分别为两个项目安装插件:

npm
yarn
pnpm
bun
npm add @module-federation/modern-js

注册插件

安装插件后,你需要在 modern.config.js 中注册插件:

import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';

export default defineConfig({
  runtime: {
    router: true,
  },
  plugins: [
    appTools({
      bundler: 'rspack',
    }),
    moduleFederationPlugin(),
  ],
});

生产者导出模块

接下来,我们先修改生产者的代码,导出 Module Federation 模块。

我们创建 src/components/Button.tsx 文件,导出一个 Button 组件:

src/components/Button.tsx
import React from 'react';

export const Button = () => {
  return <button type="button">Remote Button</button>;
};

随后,在项目根目录添加 module-federation.config.ts,配置 Module Federation 模块的名称、共享依赖和导出内容:

module-federation.config.ts
import { createModuleFederationConfig } from '@module-federation/modern-js';

export default createModuleFederationConfig({
  name: 'remote',
  manifest: {
    filePath: 'static',
  },
  filename: 'static/remoteEntry.js',
  exposes: {
    './Button': './src/components/Button.tsx',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
});
TIP

在上述代码块中,我们为 Module Federation 导出的 manifest 和 remoteEntry.js 都设置了 static 前缀,这是因为 Modern.js 要求将所有需要暴露的资源都放在 static/ 目录下,Modern.js 的服务器在生产环境时也只会托管 static/ 目录。

另外,我们还需要修改 modern.config.ts,为生产者提供一个开发环境的端口,让消费者可以通过此端口访问生产者的资源:

modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';

export default defineConfig({
  dev: {
    port: 3051,
  },
  runtime: {
    router: true,
  },
  plugins: [
    appTools({
      bundler: 'rspack',
    }),
    moduleFederationPlugin(),
  ],
});

消费者使用模块

现在,我们修改消费者的代码,使用生产者导出的模块。

在项目根目录添加 module-federation.config.ts,配置 Module Federation 模块的名称、共享依赖和使用的远程模块:

module-federation.config.ts
import { createModuleFederationConfig } from '@module-federation/modern-js';

export default createModuleFederationConfig({
  name: 'host',
  remotes: {
    remote: 'remote@http://localhost:3051/static/mf-manifest.json',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
});

mf-manifest.json 是生产者在打包后产出的文件,包含了生产者导出的所有模块信息。

我们创建新的路由文件 src/routes/remote/page.tsx,引入生产者模块:

src/routes/remote/page.tsx
import React, { useState, Suspense } from 'react';
import { Button } from 'remote/Button';

const Index = (): JSX.Element => {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <Button />
      </Suspense>
    </div>
  );
};

export default Index;

此时 remote/Button 引入会出现类型错误,这是因为本地没有远程模块的类型。Module Federation 2.0 提供了 类型提示 的能力,会在生产者构建时自动生成远程模块的类型定义,在消费者构建时自动下载。

为此我们需要在 tsconfig.json 中添加新的 path,保证类型生效:

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "*": ["./@mf-types/*"]
    }
  }
}
TIP

在消费者中,我们通过 remote/Button 来引用远程模块。这里简单介绍下这个路径具体代表了什么,可以先将它抽象成 [remoteAlias]/[remoteExpose]

第一段 remoteAlias 是生产者在消费者中的别名,它是消费者 module-federation.config.ts 中配置的 remotes 字段的 key

{
  remotes: {
    [remoteAlias]: '[remoteModuleName]@[URL_ADDRESS]',
  }
}

这里我们也将远程地址抽象为 [remoteModuleName]@[URL_ADDRESS]@ 前的部分必须对应生产者的模块名。

第二段 remoteExpose 是生产者在 module-federation.config.ts 中配置的 exposes 字段的 key

启动应用

现在,生产者应用和消费者应用都已经搭建完毕,我们可以在本地运行 modern dev 启动两个应用。

启动后,消费者中对生产者模块的引入,不会再出现抛错,类型已经下载到消费者应用中。

NOTE

修改生产者代码后,消费者会自动拉取生产者的类型。

访问 http://localhost:8080/remote,可以看到页面中已经包含了生产者的远程模块 Button 组件。

我们也可以在本地执行 modern serve 来模拟生产环境。

因为 Module Federation 插件会自动读取 Modern.js 的 output.assetPrefix 配置作为远程模块的访问地址,而该值在生产环境下构建后默认是 /。如果不做特殊处理,消费者将从自己的域名下拉取远程模块的入口文件。我们可以添加如下配置:

import { appTools, defineConfig } from '@modern-js/app-tools';
import { moduleFederationPlugin } from '@module-federation/modern-js';

const isLocal = process.env.IS_LOCAL === 'true';

// https://modernjs.dev/en/configure/app/usage
export default defineConfig({
  server: {
    port: 3051,
  },
  runtime: {
    router: true,
  },
  output: {
    // Now this configuration is only used in the local when you run modern serve command.
    // If you want to deploy the application to the platform, use your own domain name.
    // Module federation will automatically write it to mf-manifest.json, which influences consumer to fetch remoteEntry.js.
    assetPrefix: isLocal ? 'http://127.0.0.1:3051' : '/',
  },
  plugins: [
    appTools({
      bundler: 'rspack', // Set to 'webpack' to enable webpack
    }),
    moduleFederationPlugin(),
  ],
});

现在,在生产者中运行 IS_LOCAL=true modern build && modern serve,在消费者中运行 modern build && modern serve,即可在本地模拟生产环境,访问到远程模块。

上述用例可以参考:Modern.js & Module Federation 基础用法示例

相关文档