# 数据获取 :::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 使用大体分为两种部分:组件(函数)、应用。两者的区别在于是否带&#x6709;_&#x8DEF;&#x7531;_&#x529F;能。 ::: ## 如何使用 根据角色的不同,需要做不同的行为。 ### 生产者 {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。