logologo
指南
实践
配置
插件
案例
博客
生态
Module Federation Examples
Practical Module Federation
Zephyr Cloud
Nx
简体中文
English
指南
实践
配置
插件
案例
博客
Module Federation Examples
Practical Module Federation
Zephyr Cloud
Nx
简体中文
English
logologo
概览

Bridge

Bridge 介绍

React Bridge

快速开始
导出应用
加载应用
加载模块
Vue Bridge

框架

框架概览

React

Basic CRA with Rsbuild
国际化 (i18n)

Modern.js

快速开始
动态加载生产者

Next.js

Basic Example
导入组件
路由和导入页面
使用 Express.js
预设

Angular

Angular CLI 设置
Micro-frontends with Angular
服务端渲染
使用 Service Workers
Authentication with Auth0
Authentication with Okta
拆分巨石应用
改造巨石应用
Edit this page on GitHub
Next Page概览

#Data Prefetch

WARNING

该功能暂不支持 Rspack 生产者项目使用

推荐使用 bridge-react - prefetch,该方案支持 Rspack/Webpack,并且支持 SSR/CSR 。

#什么是 Data Prefetch

Data Prefetch 可以将远程模块接口请求提前到 remoteEntry 加载完成后立即发出,无需等到组件渲染,提升首屏速度。

#适用场景

项目首屏前置流程较长(例如需要鉴权等操作)或次屏希望数据直出的场景

  • 消费者常规加载流程:

Host HTML(消费者 HTML) -> Host main.js(消费者入口 js) -> Host fetch(消费者鉴权等前置动作) -> Provider main.js(生产者入口 js) -> Provider fetch(生产者发送请求)

  • 使用 Prefetch 后的加载流程
  • 在前置流程中提前调用 loadRemote 的加载流程(loadRemote 会将有 prefetch 需求的生产者接口请求同步发送)

可以看到生产者的请求被提前到跟部分 js 并行了,目前对于首屏的优化效果取决于项目加载流程,次屏理论上可以通过提前调用 loadRemote 做到直出,在前置流程长的情况下可以大大提升模块整体的渲染速度

#使用方法

  1. 给生产者和消费者安装 @module-federation/enhanced 包
npm
yarn
pnpm
npm install @module-federation/enhanced
  1. 给生产者的 expose 模块目录同级增加 .prefetch.ts(js) 文件,假如有如下 exposes
rsbuild(webpack).config.ts
new ModuleFederationPlugin({
  exposes: {
    '.': './src/index.tsx',
    './Button': './src/Button.tsx',
  },
  // ...
})

此时生产者项目有 . 和 ./Button 两个 exposes, 那么我们可以在 src 下新建 index.prefetch.ts 和 Button.prefetch.ts 两个 prefetch 文件,拿 Button 举例

注意 export 的函数必须以 default 导出或 Prefetch 结尾才会被识别成 Prefetch 函数(不区分大小写)

Button.prefetch.ts
// 这里通过使用 react-router-dom 提供的 defer API 举例,是否要使用这个 API 可以根据需求决定,参考问题解答「为什么要使用 defer、Suspense、Await 组件」
// 用户可以通过 npm install react-router-dom 来安装这个包
import { defer } from 'react-router-dom';

const defaultVal = {
  data: {
    id: 1,
    title: 'A Prefetch Title',
  }
};

// 注意 export 的函数必须以 default 导出或 Prefetch 结尾才会被识别成 Prefetch 函数(不区分大小写)
export default (params = defaultVal) => defer({
  userInfo: new Promise(resolve => {
    setTimeout(() => {
      resolve(params);
    }, 2000);
  })
})

在 Button 中使用

Button.tsx
import { Suspense } from 'react';
import { usePrefetch } from '@module-federation/enhanced/prefetch';
import { Await } from 'react-router-dom';

