diff --git a/404.html b/404.html index 4ae11e2..d92ead0 100644 --- a/404.html +++ b/404.html @@ -17,7 +17,7 @@
- + \ No newline at end of file diff --git a/assets/framework_actions.md.DId6sRkl.js b/assets/framework_actions.md.DId6sRkl.js new file mode 100644 index 0000000..c5a7e00 --- /dev/null +++ b/assets/framework_actions.md.DId6sRkl.js @@ -0,0 +1,84 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"Actions","description":"","frontmatter":{},"headers":[],"relativePath":"framework/actions.md","filePath":"framework/actions.md"}'),h={name:"framework/actions.md"};function k(p,s,l,e,E,r){return n(),a("div",null,s[0]||(s[0]=[t(`

Actions

数据变更通过路由动作(Route actions)来完成。当路由动作执行完毕后,页面上所有由加载器获取的数据都会被重新验证(revalidated),这样就能在无需编写额外代码的情况下,确保用户界面与数据保持同步。

通过 action 定义的路由动作只会在服务器端被调用,而通过 clientAction 定义的路由动作则是在浏览器(也就是客户端)中运行。

客户端 Actions

clientAction 仅在浏览器端运行,并且当同时定义了 action 和 clientAction 时,clientAction 会优先被执行。

tsx
// route('/projects/:projectId', './project.tsx')
+import type { Route } from "./+types/project";
+import { Form } from "react-router";
+import { someApi } from "./api";
+
+export async function clientAction({ request }: Route.ClientActionArgs) {
+  let formData = await request.formData();
+  let title = await formData.get("title");
+  let project = await someApi.updateProject({ title });
+  return project;
+}
+
+export default function Project({ actionData }: Route.ComponentProps) {
+  return (
+    <div>
+      <h1>Project</h1>
+      <Form method="post">
+        <input type="text" name="title" />
+        <button type="submit">Submit</button>
+      </Form>
+      {actionData ? <p>{actionData.title} updated</p> : null}
+    </div>
+  );
+}

服务端 Actions

action 仅在服务器端运行,并且会从客户端的打包代码包中被移除。

tsx
// route('/projects/:projectId', './project.tsx')
+import type { Route } from "./+types/project";
+import { Form } from "react-router";
+import { fakeDb } from "../db";
+
+export async function action({ request }: Route.ActionArgs) {
+  let formData = await request.formData();
+  let title = await formData.get("title");
+  let project = await fakeDb.updateProject({ title });
+  return project;
+}
+
+export default function Project({ actionData }: Route.ComponentProps) {
+  return (
+    <div>
+      <h1>Project</h1>
+      <Form method="post">
+        <input type="text" name="title" />
+        <button type="submit">Submit</button>
+      </Form>
+      {actionData ? <p>{actionData.title} updated</p> : null}
+    </div>
+  );
+}

调用 Actions 的方式

Actions 可以通过声明式 <Form> 组件和钩子函数 useSubmit(或者使用 <fetcher.Form>fetcher.submit)被调用,使用时传递一个路由 path 和一个 “post” 方法:

使用 Form 调用

tsx
import { Form } from "react-router";
+
+function SomeComponent() {
+  return (
+    <Form action="/projects/123" method="post">
+      <input type="text" name="title" />
+      <button type="submit">Submit</button>
+    </Form>
+  );
+}

这种方式会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。

使用 useSubmit 调用

你可以通过使用 useSubmit 钩子函数来调用 action:

ts
import { useCallback } from "react";
+import { useSubmit } from "react-router";
+import { useFakeTimer } from "fake-lib";
+
+function useQuizTimer() {
+  let submit = useSubmit();
+
+  let cb = useCallback(() => {
+    submit({ quizTimedOut: true }, { action: "/end-quiz", method: "post" });
+  }, []);
+
+  let tenMinutes = 10 * 60 * 1000;
+  useFakeTimer(tenMinutes, cb);
+}

同样,这种方式也会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。

使用 fetcher 调用

Fetchers 能够让你向 Actions 以及 Loaders 提交数据,同时不会引发导航行为,也就是说不会在浏览器历史记录中添加新的记录。

tsx
import { useFetcher } from "react-router";
+
+function Task() {
+  let fetcher = useFetcher();
+  let busy = fetcher.state !== "idle";
+
+  return (
+    <fetcher.Form method="post" action="/update-task/123">
+      <input type="text" name="title" />
+      <button type="submit">{busy ? "Saving..." : "Save"}</button>
+    </fetcher.Form>
+  );
+}

它也有一个编程式 submit 方法:

ts
fetcher.submit(
+  { title: "New Title" },
+  { action: "/update-task/123", method: "post" }
+);

阅读 使用 Fetchers 以了解更多关于 Fetchers 的信息。

`,24)]))}const y=i(h,[["render",k]]);export{g as __pageData,y as default}; diff --git a/assets/framework_actions.md.DId6sRkl.lean.js b/assets/framework_actions.md.DId6sRkl.lean.js new file mode 100644 index 0000000..c5a7e00 --- /dev/null +++ b/assets/framework_actions.md.DId6sRkl.lean.js @@ -0,0 +1,84 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"Actions","description":"","frontmatter":{},"headers":[],"relativePath":"framework/actions.md","filePath":"framework/actions.md"}'),h={name:"framework/actions.md"};function k(p,s,l,e,E,r){return n(),a("div",null,s[0]||(s[0]=[t(`

Actions

数据变更通过路由动作(Route actions)来完成。当路由动作执行完毕后,页面上所有由加载器获取的数据都会被重新验证(revalidated),这样就能在无需编写额外代码的情况下,确保用户界面与数据保持同步。

通过 action 定义的路由动作只会在服务器端被调用,而通过 clientAction 定义的路由动作则是在浏览器(也就是客户端)中运行。

客户端 Actions

clientAction 仅在浏览器端运行,并且当同时定义了 action 和 clientAction 时,clientAction 会优先被执行。

tsx
// route('/projects/:projectId', './project.tsx')
+import type { Route } from "./+types/project";
+import { Form } from "react-router";
+import { someApi } from "./api";
+
+export async function clientAction({ request }: Route.ClientActionArgs) {
+  let formData = await request.formData();
+  let title = await formData.get("title");
+  let project = await someApi.updateProject({ title });
+  return project;
+}
+
+export default function Project({ actionData }: Route.ComponentProps) {
+  return (
+    <div>
+      <h1>Project</h1>
+      <Form method="post">
+        <input type="text" name="title" />
+        <button type="submit">Submit</button>
+      </Form>
+      {actionData ? <p>{actionData.title} updated</p> : null}
+    </div>
+  );
+}

服务端 Actions

action 仅在服务器端运行,并且会从客户端的打包代码包中被移除。

tsx
// route('/projects/:projectId', './project.tsx')
+import type { Route } from "./+types/project";
+import { Form } from "react-router";
+import { fakeDb } from "../db";
+
+export async function action({ request }: Route.ActionArgs) {
+  let formData = await request.formData();
+  let title = await formData.get("title");
+  let project = await fakeDb.updateProject({ title });
+  return project;
+}
+
+export default function Project({ actionData }: Route.ComponentProps) {
+  return (
+    <div>
+      <h1>Project</h1>
+      <Form method="post">
+        <input type="text" name="title" />
+        <button type="submit">Submit</button>
+      </Form>
+      {actionData ? <p>{actionData.title} updated</p> : null}
+    </div>
+  );
+}

调用 Actions 的方式

Actions 可以通过声明式 <Form> 组件和钩子函数 useSubmit(或者使用 <fetcher.Form>fetcher.submit)被调用,使用时传递一个路由 path 和一个 “post” 方法:

使用 Form 调用

tsx
import { Form } from "react-router";
+
+function SomeComponent() {
+  return (
+    <Form action="/projects/123" method="post">
+      <input type="text" name="title" />
+      <button type="submit">Submit</button>
+    </Form>
+  );
+}

这种方式会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。

使用 useSubmit 调用

你可以通过使用 useSubmit 钩子函数来调用 action:

ts
import { useCallback } from "react";
+import { useSubmit } from "react-router";
+import { useFakeTimer } from "fake-lib";
+
+function useQuizTimer() {
+  let submit = useSubmit();
+
+  let cb = useCallback(() => {
+    submit({ quizTimedOut: true }, { action: "/end-quiz", method: "post" });
+  }, []);
+
+  let tenMinutes = 10 * 60 * 1000;
+  useFakeTimer(tenMinutes, cb);
+}

同样,这种方式也会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。

使用 fetcher 调用

Fetchers 能够让你向 Actions 以及 Loaders 提交数据,同时不会引发导航行为,也就是说不会在浏览器历史记录中添加新的记录。

tsx
import { useFetcher } from "react-router";
+
+function Task() {
+  let fetcher = useFetcher();
+  let busy = fetcher.state !== "idle";
+
+  return (
+    <fetcher.Form method="post" action="/update-task/123">
+      <input type="text" name="title" />
+      <button type="submit">{busy ? "Saving..." : "Save"}</button>
+    </fetcher.Form>
+  );
+}

它也有一个编程式 submit 方法:

ts
fetcher.submit(
+  { title: "New Title" },
+  { action: "/update-task/123", method: "post" }
+);

阅读 使用 Fetchers 以了解更多关于 Fetchers 的信息。

`,24)]))}const y=i(h,[["render",k]]);export{g as __pageData,y as default}; diff --git a/assets/framework_custom-framework.md.DIi1csnZ.js b/assets/framework_custom-framework.md.DIi1csnZ.js new file mode 100644 index 0000000..9aa53ad --- /dev/null +++ b/assets/framework_custom-framework.md.DIi1csnZ.js @@ -0,0 +1,113 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const o=JSON.parse('{"title":"自定义框架","description":"","frontmatter":{"next":false},"headers":[],"relativePath":"framework/custom-framework.md","filePath":"framework/custom-framework.md"}'),h={name:"framework/custom-framework.md"};function l(p,s,k,e,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`

自定义框架

通常可以借助 @react-router/dev 来利用 React Router 的各种框架特性,但如果不想使用它,也是有办法将 React Router 的框架特性(如加载器、动作、数据获取器等)集成到自己的打包器和服务器抽象层当中的。

客户端渲染

1. 创建一个路由

在 React Router 框架中,createBrowserRouter 是一个极为关键的浏览器运行时 API,它承担着启用路由模块相关 API(如加载器、动作等)的重要职责,以下为你详细介绍它的相关情况。

它接收一个由路由对象组成的数组作为参数,这些路由对象具备支持加载器、动作、错误边界等诸多特性的能力。

在基于 Vite 构建的 React 项目中,如果使用了 React Router,其配套的 Vite 插件提供了一种便捷的方式来生成 createBrowserRouter 所需的路由对象数组。通常,开发者会在 routes.ts 文件中按照一定的格式和规则定义路由相关的信息,然后插件会自动解析这个文件,将其中的路由配置转换为符合要求的路由对象数组。

如果开发者不想依赖于特定的插件或者希望对路由配置有更精细化的控制,也可以手动创建路由对象数组,或者借助自定义的抽象层来生成符合要求的数组,再结合自己选择的打包器(如 Webpack、Rollup 等)来处理项目。

