数据获取

远程模块通常希望能独立完成数据获取:消费者只负责加载和渲染,无需了解远程模块依赖哪些数据,也避免每个消费它的应用重复实现取数逻辑。

在 CSR 场景下,远程模块直接在组件内通过 useEffect 发起请求即可。但在 SSR 场景下 useEffect 不会执行,远程模块就无法在服务端先拿到数据再渲染。

主流的 SSR 框架(如 Next.js、Remix、Modern.js 等)通常会在路由层面提供数据预取能力:路由命中时先执行数据加载函数拿到数据,再注入对应的路由组件参与渲染。但这套机制依赖框架自身的路由系统,Module Federation 暴露的模块并不在宿主的路由树中,无法直接复用。

为了解决这一问题,Module Federation 提供了组件级别数据获取能力,以便开发者可以在 SSR 场景下获取数据并渲染组件。

什么是组件级别?

Module Federation 暴露的模块大体分为两类:

  • 组件:独立的 UI 组件、工具函数等,不带路由系统
  • 应用:带有完整路由的子应用,通常通过 Bridge 暴露和消费

本文介绍的能力面向前者,即「组件级别」数据获取。你可以参考 Demo 来了解详细的使用方式。

如何使用

数据获取能力同时支持 SSR 和 CSR 两种场景,下面分为生产者和消费者两部分来介绍:

生产者

Tip

生产者可以使用 RslibModern.js

数据获取文件约定

数据获取通过文件约定实现:

  • 每个 expose 模块都可以配套一个同名的 .data 文件。
  • .data 文件中导出的函数被称为 Data Loader,构建插件会识别这个约定。
  • 当 expose 组件被加载时,运行时会先调用 Data Loader 拿到数据,把数据注入组件后再渲染。

文件结构示例:

.
└── src
    ├── List.tsx
    └── List.data.ts

约定文件中需要导出名为 fetchData 的函数,该函数将会在远程组件渲染前执行:

List.data.ts
import type { DataFetchParams } from '@module-federation/bridge-react/data-fetch';
export type Data = {
  data: string;
};

export const fetchData = async (params: DataFetchParams): Promise<Data> => {
  console.log('params: ', params);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        data: `data: ${new Date()}`,
      });
    }, 1000);
  });
};

函数会在组件渲染前被调用,运行时会把这个数据对象注入到组件 的 props 中,默认字段名为 mfData

List.tsx
import React from 'react';
import type { Data } from './index.data';

const List = (props: {
  mfData?: Data;
}): JSX.Element => {
  return (
    <div>
     {props.mfData?.data && props.mfData?.data.map((item,index)=><p key={index}>{item}</p>)}
    </div>
  );
};

export default List;

Loader 函数默认传参

默认会往 loader 函数传递参数,其类型为 DataFetchParams,包含以下字段:

  • isDowngrade:表示当前执行上下文是否处于降级模式。例如,在服务端渲染失败,在浏览器端渲染时会重新往服务端发起请求,调用 loader 函数,此时该值为 true

在不同环境使用 Data Loader

loader 函数可能会在服务端或浏览器端执行。在服务端执行的 loader 函数,我们称为 Server Loader,在浏览器端执行的称为 Client Loader。

在 CSR 应用中,loader 函数会在浏览器端执行,即默认都是 Client Loader。

在 SSR 应用中,loader 函数只会在服务端执行,即默认都是 Server Loader。在 SSR 应用中,Module Federation 会直接在服务端调用对应的 loader 函数。在浏览器端切换路由时,Module Federation 会发送一个 http 请求到 SSR 服务,同样在服务端触发 loader 函数。

SSR 应用的 loader 函数只在服务端执行的好处
  • 简化使用方式:保证 SSR 应用获取数据的方式是同构的,开发者无需根据环境区分 loader 函数执行的代码。

  • 减少浏览器端 bundle 体积:将逻辑代码及其依赖,从浏览器端移动到了服务端。

  • 提高可维护性:将逻辑代码移动到服务端,减少了数据逻辑对前端 UI 的直接影响。此外,也避免了浏览器端 bundle 中误引入服务端依赖,或服务端 bundle 中误引入浏览器端依赖的问题。

