React Bridge(for React v16-v19)

@module-federation/bridge-react 提供了用于 React 应用的 bridge 工具函数:

  • createBridgeComponent:用于导出应用级别模块,适用于生产者包装其作为应用类型导出的模块
  • createRemoteAppComponent:用于加载应用级别模块,适用于消费者加载作为应用类型加载的模块
  • lazyLoadComponentPlugin: 用于注册 createLazyComponentprefetch API

React 版本兼容性

@module-federation/bridge-react 支持多个 React 主要版本,以确保在不同 React 版本的应用之间实现无缝集成:

  • React 16/17 (Legacy): 完全支持,使用传统 ReactDOM.renderReactDOM.unmountComponentAtNode API
  • React 18: 完全支持,使用新的 createRoothydrateRoot API
  • React 19: 完全支持,使用 createRoothydrateRoot API,但移除了对传统渲染方法的支持

版本特定导入

您可以根据项目中使用的 React 版本,选择特定的导入方式:

// Explicitly specify React 16/17 version (recommended for React 16/17)
import { createBridgeComponent } from '@module-federation/bridge-react';

// Explicitly specify React 18 version (recommended for React 18)
import { createBridgeComponent } from '@module-federation/bridge-react/v18';

// Explicitly specify React 19 version (recommended for React 19)
import { createBridgeComponent } from '@module-federation/bridge-react/v19';

这种多版本兼容性使得不同团队可以独立选择和升级他们的 React 版本,同时保持模块联邦集成的稳定性。

查看 Demo

安装

npm
yarn
pnpm
npm install @module-federation/bridge-react@latest

使用示例

导出应用类型模块

注意

使用 @module-federation/bridge-react 后不能将 react-router-dom 设置成 shared,否则构建工具将会提示异常。这是因为 @module-federation/bridge-react 通过代理 react-router-dom 实现了对路由的控制,以保证应用间路由能够正常协同工作。

在生产者项目中

假设我们需要将应用通过 @module-federation/bridge-react 导出为一个应用类型模块,应用入口为 App.tsx 文件:

步骤 1: 创建导出文件

首先,我们新建一个文件 export-app.tsx,该文件将作为应用类型模块导出的文件。我们需要使用 createBridgeComponent 来包装应用的根组件。

// ./src/export-app.tsx
import App from './src/App.tsx';
import { createBridgeComponent } from '@module-federation/bridge-react';

export default createBridgeComponent({
  rootComponent: App
});

步骤 2: 配置模块联邦

rsbuild.config.ts 配置文件中,我们需要将 export-app.tsx 作为应用类型模块导出:

// rsbuild.config.ts
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';

export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'remote1',
      exposes: {
        './export-app': './src/export-app.tsx',
      },
      shared: ['react', 'react-dom'],
    }),
  ],
});

至此,我们已经完成了应用类型模块的导出。

INFO

为什么需要使用 createBridgeComponent?

应用类型模块需要使用 createBridgeComponent 包装有三个主要原因:

  1. 支持跨框架渲染:使用 createBridgeComponent 包装的组件将符合应用类型消费者的加载协议,使得跨框架渲染成为可能。
  2. 自动注入 basename:使用 createBridgeComponent 包装的组件将自动注入 basename,确保生产者应用在消费者项目下正常工作。
  3. 包装 ErrorBoundary:使用 createBridgeComponent 包装的组件将被 ErrorBoundary 包装,确保在远程加载失败或渲染错误时自动进入 fallback 逻辑。

加载应用类型模块

在消费者项目中

步骤 1: 配置远程模块

rsbuild.config.ts 配置中,我们需要注册远程模块,这与其他模块联邦配置没有区别:

// rsbuild.config.ts
export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'host',
      remotes: {
        remote1: 'remote1@http://localhost:2001/mf-manifest.json',
      },
    }),
  ],
});

步骤 2: 加载远程应用

在消费者项目中,我们需要使用 createRemoteAppComponent 来加载应用类型模块:

// ./src/App.tsx
import { createRemoteAppComponent } from '@module-federation/bridge-react';
import { loadRemote } from '@module-federation/runtime';

// 定义错误回退组件
const FallbackErrorComp = ({ error }) => {
  return (
    <div>
      <h1>加载远程组件时出错</h1>
      <p>{error.message}</p>
      <button onClick={() => window.location.reload()}>
        重试
      </button>
    </div>
  );
};