ts
import { createBrowserRouter } from "react-router";
+
+let router = createBrowserRouter([
+  {
+    path: "/",
+    Component: Root,
+    children: [
+      {
+        path: "shows/:showId",
+        Component: Show,
+        loader: ({ request, params }) =>
+          fetch(\`/api/show/\${params.id}.json\`, {
+            signal: request.signal,
+          }),
+      },
+    ],
+  },
+]);

2. 渲染路由

然后使用 <RouterProvider> 把路由渲染到浏览器中。

tsx
import { createBrowserRouter, RouterProvider } from "react-router";
+import { createRoot } from "react-dom/client";
+
+createRoot(document.getElementById("root")).render(
+  <RouterProvider router={router} />
+);

3. 懒加载

在 React Router 中,lazy 属性为路由提供了一种非常实用的懒加载机制,它允许路由在需要的时候才去加载对应的组件及其相关定义,而不是在应用启动时就一次性全部加载:

tsx
createBrowserRouter([
+  {
+    path: "/show/:showId",
+    lazy: () => {
+      let [loader, action, Component] = await Promise.all([
+        import("./show.action.js"),
+        import("./show.loader.js"),
+        import("./show.component.js"),
+      ]);
+      return { loader, action, Component };
+    },
+  },
+]);

服务度渲染

在进行服务器端渲染并采用自定义设置时,有一些服务器端的 API 可用于渲染以及数据加载操作。

本介绍只是让你对服务器端渲染自定义设置以及相关 API 的工作原理有个大致了解,如果想要更深入地理解具体的实现细节、代码示例以及各种复杂场景下的应用方式等内容,可以查看自定义框架示例仓库

1. 定义你的路由

同客户端路由定义方式一样。

ts
export default [
+  {
+    path: "/",
+    Component: Root,
+    children: [
+      {
+        path: "shows/:showId",
+        Component: Show,
+        loader: ({ params }) => {
+          return db.loadShow(params.id);
+        },
+      },
+    ],
+  },
+];

2. 创建一个静态处理器

使用 createStaticHandler 将路由转换为请求处理器。

ts
import { createStaticHandler } from "react-router";
+import routes from "./some-routes";
+
+let { query, dataRoutes } = createStaticHandler(routes);

3. 获取路由上下文和渲染函数

React Router 被设计为能够与 Web Fetch 请求 协同工作,以实现诸如数据获取、表单提交以及页面导航等功能。然而,当服务器端所使用的请求对象类型与 Web Fetch 对象不一致时,就需要进行相应的适配操作。

假设你的服务端接收的是 Request 对象。

ts
import { renderToString } from "react-dom/server";
+import {
+  createStaticHandler,
+  createStaticRouter,
+  StaticRouterProvider,
+} from "react-router";
+
+import routes from "./some-routes.js";
+
+let { query, dataRoutes } = createStaticHandler(routes);
+
+export async function handler(request: Request) {
+  // 1. 调用 \`query\` 运行 actions/loaders 获取路由上下文
+  let context = await query(request);
+
+  // 如果 \`query\` 返回一个 Response,直接返回它(可能是一个重定向)
+  if (context instanceof Response) {
+    return context;
+  }
+
+  // 2. 创建一个静态路由用于服务端渲染
+  let router = createStaticRouter(dataRoutes, context);
+
+  // 3. 使用 StaticRouterProvider 渲染所有内容
+  let html = renderToString(
+    <StaticRouterProvider router={router} context={context} />
+  );
+
+  // 设置来自 action 和 loaders 的请求头,基于最深匹配原则
+  let leaf = context.matches[context.matches.length - 1];
+  let actionHeaders = context.actionHeaders[leaf.route.id];
+  let loaderHeaders = context.loaderHeaders[leaf.route.id];
+  let headers = new Headers(actionHeaders);
+  if (loaderHeaders) {
+    for (let [key, value] of loaderHeaders.entries()) {
+      headers.append(key, value);
+    }
+  }
+
+  headers.set("Content-Type", "text/html; charset=utf-8");
+
+  // 4. 返回一个客户端响应
+  return new Response(\`<!DOCTYPE html>\${html}\`, {
+    status: context.statusCode,
+    headers,
+  });
+}

4. 在浏览器中进行水合

在服务器端渲染的流程中,当服务器生成好要发送给客户端的 HTML 内容时,会将水合作用数据以特定的形式嵌入到 HTML 页面中,通常会挂载到 window.__staticRouterHydrationData 属性上。

当客户端接收到包含水合作用数据的 HTML 页面后,就可以利用 window.__staticRouterHydrationData 中的数据来初始化客户端的路由器并渲染 <RouterProvider> 组件,实现页面从静态到动态的转换以及后续的交互功能。

tsx
import { StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+import { RouterProvider } from "react-router/dom";
+import routes from "./app/routes.js";
+import { createBrowserRouter } from "react-router";
+
+let router = createBrowserRouter(routes, {
+  hydrationData: window.__staticRouterHydrationData,
+});
+
+hydrateRoot(
+  document,
+  <StrictMode>
+    <RouterProvider router={router} />
+  </StrictMode>
+);
`,32)]))}const g=i(h,[["render",l]]);export{o as __pageData,g as default}; diff --git a/assets/framework_custom-framework.md.DIi1csnZ.lean.js b/assets/framework_custom-framework.md.DIi1csnZ.lean.js new file mode 100644 index 0000000..9aa53ad --- /dev/null +++ b/assets/framework_custom-framework.md.DIi1csnZ.lean.js @@ -0,0 +1,113 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const o=JSON.parse('{"title":"自定义框架","description":"","frontmatter":{"next":false},"headers":[],"relativePath":"framework/custom-framework.md","filePath":"framework/custom-framework.md"}'),h={name:"framework/custom-framework.md"};function l(p,s,k,e,r,E){return t(),a("div",null,s[0]||(s[0]=[n(`

自定义框架

通常可以借助 @react-router/dev 来利用 React Router 的各种框架特性,但如果不想使用它,也是有办法将 React Router 的框架特性(如加载器、动作、数据获取器等)集成到自己的打包器和服务器抽象层当中的。

客户端渲染

1. 创建一个路由

在 React Router 框架中,createBrowserRouter 是一个极为关键的浏览器运行时 API,它承担着启用路由模块相关 API(如加载器、动作等)的重要职责,以下为你详细介绍它的相关情况。

它接收一个由路由对象组成的数组作为参数,这些路由对象具备支持加载器、动作、错误边界等诸多特性的能力。

在基于 Vite 构建的 React 项目中,如果使用了 React Router,其配套的 Vite 插件提供了一种便捷的方式来生成 createBrowserRouter 所需的路由对象数组。通常,开发者会在 routes.ts 文件中按照一定的格式和规则定义路由相关的信息,然后插件会自动解析这个文件,将其中的路由配置转换为符合要求的路由对象数组。

如果开发者不想依赖于特定的插件或者希望对路由配置有更精细化的控制,也可以手动创建路由对象数组,或者借助自定义的抽象层来生成符合要求的数组,再结合自己选择的打包器(如 Webpack、Rollup 等)来处理项目。

ts
import { createBrowserRouter } from "react-router";
+
+let router = createBrowserRouter([
+  {
+    path: "/",
+    Component: Root,
+    children: [
+      {
+        path: "shows/:showId",
+        Component: Show,
+        loader: ({ request, params }) =>
+          fetch(\`/api/show/\${params.id}.json\`, {
+            signal: request.signal,
+          }),
+      },
+    ],
+  },
+]);

2. 渲染路由

然后使用 <RouterProvider> 把路由渲染到浏览器中。

tsx
import { createBrowserRouter, RouterProvider } from "react-router";
+import { createRoot } from "react-dom/client";
+
+createRoot(document.getElementById("root")).render(
+  <RouterProvider router={router} />
+);

3. 懒加载

在 React Router 中,lazy 属性为路由提供了一种非常实用的懒加载机制,它允许路由在需要的时候才去加载对应的组件及其相关定义,而不是在应用启动时就一次性全部加载:

tsx
createBrowserRouter([
+  {
+    path: "/show/:showId",
+    lazy: () => {
+      let [loader, action, Component] = await Promise.all([
+        import("./show.action.js"),
+        import("./show.loader.js"),
+        import("./show.component.js"),
+      ]);
+      return { loader, action, Component };
+    },
+  },
+]);

服务度渲染

在进行服务器端渲染并采用自定义设置时,有一些服务器端的 API 可用于渲染以及数据加载操作。

本介绍只是让你对服务器端渲染自定义设置以及相关 API 的工作原理有个大致了解,如果想要更深入地理解具体的实现细节、代码示例以及各种复杂场景下的应用方式等内容,可以查看自定义框架示例仓库

1. 定义你的路由

同客户端路由定义方式一样。

ts
export default [
+  {
+    path: "/",
+    Component: Root,
+    children: [
+      {
+        path: "shows/:showId",
+        Component: Show,
+        loader: ({ params }) => {
+          return db.loadShow(params.id);
+        },
+      },
+    ],
+  },
+];

2. 创建一个静态处理器

使用 createStaticHandler 将路由转换为请求处理器。

ts
import { createStaticHandler } from "react-router";
+import routes from "./some-routes";
+
+let { query, dataRoutes } = createStaticHandler(routes);

3. 获取路由上下文和渲染函数

React Router 被设计为能够与 Web Fetch 请求 协同工作,以实现诸如数据获取、表单提交以及页面导航等功能。然而,当服务器端所使用的请求对象类型与 Web Fetch 对象不一致时,就需要进行相应的适配操作。

假设你的服务端接收的是 Request 对象。

ts
import { renderToString } from "react-dom/server";
+import {
+  createStaticHandler,
+  createStaticRouter,
+  StaticRouterProvider,
+} from "react-router";
+
+import routes from "./some-routes.js";
+
+let { query, dataRoutes } = createStaticHandler(routes);
+
+export async function handler(request: Request) {
+  // 1. 调用 \`query\` 运行 actions/loaders 获取路由上下文
+  let context = await query(request);
+
+  // 如果 \`query\` 返回一个 Response,直接返回它(可能是一个重定向)
+  if (context instanceof Response) {
+    return context;
+  }
+
+  // 2. 创建一个静态路由用于服务端渲染
+  let router = createStaticRouter(dataRoutes, context);
+
+  // 3. 使用 StaticRouterProvider 渲染所有内容
+  let html = renderToString(
+    <StaticRouterProvider router={router} context={context} />
+  );
+
+  // 设置来自 action 和 loaders 的请求头,基于最深匹配原则
+  let leaf = context.matches[context.matches.length - 1];
+  let actionHeaders = context.actionHeaders[leaf.route.id];
+  let loaderHeaders = context.loaderHeaders[leaf.route.id];
+  let headers = new Headers(actionHeaders);
+  if (loaderHeaders) {
+    for (let [key, value] of loaderHeaders.entries()) {
+      headers.append(key, value);
+    }
+  }
+
+  headers.set("Content-Type", "text/html; charset=utf-8");
+
+  // 4. 返回一个客户端响应
+  return new Response(\`<!DOCTYPE html>\${html}\`, {
+    status: context.statusCode,
+    headers,
+  });
+}

4. 在浏览器中进行水合

在服务器端渲染的流程中,当服务器生成好要发送给客户端的 HTML 内容时,会将水合作用数据以特定的形式嵌入到 HTML 页面中,通常会挂载到 window.__staticRouterHydrationData 属性上。

当客户端接收到包含水合作用数据的 HTML 页面后,就可以利用 window.__staticRouterHydrationData 中的数据来初始化客户端的路由器并渲染 <RouterProvider> 组件,实现页面从静态到动态的转换以及后续的交互功能。

tsx
import { StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+import { RouterProvider } from "react-router/dom";
+import routes from "./app/routes.js";
+import { createBrowserRouter } from "react-router";
+
+let router = createBrowserRouter(routes, {
+  hydrationData: window.__staticRouterHydrationData,
+});
+
+hydrateRoot(
+  document,
+  <StrictMode>
+    <RouterProvider router={router} />
+  </StrictMode>
+);
`,32)]))}const g=i(h,[["render",l]]);export{o as __pageData,g as default}; diff --git a/assets/framework_data-loading.md.BWeWrub0.js b/assets/framework_data-loading.md.BWeWrub0.js new file mode 100644 index 0000000..252efbc --- /dev/null +++ b/assets/framework_data-loading.md.BWeWrub0.js @@ -0,0 +1,80 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"数据加载","description":"","frontmatter":{},"headers":[],"relativePath":"framework/data-loading.md","filePath":"framework/data-loading.md"}'),h={name:"framework/data-loading.md"};function p(k,s,l,e,E,d){return t(),a("div",null,s[0]||(s[0]=[n(`

数据加载

数据是通过加载器(loader)和客户端加载器(clientLoader)提供给路由组件的。

加载器数据会自动从加载器中进行序列化操作,然后在组件中进行反序列化。除了像字符串、数字这类基本数据类型的值以外,加载器还能够返回 promises、maps、sets、dates 等多种类型的数据。

客户端数据加载

客户端加载器(clientLoader)用于在客户端(也就是浏览器端)获取数据。对于那些你更倾向于仅从浏览器去获取数据的页面或者整个项目而言,它是非常有用的。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+
+export async function clientLoader({ params }: Route.ClientLoaderArgs) {
+  const res = await fetch(\`/api/products/\${params.pid}\`);
+  const product = await res.json();
+  return product;
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}

服务器端数据加载

在服务器端渲染的情况下,加载器(loader)既用于初始页面加载,也用于客户端导航(client navigations)。在客户端导航期间,加载器会通过 React Router 自动从浏览器向服务器发起获取数据的 fetch 请求。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+import { fakeDb } from "../db";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  const product = await fakeDb.getProduct(params.pid);
+  return product;
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}

需要注意的是,loader 函数会从客户端的打包代码包(bundles)中移除,这样一来,你就能够使用仅在服务器端可用的应用程序接口(APIs),而无需担心它们会被包含在浏览器端的代码中。

静态数据加载

在进行预渲染时,加载器(loaders)会被用于在生产构建阶段获取数据。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  let product = await getProductFromCSVFile(params.pid);
+  return product;
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}

react-router.config.ts 文件中指定需要预渲染的 URL:

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  async prerender() {
+    let products = await readProductsFromCSVFile();
+    return products.map((product) => \`/products/\${product.id}\`);
+  },
+} satisfies Config;

需要注意的是,在服务器端渲染时,任何未进行预渲染的 URL 对应的页面将会像往常一样进行服务器端渲染。这一机制使得你能够针对单个路由预渲染部分数据,同时对其余部分仍采用服务器端渲染的方式来处理。

同时使用两种加载器

加载器(loader)与客户端加载器(clientLoader)是可以一起使用的。加载器会在服务器端用于初始的服务器端渲染(Server Side Rendering,简称 SSR)或者静态预渲染(Pre-rendering)操作,而 clientLoader 则会在后续的客户端导航(client-side navigations)过程中被使用。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+import { fakeDb } from "../db";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  return fakeDb.getProduct(params.pid);
+}
+
+export async function clientLoader({ params }: Route.ClientLoader) {
+  const res = await fetch(\`/api/products/\${params.pid}\`);
+  return res.json();
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}
`,19)]))}const y=i(h,[["render",p]]);export{g as __pageData,y as default}; diff --git a/assets/framework_data-loading.md.BWeWrub0.lean.js b/assets/framework_data-loading.md.BWeWrub0.lean.js new file mode 100644 index 0000000..252efbc --- /dev/null +++ b/assets/framework_data-loading.md.BWeWrub0.lean.js @@ -0,0 +1,80 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"数据加载","description":"","frontmatter":{},"headers":[],"relativePath":"framework/data-loading.md","filePath":"framework/data-loading.md"}'),h={name:"framework/data-loading.md"};function p(k,s,l,e,E,d){return t(),a("div",null,s[0]||(s[0]=[n(`

数据加载

数据是通过加载器(loader)和客户端加载器(clientLoader)提供给路由组件的。

加载器数据会自动从加载器中进行序列化操作,然后在组件中进行反序列化。除了像字符串、数字这类基本数据类型的值以外,加载器还能够返回 promises、maps、sets、dates 等多种类型的数据。

客户端数据加载

客户端加载器(clientLoader)用于在客户端(也就是浏览器端)获取数据。对于那些你更倾向于仅从浏览器去获取数据的页面或者整个项目而言,它是非常有用的。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+
+export async function clientLoader({ params }: Route.ClientLoaderArgs) {
+  const res = await fetch(\`/api/products/\${params.pid}\`);
+  const product = await res.json();
+  return product;
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}

服务器端数据加载

在服务器端渲染的情况下,加载器(loader)既用于初始页面加载,也用于客户端导航(client navigations)。在客户端导航期间,加载器会通过 React Router 自动从浏览器向服务器发起获取数据的 fetch 请求。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+import { fakeDb } from "../db";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  const product = await fakeDb.getProduct(params.pid);
+  return product;
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}

需要注意的是,loader 函数会从客户端的打包代码包(bundles)中移除,这样一来,你就能够使用仅在服务器端可用的应用程序接口(APIs),而无需担心它们会被包含在浏览器端的代码中。

静态数据加载

在进行预渲染时,加载器(loaders)会被用于在生产构建阶段获取数据。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  let product = await getProductFromCSVFile(params.pid);
+  return product;
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}

react-router.config.ts 文件中指定需要预渲染的 URL:

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  async prerender() {
+    let products = await readProductsFromCSVFile();
+    return products.map((product) => \`/products/\${product.id}\`);
+  },
+} satisfies Config;

需要注意的是,在服务器端渲染时,任何未进行预渲染的 URL 对应的页面将会像往常一样进行服务器端渲染。这一机制使得你能够针对单个路由预渲染部分数据,同时对其余部分仍采用服务器端渲染的方式来处理。

同时使用两种加载器

加载器(loader)与客户端加载器(clientLoader)是可以一起使用的。加载器会在服务器端用于初始的服务器端渲染(Server Side Rendering,简称 SSR)或者静态预渲染(Pre-rendering)操作,而 clientLoader 则会在后续的客户端导航(client-side navigations)过程中被使用。

tsx
// route("products/:pid", "./product.tsx");
+import type { Route } from "./+types/product";
+import { fakeDb } from "../db";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  return fakeDb.getProduct(params.pid);
+}
+
+export async function clientLoader({ params }: Route.ClientLoader) {
+  const res = await fetch(\`/api/products/\${params.pid}\`);
+  return res.json();
+}
+
+export default function Product({ loaderData }: Route.ComponentProps) {
+  const { name, description } = loaderData;
+
+  return (
+    <div>
+      <h1>{name}</h1>
+      <p>{description}</p>
+    </div>
+  );
+}
`,19)]))}const y=i(h,[["render",p]]);export{g as __pageData,y as default}; diff --git a/assets/framework_pending-ui.md.YlfuNG_q.js b/assets/framework_pending-ui.md.YlfuNG_q.js new file mode 100644 index 0000000..024ee50 --- /dev/null +++ b/assets/framework_pending-ui.md.YlfuNG_q.js @@ -0,0 +1,82 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"待处理的 UI","description":"","frontmatter":{},"headers":[],"relativePath":"framework/pending-ui.md","filePath":"framework/pending-ui.md"}'),h={name:"framework/pending-ui.md"};function l(k,s,p,e,E,r){return t(),a("div",null,s[0]||(s[0]=[n(`

待处理的 UI

当用户导航到新路由,或者向 Actions 提交数据这类操作时,用户界面(UI)应当立即呈现一种 loading 状态或者响应状态,这是在代码逻辑中要干的事情。

全局 loading

当用户在 Web 应用中导航到一个新的 URL 时,会涉及到页面加载器(loaders)以及页面渲染之间的协调关系,并且可以通过 useNavigation 这个钩子函数来获取到 loading 状态:

tsx
import { useNavigation } from "react-router";
+
+export default function Root() {
+  const navigation = useNavigation();
+  const isNavigating = Boolean(navigation.location);
+
+  return (
+    <html>
+      <body>
+        {isNavigating && <GlobalSpinner />}
+        <Outlet />
+      </body>
+    </html>
+  );
+}

局部 loading

