数据获取

TIP

Data Loader 同时支持 SSR 和 CSR 两种场景!

简介

SSR 场景中, useEffect 不会执行,这一行为导致常规情况下,无法先获取数据再渲染组件。

为了支持这一功能,主流框架一般会基于 React Router 提供的 data loader 预取数据,并注入给路由组件,路由组件通过 useLoaderData 获取数据并渲染。

这一行为强依赖路由功能,在 Module Federation 下就无法正常使用。

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

props.name || 'Module Federation

的使用大体分为两种部分:组件(函数)、应用。两者的区别在于是否带有路由功能。

如何使用

根据角色的不同,需要做不同的行为。

生产者

注意

生产者可以使用 RslibModern.js 来生成组件。

不过需要注意的是,因为「数据获取」中的数据由消费者注入。因此如果在组件中使用了「数据获取」,那么其导出的非 MF 组件无法与 MF 组件保持同构。

每个 expose 模块都可以有一个同名的 .data 文件。这些文件可以导出一个 loader 函数,我们称为 Data Loader,它会在对应的 expose 组件渲染之前执行,为组件提供数据。如下面示例:

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

其中 List.data.ts 需要导出名为 fetchData 的函数,该函数将会在 List 组件渲染前执行,并将其数据注入,示例如下:

List.data.ts
import type { DataFetchParams } from '@module-federation/bridge-react';
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);
  });
};

loader 函数数据会被注入到生产者的 props 中,key 为 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;

生产者消费自身数据

如果使用了 Modern.js 来开发生产者,并且该生产者页面也会单独访问。那么可以利用 Modern.js 提供的 Data Loader 来注入数据。

其用法与 Module Federation 除了函数名称外,基本一致。因此你可以很方便的在生产者消费 Data Loader,示例如下:

  • 在生产者页面创建 page.data.ts 文件,导出名为 loader 的函数:
page.data.ts
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;

消费者

注意

SSR 场景中,只能在 Modern.js 中使用。

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

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

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

const List = instance.createLazyComponent({
  loader: () => {
    return 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, the dataFetchMapKey key is: {dataFetchMapKey}
        </div>
      );
    }
    return <div>error type is unknown</div>;
  },
});

const Index = (): JSX.Element => {
  return (
    <div>
      <h1>Basic usage with data fetch</h1>
      <List />
    </div>
  );
};

export default Index;

loader 函数

入参

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

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

除了默认的参数以外,还可以在 createLazyComponent 中传递 dataFetchParams 字段,该字段会被透传给 loader 函数。

返回值

loader 函数的返回值只能是可序列化的数据对象

在不同环境使用 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 函数。

NOTE

SSR 应用的 loader 函数只在服务端执行可以带来以下好处:

简化使用方式:保证 SSR 应用获取数据的方式是同构的,开发者无需根据环境区分 loader 函数执行的代码。 减少浏览器端 bundle 体积:将逻辑代码及其依赖,从浏览器端移动到了服务端。 提高可维护性:将逻辑代码移动到服务端,减少了数据逻辑对前端 UI 的直接影响。此外,也避免了浏览器端 bundle 中误引入服务端依赖,或服务端 bundle 中误引入浏览器端依赖的问题。

在 SSR 应用中使用 Client Loader

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

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

Module Federation 支持在 SSR 应用中额外添加 .data.client 文件,同样具名导出 loader。此时 SSR 应用在服务端执行 Data Loader 报错降级,或浏览器端切换路由时,会像 CSR 应用一样在浏览器端执行该 loader 函数,而不是再向 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 文件约定,不能是 .loader 文件约定。

FAQ

应用级别数据获取?

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

是否支持嵌套生产者?

不支持。

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

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