数据获取

数据获取

#数据获取 复制 Markdown

Modern.js 中提供了开箱即用的数据获取能力,开发者可以通过这些 API,在项目中获取数据。需要注意的是,这些 API 并不帮助应用发起请求,而是帮助开发者更好地管理数据,提升项目的性能。

#什么是 Data Loader

Modern.js 推荐使用约定式路由做路由的管理,每个路由组件(layout.ts,page.ts 或 $.tsx)都可以有一个同名的 .data 文件。这些文件可以导出一个 loader 函数,我们称为 Data Loader,它会在对应的路由组件渲染之前执行,为组件提供数据。如下面示例:

.

└── routes

├── layout.tsx

└── user

├── layout.tsx

├── layout.data.ts

├── page.tsx

└── page.data.ts

在 routes/user/page.data.ts 文件中,可以导出一个 loader 函数:

routes/user/page.data.tsexport type ProfileData = {

/* some types */

};

export const loader = async (): Promise => {

const res = await fetch('https://api/user/profile');

return await res.json();

};

在路由组件中,你可以通过 useLoaderData 函数获取数据:

routes/user/page.tsximport { useLoaderData } from '@modern-js/runtime/router';

import type { ProfileData } from './page.data.ts';

export default function UserPage() {

const profileData = useLoaderData() as ProfileData;

return

{profileData}
;

}

Caution

路由组件和 .data 文件共享类型,要使用 import type 语法,避免引入预期之外的副作用。

在 CSR 项目中,loader 函数会在客户端执行,loader 函数内可以使用浏览器的 API。

在 SSR 项目中,不管是首屏还是在客户端的导航,loader 函数只会在服务端执行,这里可以调用任意的 Node.js API,同时这里使用的任何依赖和代码都不会包含在客户端的 bundle 中。

当在浏览器端导航时,基于约定式路由,Modern.js 能够支持所有的 loader 函数并行执行(请求)。即当访问 /user/profile 时,/user 和 /user/profile 下的 loader 函数都会并行执行(请求),这种方式解决了部分请求、渲染瀑布流的问题,较大的提升了页面性能。

Tip

loader 的职责是数据获取,入参已提供路由参数和请求信息,一般无需访问浏览器 API。建议保持环境无关性,以便未来使用 SSR。

#loader 函数

loader 函数有两个入参,分别用于获取路由参数和请求信息。

#params

params 是当路由为动态路由时的动态路由片段,会作为参数传入 loader 函数:

routes/user/[id]/page.data.tsimport { LoaderFunctionArgs } from '@modern-js/runtime/router';

// 访问 /user/123 时,函数的参数为 `{ params: { id: '123' } }`

export const loader = async ({ params }: LoaderFunctionArgs) => {

const { id } = params;

const res = await fetch(`https://api/user/${id}`);

return res.json();

};

#request

request 是一个 Fetch Request 实例。一个常见的使用场景是通过 request 获取查询参数:

import { LoaderFunctionArgs } from '@modern-js/runtime/router';

export const loader = async ({ request }: LoaderFunctionArgs) => {

const url = new URL(request.url);

const userId = url.searchParams.get('id');

return queryUser(userId);

};

#返回值

loader 函数的返回值只能是两种数据结构之一,可序列化的数据对象或者 Fetch Response 实例:

const loader = async (): Promise => {

return {

message: 'hello world',

};

};

export default loader;

默认情况下,loader 返回的响应 Content-type 是 application/json,status 为 200,你可以通过自定义 Response 来设置:

const loader = async (): Promise => {

const data = { message: 'hello world' };

return new Response(JSON.stringify(data), {

status: 200,

headers: {

'Content-Type': 'application/json; utf-8',

},

});

};

#在不同环境使用 Data Loader

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

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

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

Note

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

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

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

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

我们推荐在 loader 函数中使用 fetch API 发起请求。在 Modern.js 中默认对 fetch API 做了 polyfill,允许服务端使用该 API 发起请求,这意味你都可以在 CSR 和 SSR 时同构的获取数据:

