# 数据获取
:::tip
Data Loader 同时支持 SSR 和 CSR 两种场景!
- [Demo](https://github.com/module-federation/core/tree/main/apps/modern-component-data-fetch)
:::
## 简介
SSR 场景中, `useEffect` 不会执行,这一行为导致常规情况下,无法先获取数据再渲染组件。
为了支持这一功能,主流框架一般会基于 React Router 提供的 `data loader` 预取数据,并注入给路由组件,路由组件通过 [useLoaderData](https://reactrouter.com/api/hooks/useLoaderData#useloaderdata) 获取数据并渲染。
这一行为强依赖路由功能,在 {props.name || 'Module Federation'} 下就无法正常使用。
为了解决这一问题,Module Federation 提供了**组件**级别数据获取能力,以便开发者可以在 SSR 场景下获取数据并渲染组件。
:::tip 什么是组件级别?
Module Federation 使用大体分为两种部分:组件(函数)、应用。两者的区别在于是否带有_路由_功能。
:::
## 如何使用
根据角色的不同,需要做不同的行为。
### 生产者
{props.providerTip || React.createElement(ProviderTip)}
每个 expose 模块都可以有一个同名的 `.data` 文件。这些文件可以导出一个 loader 函数,我们称为 Data Loader,它会在对应的 `expose` 组件渲染之前执行,为组件提供数据。如下面示例:
```bash
.
└── src
├── List.tsx
└── List.data.ts
```
其中 `List.data.ts` 需要导出名为 `fetchData` 的函数,该函数将会在 `List` 组件渲染前执行,并将其数据注入,示例如下:
```ts title="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`,因此生产者需要改动代码,消费此数据,示例如下:
```tsx title="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](https://modernjs.dev/zh/guides/basic-features/data/data-fetch.html) 来注入数据。
其用法与 {props.name || 'Module Federation'} 除了函数名称外,基本一致。因此你可以很方便的在生产者消费 Data Loader,示例如下:
- 在生产者页面创建 `page.data.ts` 文件,导出名为 `loader` 的函数:
```ts title="page.data.ts"
import { fetchData } from '../components/List.data';
import type { Data } from '../components/List.data';
export const loader = fetchData
export type {Data}
```
- 在生产者 `page` 页面消费此数据:
```tsx title="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;
```
### 消费者
{props.consumer || React.createElement(Consumer)}
#### loader 函数
##### 入参
默认会往 loader 函数传递参数,其类型为 [DataFetchParams](/zh/practice/bridge/react-bridge/load-component.md#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 {props.name || 'Module Federation'} 会直接在服务端调用对应的 loader 函数。在浏览器端切换路由时,Module Federation 会发送一个 http 请求到 SSR 服务,同样在服务端触发 loader 函数。
:::info
SSR 应用的 loader 函数只在服务端执行可以带来以下好处:
- 简化使用方式:保证 SSR 应用获取数据的方式是同构的,开发者无需根据环境区分 loader 函数执行的代码。
- 减少浏览器端 bundle 体积:将逻辑代码及其依赖,从浏览器端移动到了服务端。
- 提高可维护性:将逻辑代码移动到服务端,减少了数据逻辑对前端 UI 的直接影响。此外,也避免了浏览器端 bundle 中误引入服务端依赖,或服务端 bundle 中误引入浏览器端依赖的问题。
:::
##### 在 SSR 应用中使用 Client Loader
默认情况下,在 SSR 应用中,loader 函数只会在服务端执行。但有些场景下,开发者可能期望在浏览器端发送的请求不经过 SSR 服务,直接请求数据源,例如:
1. 在浏览器端希望减少网络消耗,直接请求数据源。
2. 应用在浏览器端有数据缓存,不希望请求 SSR 服务获取数据。
{props.name || 'Module Federation'} 支持在 SSR 应用中额外添加 .data.client 文件,同样具名导出 loader。此时 SSR 应用在服务端执行 Data Loader 报错降级,或浏览器端切换路由时,会像 CSR 应用一样在浏览器端执行该 loader 函数,而不是再向 SSR 服务发送数据请求。
```ts title="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 WARNING
要使用 Client Loader,必须有对应的 Server Loader,且 Server Loader 必须是 .data 文件约定,不能是 .loader 文件约定。
:::
## FAQ
### 应用级别数据获取?
应用级别的模块,我们更希望使用 RSC 来实现,使其功能更加的完善。目前该功能正在探索中,敬请期待。
### 是否支持嵌套生产者?
不支持。
### 生产者除了使用 Rslib 插件或者 Modern.js 插件外还有其他的插件吗?
暂时只有 Rslib 插件和 Modern.js 插件才可以创建 Data Loader。