在 Web 应用开发中,不仅可以在页面整体层面展示 loading 指示器来提示用户操作正在进行中,还能够将这类指示器本地化到具体的链接上,而 <NavLink> 组件的 children、className 和 style 属性在此过程中发挥了重要作用,它们可以接收 loading 状态作为参数的函数形式来实现相应的本地化效果:

tsx
import { NavLink } from "react-router";
+
+function Navbar() {
+  return (
+    <nav>
+      <NavLink to="/home">
+        {({ isPending }) => <span>Home {isPending && <Spinner />}</span>}
+      </NavLink>
+      <NavLink
+        to="/about"
+        style={({ isPending }) => ({
+          color: isPending ? "gray" : "black",
+        })}
+      >
+        About
+      </NavLink>
+    </nav>
+  );
+}

Form 提交 loading

当用户在 Web 应用中提交表单时,用户界面(UI)应当立即通过呈现 loading 状态来对用户的这一操作行为做出响应,告知用户表单提交操作正在进行中。在实现这一功能时,使用 fetcher 表单相对更为便捷,这是因为它具备自身独立的状态。(而常规的表单提交往往会引发全局导航)。

tsx
import { useFetcher } from "react-router";
+
+function NewProjectForm() {
+  const fetcher = useFetcher();
+
+  return (
+    <fetcher.Form method="post">
+      <input type="text" name="title" />
+      <button type="submit">
+        {fetcher.state !== "idle" ? "Submitting..." : "Submit"}
+      </button>
+    </fetcher.Form>
+  );
+}

fetcher 表单提交的场景下,使用 useNavigation 处理 loading 状态。

tsx
import { useNavigation, Form } from "react-router";
+
+function NewProjectForm() {
+  const navigation = useNavigation();
+
+  return (
+    <Form method="post" action="/projects/new">
+      <input type="text" name="title" />
+      <button type="submit">
+        {navigation.formAction === "/projects/new"
+          ? "Submitting..."
+          : "Submit"}
+      </button>
+    </Form>
+  );
+}

乐观 UI

当通过表单提交数据能够知晓用户界面(UI)的未来状态时,便可以实现乐观(Optimistic)UI,从而为用户带来即时的用户体验(UX)提升:

tsx
function Task({ task }) {
+  const fetcher = useFetcher();
+
+  let isComplete = task.status === "complete";
+  if (fetcher.formData) {
+    isComplete = fetcher.formData.get("status");
+  }
+
+  return (
+    <div>
+      <div>{task.title}</div>
+      <fetcher.Form method="post">
+        <button
+          name="status"
+          value={isComplete ? "incomplete" : "complete"}
+        >
+          {isComplete ? "Mark Incomplete" : "Mark Complete"}
+        </button>
+      </fetcher.Form>
+    </div>
+  );
+}
`,16)]))}const y=i(h,[["render",l]]);export{g as __pageData,y as default}; diff --git a/assets/framework_pending-ui.md.YlfuNG_q.lean.js b/assets/framework_pending-ui.md.YlfuNG_q.lean.js new file mode 100644 index 0000000..024ee50 --- /dev/null +++ b/assets/framework_pending-ui.md.YlfuNG_q.lean.js @@ -0,0 +1,82 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"待处理的 UI","description":"","frontmatter":{},"headers":[],"relativePath":"framework/pending-ui.md","filePath":"framework/pending-ui.md"}'),h={name:"framework/pending-ui.md"};function l(k,s,p,e,E,r){return t(),a("div",null,s[0]||(s[0]=[n(`

待处理的 UI

当用户导航到新路由,或者向 Actions 提交数据这类操作时,用户界面(UI)应当立即呈现一种 loading 状态或者响应状态,这是在代码逻辑中要干的事情。

全局 loading

当用户在 Web 应用中导航到一个新的 URL 时,会涉及到页面加载器(loaders)以及页面渲染之间的协调关系,并且可以通过 useNavigation 这个钩子函数来获取到 loading 状态:

tsx
import { useNavigation } from "react-router";
+
+export default function Root() {
+  const navigation = useNavigation();
+  const isNavigating = Boolean(navigation.location);
+
+  return (
+    <html>
+      <body>
+        {isNavigating && <GlobalSpinner />}
+        <Outlet />
+      </body>
+    </html>
+  );
+}

局部 loading

在 Web 应用开发中,不仅可以在页面整体层面展示 loading 指示器来提示用户操作正在进行中,还能够将这类指示器本地化到具体的链接上,而 <NavLink> 组件的 children、className 和 style 属性在此过程中发挥了重要作用,它们可以接收 loading 状态作为参数的函数形式来实现相应的本地化效果:

tsx
import { NavLink } from "react-router";
+
+function Navbar() {
+  return (
+    <nav>
+      <NavLink to="/home">
+        {({ isPending }) => <span>Home {isPending && <Spinner />}</span>}
+      </NavLink>
+      <NavLink
+        to="/about"
+        style={({ isPending }) => ({
+          color: isPending ? "gray" : "black",
+        })}
+      >
+        About
+      </NavLink>
+    </nav>
+  );
+}

Form 提交 loading

当用户在 Web 应用中提交表单时,用户界面(UI)应当立即通过呈现 loading 状态来对用户的这一操作行为做出响应,告知用户表单提交操作正在进行中。在实现这一功能时,使用 fetcher 表单相对更为便捷,这是因为它具备自身独立的状态。(而常规的表单提交往往会引发全局导航)。

tsx
import { useFetcher } from "react-router";
+
+function NewProjectForm() {
+  const fetcher = useFetcher();
+
+  return (
+    <fetcher.Form method="post">
+      <input type="text" name="title" />
+      <button type="submit">
+        {fetcher.state !== "idle" ? "Submitting..." : "Submit"}
+      </button>
+    </fetcher.Form>
+  );
+}

fetcher 表单提交的场景下,使用 useNavigation 处理 loading 状态。

tsx
import { useNavigation, Form } from "react-router";
+
+function NewProjectForm() {
+  const navigation = useNavigation();
+
+  return (
+    <Form method="post" action="/projects/new">
+      <input type="text" name="title" />
+      <button type="submit">
+        {navigation.formAction === "/projects/new"
+          ? "Submitting..."
+          : "Submit"}
+      </button>
+    </Form>
+  );
+}

乐观 UI

当通过表单提交数据能够知晓用户界面(UI)的未来状态时,便可以实现乐观(Optimistic)UI,从而为用户带来即时的用户体验(UX)提升:

tsx
function Task({ task }) {
+  const fetcher = useFetcher();
+
+  let isComplete = task.status === "complete";
+  if (fetcher.formData) {
+    isComplete = fetcher.formData.get("status");
+  }
+
+  return (
+    <div>
+      <div>{task.title}</div>
+      <fetcher.Form method="post">
+        <button
+          name="status"
+          value={isComplete ? "incomplete" : "complete"}
+        >
+          {isComplete ? "Mark Incomplete" : "Mark Complete"}
+        </button>
+      </fetcher.Form>
+    </div>
+  );
+}
`,16)]))}const y=i(h,[["render",l]]);export{g as __pageData,y as default}; diff --git a/assets/framework_rendering-strategies.md.DL_QGqDy.js b/assets/framework_rendering-strategies.md.DL_QGqDy.js new file mode 100644 index 0000000..66b93f1 --- /dev/null +++ b/assets/framework_rendering-strategies.md.DL_QGqDy.js @@ -0,0 +1,16 @@ +import{_ as i,c as a,a0 as t,o as e}from"./chunks/framework.xCeNF-Bo.js";const c=JSON.parse('{"title":"渲染策略","description":"","frontmatter":{},"headers":[],"relativePath":"framework/rendering-strategies.md","filePath":"framework/rendering-strategies.md"}'),n={name:"framework/rendering-strategies.md"};function l(p,s,h,k,r,d){return e(),a("div",null,s[0]||(s[0]=[t(`

渲染策略

在 React Router 中存在三种渲染策略:

客户端渲染

在用户浏览应用程序进行页面导航时,路由始终是在客户端进行渲染的。如果您打算构建一个单页应用(Single Page App,简称 SPA),那么需要禁用服务器端渲染。

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  ssr: false,
+} satisfies Config;

服务器端渲染

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  ssr: true,
+} satisfies Config;

服务器端渲染需要有支持它的部署环境。尽管它是一个全局性的设置,但个别路由仍然可以采用静态预渲染的方式。此外,路由还能够借助客户端加载器(clientLoader)进行客户端数据加载,以此避免对其对应的那部分用户界面(UI)进行服务器端渲染及数据获取操作。

静态预渲染

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  // 在构建时返回一个要进行预渲染的 URL 列表
+  async prerender() {
+    return ["/", "/about", "/contact"];
+  },
+} satisfies Config;

静态预渲染是一种在构建时进行的操作,它会为一系列的 URL 生成静态 HTML 以及客户端导航数据有效负载。这对于搜索引擎优化(SEO)和性能提升方面很有帮助,尤其适用于那些没有采用服务器端渲染的部署环境。在进行预渲染时,路由模块加载器(route module loaders)会被用于在构建时获取数据。

`,12)]))}const g=i(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/assets/framework_rendering-strategies.md.DL_QGqDy.lean.js b/assets/framework_rendering-strategies.md.DL_QGqDy.lean.js new file mode 100644 index 0000000..66b93f1 --- /dev/null +++ b/assets/framework_rendering-strategies.md.DL_QGqDy.lean.js @@ -0,0 +1,16 @@ +import{_ as i,c as a,a0 as t,o as e}from"./chunks/framework.xCeNF-Bo.js";const c=JSON.parse('{"title":"渲染策略","description":"","frontmatter":{},"headers":[],"relativePath":"framework/rendering-strategies.md","filePath":"framework/rendering-strategies.md"}'),n={name:"framework/rendering-strategies.md"};function l(p,s,h,k,r,d){return e(),a("div",null,s[0]||(s[0]=[t(`

渲染策略

在 React Router 中存在三种渲染策略:

客户端渲染

在用户浏览应用程序进行页面导航时,路由始终是在客户端进行渲染的。如果您打算构建一个单页应用(Single Page App,简称 SPA),那么需要禁用服务器端渲染。

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  ssr: false,
+} satisfies Config;

服务器端渲染

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  ssr: true,
+} satisfies Config;

服务器端渲染需要有支持它的部署环境。尽管它是一个全局性的设置,但个别路由仍然可以采用静态预渲染的方式。此外,路由还能够借助客户端加载器(clientLoader)进行客户端数据加载,以此避免对其对应的那部分用户界面(UI)进行服务器端渲染及数据获取操作。

静态预渲染

ts
import type { Config } from "@react-router/dev/config";
+
+export default {
+  // 在构建时返回一个要进行预渲染的 URL 列表
+  async prerender() {
+    return ["/", "/about", "/contact"];
+  },
+} satisfies Config;

静态预渲染是一种在构建时进行的操作,它会为一系列的 URL 生成静态 HTML 以及客户端导航数据有效负载。这对于搜索引擎优化(SEO)和性能提升方面很有帮助,尤其适用于那些没有采用服务器端渲染的部署环境。在进行预渲染时,路由模块加载器(route module loaders)会被用于在构建时获取数据。

`,12)]))}const g=i(n,[["render",l]]);export{c as __pageData,g as default}; diff --git a/assets/framework_route-module.md.B93mn3gQ.js b/assets/framework_route-module.md.B93mn3gQ.js new file mode 100644 index 0000000..cf47cce --- /dev/null +++ b/assets/framework_route-module.md.B93mn3gQ.js @@ -0,0 +1,160 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"路由模块","description":"","frontmatter":{},"headers":[],"relativePath":"framework/route-module.md","filePath":"framework/route-module.md"}'),l={name:"framework/route-module.md"};function h(p,s,k,e,E,r){return t(),a("div",null,s[0]||(s[0]=[n(`

路由模块

routes.ts 文件中所引用的文件被称作路由模块。

ts
route("teams/:teamId", "./team.tsx"),
+//                路由模块 ^^^^^^^^

路由模块是 React Router 框架功能的基础,它们定义了以下内容:

本指南只是对每个路由模块功能进行了简要概述,后续的入门指南将会更详细地涵盖这些功能,以帮助开发者更深入地理解和运用它们来构建高质量、功能完备的 Web 应用路由体系。

组件(default)

在路由匹配时,定义的组件将会被渲染。

tsx
export default function MyRouteComponent() {
+  return (
+    <div>
+      <h1>Look ma!</h1>
+      <p>I'm still using React Router after like 10 years.</p>
+    </div>
+  );
+}

loader

路由加载器(Route loaders)会在路由组件被渲染之前为其提供数据。它们仅在服务器端渲染(server rendering)时在服务器上被调用,或者在预渲染(pre-rendering)构建过程中被调用。

tsx
export async function loader() {
+  return { message: "Hello, world!" };
+}
+
+export default function MyRoute({ loaderData }) {
+  return <h1>{loaderData.message}</h1>;
+}

也可参考:

clientLoader

仅在浏览器中被调用,路由客户端加载器(route client loaders)除了可以补充路由加载器(route loaders)所提供的数据之外,也能够替代路由加载器,为路由组件提供数据。

tsx
export async function clientLoader({ serverLoader }) {
+  // 服务端加载器调用
+  const serverData = await serverLoader();
+  // 和/或 客户端数据获取
+  const data = getDataFromClient();
+  // 通过 \`useLoaderData()\` 来返回要暴露(提供)的数据
+  return data;
+}

客户端加载器(Client loaders)能够通过在函数上设置 hydrate 属性,参与服务器端渲染页面的初始页面加载水合(initial page load hydration)过程:

tsx
export async function clientLoader() {
+  // ...
+}
+clientLoader.hydrate = true as const;

提示:

通过使用 as const,TypeScript 将会推断出 clientLoader.hydrate 的类型为 true 而非 boolean 类型。这样一来,React Router 就能够基于 clientLoader.hydrate 的值来推导出 loaderData 的类型了。

也可参考:

action

路由操作(Route actions)允许进行服务器端的数据变更,并且当从 <Form>useFetcher 以及 useSubmit 进行调用时,会自动重新验证页面上所有的加载器(loader)数据:

tsx
// route("/list", "./list.tsx")
+import { Form } from "react-router";
+import { TodoList } from "~/components/TodoList";
+
+// action 完成后数据才会加载...
+export async function loader() {
+  const items = await fakeDb.getItems();
+  return { items };
+}
+
+// ...以便此处的列表能够自动更新
+export default function Items({ loaderData }) {
+  return (
+    <div>
+      <List items={loaderData.items} />
+      <Form method="post" navigate={false} action="/list">
+        <input type="text" name="title" />
+        <button type="submit">Create Todo</button>
+      </Form>
+    </div>
+  );
+}
+
+export async function action({ request }) {
+  const data = await request.formData();
+  const todo = await fakeDb.addItem({
+    title: data.get("title"),
+  });
+  return { ok: true };
+}

clientAction

就像 action 一样,不过它仅在浏览器中被调用:

tsx
export async function clientAction({ serverAction }) {
+  fakeInvalidateClientSideCache();
+  // 如果有需要的话,仍然可以调用服务器端操作(server action)
+  const data = await serverAction();
+  return data;
+}

也可参考:

ErrorBoundary

当其他路由模块的应用程序接口(APIs)抛出异常时,路由模块的错误边界(ErrorBoundary)将会替代路由组件进行渲染:

tsx
import { isRouteErrorResponse, useRouteError } from "react-router";
+
+export function ErrorBoundary() {
+  const error = useRouteError();
+
+  if (isRouteErrorResponse(error)) {
+    return (
+      <div>
+        <h1>
+          {error.status} {error.statusText}
+        </h1>
+        <p>{error.data}</p>
+      </div>
+    );
+  } else if (error instanceof Error) {
+    return (
+      <div>
+        <h1>Error</h1>
+        <p>{error.message}</p>
+        <p>The stack trace is:</p>
+        <pre>{error.stack}</pre>
+      </div>
+    );
+  } else {
+    return <h1>Unknown Error</h1>;
+  }
+}