// 定义加载中组件
const FallbackComp = <div data-test-id="loading">加载中...</div>;

// 使用 createRemoteAppComponent 创建远程组件
const Remote1App = createRemoteAppComponent({
  // loader 用于加载远程模块,例如:loadRemote('remote1/export-app'),import('remote1/export-app')
  loader: () => loadRemote('remote1/export-app'),
  // fallback 用于在远程模块加载失败时显示的组件
  fallback: FallbackErrorComp,
  // loading 用于在加载远程模块时显示的组件
  loading: FallbackComp,
});

const App = () => {
  return (
    <BrowserRouter basename="/">
      <Routes>
        <Route path="/" Component={Home} />
        <Route
          path="/remote1/*"
          // 使用 Remote1App 组件,将会被懒加载
          Component={() => (
            <Remote1App
              // 设置远程应用的 basename
              basename="/remote1"
              // 其他属性将传递给远程组件
              props1={'props_value'}
              props2={'another_props_value'}
            />
          )}
        />
      </Routes>
    </BrowserRouter>
  );
};
INFO

关于远程组件加载

  1. 通过 createRemoteAppComponent 导出的远程模块将自动使用 react-bridge 加载协议加载模块,使得应用的跨框架渲染成为可能。
  2. 此外,createRemoteAppComponent 将自动处理模块加载、模块销毁、错误处理、加载中、路由等逻辑,让开发者只需要关注如何使用远程组件。

注册 lazyLoadComponentPlugin

在运行时注册 lazyLoadComponentPlugin 插件,用于注册 createLazyComponentprefetch API。

Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { getInstance } from '@module-federation/runtime';
import { lazyLoadComponentPlugin } from '@module-federation/bridge-react';

const instance = getInstance();
instance.registerPlugins([lazyLoadComponentPlugin()]);

// 注册后,即可使用 `createLazyComponent`、`prefetch` API。

instance.prefetch({
  id: 'dynamic_remote'
});

const LazyComponent = instance.createLazyComponent({
  loader: () => loadRemote('dynamic_remote'),
  loading: 'loading...',
  fallback: ({ error }) => {
    if (error instanceof Error && error.message.includes('not exist')) {
      return <div>fallback - not existed id</div>;
    }
    return <div>fallback</div>;
  },
});

API 参考

createBridgeComponent

createBridgeComponent 用于导出远程 React 组件。

/**
 * 创建用于远程加载的 Bridge Component
 * @param bridgeInfo - Bridge Component配置信息
 * @returns 返回一个提供 render 和 destroy 方法的函数
 */
function createBridgeComponent<T>(
  bridgeInfo: ProviderFnParams<T>
): () => {
  render(info: RenderFnParams): Promise<void>;
  destroy(info: DestroyParams): Promise<void>;
};

/**
 * Bridge Component 配置信息
 */
interface ProviderFnParams<T> {
  /** 要远程加载的根组件 */
  rootComponent: React.ComponentType<T>;

  /**
   * 自定义渲染函数,用于自定义渲染逻辑
   * @param App - React 元素
   * @param id - DOM 元素或字符串 ID
   * @returns React 根元素或 Promise
   */
  render?: (
    App: React.ReactElement,
    id?: HTMLElement | string,
  ) => RootType | Promise<RootType>;

  /**
   * 自定义 createRoot 函数,用于 React 18 及以上版本
   * @param container - DOM 容器
   * @param options - CreateRoot 选项
   * @returns React 根节点
   */
  createRoot?: (
    container: Element | DocumentFragment,
    options?: CreateRootOptions,
  ) => Root;

  /**
   * React 18 和 19 的 createRoot 默认选项
   * 这些选项将在创建根节点时使用,除非在渲染参数中被 rootOptions 覆盖
   * @example
   * {
   *   identifierPrefix: 'app-',
   *   onRecoverableError: (err) => console.error(err)
   * }
   */
  defaultRootOptions?: CreateRootOptions;
}

createRemoteAppComponent

createRemoteAppComponent 用于加载远程 React 组件。

/**
 * 创建远程 React 组件
 * @param options - 远程组件配置选项
 * @returns 返回一个可以接收 props 并渲染远程组件的 React 组件
 */