interface UserInfo {
  id: number;
  title: string;
};
const reFetchParams = {
  data: {
    id: 2,
    title: 'Another Prefetch Title',
  }
}
export default function Button () {
  const [prefetchResult, reFetchUserInfo] = usePrefetch<UserInfo>({
    // 对应生产者 ModuleFederationPlugin 中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`
    id: 'app2/Button',
    // 可选参数,使用 defer 后必填
    deferId: 'userInfo',
    // default 导出默认可以不传 functionId,此处为举例说明,如果非 default 导出则需要填函数名,
    // functionId: 'default',
  });

  return (
    <>
      <button onClick={() => reFetchUserInfo(reFetchParams)}>重新发送带参数的请求</button>
      <Suspense fallback={<p>Loading...</p>}>
        <Await
          resolve={prefetchResult}
          children={userInfo => (
            <div>
              <div>{userInfo.data.id}</div>
              <div>{userInfo.data.title}</div>
            </div>
          )}
        ></Await>
      </Suspense>
    </>
  )
};
  1. 生产者的 ModuleFederationPlugin 配置处设置 dataPrefetch: true
  new ModuleFederationPlugin({
    // ...
    dataPrefetch: true
  }),
  1. 消费者的 ModuleFederationPlugin 配置处设置 dataPrefetch: true
  new ModuleFederationPlugin({
    // ...
    dataPrefetch: true
  }),

这样就完成了接口预请求,在 Button 被消费者使用后会提前将接口请求发送出去(加载 js 资源时就会发出,正常项目需要等到组件渲染时), 在上面的例子中 Button 首先会渲染 loading..., 然后在 2s 后展示数据 点击重新发送带参数的请求可以重新触发请求并且添加参数,用于更新组件

#查看优化效果

在浏览器控制台中打开日志模式查看输出(最好使用浏览器缓存模式模拟用户场景,否则数据可能不准确) 默认优化效果是数据3减去数据1(模拟用户在 useEffect 中发送请求),如果你的请求不是在 useEffect 中发送,那么可以在接口执行处手动调用 performance.now() 来减去数据1

localStorage.setItem('FEDERATION_DEBUG', 1)

#API

#usePrefetch

#功能

  • 用于获取预请求数据结果以及控制重新发起请求

#类型

type Options: <T>{
  id: string; // 必填,对应生产者 MF 配置中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`。
  functionId?: string; // 可选(默认是 default),用于获取 .prefetch.ts 文件中 export 的函数名称,函数需要以 Prefetch 结尾(不区分大小写)
  deferId?: string; // 可选(使用 defer 后必填),使用 defer 后函数返回值为对象(对象中可以有多个 key 对应多个请求),deferId 为对象中的某个 key,用于获取具体请求
  cacheStrategy?: () => boolean; // 可选,一般不用手动管理,由框架管理,用于控制是否更新请求结果缓存,目前在组件卸载后或手动执行 reFetch 函数会刷新缓存
} => [
  Promise<T>,
  reFetch: (refetchParams?: refetchParams) => void, // 用于重新发起请求,常用于组件内部状态更新后接口需要重新请求获取数据的场景,调用此函数会重新发起请求并更新请求结果缓存
];

type refetchParams: any; // 用于组件内重新发起请求携带参数

#使用方法

import { Suspense } from 'react';
import { usePrefetch } from '@module-federation/enhanced/prefetch';
import { Await } from 'react-router-dom';

export const Button = () => {
  const [userInfoPrefetch, reFetchUserInfo] = usePrefetch<UserInfo>({
    // 对应生产者 MF 配置中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`
    id: 'app2/Button',
    // 可选参数,使用 defer 后必填
    deferId: 'userInfo'
    // default 导出默认可以不传 functionId,此处为举例说明,如果非 default 导出则需要填函数名,
    // functionId: 'default',
  });

  return (
    <>
      <button onClick={() => reFetchUserInfo(reFetchParams)}>重新发送带参数的请求</button>
      <Suspense fallback={<p>Loading...</p>}>
        <Await
          resolve={prefetchResult}
          children={userInfo => (
            <div>
              <div>{userInfo.data.id}</div>
              <div>{userInfo.data.title}</div>
            </div>
          )}
        ></Await>
      </Suspense>
    <>
  )
}

#loadRemote

#功能

用户如果在消费者项目中手动调用 loadRemote API,那么会认为消费者不仅希望加载生产者的静态资源,同时希望将接口请求也提前发出,这可以让项目获得更快的渲染速度, 这在首屏有前置请求场景或者希望次屏直出这些场景中尤其有效,适用于在当前页面提前加载次屏模块的场景