HydrateFallback

在初始页面加载时,只有在客户端加载器(client loader)完成工作之后,路由组件才会进行渲染。如果(相关内容)被导出,那么一个水合回退(HydrateFallback)组件可以立即渲染,取代路由组件的位置:

tsx
export async function clientLoader() {
+  const data = await fakeLoadLocalGameData();
+  return data;
+}
+
+export function HydrateFallback() {
+  return <p>Loading Game...</p>;
+}
+
+export default function Component({ loaderData }) {
+  return <Game data={loaderData} />;
+}

headers

路由头部(Route headers)用于定义在服务器端渲染时随响应一同发送的 HTTP 头部信息:

tsx
export function headers() {
+  return {
+    "X-Stretchy-Pants": "its for fun",
+    "Cache-Control": "max-age=300, s-maxage=3600",
+  };
+}

handle

路由句柄(Route handle)允许应用程序向 useMatches 中的路由匹配结果添加任何内容,以创建抽象概念(比如面包屑导航等):

tsx
export const handle = {
+  its: "all yours",
+};

路由链接(Route links)用于定义 <link> 元素,这些 <link> 元素将会在文档的 <head> 部分被渲染出来:

tsx
export function links() {
+  return [
+    {
+      rel: "icon",
+      href: "/favicon.png",
+      type: "image/png",
+    },
+    {
+      rel: "stylesheet",
+      href: "https://example.com/some/styles.css",
+    },
+    {
+      rel: "preload",
+      href: "/images/banner.jpg",
+      as: "image",
+    },
+  ];
+}

所有的路由链接(Route links)将会被汇总并通过 <Links /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Links } from "react-router";
+
+export default function Root() {
+  return (
+    <html>
+      <head>
+        <Links />
+      </head>
+
+      <body />
+    </html>
+  );
+}

meta

路由元数据(Route meta)用于定义那些将会在文档的 <head> 部分被渲染的元标签(meta tags):

tsx
export function meta() {
+  return [
+    { title: "Very cool app" },
+    {
+      property: "og:title",
+      content: "Very cool app",
+    },
+    {
+      name: "description",
+      content: "This app is the best",
+    },
+  ];
+}

所有路由的元数据(meta)将会被汇总并通过 <Meta /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Meta } from "react-router";
+
+export default function Root() {
+  return (
+    <html>
+      <head>
+        <Meta />
+      </head>
+
+      <body />
+    </html>
+  );
+}

也可参考:

shouldRevalidate

默认情况下,在执行操作(actions)之后,所有路由都会进行重新验证(revalidated)。而此功能允许某个路由选择不针对那些不会影响其自身数据的操作进行重新验证。

tsx
import type { ShouldRevalidateFunctionArgs } from "react-router";
+
+export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) {
+  return true;
+}
`,57)]))}const o=i(l,[["render",h]]);export{g as __pageData,o as default}; diff --git a/assets/framework_route-module.md.B93mn3gQ.lean.js b/assets/framework_route-module.md.B93mn3gQ.lean.js new file mode 100644 index 0000000..cf47cce --- /dev/null +++ b/assets/framework_route-module.md.B93mn3gQ.lean.js @@ -0,0 +1,160 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"路由模块","description":"","frontmatter":{},"headers":[],"relativePath":"framework/route-module.md","filePath":"framework/route-module.md"}'),l={name:"framework/route-module.md"};function h(p,s,k,e,E,r){return t(),a("div",null,s[0]||(s[0]=[n(`

路由模块

routes.ts 文件中所引用的文件被称作路由模块。

ts
route("teams/:teamId", "./team.tsx"),
+//                路由模块 ^^^^^^^^

路由模块是 React Router 框架功能的基础,它们定义了以下内容:

本指南只是对每个路由模块功能进行了简要概述,后续的入门指南将会更详细地涵盖这些功能,以帮助开发者更深入地理解和运用它们来构建高质量、功能完备的 Web 应用路由体系。

组件(default)

在路由匹配时,定义的组件将会被渲染。

tsx
export default function MyRouteComponent() {
+  return (
+    <div>
+      <h1>Look ma!</h1>
+      <p>I'm still using React Router after like 10 years.</p>
+    </div>
+  );
+}

loader

路由加载器(Route loaders)会在路由组件被渲染之前为其提供数据。它们仅在服务器端渲染(server rendering)时在服务器上被调用,或者在预渲染(pre-rendering)构建过程中被调用。

tsx
export async function loader() {
+  return { message: "Hello, world!" };
+}
+
+export default function MyRoute({ loaderData }) {
+  return <h1>{loaderData.message}</h1>;
+}

也可参考:

clientLoader

仅在浏览器中被调用,路由客户端加载器(route client loaders)除了可以补充路由加载器(route loaders)所提供的数据之外,也能够替代路由加载器,为路由组件提供数据。

tsx
export async function clientLoader({ serverLoader }) {
+  // 服务端加载器调用
+  const serverData = await serverLoader();
+  // 和/或 客户端数据获取
+  const data = getDataFromClient();
+  // 通过 \`useLoaderData()\` 来返回要暴露(提供)的数据
+  return data;
+}

客户端加载器(Client loaders)能够通过在函数上设置 hydrate 属性,参与服务器端渲染页面的初始页面加载水合(initial page load hydration)过程:

tsx
export async function clientLoader() {
+  // ...
+}
+clientLoader.hydrate = true as const;

提示:

通过使用 as const,TypeScript 将会推断出 clientLoader.hydrate 的类型为 true 而非 boolean 类型。这样一来,React Router 就能够基于 clientLoader.hydrate 的值来推导出 loaderData 的类型了。

也可参考:

action

路由操作(Route actions)允许进行服务器端的数据变更,并且当从 <Form>useFetcher 以及 useSubmit 进行调用时,会自动重新验证页面上所有的加载器(loader)数据:

tsx
// route("/list", "./list.tsx")
+import { Form } from "react-router";
+import { TodoList } from "~/components/TodoList";
+
+// action 完成后数据才会加载...
+export async function loader() {
+  const items = await fakeDb.getItems();
+  return { items };
+}
+
+// ...以便此处的列表能够自动更新
+export default function Items({ loaderData }) {
+  return (
+    <div>
+      <List items={loaderData.items} />
+      <Form method="post" navigate={false} action="/list">
+        <input type="text" name="title" />
+        <button type="submit">Create Todo</button>
+      </Form>
+    </div>
+  );
+}
+
+export async function action({ request }) {
+  const data = await request.formData();
+  const todo = await fakeDb.addItem({
+    title: data.get("title"),
+  });
+  return { ok: true };
+}

clientAction

就像 action 一样,不过它仅在浏览器中被调用:

tsx
export async function clientAction({ serverAction }) {
+  fakeInvalidateClientSideCache();
+  // 如果有需要的话,仍然可以调用服务器端操作(server action)
+  const data = await serverAction();
+  return data;
+}

也可参考:

ErrorBoundary

当其他路由模块的应用程序接口(APIs)抛出异常时,路由模块的错误边界(ErrorBoundary)将会替代路由组件进行渲染:

tsx
import { isRouteErrorResponse, useRouteError } from "react-router";
+
+export function ErrorBoundary() {
+  const error = useRouteError();
+
+  if (isRouteErrorResponse(error)) {
+    return (
+      <div>
+        <h1>
+          {error.status} {error.statusText}
+        </h1>
+        <p>{error.data}</p>
+      </div>
+    );
+  } else if (error instanceof Error) {
+    return (
+      <div>
+        <h1>Error</h1>
+        <p>{error.message}</p>
+        <p>The stack trace is:</p>
+        <pre>{error.stack}</pre>
+      </div>
+    );
+  } else {
+    return <h1>Unknown Error</h1>;
+  }
+}

HydrateFallback

在初始页面加载时,只有在客户端加载器(client loader)完成工作之后,路由组件才会进行渲染。如果(相关内容)被导出,那么一个水合回退(HydrateFallback)组件可以立即渲染,取代路由组件的位置:

tsx
export async function clientLoader() {
+  const data = await fakeLoadLocalGameData();
+  return data;
+}
+
+export function HydrateFallback() {
+  return <p>Loading Game...</p>;
+}
+
+export default function Component({ loaderData }) {
+  return <Game data={loaderData} />;
+}

headers

路由头部(Route headers)用于定义在服务器端渲染时随响应一同发送的 HTTP 头部信息:

tsx
export function headers() {
+  return {
+    "X-Stretchy-Pants": "its for fun",
+    "Cache-Control": "max-age=300, s-maxage=3600",
+  };
+}

handle

路由句柄(Route handle)允许应用程序向 useMatches 中的路由匹配结果添加任何内容,以创建抽象概念(比如面包屑导航等):

tsx
export const handle = {
+  its: "all yours",
+};

路由链接(Route links)用于定义 <link> 元素,这些 <link> 元素将会在文档的 <head> 部分被渲染出来:

tsx
export function links() {
+  return [
+    {
+      rel: "icon",
+      href: "/favicon.png",
+      type: "image/png",
+    },
+    {
+      rel: "stylesheet",
+      href: "https://example.com/some/styles.css",
+    },
+    {
+      rel: "preload",
+      href: "/images/banner.jpg",
+      as: "image",
+    },
+  ];
+}

所有的路由链接(Route links)将会被汇总并通过 <Links /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Links } from "react-router";
+
+export default function Root() {
+  return (
+    <html>
+      <head>
+        <Links />
+      </head>
+
+      <body />
+    </html>
+  );
+}

meta

路由元数据(Route meta)用于定义那些将会在文档的 <head> 部分被渲染的元标签(meta tags):

tsx
export function meta() {
+  return [
+    { title: "Very cool app" },
+    {
+      property: "og:title",
+      content: "Very cool app",
+    },
+    {
+      name: "description",
+      content: "This app is the best",
+    },
+  ];
+}

所有路由的元数据(meta)将会被汇总并通过 <Meta /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Meta } from "react-router";
+
+export default function Root() {
+  return (
+    <html>
+      <head>
+        <Meta />
+      </head>
+
+      <body />
+    </html>
+  );
+}

也可参考:

shouldRevalidate

默认情况下,在执行操作(actions)之后,所有路由都会进行重新验证(revalidated)。而此功能允许某个路由选择不针对那些不会影响其自身数据的操作进行重新验证。

tsx
import type { ShouldRevalidateFunctionArgs } from "react-router";
+
+export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) {
+  return true;
+}
`,57)]))}const o=i(l,[["render",h]]);export{g as __pageData,o as default}; diff --git a/assets/framework_routing.md.Hy04wNhs.js b/assets/framework_routing.md.Hy04wNhs.js new file mode 100644 index 0000000..5777ecd --- /dev/null +++ b/assets/framework_routing.md.Hy04wNhs.js @@ -0,0 +1,134 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.xCeNF-Bo.js";const o=JSON.parse('{"title":"路由","description":"","frontmatter":{},"headers":[],"relativePath":"framework/routing.md","filePath":"framework/routing.md"}'),h={name:"framework/routing.md"};function p(l,s,k,e,E,d){return n(),a("div",null,s[0]||(s[0]=[t(`

路由

配置路由

路由是在 app/routes.ts 文件中进行配置的。每条路由都包含两个必需的部分:一个用于匹配 URL 的 URL 模式,以及一个指向定义其行为的路由模块的文件路径。

tsx
import { type RouteConfig, route } from "@react-router/dev/routes";
+
+export default [
+  route("some/path", "./some/file.tsx"),
+  //  匹配模式 ^           ^ 模块文件路径
+] satisfies RouteConfig;

以下是一个更复杂些的路由配置示例:

ts
import {
+  type RouteConfig,
+  route,
+  index,
+  layout,
+  prefix,
+} from "@react-router/dev/routes";
+
+export default [
+  index("./home.tsx"),
+  route("about", "./about.tsx"),
+
+  layout("./auth/layout.tsx", [
+    route("login", "./auth/login.tsx"),
+    route("register", "./auth/register.tsx"),
+  ]),
+
+  ...prefix("concerts", [
+    index("./concerts/home.tsx"),
+    route(":city", "./concerts/city.tsx"),
+    route("trending", "./concerts/trending.tsx"),
+  ]),
+] satisfies RouteConfig;

如果你更倾向于通过文件命名约定而非配置的方式来定义路由,那么 @react-router/fs-routes 包提供了一种基于文件系统的路由约定

路由模块

routes.ts 文件中引用的文件定义了每条路由的行为。

ts
route("teams/:teamId", "./team.tsx"),
+//                路由模块 ^^^^^^^^

以下是一个简单的路由模块示例:

tsx
// 提供类型安全 / 类型推断
+import type { Route } from "./+types/team";
+
+// 向组件提供 \`loaderData\`
+export async function loader({ params }: Route.LoaderArgs) {
+  let team = await fetchTeam(params.teamId);
+  return { name: team.name };
+}
+
+// 在加载器完成后渲染组件
+export default function Component({ loaderData }: Route.ComponentProps) {
+  return <h1>{loaderData.name}</h1>;
+}

路由模块具备更多功能,比如操作(actions)、头部信息(headers)以及错误边界(error boundaries)等,但这些内容将会在下一篇指南: 路由模块中进行介绍。

嵌套路由

它允许路由被嵌套在父路由内部。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
+
+export default [
+  // 父路由
+  route("dashboard", "./dashboard.tsx", [
+    // 子路由
+    index("./home.tsx"),
+    route("settings", "./settings.tsx"),
+  ]),
+] satisfies RouteConfig;

父路由的路径会自动包含在子路由中,所以以上配置会同时创建出 “/dashboard”“/dashboard/settings” 两个 URL 路由。

子路由是通过父路由中的 <Outlet/> 组件来进行渲染的。

tsx
import { Outlet } from "react-router";
+
+export default function Dashboard() {
+  return (
+    <div>
+      <h1>Dashboard</h1>
+      {/* 将会是 home.tsx 或者 settings.tsx */}
+      <Outlet />
+    </div>
+  );
+}

根路由

routes.ts 文件中的每条路由都嵌套在特殊的 app/root.tsx 模块内部。

布局路由

layout 是一种在路由体系中有着独特作用的路由类型。它能够为其子路由创建新的嵌套层级,不过值得注意的是,它并不会给 URL 添加额外的路径片段。可以把它类比为根路由,根路由构建了整个应用最基础的页面布局框架,而布局路由同样是起着构建页面布局框架的作用,只是它可以在任意层级被添加,从而灵活地塑造应用内不同部分的页面层级结构和布局样式。

ts
import {
+  type RouteConfig,
+  route,
+  layout,
+  index,
+  prefix,
+} from "@react-router/dev/routes";
+
+export default [
+  layout("./marketing/layout.tsx", [
+    index("./marketing/home.tsx"),
+    route("contact", "./marketing/contact.tsx"),
+  ]),
+  ...prefix("projects", [
+    index("./projects/home.tsx"),
+    layout("./projects/project-layout.tsx", [
+      route(":pid", "./projects/project.tsx"),
+      route(":pid/edit", "./projects/edit-project.tsx"),
+    ]),
+  ]),
+] satisfies RouteConfig;

索引路由

索引路由是路由体系中一种比较特殊且实用的路由类型。它主要用于在某个父路由下,当用户访问父路由对应的 URL 且没有指定具体的子路由路径时,来确定默认展示的页面内容。简单来说,就是为父路由所代表的页面层级设定一个默认的 “首页” 或者说 “主页面”。

ts
index(componentFile),

