logo
  • 指南
  • 配置
  • 插件
  • API
  • 示例
  • 社区
  • Modern.js 2.x 文档
  • 简体中文
    • 简体中文
    • English
    • 开始
      介绍
      快速上手
      版本升级
      名词解释
      技术栈
      核心概念
      页面入口
      构建工具
      Web 服务器
      基础功能
      路由
      路由基础
      配置式路由
      数据管理
      数据获取
      数据写入
      数据缓存
      渲染
      服务端渲染(SSR)
      服务端流式渲染(Streaming SSR)
      渲染缓存
      静态站点生成(SSG)
      渲染预处理 (Render Preprocessing)
      样式开发
      引入 CSS
      使用 CSS Modules
      使用 CSS-in-JS
      使用 Tailwind CSS
      HTML 模板
      引用静态资源
      引用 JSON 文件
      引用 SVG 资源
      引用 Wasm 资源
      调试
      数据模拟(Mock)
      网络代理
      使用 Rsdoctor
      使用 Storybook
      测试
      Playwright
      Vitest
      Jest
      Cypress
      路径别名
      环境变量
      构建产物目录
      部署应用
      进阶功能
      使用 Rspack
      使用 BFF
      基础用法
      运行时框架
      扩展 BFF Server
      扩展一体化调用 SDK
      文件上传
      跨项目调用
      优化页面性能
      代码分割
      静态资源内联
      产物体积优化
      React Compiler
      提升构建性能
      浏览器兼容性
      配置底层工具
      源码构建模式
      服务端监控
      Monitors
      日志事件
      指标事件
      国际化
      基础概念
      快速开始
      配置说明
      语言检测
      资源加载
      路由集成
      API 参考
      高级用法
      最佳实践
      自定义 Web Server
      专题详解
      模块联邦
      简介
      开始使用
      应用级别模块
      服务端渲染
      部署
      集成国际化能力
      常见问题
      依赖安装问题
      命令行问题
      构建相关问题
      热更新问题
      已下线功能
      📝 编辑此页面
      上一页配置式路由下一页数据写入

      #数据获取

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

      #什么是 Data Loader

      Note

      Modern.js v1 项目通过 useLoader 获取数据,这已经不是我们推荐的用法,建议迁移到 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.ts
      export type ProfileData = {
        /*  some types */
      };
      
      export const loader = async (): Promise<ProfileData> => {
        const res = await fetch('https://api/user/profile');
        return await res.json();
      };
      兼容性
      • 在之前的版本中,Data Loader 是定义在 .loader 文件中的。当前版本中,我们推荐定义在 .data 文件中,同时我们会保持对 .loader 文件的兼容。
      • 在 .loader 文件中,Data Loader 可以默认导出。但在 data 文件中,Data Loader 需要以 loader 具名导出。
        // xxx.loader.ts
      export default () => {}
      
      // xxx.data.ts
      export const loader = () => {}

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

      routes/user/page.tsx
      import { useLoaderData } from '@modern-js/runtime/router';
      import type { ProfileData } from './page.data.ts';
      
      export default function UserPage() {
        const profileData = useLoaderData() as ProfileData;
        return <div>{profileData}</div>;
      }
      Caution

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

      在 CSR 项目中,loader 函数会在客户端执行,loader 函数内可以使用浏览器的 API(但通常不需要,也不推荐)。

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

      Tip

      在以后的版本中,Modern.js 可能会支持在 CSR 环境下,loader 函数也在服务端运行,以提高性能和安全性,所以这里建议尽可能地保证 loader 的纯粹,只做数据获取的场景。

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

      #loader 函数

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

      #params

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

      routes/user/[id]/page.data.ts
      import { 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<ProfileData> => {
        return {
          message: 'hello world',
        };
      };
      export default loader;

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

      const loader = async (): Promise<ProfileData> => {
        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');
        return {
          message: res.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 (
          <div>
            <h1>{error.status}</h1>
            <h2>{error.statusText}</h2>
          </div>
        );
      };
      
      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 <div className="error">{error.data}</div>;
      };
      
      export default ErrorBoundary;

      #获取上层组件的数据

      很多场景下,子组件需要获取到上层组件 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 (
          <div>
            <h1>{data.name}</h1>
            <h2>{data.age}</h2>
          </div>
        );
      }

      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.ts
      import { 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.tsx
      import { Await, defer, useLoaderData, Outlet } from '@modern-js/runtime/router';
      
      export default function UserLayout() {
        const { userInfo } = useLoaderData() as { userInfo: Promise<UserInfo> };
        return (
          <div>
            <React.Suspense fallback={<p>Loading...</p>}>
              <Await
                resolve={userInfo}
                children={userInfo => (
                  <div>
                    <span>{userInfo.name}</span>
                    <span>{userInfo.age}</span>
                    <Outlet />
                  </div>
                )}
              ></Await>
            </React.Suspense>
          </div>
        );
      }
      Tip

      <Await> 组件的具体用法请查看 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 会重新加载对应路由路径的数据:

      1. 在 Data Action 触发后
      2. URL 参数发生变化后
      3. 用户点击的链接与当前页面的 URL 相同
      4. 在路由组件中定义了 shouldRevalidate 函数,该函数返回 true
      Tip

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

      #shouldRevalidate

      Warning

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

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

      routes/user/layout.tsx
      import 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。

      #错误用法

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

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

      // This won't work!
      export default () => {
        return {
          user: {},
          method: () => {},
        };
      };
      1. 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();
      }
      1. 不能从路由组件中引入 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 <div>{profileData}</div>;
      }
      
      // 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<ProfileData> => {
        const res = await fetch('https://api/user/profile');
        return await res.json();
      };
      1. 在服务端运行时,loader 函数会被打包为一个统一的 bundle,所以我们不推荐服务端的代码使用 __filename 和 __dirname。

      #常见问题

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

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

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