function createRemoteAppComponent<T, E extends keyof T = 'default'>(
  options: RemoteComponentParams<T, E>
): React.ForwardRefExoticComponent<
  React.PropsWithoutRef<RemoteComponentProps> & React.RefAttributes<HTMLDivElement>
>;

/**
 * 远程组件配置参数
 */
interface RemoteComponentParams<
  T = Record<string, unknown>,
  E extends keyof T = keyof T
> {
  /**
   * 加载远程模块的函数
   * 例如:() => loadRemote('remote1/export-app') 或 () => import('remote1/export-app')
   */
  loader: () => Promise<T>;

  /** 加载远程模块时显示的组件 */
  loading: React.ReactNode;

  /** 加载或渲染远程模块失败时显示的错误组件 */
  fallback: React.ComponentType<{ error: Error }>;

  /**
   * 指定模块导出名称
   * 默认为 'default'
   */
  export?: E;
}

/**
 * 远程组件属性
 */
interface RemoteComponentProps<T = Record<string, unknown>> {
  /** 传递给远程组件的属性 */
  props?: T;

  /**
   * 内存路由配置,用于将子应用路由作为 memoryRouter 控制
   * 不会直接在浏览器地址栏中显示 URL
   */
  memoryRoute?: { entryPath: string };

  /** 基础路径名 */
  basename?: string;

  /** 样式 */
  style?: React.CSSProperties;

  /** 类名 */
  className?: string;
}

createLazyComponent

该 API 需要先注册 lazyLoadComponentPlugin 插件,才可以调用。

  Type declaration
declare function createLazyComponent(
  props: CreateLazyComponentOptions
): (props: ComponentType) => React.JSX.Element;

type CreateLazyComponentOptions<T, E extends keyof T> = {
  loader: () => Promise<T>;
  loading: React.ReactNode;
  delayLoading?: number;
  fallback: ReactNode | ((errorInfo: ErrorInfo) => ReactNode);
  export?: E;
  dataFetchParams?: DataFetchParams;
  noSSR?: boolean;
  injectScript?: boolean;
  injectLink?: boolean;
};

type ComponentType = T[E] extends (...args: any) => any
  ? Parameters<T[E]>[0] extends undefined
    ? Record<string, never>
    : Parameters<T[E]>[0]
  : Record<string, never>;

type DataFetchParams = {
  isDowngrade: boolean;
} & Record<string, unknown>;

type ErrorInfo = {
  error: Error;
  errorType: number;
  dataFetchMapKey?: string;
};

该函数除了支持加载组件之外,还支持下列能力:

  1. SSR 模式中会注入对应生产者的样式标签/脚本资源 ,此行为可以帮助避免流式渲染带来的 CSS 闪烁问题以及加速 PID (首屏可交互时间)。
  2. 如果生产者存在数据获取函数,那么会自动调用此函数并注入数据。
import React, { FC, memo, useEffect } from 'react';
import { getInstance } from '@module-federation/enhanced/runtime';
import { createLazyComponent, ERROR_TYPE } from '@module-federation/bridge-react';

const instance = getInstance();
const LazyComponent = instance.createLazyComponent({
  loader: () => import('remote/Image'),
  loading: <div>loading...</div>,
  fallback: ({error,errorType,dataFetchMapKey}) => {
    console.error(error)
    if(errorType === ERROR_TYPE.LOAD_REMOTE){
      return <div>load remote failed</div>
    }
    if(errorType === ERROR_TYPE.DATA_FETCH){
      return <div>data fetch failed, the dataFetchMapKey key is: {dataFetchMapKey}</div>
    }
    return <div>error type is unknown</div>;
  },
});

const App: FC = () => {
  return <>
    <LazyComponent />
  </>;
};
export default App;

loader

  • 类型:() => Promise<T>
  • 是否必填:是
  • 默认值:undefined

加载远程组件的函数,通常为 ()=>loadRemote(id) 或者 ()=>import(id)

loading

  • 类型:React.ReactNode
  • 是否必填:是
  • 默认值:undefined

设置模块载入状态。

delayLoading

  • 类型:number
  • 是否必填:否
  • 默认值:undefined

设置显示延迟加载时间,单位为毫秒,如果加载时间小于该时间,那么不会显示 loading 状态。

fallback

  • 类型:(({ error }: { error: ErrorInfo}) => React.ReactElement)
  • 是否必填:是
  • 默认值:undefined

当组件加载渲染失败时,所渲染的容错组件。