索引路由有着特定的渲染方式,它会在其父路由对应的 URL 下,渲染到父路由的 Outlet 组件中,就如同是父路由的默认子路由一样。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
+
+export default [
+  // 渲染到根路由 root.tsx Outlet 内
+  index("./home.tsx"),
+  route("dashboard", "./dashboard.tsx", [
+    // 在 /dashboard 路由下, 渲染到 dashboard.tsx Outlet 组件内
+    index("./dashboard-home.tsx"),
+    route("settings", "./dashboard-settings.tsx"),
+  ]),
+] satisfies RouteConfig;

提示:

索引路由没有子路由

路由前缀

路由前缀是一种在路由配置中很实用的特性。通过使用 prefix,开发者能够为一组路由添加一个公共的路径前缀,而且关键的是,无需特意去引入一个父路由文件来实现这一效果。

ts
import {
+  type RouteConfig,
+  route,
+  layout,
+  index,
+  prefix,
+} from "@react-router/dev/routes";
+
+export default [
+  layout("./marketing/layout.tsx", [
+    index("./marketing/home.tsx"),
+    route("contact", "./marketing/contact.tsx"),
+  ]),
+  ...prefix("projects", [
+    index("./projects/home.tsx"),
+    layout("./projects/project-layout.tsx", [
+      route(":pid", "./projects/project.tsx"),
+      route(":pid/edit", "./projects/edit-project.tsx"),
+    ]),
+  ]),
+] satisfies RouteConfig;

动态路由匹配

很多时候,我们需要将给定匹配模式的路由映射到同一个组件。我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数。路径参数将会从匹配的 URL 中解析,并作为 params 提供给其他路由接口。

ts
route("teams/:teamId", "./team.tsx"),
ts
import type { Route } from "./+types/team";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  //                           ^? { teamId: string }
+}
+
+export default function Component({ params }: Route.ComponentProps) {
+  params.teamId;
+  //        ^ string
+}

可选参数

你可以在路由参数后面添加 ?,使其成为可选的路由参数。

ts
route(":lang?/categories", "./categories.tsx"),

你甚至可以设置可选的静态路由参数,具体如下:

ts
route("users/:userId/edit?", "./user.tsx");

通配符

通配符片段也被称作 “catchall” 以及 “star” 匹配。当一个路由路径模式以 /* 结尾时,它就具备了通配符的特性。其原理在于,这样的路由路径能够匹 / 之后的任意字符,这里的任意字符包含了其他的 / 字符也没问题。

ts
route("files/*", "./files.tsx"),
tsx
export async function loader({ params }: Route.LoaderArgs) {
+  // params["*"] 将会包含在 files/ 之后剩余的 URL 内容。
+}

你甚至可以对 * 进行解构,只是需要给它赋一个新的名字。习惯命名为 splat

ts
const { "*": splat } = params;

组件路由

你也可以使用与 URL 相匹配的组件,并将它们映射到组件树中的任意元素位置:

tsx
import { Routes, Route } from "react-router";
+
+function Wizard() {
+  return (
+    <div>
+      <h1>Some Wizard with Steps</h1>
+      <Routes>
+        <Route index element={<StepOne />} />
+        <Route path="step-2" element={<StepTwo />} />
+        <Route path="step-3" element={<StepThree />}>
+      </Routes>
+    </div>
+  );
+}

需要注意的是,这些路由并不参与数据加载、操作(actions)、代码分割,也不具备其他路由模块所拥有的功能特性,所以相较于路由模块而言,它们的应用场景更为有限。

`,52)]))}const g=i(h,[["render",p]]);export{o as __pageData,g as default}; diff --git a/assets/framework_routing.md.Hy04wNhs.lean.js b/assets/framework_routing.md.Hy04wNhs.lean.js new file mode 100644 index 0000000..5777ecd --- /dev/null +++ b/assets/framework_routing.md.Hy04wNhs.lean.js @@ -0,0 +1,134 @@ +import{_ as i,c as a,a0 as t,o as n}from"./chunks/framework.xCeNF-Bo.js";const o=JSON.parse('{"title":"路由","description":"","frontmatter":{},"headers":[],"relativePath":"framework/routing.md","filePath":"framework/routing.md"}'),h={name:"framework/routing.md"};function p(l,s,k,e,E,d){return n(),a("div",null,s[0]||(s[0]=[t(`

路由

配置路由

路由是在 app/routes.ts 文件中进行配置的。每条路由都包含两个必需的部分:一个用于匹配 URL 的 URL 模式,以及一个指向定义其行为的路由模块的文件路径。

tsx
import { type RouteConfig, route } from "@react-router/dev/routes";
+
+export default [
+  route("some/path", "./some/file.tsx"),
+  //  匹配模式 ^           ^ 模块文件路径
+] satisfies RouteConfig;

以下是一个更复杂些的路由配置示例:

ts
import {
+  type RouteConfig,
+  route,
+  index,
+  layout,
+  prefix,
+} from "@react-router/dev/routes";
+
+export default [
+  index("./home.tsx"),
+  route("about", "./about.tsx"),
+
+  layout("./auth/layout.tsx", [
+    route("login", "./auth/login.tsx"),
+    route("register", "./auth/register.tsx"),
+  ]),
+
+  ...prefix("concerts", [
+    index("./concerts/home.tsx"),
+    route(":city", "./concerts/city.tsx"),
+    route("trending", "./concerts/trending.tsx"),
+  ]),
+] satisfies RouteConfig;

如果你更倾向于通过文件命名约定而非配置的方式来定义路由,那么 @react-router/fs-routes 包提供了一种基于文件系统的路由约定

路由模块

routes.ts 文件中引用的文件定义了每条路由的行为。

ts
route("teams/:teamId", "./team.tsx"),
+//                路由模块 ^^^^^^^^

以下是一个简单的路由模块示例:

tsx
// 提供类型安全 / 类型推断
+import type { Route } from "./+types/team";
+
+// 向组件提供 \`loaderData\`
+export async function loader({ params }: Route.LoaderArgs) {
+  let team = await fetchTeam(params.teamId);
+  return { name: team.name };
+}
+
+// 在加载器完成后渲染组件
+export default function Component({ loaderData }: Route.ComponentProps) {
+  return <h1>{loaderData.name}</h1>;
+}

路由模块具备更多功能,比如操作(actions)、头部信息(headers)以及错误边界(error boundaries)等,但这些内容将会在下一篇指南: 路由模块中进行介绍。

嵌套路由

它允许路由被嵌套在父路由内部。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
+
+export default [
+  // 父路由
+  route("dashboard", "./dashboard.tsx", [
+    // 子路由
+    index("./home.tsx"),
+    route("settings", "./settings.tsx"),
+  ]),
+] satisfies RouteConfig;

父路由的路径会自动包含在子路由中,所以以上配置会同时创建出 “/dashboard”“/dashboard/settings” 两个 URL 路由。

子路由是通过父路由中的 <Outlet/> 组件来进行渲染的。

tsx
import { Outlet } from "react-router";
+
+export default function Dashboard() {
+  return (
+    <div>
+      <h1>Dashboard</h1>
+      {/* 将会是 home.tsx 或者 settings.tsx */}
+      <Outlet />
+    </div>
+  );
+}

根路由

routes.ts 文件中的每条路由都嵌套在特殊的 app/root.tsx 模块内部。

布局路由

layout 是一种在路由体系中有着独特作用的路由类型。它能够为其子路由创建新的嵌套层级,不过值得注意的是,它并不会给 URL 添加额外的路径片段。可以把它类比为根路由,根路由构建了整个应用最基础的页面布局框架,而布局路由同样是起着构建页面布局框架的作用,只是它可以在任意层级被添加,从而灵活地塑造应用内不同部分的页面层级结构和布局样式。

ts
import {
+  type RouteConfig,
+  route,
+  layout,
+  index,
+  prefix,
+} from "@react-router/dev/routes";
+
+export default [
+  layout("./marketing/layout.tsx", [
+    index("./marketing/home.tsx"),
+    route("contact", "./marketing/contact.tsx"),
+  ]),
+  ...prefix("projects", [
+    index("./projects/home.tsx"),
+    layout("./projects/project-layout.tsx", [
+      route(":pid", "./projects/project.tsx"),
+      route(":pid/edit", "./projects/edit-project.tsx"),
+    ]),
+  ]),
+] satisfies RouteConfig;

索引路由

索引路由是路由体系中一种比较特殊且实用的路由类型。它主要用于在某个父路由下,当用户访问父路由对应的 URL 且没有指定具体的子路由路径时,来确定默认展示的页面内容。简单来说,就是为父路由所代表的页面层级设定一个默认的 “首页” 或者说 “主页面”。

ts
index(componentFile),

索引路由有着特定的渲染方式,它会在其父路由对应的 URL 下,渲染到父路由的 Outlet 组件中,就如同是父路由的默认子路由一样。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
+
+export default [
+  // 渲染到根路由 root.tsx Outlet 内
+  index("./home.tsx"),
+  route("dashboard", "./dashboard.tsx", [
+    // 在 /dashboard 路由下, 渲染到 dashboard.tsx Outlet 组件内
+    index("./dashboard-home.tsx"),
+    route("settings", "./dashboard-settings.tsx"),
+  ]),
+] satisfies RouteConfig;

提示:

索引路由没有子路由

路由前缀

路由前缀是一种在路由配置中很实用的特性。通过使用 prefix,开发者能够为一组路由添加一个公共的路径前缀,而且关键的是,无需特意去引入一个父路由文件来实现这一效果。

ts
import {
+  type RouteConfig,
+  route,
+  layout,
+  index,
+  prefix,
+} from "@react-router/dev/routes";
+
+export default [
+  layout("./marketing/layout.tsx", [
+    index("./marketing/home.tsx"),
+    route("contact", "./marketing/contact.tsx"),
+  ]),
+  ...prefix("projects", [
+    index("./projects/home.tsx"),
+    layout("./projects/project-layout.tsx", [
+      route(":pid", "./projects/project.tsx"),
+      route(":pid/edit", "./projects/edit-project.tsx"),
+    ]),
+  ]),
+] satisfies RouteConfig;

动态路由匹配

很多时候,我们需要将给定匹配模式的路由映射到同一个组件。我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数。路径参数将会从匹配的 URL 中解析,并作为 params 提供给其他路由接口。

ts
route("teams/:teamId", "./team.tsx"),
ts
import type { Route } from "./+types/team";
+
+export async function loader({ params }: Route.LoaderArgs) {
+  //                           ^? { teamId: string }
+}
+
+export default function Component({ params }: Route.ComponentProps) {
+  params.teamId;
+  //        ^ string
+}

可选参数

你可以在路由参数后面添加 ?,使其成为可选的路由参数。

ts
route(":lang?/categories", "./categories.tsx"),

你甚至可以设置可选的静态路由参数,具体如下:

ts
route("users/:userId/edit?", "./user.tsx");

通配符