在 SSR 应用中使用 Client Loader

默认情况下,在 SSR 应用中,loader 函数只会在服务端执行。但有些场景下,开发者可能期望在浏览器端发送的请求不经过 SSR 服务,直接请求数据源,例如:

  1. 在浏览器端希望减少网络消耗,直接请求数据源。
  2. 应用在浏览器端有数据缓存,不希望请求 SSR 服务获取数据。

Module Federation 支持在 SSR 应用中额外添加 .data.client 文件,同样具名导出。此时,SSR 应用在服务端执行 Data Loader 报错降级,或浏览器端切换路由时,会像 CSR 应用一样在浏览器端执行该函数,而不是再向 SSR 服务发送数据请求。

List.data.client.ts
import cache from 'my-cache';

export async function loader({ params }) {
  if (cache.has(params.id)) {
    return cache.get(params.id);
  }
  const res = await fetch('URL_ADDRESS?id={params.id}');
  return {
    message: res.message,
  }
}
WARNING

要使用 Client Loader,必须有对应的 Server Loader,且 Server Loader 必须是 .data 文件约定。

生产者消费自身应用数据

生产者本身也可能作为独立页面被访问。这时它需要在两种场景下都能取到数据:

  • 被消费者加载时由 Data Loader 注入
  • 独立访问时由生产者自己的框架机制取数

例如,如果生产者使用 Modern.js,它既要支持被消费者加载获取数据,也要支持在生产者页面独立访问获取数据。这时可以通过以下方式实现:

  • 在生产者页面创建 page.data.ts 文件,导出名为 loader 的函数:
Info

Modern.js 约定 page.data.ts 中导出 loader 函数来 获取数据

page.data.ts
// 从 List.data.ts 中复用 fetchData 函数作为 loader 函数的实现
import { fetchData } from '../components/List.data';
import type { Data } from '../components/List.data';

export const loader = fetchData

export type {Data}
  • 在生产者 page 页面消费此数据:
page.tsx
import { useLoaderData } from '@modern-js/runtime/router';
import List from '../components/List';
import './index.css';

import type { Data } from './page.data';

const Index = () => {
  const data = useLoaderData() as Data;
  console.log('page data', data);

  return (
    <div className="container-box">
      <List mfData={data} />
    </div>
)};

export default Index;

消费者

注意

目前仅 Modern.js 支持在 SSR 环境下使用数据获取能力。

在消费者中,我们需要通过 createLazyComponent API 来加载远程组件,并获取数据。

import { getInstance } from '@module-federation/enhanced/runtime';
import { ERROR_TYPE } from '@module-federation/bridge-react/data-fetch';
import { lazyLoadComponentPlugin } from '@module-federation/bridge-react/data-fetch';

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

const List = instance.createLazyComponent({
  loader: () => import('remote/List'),
  loading: 'loading...',
  export: 'default',
  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: {dataFetchMapKey}</div>;
    }
    return <div>error type is unknown</div>;
  },
});

export default function Index() {
  return <List />;
}

传递自定义参数

除了运行时默认注入的数据结构,消费者还可以通过 createLazyComponentdataFetchParams 字段向 loader 函数透传自定义参数,例如用户标识、语言、租户等宿主侧上下文。

该字段会与默认参数合并后传入 loader。

FAQ

应用级别数据获取?

应用级别的模块,我们更希望使用 RSC 来实现,使其功能更加的完善。目前该功能正在探索中,敬请期待。

是否支持嵌套生产者?

不支持。

生产者除了使用 Rslib 插件或者 Modern.js 插件外还有其他的插件吗?

暂时只有 Rslib 插件和 Modern.js 插件才可以创建 Data Loader。