React Bridge

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

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

查看 Demo

安装

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

示例

导出应用类型模块

DANGER

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

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

  • Step1: 首先,我们新建一个文件 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
});
  • Step2: 在 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 包装?原因主要有三点:

  1. 支持跨框架渲染。通过 createBridgeComponent 包装的组件将符合应用类型消费方的加载协议,这使得跨框架渲染成为可能
  2. 自动注入 basename。通过 createBridgeComponent 包装的组件将自动注入 basename,这能保证生产者应用在消费者项目下也能正常工作
  3. 包裹 ErrorBoundary。通过 createBridgeComponent 包装的组件将包裹 ErrorBoundary,以保证在远程加载失败或渲染出错时,能够自动进入兜底逻辑

加载应用类型模块

Host

  • Step1: 在 rsbuild.config.ts 配置中,我们需要注册远程模块,这点与其它 Module Federation 配置无异。
// rsbuild.config.ts
export default defineConfig({
  plugins: [
    pluginReact(),
    pluginModuleFederation({
      name: 'host',
      remotes: {
        remote1: 'remote1@http://localhost:2001/mf-manifest.json',
      },
    }),
  ],
});
  • Step2: 在消费者项目中,我们需要加载应用类型模块,我们使用 createRemoteComponent 来加载应用类型模块
// ./src/App.tsx
import React from 'react';
import { createRemoteComponent } from '@module-federation/bridge-react';
import styles from './index.module.less';

// 定义 FallbackErrorComp 组件
const FallbackErrorComp = (info: any) => {
  return (
    <div>
      <h2>This is ErrorBoundary Component</h2>
      <p>Something went wrong:</p>
      <pre style={{ color: 'red' }}>{info?.error.message}</pre>
      <button onClick={() => info.resetErrorBoundary()}>
        resetErrorBoundary(try again)
      </button>
    </div>
  );
};

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

// 使用 createRemoteComponent 导出远程组件
const Remote1App = createRemoteComponent({
  // 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
              // 可设置 className 和 style 样式,将自动注入到组件上
              className={styles.remote1}
              style={{ color: 'red' }}
              // name 和 age 为远程组件 props, 将自动透传到远程组件
              name={'Ming'}
              age={12}
              // 可设置 ref, 将自动转发到远程组件,可获取 ref 对象操作 dom
              ref={ref}
            />
          )}
        />
      </Routes>
    </BrowserRouter>)
};

至此,我们完成了应用类型模块的加载。

INFO
  1. 通过 createRemoteComponent 导出的远程模块将会自动使用 react-bridge 加载协议加载模块, 这使得应用的跨框架渲染成为可能。

  2. 此外,createRemoteComponent 会自动处理模块加载、模块销毁、错误处理、loading、路由 等逻辑, 开发者只需要关注如何使用远程组件即可。

  3. 通过 createRemoteComponent 导出的远程模块,你可以像使用普通 React 组件一样使用远程组件:传递 className、style、props、ref 等属性均会自动透传到远程组件, 这使得用户在体验上几乎等同于使用本地组件

方法

createBridgeComponent

export declare function createBridgeComponent<T>(bridgeInfo: ProviderFnParams<T>): () => {
    render(info: RenderFnParams): Promise<void>;
    destroy(info: {
        dom: HTMLElement;
    }): Promise<void>;
};

type ProviderFnParams<T> = {
  rootComponent: React.ComponentType<T>;
  render?: (
    App: React.ReactElement,
    id?: HTMLElement | string,
  ) => RootType | Promise<RootType>;
};

export declare interface RenderFnParams extends ProviderParams {
    dom: HTMLElement;
}

export declare interface ProviderParams {
    moduleName?: string;
    basename?: string;
    memoryRoute?: {
        entryPath: string;
    };
    style?: React.CSSProperties;
    className?: string;
}
  • bridgeInfo
    • type:
type ProviderFnParams<T> = {
  rootComponent: React.ComponentType<T>;
  render?: (
    App: React.ReactElement,
    id?: HTMLElement | string,
  ) => RootType | Promise<RootType>;
};
  • 作用: 用于传递根组件
  • ReturnType
    • type:

      () => {
        render(info: {
          moduleName?: string;
          basename?: string;
          memoryRoute?: {
            entryPath: string;
          };
          style?: React.CSSProperties;
          className?: string;
          dom?: HTMLElement;
      }): Promise<void>;
        destroy(info: { dom: HTMLElement}): Promise<void>;
      }

createRemoteComponent

import { createRemoteComponent } from '@module-federation/bridge-react';
import type { ProviderParams } from '@module-federation/bridge-react';

function createRemoteComponent<T, `E extends keyof T`>(
  options: {
    // 加载远程应用的函数,例如:loadRemote('remote1/export-app')、import('remote1/export-app')
    loader: () => Promise<T>,
    // 默认为 default,用于指定模块的 export
    export?: E;
    loading: React.ReactNode;
    fallback: ComponentType<{ error: any; }>;
  }
): (props: {
    basename?: ProviderParams['basename'];
    memoryRoute?: { entryPath: string };
} & RawComponentType) => React.JSX.Element;
  • options
    • loader
      • type: () => Promise<Module>
      • 作用: 用于加载远程模块的函数,例如:loadRemote('remote1/export-app')import('remote1/export-app')
const Remote1App = createRemoteComponent({
  // loader 用于加载远程模块,例如:loadRemote('remote1/export-app')、import('remote1/export-app')
  loader: () => loadRemote('remote1/export-app'),
  // fallback 用于在加载远程模块失败时展示的组件
  fallback: FallbackErrorComp,
  // loading 用于在加载远程模块时展示的组件
  loading: FallbackComp,
});

const Remote2App = createRemoteComponent({
  // loader 用于加载远程模块,例如:loadRemote('remote2/export-app')、import('remote2/export-app')
  loader: () => import('remote2/export-app'),
  // fallback 用于在加载远程模块失败时展示的组件
  fallback: FallbackErrorComp,
  // loading 用于在加载远程模块时展示的组件
  loading: FallbackComp,
});
  • export
    • type: string
    • 作用: 可以指定模块的 export
// remote
export const provider = createBridgeComponent({
  rootComponent: App
});

// host
const Remote1App = createRemoteComponent({
  loader: () => loadRemote('remote1/export-app'),
  export: 'provider'
});
  • loading

    • type: React.ReactNode
    • 作用: 加载远程模块时显示的组件
  • fallback

    • type: ComponentType<{ error: any; }>
    • 作用: 加载、渲染远程模块过程中展示的错误
  • ReturnType

    • type: (props: PropsInfo)=> React.JSX.Element
    • 作用: 用于渲染远程模块组件
const Remote1App = createRemoteComponent({
  // loader 用于加载远程模块,例如:loadRemote('remote1/export-app')、import('remote1/export-app')
  loader: () => loadRemote('remote1/export-app'),
  // fallback 用于在加载远程模块失败时展示的组件
  fallback: FallbackErrorComp,
  // loading 用于在加载远程模块时展示的组件
  loading: FallbackComp,
});


function App() {
  return (<BrowserRouter basename="/">
    <Routes>
     <Route
          path="/remote1/*"
          Component={() => (
            <Remote1App
              className={styles.remote1}
              props1={'props_value'}
              props2={'another_props_value'}
              ref={ref}
              {/* 通过 memoryRoute 来将子应用路由控制为 memoryRouter,将不会直接将 url 展示在浏览器地址上  */}
              memoryRoute={{ entryPath: '/detail' }}
            />
          )}
        />
    </Routes>
  </BrowserRouter>)
}