通配符片段也被称作 “catchall” 以及 “star” 匹配。当一个路由路径模式以 /* 结尾时,它就具备了通配符的特性。其原理在于,这样的路由路径能够匹 / 之后的任意字符,这里的任意字符包含了其他的 / 字符也没问题。

ts
route("files/*", "./files.tsx"),
tsx
export async function loader({ params }: Route.LoaderArgs) {
+  // params["*"] 将会包含在 files/ 之后剩余的 URL 内容。
+}

你甚至可以对 * 进行解构,只是需要给它赋一个新的名字。习惯命名为 splat

ts
const { "*": splat } = params;

组件路由

你也可以使用与 URL 相匹配的组件,并将它们映射到组件树中的任意元素位置:

tsx
import { Routes, Route } from "react-router";
+
+function Wizard() {
+  return (
+    <div>
+      <h1>Some Wizard with Steps</h1>
+      <Routes>
+        <Route index element={<StepOne />} />
+        <Route path="step-2" element={<StepTwo />} />
+        <Route path="step-3" element={<StepThree />}>
+      </Routes>
+    </div>
+  );
+}

需要注意的是,这些路由并不参与数据加载、操作(actions)、代码分割,也不具备其他路由模块所拥有的功能特性,所以相较于路由模块而言,它们的应用场景更为有限。

`,52)]))}const g=i(h,[["render",p]]);export{o as __pageData,g as default}; diff --git a/assets/framework_testing.md.Bi8pRi2m.js b/assets/framework_testing.md.Bi8pRi2m.js new file mode 100644 index 0000000..ed280fa --- /dev/null +++ b/assets/framework_testing.md.Bi8pRi2m.js @@ -0,0 +1,50 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"测试","description":"","frontmatter":{},"headers":[],"relativePath":"framework/testing.md","filePath":"framework/testing.md"}'),h={name:"framework/testing.md"};function k(l,s,p,e,E,r){return t(),a("div",null,s[0]||(s[0]=[n(`

测试

当组件使用了诸如 useLoaderData<Link> 等与 React Router 相关的特性时,有一个重要的要求,那就是这些组件必须在 React Router 应用的上下文环境中进行渲染。为了能够在隔离的情况下对这些组件进行测试,createRoutesStub 函数应运而生,它能够帮助创建相应的上下文。

当我们有一个登录表单组件依赖 useActionData 时:

tsx
import { useActionData } from "react-router";
+
+export function LoginForm() {
+  const errors = useActionData();
+  return (
+    <Form method="post">
+      <label>
+        <input type="text" name="username" />
+        {errors?.username && <div>{errors.username}</div>}
+      </label>
+
+      <label>
+        <input type="password" name="password" />
+        {errors?.password && <div>{errors.password}</div>}
+      </label>
+
+      <button type="submit">Login</button>
+    </Form>
+  );
+}

createRoutesStub 函数在测试如上述依赖 useActionData 的登录表单组件这类与 React Router 紧密相关的组件时,发挥着重要作用。它接收一个对象数组作为参数,这些对象类似于包含 Loaders、 Actions 以及组件的路由模块。

ts
import { createRoutesStub } from "react-router";
+import * as Test from "@testing-library/react";
+import { LoginForm } from "./LoginForm";
+
+test("LoginForm renders error messages", async () => {
+  const USER_MESSAGE = "Username is required";
+  const PASSWORD_MESSAGE = "Password is required";
+
+  const Stub = createRoutesStub([
+    {
+      path: "/login",
+      Component: LoginForm,
+      action() {
+        return {
+          errors: {
+            username: USER_MESSAGE,
+            password: PASSWORD_MESSAGE,
+          },
+        };
+      },
+    },
+  ]);
+
+  // 渲染 \`/login\` 路由模拟
+  Test.render(<Stub initialEntries={["/login"]} />);
+
+  // 模拟交互过程
+  Test.user.click(screen.getByText("Login"));
+  await Test.waitFor(() => screen.findByText(USER_MESSAGE));
+  await Test.waitFor(() => screen.findByText(PASSWORD_MESSAGE));
+});
`,6)]))}const y=i(h,[["render",k]]);export{g as __pageData,y as default}; diff --git a/assets/framework_testing.md.Bi8pRi2m.lean.js b/assets/framework_testing.md.Bi8pRi2m.lean.js new file mode 100644 index 0000000..ed280fa --- /dev/null +++ b/assets/framework_testing.md.Bi8pRi2m.lean.js @@ -0,0 +1,50 @@ +import{_ as i,c as a,a0 as n,o as t}from"./chunks/framework.xCeNF-Bo.js";const g=JSON.parse('{"title":"测试","description":"","frontmatter":{},"headers":[],"relativePath":"framework/testing.md","filePath":"framework/testing.md"}'),h={name:"framework/testing.md"};function k(l,s,p,e,E,r){return t(),a("div",null,s[0]||(s[0]=[n(`

测试

当组件使用了诸如 useLoaderData<Link> 等与 React Router 相关的特性时,有一个重要的要求,那就是这些组件必须在 React Router 应用的上下文环境中进行渲染。为了能够在隔离的情况下对这些组件进行测试,createRoutesStub 函数应运而生,它能够帮助创建相应的上下文。

当我们有一个登录表单组件依赖 useActionData 时:

tsx
import { useActionData } from "react-router";
+
+export function LoginForm() {
+  const errors = useActionData();
+  return (
+    <Form method="post">
+      <label>
+        <input type="text" name="username" />
+        {errors?.username && <div>{errors.username}</div>}
+      </label>
+
+      <label>
+        <input type="password" name="password" />
+        {errors?.password && <div>{errors.password}</div>}
+      </label>
+
+      <button type="submit">Login</button>
+    </Form>
+  );
+}

createRoutesStub 函数在测试如上述依赖 useActionData 的登录表单组件这类与 React Router 紧密相关的组件时,发挥着重要作用。它接收一个对象数组作为参数,这些对象类似于包含 Loaders、 Actions 以及组件的路由模块。

ts
import { createRoutesStub } from "react-router";
+import * as Test from "@testing-library/react";
+import { LoginForm } from "./LoginForm";
+
+test("LoginForm renders error messages", async () => {
+  const USER_MESSAGE = "Username is required";
+  const PASSWORD_MESSAGE = "Password is required";
+
+  const Stub = createRoutesStub([
+    {
+      path: "/login",
+      Component: LoginForm,
+      action() {
+        return {
+          errors: {
+            username: USER_MESSAGE,
+            password: PASSWORD_MESSAGE,
+          },
+        };
+      },
+    },
+  ]);
+
+  // 渲染 \`/login\` 路由模拟
+  Test.render(<Stub initialEntries={["/login"]} />);
+
+  // 模拟交互过程
+  Test.user.click(screen.getByText("Login"));
+  await Test.waitFor(() => screen.findByText(USER_MESSAGE));
+  await Test.waitFor(() => screen.findByText(PASSWORD_MESSAGE));
+});
`,6)]))}const y=i(h,[["render",k]]);export{g as __pageData,y as default}; diff --git a/assets/library_installation.md.B3NyLOOZ.js b/assets/library_installation.md.B3NyLOOZ.js new file mode 100644 index 0000000..50e2a42 --- /dev/null +++ b/assets/library_installation.md.B3NyLOOZ.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as e,a as n,o as i}from"./chunks/framework.xCeNF-Bo.js";const _=JSON.parse('{"title":"安装","description":"","frontmatter":{},"headers":[],"relativePath":"library/installation.md","filePath":"library/installation.md"}'),o={name:"library/installation.md"};function s(l,a,c,d,p,m){return i(),r("div",null,a[0]||(a[0]=[e("h1",{id:"安装",tabindex:"-1"},[n("安装 "),e("a",{class:"header-anchor",href:"#安装","aria-label":'Permalink to "安装"'},"​")],-1)]))}const h=t(o,[["render",s]]);export{_ as __pageData,h as default}; diff --git a/assets/library_installation.md.B3NyLOOZ.lean.js b/assets/library_installation.md.B3NyLOOZ.lean.js new file mode 100644 index 0000000..50e2a42 --- /dev/null +++ b/assets/library_installation.md.B3NyLOOZ.lean.js @@ -0,0 +1 @@ +import{_ as t,c as r,j as e,a as n,o as i}from"./chunks/framework.xCeNF-Bo.js";const _=JSON.parse('{"title":"安装","description":"","frontmatter":{},"headers":[],"relativePath":"library/installation.md","filePath":"library/installation.md"}'),o={name:"library/installation.md"};function s(l,a,c,d,p,m){return i(),r("div",null,a[0]||(a[0]=[e("h1",{id:"安装",tabindex:"-1"},[n("安装 "),e("a",{class:"header-anchor",href:"#安装","aria-label":'Permalink to "安装"'},"​")],-1)]))}const h=t(o,[["render",s]]);export{_ as __pageData,h as default}; diff --git a/framework/actions.html b/framework/actions.html index f819b5f..80d3d4d 100644 --- a/framework/actions.html +++ b/framework/actions.html @@ -13,13 +13,13 @@ - + -
Skip to content

Actions

数据变更通过路由动作(Route actions)来完成。当路由动作执行完毕后,页面上所有由加载器获取的数据都会被重新验证(revalidated),这样就能在无需编写额外代码的情况下,确保用户界面与数据保持同步。

通过 action 定义的路由动作只会在服务器端被调用,而通过 clientAction 定义的路由动作则是在浏览器(也就是客户端)中运行。

客户端 Actions

clientAction 仅在浏览器端运行,并且当同时定义了 action 和 clientAction 时,clientAction 会优先被执行。

tsx
// route('/projects/:projectId', './project.tsx')
+    
Skip to content

Actions

数据变更通过路由动作(Route actions)来完成。当路由动作执行完毕后,页面上所有由加载器获取的数据都会被重新验证(revalidated),这样就能在无需编写额外代码的情况下,确保用户界面与数据保持同步。

通过 action 定义的路由动作只会在服务器端被调用,而通过 clientAction 定义的路由动作则是在浏览器(也就是客户端)中运行。

客户端 Actions

clientAction 仅在浏览器端运行,并且当同时定义了 action 和 clientAction 时,clientAction 会优先被执行。

tsx
// route('/projects/:projectId', './project.tsx')
 import type { Route } from "./+types/project";
 import { Form } from "react-router";
 import { someApi } from "./api";
@@ -42,7 +42,7 @@
       {actionData ? <p>{actionData.title} updated</p> : null}
     </div>
   );
-}

服务端 Actions

action 仅在服务器端运行,并且会从客户端的打包代码包中被移除。

tsx
// route('/projects/:projectId', './project.tsx')
+}

服务端 Actions

action 仅在服务器端运行,并且会从客户端的打包代码包中被移除。

tsx
// route('/projects/:projectId', './project.tsx')
 import type { Route } from "./+types/project";
 import { Form } from "react-router";
 import { fakeDb } from "../db";
@@ -103,7 +103,7 @@
   { title: "New Title" },
   { action: "/update-task/123", method: "post" }
 );

阅读 使用 Fetchers 以了解更多关于 Fetchers 的信息。

- + \ No newline at end of file diff --git a/framework/custom-framework.html b/framework/custom-framework.html new file mode 100644 index 0000000..b54afec --- /dev/null +++ b/framework/custom-framework.html @@ -0,0 +1,138 @@ + + + + + + 自定义框架 | React Router7 中文文档 + + + + + + + + + + + + + + + +
Skip to content

自定义框架

通常可以借助 @react-router/dev 来利用 React Router 的各种框架特性,但如果不想使用它,也是有办法将 React Router 的框架特性(如加载器、动作、数据获取器等)集成到自己的打包器和服务器抽象层当中的。

客户端渲染

1. 创建一个路由

在 React Router 框架中,createBrowserRouter 是一个极为关键的浏览器运行时 API,它承担着启用路由模块相关 API(如加载器、动作等)的重要职责,以下为你详细介绍它的相关情况。

它接收一个由路由对象组成的数组作为参数,这些路由对象具备支持加载器、动作、错误边界等诸多特性的能力。

在基于 Vite 构建的 React 项目中,如果使用了 React Router,其配套的 Vite 插件提供了一种便捷的方式来生成 createBrowserRouter 所需的路由对象数组。通常,开发者会在 routes.ts 文件中按照一定的格式和规则定义路由相关的信息,然后插件会自动解析这个文件,将其中的路由配置转换为符合要求的路由对象数组。

如果开发者不想依赖于特定的插件或者希望对路由配置有更精细化的控制,也可以手动创建路由对象数组,或者借助自定义的抽象层来生成符合要求的数组,再结合自己选择的打包器(如 Webpack、Rollup 等)来处理项目。

ts
import { createBrowserRouter } from "react-router";
+
+let router = createBrowserRouter([
+  {
+    path: "/",
+    Component: Root,
+    children: [
+      {
+        path: "shows/:showId",
+        Component: Show,
+        loader: ({ request, params }) =>
+          fetch(`/api/show/${params.id}.json`, {
+            signal: request.signal,
+          }),
+      },
+    ],
+  },
+]);

2. 渲染路由

然后使用 <RouterProvider> 把路由渲染到浏览器中。

tsx
import { createBrowserRouter, RouterProvider } from "react-router";
+import { createRoot } from "react-dom/client";
+
+createRoot(document.getElementById("root")).render(
+  <RouterProvider router={router} />
+);

3. 懒加载

在 React Router 中,lazy 属性为路由提供了一种非常实用的懒加载机制,它允许路由在需要的时候才去加载对应的组件及其相关定义,而不是在应用启动时就一次性全部加载:

tsx
createBrowserRouter([
+  {
+    path: "/show/:showId",
+    lazy: () => {
+      let [loader, action, Component] = await Promise.all([
+        import("./show.action.js"),
+        import("./show.loader.js"),
+        import("./show.component.js"),
+      ]);
+      return { loader, action, Component };
+    },
+  },
+]);

服务度渲染

在进行服务器端渲染并采用自定义设置时,有一些服务器端的 API 可用于渲染以及数据加载操作。

本介绍只是让你对服务器端渲染自定义设置以及相关 API 的工作原理有个大致了解,如果想要更深入地理解具体的实现细节、代码示例以及各种复杂场景下的应用方式等内容,可以查看自定义框架示例仓库

1. 定义你的路由

同客户端路由定义方式一样。

ts
export default [
+  {
+    path: "/",
+    Component: Root,
+    children: [
+      {
+        path: "shows/:showId",
+        Component: Show,
+        loader: ({ params }) => {
+          return db.loadShow(params.id);
+        },
+      },
+    ],
+  },
+];

2. 创建一个静态处理器

使用 createStaticHandler 将路由转换为请求处理器。

ts
import { createStaticHandler } from "react-router";
+import routes from "./some-routes";
+
+let { query, dataRoutes } = createStaticHandler(routes);

3. 获取路由上下文和渲染函数

React Router 被设计为能够与 Web Fetch 请求 协同工作,以实现诸如数据获取、表单提交以及页面导航等功能。然而,当服务器端所使用的请求对象类型与 Web Fetch 对象不一致时,就需要进行相应的适配操作。

假设你的服务端接收的是 Request 对象。

ts
import { renderToString } from "react-dom/server";
+import {
+  createStaticHandler,
+  createStaticRouter,
+  StaticRouterProvider,
+} from "react-router";
+
+import routes from "./some-routes.js";
+
+let { query, dataRoutes } = createStaticHandler(routes);
+
+export async function handler(request: Request) {
+  // 1. 调用 `query` 运行 actions/loaders 获取路由上下文
+  let context = await query(request);
+
+  // 如果 `query` 返回一个 Response,直接返回它(可能是一个重定向)
+  if (context instanceof Response) {
+    return context;
+  }
+
+  // 2. 创建一个静态路由用于服务端渲染
+  let router = createStaticRouter(dataRoutes, context);
+
+  // 3. 使用 StaticRouterProvider 渲染所有内容
+  let html = renderToString(
+    <StaticRouterProvider router={router} context={context} />
+  );
+
+  // 设置来自 action 和 loaders 的请求头,基于最深匹配原则
+  let leaf = context.matches[context.matches.length - 1];
+  let actionHeaders = context.actionHeaders[leaf.route.id];
+  let loaderHeaders = context.loaderHeaders[leaf.route.id];
+  let headers = new Headers(actionHeaders);
+  if (loaderHeaders) {
+    for (let [key, value] of loaderHeaders.entries()) {
+      headers.append(key, value);
+    }
+  }
+
+  headers.set("Content-Type", "text/html; charset=utf-8");
+
+  // 4. 返回一个客户端响应
+  return new Response(`<!DOCTYPE html>${html}`, {
+    status: context.statusCode,
+    headers,
+  });
+}

4. 在浏览器中进行水合

在服务器端渲染的流程中,当服务器生成好要发送给客户端的 HTML 内容时,会将水合作用数据以特定的形式嵌入到 HTML 页面中,通常会挂载到 window.__staticRouterHydrationData 属性上。

当客户端接收到包含水合作用数据的 HTML 页面后,就可以利用 window.__staticRouterHydrationData 中的数据来初始化客户端的路由器并渲染 <RouterProvider> 组件,实现页面从静态到动态的转换以及后续的交互功能。

tsx
import { StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+import { RouterProvider } from "react-router/dom";
+import routes from "./app/routes.js";
+import { createBrowserRouter } from "react-router";
+
+let router = createBrowserRouter(routes, {
+  hydrationData: window.__staticRouterHydrationData,
+});
+
+hydrateRoot(
+  document,
+  <StrictMode>
+    <RouterProvider router={router} />
+  </StrictMode>
+);
+ + + + \ No newline at end of file diff --git a/framework/data-loading.html b/framework/data-loading.html index e3820a0..3d9d872 100644 --- a/framework/data-loading.html +++ b/framework/data-loading.html @@ -13,13 +13,13 @@ - + -
Skip to content

数据加载

数据是通过加载器(loader)和客户端加载器(clientLoader)提供给路由组件的。

加载器数据会自动从加载器中进行序列化操作,然后在组件中进行反序列化。除了像字符串、数字这类基本数据类型的值以外,加载器还能够返回 promises、maps、sets、dates 等多种类型的数据。

客户端数据加载

客户端加载器(clientLoader)用于在客户端(也就是浏览器端)获取数据。对于那些你更倾向于仅从浏览器去获取数据的页面或者整个项目而言,它是非常有用的。

tsx
// route("products/:pid", "./product.tsx");
+    
Skip to content

数据加载

数据是通过加载器(loader)和客户端加载器(clientLoader)提供给路由组件的。

加载器数据会自动从加载器中进行序列化操作,然后在组件中进行反序列化。除了像字符串、数字这类基本数据类型的值以外,加载器还能够返回 promises、maps、sets、dates 等多种类型的数据。

客户端数据加载

客户端加载器(clientLoader)用于在客户端(也就是浏览器端)获取数据。对于那些你更倾向于仅从浏览器去获取数据的页面或者整个项目而言,它是非常有用的。

tsx
// route("products/:pid", "./product.tsx");
 import type { Route } from "./+types/product";
 
 export async function clientLoader({ params }: Route.ClientLoaderArgs) {
@@ -36,7 +36,7 @@
       <p>{description}</p>
     </div>
   );
-}

服务器端数据加载

在服务器端渲染的情况下,加载器(loader)既用于初始页面加载,也用于客户端导航(client navigations)。在客户端导航期间,加载器会通过 React Router 自动从浏览器向服务器发起获取数据的 fetch 请求。

tsx
// route("products/:pid", "./product.tsx");
+}

服务器端数据加载

在服务器端渲染的情况下,加载器(loader)既用于初始页面加载,也用于客户端导航(client navigations)。在客户端导航期间,加载器会通过 React Router 自动从浏览器向服务器发起获取数据的 fetch 请求。

tsx
// route("products/:pid", "./product.tsx");
 import type { Route } from "./+types/product";
 import { fakeDb } from "../db";
 
@@ -53,7 +53,7 @@
       <p>{description}</p>
     </div>
   );
-}

需要注意的是,loader 函数会从客户端的打包代码包(bundles)中移除,这样一来,你就能够使用仅在服务器端可用的应用程序接口(APIs),而无需担心它们会被包含在浏览器端的代码中。

静态数据加载

在进行预渲染时,加载器(loaders)会被用于在生产构建阶段获取数据。

tsx
// route("products/:pid", "./product.tsx");
+}

需要注意的是,loader 函数会从客户端的打包代码包(bundles)中移除,这样一来,你就能够使用仅在服务器端可用的应用程序接口(APIs),而无需担心它们会被包含在浏览器端的代码中。

静态数据加载

在进行预渲染时,加载器(loaders)会被用于在生产构建阶段获取数据。

tsx
// route("products/:pid", "./product.tsx");
 import type { Route } from "./+types/product";
 
 export async function loader({ params }: Route.LoaderArgs) {
@@ -69,14 +69,14 @@
       <p>{description}</p>
     </div>
   );
-}

react-router.config.ts 文件中指定需要预渲染的 URL:

ts
import type { Config } from "@react-router/dev/config";
+}

react-router.config.ts 文件中指定需要预渲染的 URL:

ts
import type { Config } from "@react-router/dev/config";
 
 export default {
   async prerender() {
     let products = await readProductsFromCSVFile();
     return products.map((product) => `/products/${product.id}`);
   },
-} satisfies Config;

需要注意的是,在服务器端渲染时,任何未进行预渲染的 URL 对应的页面将会像往常一样进行服务器端渲染。这一机制使得你能够针对单个路由预渲染部分数据,同时对其余部分仍采用服务器端渲染的方式来处理。

同时使用两种加载器

加载器(loader)与客户端加载器(clientLoader)是可以一起使用的。加载器会在服务器端用于初始的服务器端渲染(Server Side Rendering,简称 SSR)或者静态预渲染(Pre-rendering)操作,而 clientLoader 则会在后续的客户端导航(client-side navigations)过程中被使用。

tsx
// route("products/:pid", "./product.tsx");
+} satisfies Config;

需要注意的是,在服务器端渲染时,任何未进行预渲染的 URL 对应的页面将会像往常一样进行服务器端渲染。这一机制使得你能够针对单个路由预渲染部分数据,同时对其余部分仍采用服务器端渲染的方式来处理。

同时使用两种加载器

加载器(loader)与客户端加载器(clientLoader)是可以一起使用的。加载器会在服务器端用于初始的服务器端渲染(Server Side Rendering,简称 SSR)或者静态预渲染(Pre-rendering)操作,而 clientLoader 则会在后续的客户端导航(client-side navigations)过程中被使用。

tsx
// route("products/:pid", "./product.tsx");
 import type { Route } from "./+types/product";
 import { fakeDb } from "../db";
 
@@ -99,7 +99,7 @@
     </div>
   );
 }
- + \ No newline at end of file diff --git a/framework/installation.html b/framework/installation.html index 564c44a..283ec73 100644 --- a/framework/installation.html +++ b/framework/installation.html @@ -19,10 +19,10 @@ -
Skip to content

安装

大多数项目都是从一个模板开始的。让我们使用由 React Router 维护的一个基础模板:

sh
npx create-react-router@latest my-react-router-app

现在切换到新的目录并启动应用程序。

sh
cd my-react-router-app
+    
Skip to content

安装

大多数项目都是从一个模板开始的。让我们使用由 React Router 维护的一个基础模板:

sh
npx create-react-router@latest my-react-router-app

现在切换到新的目录并启动应用程序。

sh
cd my-react-router-app
 npm i
 npm run dev

现在你可以打开浏览器,访问 http://localhost:5173

你可以在 GitHub 上查看该模板,了解如何手动搭建你的项目。

查看所有模版,看一下哪个模版适合你的部署需求。

- + \ No newline at end of file diff --git a/framework/navigating.html b/framework/navigating.html index 1104185..062bc68 100644 --- a/framework/navigating.html +++ b/framework/navigating.html @@ -19,7 +19,7 @@ -
Skip to content

导航

用户可以通过 <Link><NavLink><Form>redirect 以及 useNavigate 这些方式来在你的应用中进行导航。

如果你需要一个激活状态或者加载状态,使用 NavLink 导航组件很合适。

tsx
import { NavLink } from "react-router";
+    
Skip to content

导航

用户可以通过 <Link><NavLink><Form>redirect 以及 useNavigate 这些方式来在你的应用中进行导航。

如果你需要一个激活状态或者加载状态,使用 NavLink 导航组件很合适。

tsx
import { NavLink } from "react-router";
 
 export function MyAppNav() {
   return (
@@ -106,7 +106,7 @@
     navigate("/logout");
   });
 }
- + \ No newline at end of file diff --git a/framework/pending-ui.html b/framework/pending-ui.html index 24ed937..c6508a5 100644 --- a/framework/pending-ui.html +++ b/framework/pending-ui.html @@ -13,14 +13,95 @@ - + - - +
Skip to content

待处理的 UI

当用户导航到新路由,或者向 Actions 提交数据这类操作时,用户界面(UI)应当立即呈现一种 loading 状态或者响应状态,这是在代码逻辑中要干的事情。

全局 loading

当用户在 Web 应用中导航到一个新的 URL 时,会涉及到页面加载器(loaders)以及页面渲染之间的协调关系,并且可以通过 useNavigation 这个钩子函数来获取到 loading 状态:

tsx
import { useNavigation } from "react-router";
+
+export default function Root() {
+  const navigation = useNavigation();
+  const isNavigating = Boolean(navigation.location);
+
+  return (
+    <html>
+      <body>
+        {isNavigating && <GlobalSpinner />}
+        <Outlet />
+      </body>
+    </html>
+  );
+}

局部 loading

在 Web 应用开发中,不仅可以在页面整体层面展示 loading 指示器来提示用户操作正在进行中,还能够将这类指示器本地化到具体的链接上,而 <NavLink> 组件的 children、className 和 style 属性在此过程中发挥了重要作用,它们可以接收 loading 状态作为参数的函数形式来实现相应的本地化效果:

tsx
import { NavLink } from "react-router";
+
+function Navbar() {
+  return (
+    <nav>
+      <NavLink to="/home">
+        {({ isPending }) => <span>Home {isPending && <Spinner />}</span>}
+      </NavLink>
+      <NavLink
+        to="/about"
+        style={({ isPending }) => ({
+          color: isPending ? "gray" : "black",
+        })}
+      >
+        About
+      </NavLink>
+    </nav>
+  );
+}

Form 提交 loading

当用户在 Web 应用中提交表单时,用户界面(UI)应当立即通过呈现 loading 状态来对用户的这一操作行为做出响应,告知用户表单提交操作正在进行中。在实现这一功能时,使用 fetcher 表单相对更为便捷,这是因为它具备自身独立的状态。(而常规的表单提交往往会引发全局导航)。

tsx
import { useFetcher } from "react-router";
+
+function NewProjectForm() {
+  const fetcher = useFetcher();
+
+  return (
+    <fetcher.Form method="post">
+      <input type="text" name="title" />
+      <button type="submit">
+        {fetcher.state !== "idle" ? "Submitting..." : "Submit"}
+      </button>
+    </fetcher.Form>
+  );
+}

fetcher 表单提交的场景下,使用 useNavigation 处理 loading 状态。

tsx
import { useNavigation, Form } from "react-router";
+
+function NewProjectForm() {
+  const navigation = useNavigation();
+
+  return (
+    <Form method="post" action="/projects/new">
+      <input type="text" name="title" />
+      <button type="submit">
+        {navigation.formAction === "/projects/new"
+          ? "Submitting..."
+          : "Submit"}
+      </button>
+    </Form>
+  );
+}

乐观 UI

当通过表单提交数据能够知晓用户界面(UI)的未来状态时,便可以实现乐观(Optimistic)UI,从而为用户带来即时的用户体验(UX)提升:

tsx
function Task({ task }) {
+  const fetcher = useFetcher();
+
+  let isComplete = task.status === "complete";
+  if (fetcher.formData) {
+    isComplete = fetcher.formData.get("status");
+  }
+
+  return (
+    <div>
+      <div>{task.title}</div>
+      <fetcher.Form method="post">
+        <button
+          name="status"
+          value={isComplete ? "incomplete" : "complete"}
+        >
+          {isComplete ? "Mark Incomplete" : "Mark Complete"}
+        </button>
+      </fetcher.Form>
+    </div>
+  );
+}
+ \ No newline at end of file diff --git a/framework/rendering-strategies.html b/framework/rendering-strategies.html index f0faa92..67fdd9c 100644 --- a/framework/rendering-strategies.html +++ b/framework/rendering-strategies.html @@ -13,21 +13,21 @@ - + -
Skip to content

渲染策略

在 React Router 中存在三种渲染策略:

  • 客户端渲染
  • 服务器端渲染
  • 静态预渲染

客户端渲染

在用户浏览应用程序进行页面导航时,路由始终是在客户端进行渲染的。如果您打算构建一个单页应用(Single Page App,简称 SPA),那么需要禁用服务器端渲染。

ts
import type { Config } from "@react-router/dev/config";
+    
Skip to content

渲染策略

在 React Router 中存在三种渲染策略:

  • 客户端渲染
  • 服务器端渲染
  • 静态预渲染

客户端渲染

在用户浏览应用程序进行页面导航时,路由始终是在客户端进行渲染的。如果您打算构建一个单页应用(Single Page App,简称 SPA),那么需要禁用服务器端渲染。

ts
import type { Config } from "@react-router/dev/config";
 
 export default {
   ssr: false,
-} satisfies Config;

服务器端渲染

ts
import type { Config } from "@react-router/dev/config";
+} satisfies Config;

服务器端渲染

ts
import type { Config } from "@react-router/dev/config";
 
 export default {
   ssr: true,
-} satisfies Config;

服务器端渲染需要有支持它的部署环境。尽管它是一个全局性的设置,但个别路由仍然可以采用静态预渲染的方式。此外,路由还能够借助客户端加载器(clientLoader)进行客户端数据加载,以此避免对其对应的那部分用户界面(UI)进行服务器端渲染及数据获取操作。

静态预渲染

ts
import type { Config } from "@react-router/dev/config";
+} satisfies Config;

服务器端渲染需要有支持它的部署环境。尽管它是一个全局性的设置,但个别路由仍然可以采用静态预渲染的方式。此外,路由还能够借助客户端加载器(clientLoader)进行客户端数据加载,以此避免对其对应的那部分用户界面(UI)进行服务器端渲染及数据获取操作。

静态预渲染

ts
import type { Config } from "@react-router/dev/config";
 
 export default {
   // 在构建时返回一个要进行预渲染的 URL 列表
@@ -35,7 +35,7 @@
     return ["/", "/about", "/contact"];
   },
 } satisfies Config;

静态预渲染是一种在构建时进行的操作,它会为一系列的 URL 生成静态 HTML 以及客户端导航数据有效负载。这对于搜索引擎优化(SEO)和性能提升方面很有帮助,尤其适用于那些没有采用服务器端渲染的部署环境。在进行预渲染时,路由模块加载器(route module loaders)会被用于在构建时获取数据。

- + \ No newline at end of file diff --git a/framework/route-module.html b/framework/route-module.html index 392727d..40109f6 100644 --- a/framework/route-module.html +++ b/framework/route-module.html @@ -13,14 +13,14 @@ - + -
Skip to content

路由模块

routes.ts 文件中所引用的文件被称作路由模块。

ts
route("teams/:teamId", "./team.tsx"),
-//                路由模块 ^^^^^^^^

路由模块是 React Router 框架功能的基础,它们定义了以下内容:

  • 自动代码分割
  • 数据加载
  • Actions
  • 重新验证
  • 错误边界
  • 其他功能

本指南只是对每个路由模块功能进行了简要概述,后续的入门指南将会更详细地涵盖这些功能,以帮助开发者更深入地理解和运用它们来构建高质量、功能完备的 Web 应用路由体系。

组件(default)

在路由匹配时,定义的组件将会被渲染。

tsx
export default function MyRouteComponent() {
+    
Skip to content

路由模块

routes.ts 文件中所引用的文件被称作路由模块。

ts
route("teams/:teamId", "./team.tsx"),
+//                路由模块 ^^^^^^^^

路由模块是 React Router 框架功能的基础,它们定义了以下内容:

  • 自动代码分割
  • 数据加载
  • Actions
  • 重新验证
  • 错误边界
  • 其他功能

本指南只是对每个路由模块功能进行了简要概述,后续的入门指南将会更详细地涵盖这些功能,以帮助开发者更深入地理解和运用它们来构建高质量、功能完备的 Web 应用路由体系。

组件(default)

在路由匹配时,定义的组件将会被渲染。

tsx
export default function MyRouteComponent() {
   return (
     <div>
       <h1>Look ma!</h1>
@@ -103,7 +103,7 @@
   } else {
     return <h1>Unknown Error</h1>;
   }
-}

HydrateFallback

在初始页面加载时,只有在客户端加载器(client loader)完成工作之后,路由组件才会进行渲染。如果(相关内容)被导出,那么一个水合回退(HydrateFallback)组件可以立即渲染,取代路由组件的位置:

tsx
export async function clientLoader() {
+}

HydrateFallback

在初始页面加载时,只有在客户端加载器(client loader)完成工作之后,路由组件才会进行渲染。如果(相关内容)被导出,那么一个水合回退(HydrateFallback)组件可以立即渲染,取代路由组件的位置:

tsx
export async function clientLoader() {
   const data = await fakeLoadLocalGameData();
   return data;
 }
@@ -138,7 +138,7 @@
       as: "image",
     },
   ];
-}

所有的路由链接(Route links)将会被汇总并通过 <Links /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Links } from "react-router";
+}

所有的路由链接(Route links)将会被汇总并通过 <Links /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Links } from "react-router";
 
 export default function Root() {
   return (
@@ -162,7 +162,7 @@
       content: "This app is the best",
     },
   ];
-}

所有路由的元数据(meta)将会被汇总并通过 <Meta /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Meta } from "react-router";
+}

所有路由的元数据(meta)将会被汇总并通过 <Meta /> 组件进行渲染,通常该组件会在应用的根组件中被渲染:

tsx
import { Meta } from "react-router";
 
 export default function Root() {
   return (
@@ -179,7 +179,7 @@
 export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) {
   return true;
 }
- + \ No newline at end of file diff --git a/framework/routing.html b/framework/routing.html index 22bb88f..ac7f57f 100644 --- a/framework/routing.html +++ b/framework/routing.html @@ -13,18 +13,18 @@ - + -
Skip to content

路由

配置路由

路由是在 app/routes.ts 文件中进行配置的。每条路由都包含两个必需的部分:一个用于匹配 URL 的 URL 模式,以及一个指向定义其行为的路由模块的文件路径。

tsx
import { type RouteConfig, route } from "@react-router/dev/routes";
+    
Skip to content

路由

配置路由

路由是在 app/routes.ts 文件中进行配置的。每条路由都包含两个必需的部分:一个用于匹配 URL 的 URL 模式,以及一个指向定义其行为的路由模块的文件路径。

tsx
import { type RouteConfig, route } from "@react-router/dev/routes";
 
 export default [
   route("some/path", "./some/file.tsx"),
   //  匹配模式 ^           ^ 模块文件路径
-] satisfies RouteConfig;

以下是一个更复杂些的路由配置示例:

ts
import {
+] satisfies RouteConfig;

以下是一个更复杂些的路由配置示例:

ts
import {
   type RouteConfig,
   route,
   index,
@@ -46,8 +46,8 @@
     route(":city", "./concerts/city.tsx"),
     route("trending", "./concerts/trending.tsx"),
   ]),
-] satisfies RouteConfig;

如果你更倾向于通过文件命名约定而非配置的方式来定义路由,那么 @react-router/fs-routes 包提供了一种基于文件系统的路由约定

路由模块

routes.ts 文件中引用的文件定义了每条路由的行为。

ts
route("teams/:teamId", "./team.tsx"),
-//                路由模块 ^^^^^^^^

以下是一个简单的路由模块示例:

tsx
// 提供类型安全 / 类型推断
+] satisfies RouteConfig;

如果你更倾向于通过文件命名约定而非配置的方式来定义路由,那么 @react-router/fs-routes 包提供了一种基于文件系统的路由约定

路由模块

routes.ts 文件中引用的文件定义了每条路由的行为。

ts
route("teams/:teamId", "./team.tsx"),
+//                路由模块 ^^^^^^^^

以下是一个简单的路由模块示例:

tsx
// 提供类型安全 / 类型推断
 import type { Route } from "./+types/team";
 
 // 向组件提供 `loaderData`
@@ -59,7 +59,7 @@
 // 在加载器完成后渲染组件
 export default function Component({ loaderData }: Route.ComponentProps) {
   return <h1>{loaderData.name}</h1>;
-}

路由模块具备更多功能,比如操作(actions)、头部信息(headers)以及错误边界(error boundaries)等,但这些内容将会在下一篇指南: 路由模块中进行介绍。

嵌套路由

它允许路由被嵌套在父路由内部。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
+}

路由模块具备更多功能,比如操作(actions)、头部信息(headers)以及错误边界(error boundaries)等,但这些内容将会在下一篇指南: 路由模块中进行介绍。

嵌套路由

它允许路由被嵌套在父路由内部。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
 
 export default [
   // 父路由
@@ -68,7 +68,7 @@
     index("./home.tsx"),
     route("settings", "./settings.tsx"),
   ]),
-] satisfies RouteConfig;

父路由的路径会自动包含在子路由中,所以以上配置会同时创建出 “/dashboard”“/dashboard/settings” 两个 URL 路由。

子路由是通过父路由中的 <Outlet/> 组件来进行渲染的。

tsx
import { Outlet } from "react-router";
+] satisfies RouteConfig;

父路由的路径会自动包含在子路由中,所以以上配置会同时创建出 “/dashboard”“/dashboard/settings” 两个 URL 路由。

子路由是通过父路由中的 <Outlet/> 组件来进行渲染的。

tsx
import { Outlet } from "react-router";
 
 export default function Dashboard() {
   return (
@@ -78,7 +78,7 @@
       <Outlet />
     </div>
   );
-}

根路由

routes.ts 文件中的每条路由都嵌套在特殊的 app/root.tsx 模块内部。

布局路由

layout 是一种在路由体系中有着独特作用的路由类型。它能够为其子路由创建新的嵌套层级,不过值得注意的是,它并不会给 URL 添加额外的路径片段。可以把它类比为根路由,根路由构建了整个应用最基础的页面布局框架,而布局路由同样是起着构建页面布局框架的作用,只是它可以在任意层级被添加,从而灵活地塑造应用内不同部分的页面层级结构和布局样式。

ts
import {
+}

根路由

routes.ts 文件中的每条路由都嵌套在特殊的 app/root.tsx 模块内部。

布局路由

layout 是一种在路由体系中有着独特作用的路由类型。它能够为其子路由创建新的嵌套层级,不过值得注意的是,它并不会给 URL 添加额外的路径片段。可以把它类比为根路由,根路由构建了整个应用最基础的页面布局框架,而布局路由同样是起着构建页面布局框架的作用,只是它可以在任意层级被添加,从而灵活地塑造应用内不同部分的页面层级结构和布局样式。

ts
import {
   type RouteConfig,
   route,
   layout,
@@ -98,7 +98,7 @@
       route(":pid/edit", "./projects/edit-project.tsx"),
     ]),
   ]),
-] satisfies RouteConfig;

索引路由

索引路由是路由体系中一种比较特殊且实用的路由类型。它主要用于在某个父路由下,当用户访问父路由对应的 URL 且没有指定具体的子路由路径时,来确定默认展示的页面内容。简单来说,就是为父路由所代表的页面层级设定一个默认的 “首页” 或者说 “主页面”。

ts
index(componentFile),

索引路由有着特定的渲染方式,它会在其父路由对应的 URL 下,渲染到父路由的 Outlet 组件中,就如同是父路由的默认子路由一样。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
+] satisfies RouteConfig;

索引路由

索引路由是路由体系中一种比较特殊且实用的路由类型。它主要用于在某个父路由下,当用户访问父路由对应的 URL 且没有指定具体的子路由路径时,来确定默认展示的页面内容。简单来说,就是为父路由所代表的页面层级设定一个默认的 “首页” 或者说 “主页面”。

ts
index(componentFile),

索引路由有着特定的渲染方式,它会在其父路由对应的 URL 下,渲染到父路由的 Outlet 组件中,就如同是父路由的默认子路由一样。

ts
import { type RouteConfig, route, index } from "@react-router/dev/routes";
 
 export default [
   // 渲染到根路由 root.tsx Outlet 内
@@ -108,7 +108,7 @@
     index("./dashboard-home.tsx"),
     route("settings", "./dashboard-settings.tsx"),
   ]),
-] satisfies RouteConfig;

提示:

索引路由没有子路由

路由前缀

路由前缀是一种在路由配置中很实用的特性。通过使用 prefix,开发者能够为一组路由添加一个公共的路径前缀,而且关键的是,无需特意去引入一个父路由文件来实现这一效果。

ts
import {
+] satisfies RouteConfig;

提示:

索引路由没有子路由

路由前缀

路由前缀是一种在路由配置中很实用的特性。通过使用 prefix,开发者能够为一组路由添加一个公共的路径前缀,而且关键的是,无需特意去引入一个父路由文件来实现这一效果。

ts
import {
   type RouteConfig,
   route,
   layout,
@@ -128,7 +128,7 @@
       route(":pid/edit", "./projects/edit-project.tsx"),
     ]),
   ]),
-] satisfies RouteConfig;

动态路由匹配

很多时候,我们需要将给定匹配模式的路由映射到同一个组件。我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数。路径参数将会从匹配的 URL 中解析,并作为 params 提供给其他路由接口。

ts
route("teams/:teamId", "./team.tsx"),
ts
import type { Route } from "./+types/team";
+] satisfies RouteConfig;

动态路由匹配

很多时候,我们需要将给定匹配模式的路由映射到同一个组件。我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数。路径参数将会从匹配的 URL 中解析,并作为 params 提供给其他路由接口。

ts
route("teams/:teamId", "./team.tsx"),
ts
import type { Route } from "./+types/team";
 
 export async function loader({ params }: Route.LoaderArgs) {
   //                           ^? { teamId: string }
@@ -137,7 +137,7 @@
 export default function Component({ params }: Route.ComponentProps) {
   params.teamId;
   //        ^ string
-}

可选参数

你可以在路由参数后面添加 ?,使其成为可选的路由参数。

ts
route(":lang?/categories", "./categories.tsx"),

你甚至可以设置可选的静态路由参数,具体如下:

ts
route("users/:userId/edit?", "./user.tsx");

通配符

通配符片段也被称作 “catchall” 以及 “star” 匹配。当一个路由路径模式以 /* 结尾时,它就具备了通配符的特性。其原理在于,这样的路由路径能够匹 / 之后的任意字符,这里的任意字符包含了其他的 / 字符也没问题。

ts
route("files/*", "./files.tsx"),
tsx
export async function loader({ params }: Route.LoaderArgs) {
+}

可选参数

你可以在路由参数后面添加 ?,使其成为可选的路由参数。

ts
route(":lang?/categories", "./categories.tsx"),

你甚至可以设置可选的静态路由参数,具体如下:

ts
route("users/:userId/edit?", "./user.tsx");

通配符

通配符片段也被称作 “catchall” 以及 “star” 匹配。当一个路由路径模式以 /* 结尾时,它就具备了通配符的特性。其原理在于,这样的路由路径能够匹 / 之后的任意字符,这里的任意字符包含了其他的 / 字符也没问题。

ts
route("files/*", "./files.tsx"),
tsx
export async function loader({ params }: Route.LoaderArgs) {
   // params["*"] 将会包含在 files/ 之后剩余的 URL 内容。
 }

你甚至可以对 * 进行解构,只是需要给它赋一个新的名字。习惯命名为 splat

ts
const { "*": splat } = params;

组件路由

你也可以使用与 URL 相匹配的组件,并将它们映射到组件树中的任意元素位置:

tsx
import { Routes, Route } from "react-router";
 
@@ -153,7 +153,7 @@
     </div>
   );
 }

需要注意的是,这些路由并不参与数据加载、操作(actions)、代码分割,也不具备其他路由模块所拥有的功能特性,所以相较于路由模块而言,它们的应用场景更为有限。

- + \ No newline at end of file diff --git a/framework/testing.html b/framework/testing.html new file mode 100644 index 0000000..62e9906 --- /dev/null +++ b/framework/testing.html @@ -0,0 +1,75 @@ + + + + + + 测试 | React Router7 中文文档 + + + + + + + + + + + + + + + +
Skip to content

测试

当组件使用了诸如 useLoaderData<Link> 等与 React Router 相关的特性时,有一个重要的要求,那就是这些组件必须在 React Router 应用的上下文环境中进行渲染。为了能够在隔离的情况下对这些组件进行测试,createRoutesStub 函数应运而生,它能够帮助创建相应的上下文。

当我们有一个登录表单组件依赖 useActionData 时:

tsx
import { useActionData } from "react-router";
+
+export function LoginForm() {
+  const errors = useActionData();
+  return (
+    <Form method="post">
+      <label>
+        <input type="text" name="username" />
+        {errors?.username && <div>{errors.username}</div>}
+      </label>
+
+      <label>
+        <input type="password" name="password" />
+        {errors?.password && <div>{errors.password}</div>}
+      </label>
+
+      <button type="submit">Login</button>
+    </Form>
+  );
+}

createRoutesStub 函数在测试如上述依赖 useActionData 的登录表单组件这类与 React Router 紧密相关的组件时,发挥着重要作用。它接收一个对象数组作为参数,这些对象类似于包含 Loaders、 Actions 以及组件的路由模块。

ts
import { createRoutesStub } from "react-router";
+import * as Test from "@testing-library/react";
+import { LoginForm } from "./LoginForm";
+
+test("LoginForm renders error messages", async () => {
+  const USER_MESSAGE = "Username is required";
+  const PASSWORD_MESSAGE = "Password is required";
+
+  const Stub = createRoutesStub([
+    {
+      path: "/login",
+      Component: LoginForm,
+      action() {
+        return {
+          errors: {
+            username: USER_MESSAGE,
+            password: PASSWORD_MESSAGE,
+          },
+        };
+      },
+    },
+  ]);
+
+  // 渲染 `/login` 路由模拟
+  Test.render(<Stub initialEntries={["/login"]} />);
+
+  // 模拟交互过程
+  Test.user.click(screen.getByText("Login"));
+  await Test.waitFor(() => screen.findByText(USER_MESSAGE));
+  await Test.waitFor(() => screen.findByText(PASSWORD_MESSAGE));
+});
+ + + + \ No newline at end of file diff --git a/hashmap.json b/hashmap.json index 5abe744..de355ef 100644 --- a/hashmap.json +++ b/hashmap.json @@ -1 +1 @@ -{"framework_actions.md":"DNpxGeQj","framework_data-loading.md":"xZn9LZvI","framework_installation.md":"jCbgHnZm","framework_navigating.md":"WXgquIqj","framework_pending-ui.md":"Dq29rwSC","framework_rendering-strategies.md":"Cj6_ELgy","framework_route-module.md":"Dj8PfPey","framework_routing.md":"BkROF7Sx","home.md":"BxIXV_5a","how-tos_fetchers.md":"CQaCJ0h4","index.md":"BqvAZaqY"} +{"framework_actions.md":"DId6sRkl","framework_custom-framework.md":"DIi1csnZ","framework_data-loading.md":"BWeWrub0","framework_installation.md":"jCbgHnZm","framework_navigating.md":"WXgquIqj","framework_pending-ui.md":"YlfuNG_q","framework_rendering-strategies.md":"DL_QGqDy","framework_route-module.md":"B93mn3gQ","framework_routing.md":"Hy04wNhs","framework_testing.md":"Bi8pRi2m","home.md":"BxIXV_5a","how-tos_fetchers.md":"CQaCJ0h4","index.md":"BqvAZaqY","library_installation.md":"B3NyLOOZ"} diff --git a/home.html b/home.html index 1cc6741..bcac8ce 100644 --- a/home.html +++ b/home.html @@ -19,7 +19,7 @@ -
Skip to content

React Router 主页

React Router 是一款适用于 React 的多策略路由器,它让你从 React 18 到 React 19 平滑升级。你既可以将它最大限度地作为一个 React 框架来使用,也可以在自己的架构中把它当作一个库来最低限度地使用。

如果你了解未来特性(flags)相关内容,从 React Router v6 或者 Remix 进行升级通常是不会造成破坏的。

React Router 做为库使用

与以往版本一样,React Router 仍然可以当作一个简单的、声明式的路由库来使用。它唯一的任务就是将 URL 与一组组件进行匹配,提供对 URL 数据的访问途径,并实现在应用内进行导航。

这种使用策略在那些拥有自身前端基础设施的 “单页应用”(Single Page Apps)以及寻求轻松升级的 React Router v6 应用中很受欢迎。

它尤其适用于离线 + 同步架构,在这类架构中,待处理状态(pending states)很少出现,而且用户会有长时间持续的会话。像待处理状态、代码拆分、服务器端渲染、搜索引擎优化(SEO)以及初始页面加载时间等框架特性,都可以被舍弃,以换取即时的、本地优先的交互体验。

tsx
ReactDOM.createRoot(root).render(
+    
Skip to content

React Router 主页

React Router 是一款适用于 React 的多策略路由器,它让你从 React 18 到 React 19 平滑升级。你既可以将它最大限度地作为一个 React 框架来使用,也可以在自己的架构中把它当作一个库来最低限度地使用。

如果你了解未来特性(flags)相关内容,从 React Router v6 或者 Remix 进行升级通常是不会造成破坏的。

React Router 做为库使用

与以往版本一样,React Router 仍然可以当作一个简单的、声明式的路由库来使用。它唯一的任务就是将 URL 与一组组件进行匹配,提供对 URL 数据的访问途径,并实现在应用内进行导航。

这种使用策略在那些拥有自身前端基础设施的 “单页应用”(Single Page Apps)以及寻求轻松升级的 React Router v6 应用中很受欢迎。

它尤其适用于离线 + 同步架构,在这类架构中,待处理状态(pending states)很少出现,而且用户会有长时间持续的会话。像待处理状态、代码拆分、服务器端渲染、搜索引擎优化(SEO)以及初始页面加载时间等框架特性,都可以被舍弃,以换取即时的、本地优先的交互体验。

tsx
ReactDOM.createRoot(root).render(
   <BrowserRouter>
     <Routes>
       <Route path="/" element={<Home />} />
@@ -78,7 +78,7 @@
   await fakeSetLikedShow(formData.get("liked"));
   return { ok: true };
 }

路由模块还为搜索引擎优化(SEO)、资源加载、错误边界以及更多方面提供了规范。

快速开始,作为框架来使用。

- + \ No newline at end of file diff --git a/how-tos/fetchers.html b/how-tos/fetchers.html index 0b99a13..53c680b 100644 --- a/how-tos/fetchers.html +++ b/how-tos/fetchers.html @@ -19,8 +19,8 @@ - - + + \ No newline at end of file diff --git a/index.html b/index.html index d24a09a..e4ab5c7 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@
Skip to content

Router7

下一代 React 路由框架

一款以用户为核心、聚焦标准、采用多策略的路由器,你可以将其部署在任何地方。

React Router7React Router7
- + \ No newline at end of file diff --git a/library/installation.html b/library/installation.html new file mode 100644 index 0000000..fd4c9f7 --- /dev/null +++ b/library/installation.html @@ -0,0 +1,26 @@ + + + + + + 安装 | React Router7 中文文档 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file