#使用方法

import { loadRemote } from '@module-federation/enhanced/runtime';

loadRemote('app2/Button');

#注意

注意这可能会导致数据缓存问题,即生产者会优先使用预请求的接口结果(用户可能通过交互操作已经修改了服务端的数据),这种情况下会导致渲染使用到过时的数据,请按照项目情况合理使用

#问题解答

#1. 和 React Router v6 的 Data Loader 有区别吗

React Router 的 Data Loader 只能给单体项目使用,即跨项目无法复用,同时 Data Loader 是按路由绑定的,而非按模块(expose)绑定的,Data Prefetch 更加适合远程加载的场景

#2. 为什么要使用 defer、Suspense、Await 组件?参考链接

defer 和 Await 组件是 React Router v6 提供的用于数据加载和渲染 loading 时使用的 API 和组件,二者通常配合 React 的 Suspense 来完成:渲染 loading -> 渲染内容 的过程。可以看到 defer 返回的是一个对象,在执行 Prefetch 函数时这个对象中所有 key 对应的请求(也就是 value) 会一次性全部发送出去,而 defer 会追踪这些 Promise 的状态,配合 Suspense 和 Await 完成渲染,同时这些请求不会阻塞组件的渲染(在组件渲染完成前会展示 loading...)

#3. 能不能不使用 defer、Suspense 和 Await 这一套?

可以,但是如果 export 的函数中有阻塞函数执行操作的话(例如有 await 或返回是 Promise),会让组件等待函数执行完成后再渲染,即使组件内容早已加载出来,但组件仍然可能会等待接口完成再渲染,例如

export default (params) => (
  new Promise(resolve => {
    setTimeout(() => {
      resolve(params);
    }, 2000);
  })
)

#4. 为什么不默认全部 defer?

让开发者场景更加可控,在某些场景下开发者更希望用户一次性看到完整的页面,而非渲染 loading,这可以让开发者更好的权衡场景,参考

#5. Prefetch 能不能携带参数?

首次请求由于请求时间和 js 资源加载属于并行,所以不支持从组件内部传递参数,可以手动设置默认值。而 usePrefetch 会返回 reFetch 函数,此函数用于在组件内部重新发送请求更新数据,此时可以携带参数

#6. 业务想最小成本改造如何操作?

  1. 将需要 prefetch 的接口放到 .prefetch.ts 中
  2. prefetch 函数用 defer 包裹返回一个对象(也可以直接返回一个对象,如果返回一个值的话会和组件 js 的加载互相 await 阻塞)
  3. 业务组件中一般是在 useEffect 中发请求:
import { useState } from 'react';
import { usePrefetch } from '@module-federation/enhanced/prefetch';

export const Button = () => {
  const [state, setState] = useState(defaultVal);
  const [userInfoPrefetch, reFetchUserInfo] = usePrefetch<UserInfo>({
    // 对应生产者 MF 配置中的 (name + expose),例如 `app2/Button` 用于消费 `Button.prefetch.ts`
    id: 'app2/Button',
    // 可选参数,使用 defer 后必填
    deferId: 'userInfo',
    // default 导出默认可以不传 functionId,此处为举例说明,如果非 default 导出则需要填函数名,
    // functionId: 'default',
  });

  useEffect(() => {
    // 常规场景一般在这里发请求
    userInfoPrefetch
      .then(data => (
        // 更新数据
        setState(data)
      ));
  }, []);

  return (
    <>{state.defaultVal}<>
  )
}

#7. 为什么我定义了 Prefetch 函数但是不生效?

注意 export 的函数必须以 default 导出或 Prefetch 结尾才会被识别成 Prefetch 函数(不区分大小写)

#8. 模块在次屏可以优化性能吗

可以,并且效果比较明显,由于 Data Prefetch 是针对所有 expose 模块的,所以次屏模块也可以优化性能

import { loadRemote } from '@module-federation/enhanced/runtime';

loadRemote('app2/Button');

#9. Vue 或其他框架想用怎么办

我们提供了通用的 Web API,只是未在 Vue 等其他框架中提供类似 usePrefetch 这种 hook,后续会进行支持