export async function loader() {

const res = await fetch('URL_ADDRESS');

const data = await res.json();

return {

message: data.message,

};

}

#错误处理

#基本用法

在 loader 函数中,可以通过 throw error 或者 throw response 的方式处理错误,当 loader 函数中有错误被抛出时,Modern.js 会停止执行当前 loader 中的代码,并将前端 UI 切换到定义的 ErrorBoundary 组件:

// routes/user/profile/page.data.ts

export async function loader() {

const res = await fetch('https://api/user/profile');

if (!res.ok) {

throw res;

}

return res.json();

}

// routes/user/profile/error.tsx

import { useRouteError } from '@modern-js/runtime/router';

const ErrorBoundary = () => {

const error = useRouteError() as Response;

return (

{error.status}

{error.statusText}

);

};

export default ErrorBoundary;

#修改 HTTP 状态码

在 SSR 项目中你可以通过在 loader 函数中 throw response 的方式,控制页面的状态码,展示对应的 UI。

如以下示例,页面的状态码将与这个 response 保持一致,页面也会展示为 ErrorBoundary 的 UI:

// routes/user/profile/page.data.ts

export async function loader() {

const user = await fetchUser();

if (!user) {

throw new Response('The user was not found', { status: 404 });

}

return user;

}

// routes/error.tsx

import { useRouteError } from '@modern-js/runtime/router';

const ErrorBoundary = () => {

const error = useRouteError() as { data: string };

return

{error.data}
;

};

export default ErrorBoundary;

Tip

throw 和上文介绍的 return Response 都可以修改状态码,区别是 throw 会进入 ErrorBoundary,而 return 不会。

#获取上层组件的数据

很多场景下,子组件需要获取到上层组件 loader 中的数据,你可以通过 useRouteLoaderData 方便地获取到上层组件的数据:

// routes/user/profile/page.tsx

import { useRouteLoaderData } from '@modern-js/runtime/router';

export function UserLayout() {

// 获取 routes/user/layout.data.ts 中 `loader` 返回的数据

const data = useRouteLoaderData('user/layout');

return (

{data.name}

{data.age}

);

}

userRouteLoaderData 接受一个参数 routeId。在使用约定式路由时,Modern.js 会为你自动生成 routeId,routeId 的值是对应组件相对于 src/routes 的路径,如上面的例子中,子组件想要获取 routes/user/layout.tsx 中 loader 返回的数据,routeId 的值就是 user/layout。

在多入口场景下,routeId 的值需要加上对应入口的名称,入口名称非指定情况下一般是入口的目录名,如以下目录结构:

.

└── src

├── entry1

│ └── routes

│ └── layout.tsx

└── entry2

└── routes

└── layout.tsx

如果想获取 entry1/routes/layout.tsx 中 loader 返回的数据,routeId 的值就是 entry1_layout。

#Loading UI (Experimental)

Experimental

此功能当前是实验性功能,后续 API 可能有调整。

创建 user/layout.data.ts,并添加以下代码:

routes/user/layout.data.tsimport { defer } from '@modern-js/runtime/router';

export const loader = () =>

defer({

userInfo: new Promise(resolve => {

setTimeout(() => {

resolve({

age: 1,

name: 'user layout',

});

}, 1000);

}),

});

在 user/layout.tsx 中添加以下代码:

routes/user/layout.tsximport { Await, defer, useLoaderData, Outlet } from '@modern-js/runtime/router';

export default function UserLayout() {

const { userInfo } = useLoaderData() as { userInfo: Promise };

return (

Loading...

}>

resolve={userInfo}

children={userInfo => (

{userInfo.name}

{userInfo.age}

)}

>

);

}

Tip

组件的具体用法请查看 Await,defer 的具体用法请查看 defer。

#数据缓存

在路由导航时,Modern.js 只会加载路由变化的部分的数据。如当前路由是 a/b,a 路径对应的 Data Loader 已经执行过,当从 /a/b 跳转到 /a/c时,a 路径对应的 Data Loader 不会重新执行,c 路径对应的 Data Loader 会执行,并获取了数据。