export

  • 类型:string
  • 是否必填:否
  • 默认值:'default'

如果远程组件是具名导出,那么可以通过此参数指定需要导出的组件名称,默认加载 default 导出。

dataFetchParams

  • 类型:DataFetchParams
  • 是否必填:否
  • 默认值:undefined

如果远程组件存在数据获取函数,设置后会传递给数据获取函数。

noSSR

  • 类型:boolean
  • 是否必填:否
  • 默认值:false

设置 true 后该组件不会在 SSR 场景渲染。

injectScript

  • 类型:boolean
  • 是否必填:否
  • 默认值:false

SSR 环境中,如果设置 true 后创建的组件会注入对应脚本资源 script。

例如 remote/button__federation_button.js ,那么在 SSR 返回的 html 中会在组件前面注入相应的 script ,来加速交互速度。

<script async src="__federation_button.js" crossOrigin="anonymous"/>
<button>remote button</button>
  • 类型:boolean
  • 是否必填:否
  • 默认值:true

SSR 环境中,如果设置 true 后创建的组件会注入对应样式资源 link。

例如 remote/button__federation_button.css ,那么在 SSR 返回的 html 中会在组件前面注入相应的 link ,来避免页面闪烁的问题。

<link href="__federation_button.css" rel="stylesheet" type="text/css">
<button>remote button</button>

prefetch

该 API 需要先注册 lazyLoadComponentPlugin 插件,才可以调用。

  Type declaration
type PrefetchOptions = {
  id: string;
  dataFetchParams?: DataFetchParams;
  preloadComponentResource?: boolean;
};
type DataFetchParams = {
  isDowngrade: boolean;
  _id?: string;
} & Record<string, unknown>;

预加载组件资源文件以及组件的 data loader 。

id

  • 类型:string
  • 是否必填:是
  • 默认值:undefined

预加载组件的 id 。

preloadComponentResource

  • 类型:boolean
  • 是否必填:否
  • 默认值:false

是否预加载组件的资源文件。

dataFetchParams

  • 类型:DataFetchParams
  • 是否必填:否
  • 默认值:undefined

如果远程组件存在数据获取函数,设置后会传递给数据获取函数。

使用示例

使用 export 指定模块导出

// 远程应用
export const provider = createBridgeComponent({
  rootComponent: App
});

// 宿主应用
const Remote1App = createRemoteAppComponent({
  loader: () => loadRemote('remote1/export-app'),
  export: 'provider', // 指定使用 provider 导出
  fallback: FallbackErrorComp,
  loading: FallbackComp,
});

使用 memoryRoute 控制路由

function App() {
  return (
    <BrowserRouter basename="/">
      <Routes>
        <Route
          path="/remote1/*"
          Component={() => (
            <Remote1App
              className={styles.remote1}
              style={{ color: 'red' }}
              // 使用 memoryRoute 将子应用路由作为 memoryRouter 控制
              // 不会直接在浏览器地址栏中显示 URL
              memoryRoute={{ entryPath: '/detail' }}
              // 其他属性将传递给远程组件
              props1={'props_value'}
              props2={'another_props_value'}
              ref={ref}
            />
          )}
        />
      </Routes>
    </BrowserRouter>
  );
}

为 React 18 和 19 配置 createRoot 选项

对于 React 18 和 19,@module-federation/bridge-react 支持向 createRoot 方法传递选项,允许您自定义根组件的行为:

创建bridge component时设置默认选项

import { createBridgeComponent } from '@module-federation/bridge-react';

export default createBridgeComponent({
  rootComponent: App,
  // 为使用此组件的所有实例设置默认 createRoot 选项
  defaultRootOptions: {
    identifierPrefix: 'my-app-',
    onRecoverableError: (error) => {
      console.error('可恢复的渲染错误:', error);
    }
  }
});

在渲染时设置选项

// 在消费者应用中
const RemoteApp = createRemoteAppComponent({
  url: 'http://localhost:3001/remoteEntry.js',
  scope: 'remote',
  module: './App',
});

// 在渲染时传递 rootOptions
<RemoteApp
  props={{ message: 'Hello' }}
  rootOptions={{
    identifierPrefix: 'instance-',
    onRecoverableError: (error) => {
      console.error('此实例的可恢复错误:', error);
    }
  }}
/>

渲染时传递的选项将覆盖创建组件时设置的默认选项。