From 155d54df53e38d341bc1733781c56d5e39607352 Mon Sep 17 00:00:00 2001 From: gongph Date: Sun, 8 Dec 2024 07:44:03 +0800 Subject: [PATCH] doc: add action md --- .vitepress/config.mjs | 13 +-- src/framework/actions.md | 150 ++++++++++++++++++++++++++++++ src/framework/data-loading.md | 146 +++++++++++++++++++++++++++++ src/framework/navigating.md | 168 ++++++++++++++++++++++++++++++++++ src/framework/pending-ui.md | 1 + src/how-tos/fetchers.md | 1 + 6 files changed, 473 insertions(+), 6 deletions(-) create mode 100644 src/framework/actions.md create mode 100644 src/framework/data-loading.md create mode 100644 src/framework/navigating.md create mode 100644 src/framework/pending-ui.md create mode 100644 src/how-tos/fetchers.md diff --git a/.vitepress/config.mjs b/.vitepress/config.mjs index 82d279b..7ba9c3c 100644 --- a/.vitepress/config.mjs +++ b/.vitepress/config.mjs @@ -20,6 +20,7 @@ export default defineConfig({ { text: "指南", link: "/home" }, { text: "参考", link: "https://api.reactrouter.com/v7/" }, ], + outline: [2, 3], sidebar: [ { text: "指南", @@ -32,12 +33,12 @@ export default defineConfig({ { text: "路由", link: "/framework/routing" }, { text: "路由模块", link: "/framework/route-module" }, { text: "渲染策略", link: "/framework/rendering-strategies" }, - { text: "数据加载", link: "/framework/routing" }, - { text: "Actions", link: "/framework/routing" }, - { text: "导航", link: "/framework/routing" }, + { text: "数据加载", link: "/framework/data-loading" }, + { text: "Actions", link: "/framework/actions" }, + { text: "导航", link: "/framework/navigating" }, { - text: "加载中的用户界面", - link: "/framework/routing", + text: "待处理的UI", + link: "/framework/pending-ui", }, { text: "测试", link: "/framework/routing" }, { text: "自定义框架", link: "/framework/routing" }, @@ -76,7 +77,7 @@ export default defineConfig({ items: [ { text: "错误边界", link: "/framework/routing" }, { text: "错误上报", link: "/framework/routing" }, - { text: "使用Fetchers", link: "/framework/routing" }, + { text: "使用Fetchers", link: "/how-tos/fetchers" }, { text: "File Route 规范", link: "/framework/routing", diff --git a/src/framework/actions.md b/src/framework/actions.md new file mode 100644 index 0000000..3504352 --- /dev/null +++ b/src/framework/actions.md @@ -0,0 +1,150 @@ +# Actions + +数据变更通过路由动作(Route actions)来完成。当路由动作执行完毕后,页面上所有由加载器获取的数据都会被重新验证(revalidated),这样就能在无需编写额外代码的情况下,确保用户界面与数据保持同步。 + +通过 `action` 定义的路由动作只会在服务器端被调用,而通过 `clientAction` 定义的路由动作则是在浏览器(也就是客户端)中运行。 + +## 客户端 Actions + +`clientAction` 仅在浏览器端运行,并且当同时定义了 action 和 clientAction 时,clientAction 会优先被执行。 + +::: code-group + +```tsx [app/project.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 ( +
+

Project

+
+ + +
+ {actionData ?

{actionData.title} updated

: null} +
+ ); +} +``` + +::: + +## 服务端 Actions + +`action` 仅在服务器端运行,并且会从客户端的打包代码包中被移除。 + +::: code-group + +```tsx [app/project.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 ( +
+

Project

+
+ + +
+ {actionData ?

{actionData.title} updated

: null} +
+ ); +} +``` + +::: + +## 调用 Actions 的方式 + +Actions 可以通过声明式 `
` 组件和钩子函数 `useSubmit`(或者使用 `` 和 `fetcher.submit`)被调用,使用时传递一个路由 path 和一个 “post” 方法: + +### 使用 Form 调用 + +```tsx +import { Form } from "react-router"; + +function SomeComponent() { + return ( + + + + + ); +} +``` + +这种方式会造成一次浏览器导航,并且会在浏览器的历史记录中添加一个新的记录。 + +### 使用 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 ( + + + + + ); +} +``` + +它也有一个编程式 `submit` 方法: + +```ts +fetcher.submit( + { title: "New Title" }, + { action: "/update-task/123", method: "post" } +); +``` + +阅读 [使用 Fetchers](../how-tos/fetchers.md) 以了解更多关于 Fetchers 的信息。 diff --git a/src/framework/data-loading.md b/src/framework/data-loading.md new file mode 100644 index 0000000..e0454b5 --- /dev/null +++ b/src/framework/data-loading.md @@ -0,0 +1,146 @@ +# 数据加载 + +数据是通过加载器(`loader`)和客户端加载器(`clientLoader`)提供给路由组件的。 + +加载器数据会自动从加载器中进行序列化操作,然后在组件中进行反序列化。除了像字符串、数字这类基本数据类型的值以外,加载器还能够返回 promises、maps、sets、dates 等多种类型的数据。 + +## 客户端数据加载 + +客户端加载器(`clientLoader`)用于在客户端(也就是浏览器端)获取数据。对于那些你更倾向于仅从浏览器去获取数据的页面或者整个项目而言,它是非常有用的。 + +::: code-group + +```tsx [app/product.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 ( +
+

{name}

+

{description}

+
+ ); +} +``` + +::: + +## 服务器端数据加载 + +在服务器端渲染的情况下,加载器(`loader`)既用于初始页面加载,也用于客户端导航(client navigations)。在客户端导航期间,加载器会通过 React Router 自动从浏览器向服务器发起获取数据的 `fetch` 请求。 + +::: code-group + +```tsx [app/product.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 ( +
+

{name}

+

{description}

+
+ ); +} +``` + +::: + +需要注意的是,`loader` 函数会从客户端的打包代码包(bundles)中移除,这样一来,你就能够使用仅在服务器端可用的应用程序接口(APIs),而无需担心它们会被包含在浏览器端的代码中。 + +## 静态数据加载 + +在进行预渲染时,加载器(loaders)会被用于在生产构建阶段获取数据。 + +::: code-group + +```tsx [app/product.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 ( +
+

{name}

+

{description}

+
+ ); +} +``` + +::: + +在 `react-router.config.ts` 文件中指定需要预渲染的 URL: + +::: code-group + +```ts [react-router.config.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)过程中被使用。 + +::: code-group + +```tsx [app/product.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 ( +
+

{name}

+

{description}

+
+ ); +} +``` + +::: diff --git a/src/framework/navigating.md b/src/framework/navigating.md new file mode 100644 index 0000000..48fd581 --- /dev/null +++ b/src/framework/navigating.md @@ -0,0 +1,168 @@ +# 导航 + +用户可以通过 ``、``、`
`、`redirect` 以及 `useNavigate` 这些方式来在你的应用中进行导航。 + +## NavLink + +如果你需要一个激活状态或者加载状态,使用 `NavLink` 导航组件很合适。 + +```tsx +import { NavLink } from "react-router"; + +export function MyAppNav() { + return ( + + ); +} +``` + +`` 组件会为不同的状态渲染默认的类名,这一特性方便了你使用 CSS 来对其进行样式设置: + +```css +a.active { + color: red; +} + +a.pending { + animate: pulse 1s infinite; +} + +a.transitioning { + /* 运行中的css动画 */ +} +``` + +不仅如此,它还具备 `className`、`style` 和 `children` 这几个带有状态信息的回调属性,可用于内联样式(inline styling)或有条件的渲染: + +```tsx +// className + + [ + isPending ? "pending" : "", + isActive ? "active" : "", + isTransitioning ? "transitioning" : "", + ].join(" ") + } +> + Messages + +``` + +```tsx +// style + { + return { + fontWeight: isActive ? "bold" : "", + color: isPending ? "red" : "black", + viewTransitionName: isTransitioning ? "slide" : "", + }; + }} +> + Messages + +``` + +```tsx +// children + + {({ isActive, isPending, isTransitioning }) => ( + Tasks + )} + +``` + +## Link + +当跳转链接不需要激活样式时,使用 ``: + +```tsx +import { Link } from "react-router"; + +export function LoggedOutMessage() { + return ( +

+ 你已经退出. 再次登录 +

+ ); +} +``` + +## Form + +`` 组件通过用户提供的 `URLSearchParams` 来进行导航: + +```tsx + + + +``` + +如果你在输入框输入 `"journey"` 并提交,那么会导航到: + +```js +/search?q=journey +``` + +带有 `
` 的表单会被导航到 action 属性所指定的路由路径对应的页面。此时提交的数据会以 `FormData` 的形式进行发送,而不是像使用 `URLSearchParams` 那样(前面介绍过 URLSearchParams 主要用于构建 URL 查询参数并在 get 方法等场景下传递数据)。然而,更推荐使用 `useFetcher()` 提交表单数据,详细说明可以参考 [使用 Fetcher](../how-tos/fetchers.md)。 + +## redirect + +你可以在路由 loaders 和 actions 中使用 `redirect` 函数来进行重定向: + +```tsx +import { redirect } from "react-router"; + +export async function loader({ request }) { + let user = await getUser(request); + if (!user) { + return redirect("/login"); + } + return { userName: user.name }; +} +``` + +常见做法是,重定向到一个新的浏览器记录: + +```tsx +import { redirect } from "react-router"; + +export async function action({ request }) { + let formData = await request.formData(); + let project = await createProject(formData); + return redirect(`/projects/${project.id}`); +} +``` + +## useNavigate + +这个钩子(hook)允许代码在无需用户交互的情况下将用户导航到新的页面。不过,其使用场景相对来说并不常见,并且在有可能的情况下,推荐优先使用本指南中介绍的其他方式来实现相关功能。 + +虽然前面提到在有其他更符合常规交互模式时,应优先选用它们来实现页面导航,但 `useNavigate` 钩子函数在某些特定的、用户没有主动交互但又确实需要进行导航的情况下,有着合理且必要的应用场景,比如: + +- 用户长时间无操作后自动登出 +- 限时交互的用户界面等 + +```ts +import { useNavigate } from "react-router"; + +export function useLogoutAfterInactivity() { + let navigate = useNavigate(); + + useFakeInactivityHook(() => { + navigate("/logout"); + }); +} +``` diff --git a/src/framework/pending-ui.md b/src/framework/pending-ui.md new file mode 100644 index 0000000..9900dc4 --- /dev/null +++ b/src/framework/pending-ui.md @@ -0,0 +1 @@ +# 待处理的 UI diff --git a/src/how-tos/fetchers.md b/src/how-tos/fetchers.md new file mode 100644 index 0000000..ddf747b --- /dev/null +++ b/src/how-tos/fetchers.md @@ -0,0 +1 @@ +# 使用 Fetchers