这种默认的优化策略避免了无效重复数据的请求。此时你可能会疑惑,如何更新 a 路径对应 Data Loader 的数据?

在 Modern.js 中,以下几种情况,Modern.js 会重新加载对应路由路径的数据:

在 Data Action 触发后

URL 参数发生变化后

用户点击的链接与当前页面的 URL 相同

在路由组件中定义了 shouldRevalidate 函数,该函数返回 true

Tip

如果你在路由上定义了 shouldRevalidate 函数,会先检查该函数,判断是否需要重新加载数据。

#shouldRevalidate

Warning

目前 shouldRevalidate 只会在 CSR 和 Streaming SSR 下生效。

在路由组件(layout.tsx,page.tsx,$.tsx)中,我们可以导出一个 shouldRevalidate 函数。每次项目中的路由变化时,这个函数会被触发,该函数可以控制要重新加载哪些路由中的数据。如果这个函数返回 true,Modern.js 就会重新加载对应路由的数据。

routes/user/layout.tsximport type { ShouldRevalidateFunction } from '@modern-js/runtime/router';

export const shouldRevalidate: ShouldRevalidateFunction = ({

actionResult,

currentParams,

currentUrl,

defaultShouldRevalidate,

formAction,

formData,

formEncType,

formMethod,

nextParams,

nextUrl,

}) => {

return true;

};

Tip

shouldRevalidate 函数的更多信息可以参考 react-router。

#错误用法

loader 中只能返回可序列化的数据,在 SSR 环境下,loader 函数的返回值会被序列化为 JSON 字符串,然后在浏览器端被反序列化为对象。因此,loader 函数中不能返回不可序列化的数据(如函数)。

Warning

目前 CSR 下没有这个限制,但我们强烈推荐你遵循该限制,且未来我们可能在 CSR 下也加上该限制。

// This won't work!

export default () => {

return {

user: {},

method: () => {},

};

};

Modern.js 会帮你调用 loader 函数,你不应该自己调用 loader 函数:

// This won't work!

export const loader = async () => {

const res = fetch('https://api/user/profile');

return res.json();

};

import { loader } from './page.data.ts';

export default function RouteComp() {

const data = loader();

}

不能从路由组件中引入 loader 文件,也不能从 loader 文件引入路由组件中的变量,如果需要共享类型的话,应该使用 import type

// Not allowed

// routes/layout.tsx

import { useLoaderData } from '@modern-js/runtime/router';

import { ProfileData } from './page.data.ts'; // should use "import type" instead

export const fetch = wrapFetch(fetch);

export default function UserPage() {

const profileData = useLoaderData() as ProfileData;

return

{profileData}
;

}

// routes/layout.data.ts

import { fetch } from './layout.tsx'; // should not be imported from the routing component

export type ProfileData = {

/* some types */

};

export const loader = async (): Promise => {

const res = await fetch('https://api/user/profile');

return await res.json();

};

在服务端运行时,loader 函数会被打包为一个统一的 bundle,所以我们不推荐服务端的代码使用 __filename 和 __dirname。

#常见问题

loader 和 BFF 函数的关系是什么?

在 CSR 项目中,loader 在浏览器端执行,在 loader 可以直接调用 BFF 函数进行接口请求。

在 SSR 项目中,每个 loader 也是一个服务端接口。我们推荐使用 loader 替代 HTTP Method 为 GET 的 BFF 函数,避免多一层转发和执行。

相关风雨

ps使用通道快速抠图的详细教程
365bet下注网站

ps使用通道快速抠图的详细教程

🌊 07-03 💨 阅读 973
外表是仙女,私下里却抽烟成瘾的9位女明星,刘亦菲最令人意外!
365彩票官网app下载安装

外表是仙女,私下里却抽烟成瘾的9位女明星,刘亦菲最令人意外!

🌊 10-10 💨 阅读 5383
证券开户哪家好?2025年实用选择指南
365bet下注网站

证券开户哪家好?2025年实用选择指南

🌊 08-24 💨 阅读 6107