diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/04-images.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/04-images.mdx index f5b4fde4..e7e2f685 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/04-images.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/04-images.mdx @@ -1,21 +1,21 @@ --- -source-updated-at: 2025-05-22T15:18:56.000Z -translation-updated-at: 2025-05-23T16:44:15.624Z +source-updated-at: 2025-06-02T15:30:01.000Z +translation-updated-at: 2025-06-02T19:01:22.562Z title: 如何优化图片 nav_title: 图片 -description: 学习如何在 Next.js 中优化图片 +description: 了解如何在 Next.js 中优化图片 related: title: API 参考 - description: 查看 Next.js Image 完整功能集的 API 参考文档。 + description: 查看 Next.js Image 完整功能的 API 参考文档。 links: - app/api-reference/components/image --- Next.js 的 [``](/docs/app/api-reference/components/image) 组件扩展了 HTML `` 元素,提供以下功能: -- **尺寸优化**:自动为不同设备提供正确尺寸的图片,并使用 WebP 等现代图片格式。 -- **视觉稳定性**:在图片加载时自动防止[布局偏移 (layout shift)](https://web.dev/articles/cls)。 -- **更快页面加载**:仅当图片进入视口时通过原生浏览器懒加载加载图片,并支持可选的模糊占位图。 +- **尺寸优化**:自动为不同设备提供正确尺寸的图片,使用 WebP 等现代图片格式。 +- **视觉稳定性**:在图片加载时自动防止[布局偏移 (CLS)](https://web.dev/articles/cls)。 +- **更快页面加载**:仅当图片进入视口时通过原生浏览器懒加载进行加载,可选模糊占位图。 - **资源灵活性**:按需调整图片尺寸,即使是远程服务器存储的图片。 要开始使用 ``,请从 `next/image` 导入并在组件中渲染它。 @@ -36,16 +36,16 @@ export default function Page() { } ``` -`src` 属性可以是[本地图片](#local-images)或[远程图片](#remote-images)。 +`src` 属性可以是[本地图片](#本地图片)或[远程图片](#远程图片)。 -> **🎥 观看视频**:了解更多关于如何使用 `next/image` → [YouTube (9 分钟)](https://youtu.be/IU_qq_c_lKA)。 +> **🎥 观看视频**:了解更多关于如何使用 `next/image` → [YouTube (9分钟)](https://youtu.be/IU_qq_c_lKA)。 ## 本地图片 -您可以将静态文件(如图片和字体)存储在根目录下的 [`public`](/docs/app/api-reference/file-conventions/public-folder) 文件夹中。`public` 中的文件可以通过代码从基础 URL (`/`) 开始引用。 +您可以将静态文件(如图片和字体)存储在根目录下的 [`public`](/docs/app/api-reference/file-conventions/public-folder) 文件夹中。`public` 内的文件可以通过代码从基础 URL (`/`) 开始引用。 显示 app 和 public 文件夹的目录结构 + ) +} +``` + +```jsx filename="app/page.js" switcher +import Image from 'next/image' + +export default function Page() { + return ( + 作者照片 + ) +} +``` + +### 静态导入图片 + +您也可以导入并使用本地图片文件。Next.js 会根据导入的文件自动确定图片的固有 [`width`](/docs/app/api-reference/components/image#width-and-height) 和 [`height`](/docs/app/api-reference/components/image#width-and-height)。这些值用于确定图片比例并在加载时防止[累积布局偏移 (CLS)](https://web.dev/articles/cls)。 + +```tsx filename="app/page.tsx" switcher +import Image from 'next/image' +import ProfileImage from './profile.png' + +export default function Page() { + return ( + 作者照片 ) } @@ -71,22 +106,23 @@ export default function Page() { ```jsx filename="app/page.js" switcher import Image from 'next/image' +import ProfileImage from './profile.png' export default function Page() { return ( 作者的照片 ) } ``` -使用本地图片时,Next.js 会根据导入的文件自动确定图片的固有 [`width`](/docs/app/api-reference/components/image#width-and-height) 和 [`height`](/docs/app/api-reference/components/image#width-and-height)。这些值用于确定图片比例,并在图片加载时防止[累积布局偏移 (Cumulative Layout Shift)](https://web.dev/articles/cls)。 +在这种情况下,Next.js 期望 `app/profile.png` 文件是可用的。 ## 远程图片 @@ -99,7 +135,7 @@ export default function Page() { return ( 作者的照片 @@ -114,7 +150,7 @@ export default function Page() { return ( 作者的照片 diff --git a/apps/docs/content/zh-hans/docs/01-app/05-api-reference/03-file-conventions/route-segment-config.mdx b/apps/docs/content/zh-hans/docs/01-app/05-api-reference/03-file-conventions/route-segment-config.mdx index 1763e85d..b80960a1 100644 --- a/apps/docs/content/zh-hans/docs/01-app/05-api-reference/03-file-conventions/route-segment-config.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/05-api-reference/03-file-conventions/route-segment-config.mdx @@ -1,11 +1,11 @@ --- -source-updated-at: 2025-06-01T01:32:20.000Z -translation-updated-at: 2025-06-01T22:15:28.867Z +source-updated-at: 2025-06-02T15:30:01.000Z +translation-updated-at: 2025-06-02T19:03:01.558Z title: 路由段配置 -description: 了解如何配置 Next.js 路由段的选项 +description: 了解如何配置 Next.js 路由段的选项。 --- -> 如果启用了 [`dynamicIO`](/docs/app/api-reference/config/next-config-js/dynamicIO) 标志,本页列出的选项将被禁用,并将在未来版本中弃用。 +> 如果启用了 [`dynamicIO`](/docs/app/api-reference/config/next-config-js/dynamicIO) 标志,则本页描述的选项将被禁用,并将在未来版本中弃用。 路由段选项允许您通过直接导出以下变量来配置 [页面](/docs/app/api-reference/file-conventions/layout)、[布局](/docs/app/api-reference/file-conventions/layout) 或 [路由处理器](/docs/app/building-your-application/routing/route-handlers) 的行为: @@ -50,9 +50,9 @@ export const dynamic = 'auto' // 'auto' | 'force-dynamic' | 'error' | 'force-static' ``` -> **须知**:`app` 目录中的新模型更倾向于在 `fetch` 请求级别进行细粒度的缓存控制,而不是 `pages` 目录中 `getServerSideProps` 和 `getStaticProps` 在页面级别的全有或全无模型。`dynamic` 选项是一种方便地回归到先前模型的方式,并提供了更简单的迁移路径。 +> **须知**:`app` 目录中的新模型更倾向于在 `fetch` 请求级别进行细粒度的缓存控制,而不是 `pages` 目录中 `getServerSideProps` 和 `getStaticProps` 在页面级别的全有或全无模型。`dynamic` 选项是一种回退到先前模型的便捷方式,并提供了更简单的迁移路径。 -- **`'auto'`**(默认):默认选项,尽可能缓存而不阻止任何组件选择动态行为。 +- **`'auto'`** (默认):默认选项,尽可能缓存而不阻止任何组件选择动态行为。 - **`'force-dynamic'`**:强制 [动态渲染](/docs/app/getting-started/partial-prerendering#dynamic-rendering),这将导致路由在每次用户请求时渲染。此选项等效于: - 将布局或页面中每个 `fetch()` 请求的选项设置为 `{ cache: 'no-store', next: { revalidate: 0 } }`。 - 将段配置设置为 `export const fetchCache = 'force-no-store'` @@ -69,7 +69,7 @@ export const dynamic = 'auto' ### `dynamicParams` -控制访问未使用 [generateStaticParams](/docs/app/api-reference/functions/generate-static-params) 生成的动态段时发生的情况。 +控制访问未通过 [generateStaticParams](/docs/app/api-reference/functions/generate-static-params) 生成的动态段时发生的情况。 ```tsx filename="layout.tsx | page.tsx" switcher export const dynamicParams = true // true | false, @@ -79,15 +79,14 @@ export const dynamicParams = true // true | false, export const dynamicParams = true // true | false, ``` -- **`true`**(默认):未包含在 `generateStaticParams` 中的动态段按需生成。 +- **`true`** (默认):未包含在 `generateStaticParams` 中的动态段按需生成。 - **`false`**:未包含在 `generateStaticParams` 中的动态段将返回 404。 > **须知**: > > - 此选项替换了 `pages` 目录中 `getStaticPaths` 的 `fallback: true | false | blocking` 选项。 -> - 要静态渲染所有路径首次访问时的内容,您需要在 `generateStaticParams` 中返回一个空数组或使用 `export const dynamic = 'force-static'`。 -> - 当 `dynamicParams = true` 时,该段使用 [流式服务器渲染](/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense)。 -> - 如果使用 `dynamic = 'error'` 和 `dynamic = 'force-static'`,它们会将 `dynamicParams` 的默认值更改为 `false`。 +> - 要静态渲染所有路径首次访问时,您需要在 `generateStaticParams` 中返回空数组或使用 `export const dynamic = 'force-static'`。 +> - 当 `dynamicParams = true` 时,段使用 [流式服务器渲染](/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense)。 ### `revalidate` @@ -103,27 +102,27 @@ export const revalidate = false // false | 0 | number ``` -- **`false`**(默认):默认启发式方法缓存任何将 `cache` 选项设置为 `'force-cache'` 的 `fetch` 请求或在 [动态 API](/docs/app/getting-started/partial-prerendering#dynamic-rendering#dynamic-apis) 使用之前发现的请求。语义上等同于 `revalidate: Infinity`,这意味着资源应无限期缓存。单个 `fetch` 请求仍可以使用 `cache: 'no-store'` 或 `revalidate: 0` 来避免被缓存并使路由动态渲染。或者将 `revalidate` 设置为低于路由默认值的正数以增加路由的重新验证频率。 +- **`false`** (默认):默认启发式缓存任何将 `cache` 选项设置为 `'force-cache'` 或在 [动态 API](/docs/app/getting-started/partial-prerendering#dynamic-rendering#dynamic-apis) 使用之前发现的 `fetch` 请求。语义上等同于 `revalidate: Infinity`,这意味着资源应无限期缓存。单个 `fetch` 请求仍可以使用 `cache: 'no-store'` 或 `revalidate: 0` 来避免被缓存并使路由动态渲染。或者将 `revalidate` 设置为低于路由默认值的正数以增加路由的重新验证频率。 - **`0`**:确保布局或页面始终 [动态渲染](/docs/app/getting-started/partial-prerendering#dynamic-rendering),即使未发现动态 API 或未缓存的数据获取。此选项将未设置 `cache` 选项的 `fetch` 请求的默认值更改为 `'no-store'`,但保留选择 `'force-cache'` 或使用正 `revalidate` 的 `fetch` 请求不变。 -- **`number`**:(以秒为单位)将布局或页面的默认重新验证频率设置为 `n` 秒。 +- **`number`**:(以秒为单位) 将布局或页面的默认重新验证频率设置为 `n` 秒。 > **须知**: > > - 重新验证值必须是静态可分析的。例如 `revalidate = 600` 是有效的,但 `revalidate = 60 * 10` 不是。 > - 使用 `runtime = 'edge'` 时,重新验证值不可用。 -> - 在开发环境中,页面始终按需渲染且从不缓存。这使您可以立即看到更改而无需等待重新验证期过去。 +> - 在开发环境中,页面始终按需渲染且从不缓存。这使您可以立即查看更改而无需等待重新验证期过去。 #### 重新验证频率 -- 单个路由中每个布局和页面的最低 `revalidate` 值将决定整个路由的重新验证频率。这确保子页面与其父布局一样频繁地重新验证。 -- 单个 `fetch` 请求可以设置比路由默认 `revalidate` 更低的 `revalidate` 以增加整个路由的重新验证频率。这允许您根据某些条件动态选择更频繁地重新验证某些路由。 +- 单个路由的每个布局和页面中最低的 `revalidate` 将决定整个路由的重新验证频率。这确保了子页面与其父布局一样频繁地重新验证。 +- 单个 `fetch` 请求可以设置比路由默认 `revalidate` 更低的 `revalidate` 以增加整个路由的重新验证频率。这使您可以根据某些条件动态选择更频繁地重新验证某些路由。 ### `fetchCache`
- 这是一个高级选项,仅在需要覆盖默认行为时使用。 + 这是一个高级选项,仅在您需要覆盖默认行为时使用。 -默认情况下,Next.js **将缓存** 任何在 [动态 API](/docs/app/getting-started/partial-prerendering#dynamic-rendering#dynamic-apis) 使用之前可访问的 `fetch()` 请求,并且 **不会缓存** 在动态 API 使用之后发现的 `fetch` 请求。 +默认情况下,Next.js 会缓存任何在使用 [动态 API](/docs/app/getting-started/partial-prerendering#dynamic-rendering#dynamic-apis) 之前可访问的 `fetch()` 请求,并且不会缓存在使用动态 API 之后发现的 `fetch` 请求。 `fetchCache` 允许您覆盖布局或页面中所有 `fetch` 请求的默认 `cache` 选项。 @@ -139,18 +138,18 @@ export const fetchCache = 'auto' // 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store' ``` -- **`'auto'`**(默认):默认选项,缓存动态 API 之前的 `fetch` 请求并使用它们提供的 `cache` 选项,不缓存动态 API 之后的 `fetch` 请求。 -- **`'default-cache'`**:允许将任何 `cache` 选项传递给 `fetch`,但如果未提供选项,则将 `cache` 选项设置为 `'force-cache'`。这意味着即使是动态 API 之后的 `fetch` 请求也被视为静态。 -- **`'only-cache'`**:确保所有 `fetch` 请求选择缓存,如果未提供选项则将默认更改为 `cache: 'force-cache'`,并在任何 `fetch` 请求使用 `cache: 'no-store'` 时引发错误。 -- **`'force-cache'`**:通过将所有 `fetch` 请求的 `cache` 选项设置为 `'force-cache'` 来确保所有 `fetch` 请求选择缓存。 -- **`'default-no-store'`**:允许将任何 `cache` 选项传递给 `fetch`,但如果未提供选项,则将 `cache` 选项设置为 `'no-store'`。这意味着即使是动态 API 之前的 `fetch` 请求也被视为动态。 -- **`'only-no-store'`**:确保所有 `fetch` 请求选择不缓存,如果未提供选项则将默认更改为 `cache: 'no-store'`,并在任何 `fetch` 请求使用 `cache: 'force-cache'` 时引发错误。 -- **`'force-no-store'`**:通过将所有 `fetch` 请求的 `cache` 选项设置为 `'no-store'` 来确保所有 `fetch` 请求选择不缓存。这强制所有 `fetch` 请求在每次请求时重新获取,即使它们提供了 `'force-cache'` 选项。 +- **`'auto'`** (默认):默认选项,在使用动态 API 之前缓存 `fetch` 请求,并根据它们提供的 `cache` 选项,不缓存使用动态 API 之后的 `fetch` 请求。 +- **`'default-cache'`**:允许将任何 `cache` 选项传递给 `fetch`,但如果未提供选项,则将 `cache` 选项设置为 `'force-cache'`。这意味着即使在动态 API 之后的 `fetch` 请求也被视为静态。 +- **`'only-cache'`**:确保所有 `fetch` 请求都选择缓存,如果未提供选项,则将默认更改为 `cache: 'force-cache'`,并在任何 `fetch` 请求使用 `cache: 'no-store'` 时引发错误。 +- **`'force-cache'`**:通过将所有 `fetch` 请求的 `cache` 选项设置为 `'force-cache'` 来确保所有 `fetch` 请求都选择缓存。 +- **`'default-no-store'`**:允许将任何 `cache` 选项传递给 `fetch`,但如果未提供选项,则将 `cache` 选项设置为 `'no-store'`。这意味着即使在动态 API 之前的 `fetch` 请求也被视为动态。 +- **`'only-no-store'`**:确保所有 `fetch` 请求都选择不缓存,如果未提供选项,则将默认更改为 `cache: 'no-store'`,并在任何 `fetch` 请求使用 `cache: 'force-cache'` 时引发错误。 +- **`'force-no-store'`**:通过将所有 `fetch` 请求的 `cache` 选项设置为 `'no-store'` 来确保所有 `fetch` 请求都选择不缓存。这强制所有 `fetch` 请求在每次请求时重新获取,即使它们提供了 `'force-cache'` 选项。 #### 跨路由段行为 -- 单个路由的每个布局和页面中设置的选项需要彼此兼容。 - - 如果同时提供 `'only-cache'` 和 `'force-cache'`,则 `'force-cache'` 优先。如果同时提供 `'only-no-store'` 和 `'force-no-store'`,则 `'force-no-store'` 优先。强制选项更改整个路由的行为,因此具有 `'force-*'` 的单个段将防止由 `'only-*'` 引起的任何错误。 +- 单个路由的每个布局和页面中设置的任何选项需要相互兼容。 + - 如果同时提供 `'only-cache'` 和 `'force-cache'`,则 `'force-cache'` 优先。如果同时提供 `'only-no-store'` 和 `'force-no-store'`,则 `'force-no-store'` 优先。强制选项会更改整个路由的行为,因此带有 `'force-*'` 的单个段将防止由 `'only-*'` 引起的任何错误。 - `'only-*'` 和 `'force-*'` 选项的目的是保证整个路由要么完全静态,要么完全动态。这意味着: - 不允许在单个路由中组合 `'only-cache'` 和 `'only-no-store'`。 - 不允许在单个路由中组合 `'force-cache'` 和 `'force-no-store'`。 @@ -161,7 +160,7 @@ export const fetchCache = 'auto' ### `runtime` -我们建议使用 Node.js 运行时来渲染您的应用程序,并使用 Edge 运行时来处理中间件。 +我们建议使用 Node.js 运行时渲染您的应用程序,并使用 Edge 运行时处理中间件。 ```tsx filename="layout.tsx | page.tsx | route.ts" switcher export const runtime = 'nodejs' @@ -173,7 +172,7 @@ export const runtime = 'nodejs' // 'nodejs' | 'edge' ``` -- **`'nodejs'`**(默认) +- **`'nodejs'`** (默认) - **`'edge'`** ### `preferredRegion` @@ -188,17 +187,16 @@ export const preferredRegion = 'auto' // 'auto' | 'global' | 'home' | ['iad1', 'sfo1'] ``` -对 `preferredRegion` 的支持以及支持的区域取决于您的部署平台。 +对 `preferredRegion` 的支持以及支持的地区取决于您的部署平台。 > **须知**: > -> - 如果未指定 `preferredRegion`,它将继承最近的父布局的选项。 -> - 根布局默认为 `all` 区域。 +> - 如果未指定 `preferredRegion`,它将继承最近父布局的选项。 +> - 根布局默认为 `all` 地区。 ### `maxDuration` -默认情况下,Next.js 不限制服务器端逻辑(渲染页面或处理 API)的执行时间。 -部署平台可以使用 Next.js 构建输出中的 `maxDuration` 来添加特定的执行限制。 +默认情况下,Next.js 不限制服务器端逻辑的执行(渲染页面或处理 API)。部署平台可以使用 Next.js 构建输出中的 `maxDuration` 来添加特定的执行限制。 **注意**:此设置需要 Next.js `13.4.10` 或更高版本。 @@ -212,16 +210,16 @@ export const maxDuration = 5 > **须知**: > -> - 如果使用 [服务器操作](/docs/app/building-your-application/data-fetching/server-actions-and-mutations),请在页面级别设置 `maxDuration` 以更改页面上使用的所有服务器操作的默认超时时间。 +> - 如果使用 [服务器操作](/docs/app/building-your-application/data-fetching/server-actions-and-mutations),请在页面级别设置 `maxDuration` 以更改页面上使用的所有服务器操作的默认超时。 ### `generateStaticParams` -`generateStaticParams` 函数可以与 [动态路由段](/docs/app/api-reference/file-conventions/dynamic-routes) 结合使用,以定义将在构建时静态生成而不是在请求时按需生成的路由段参数列表。 +`generateStaticParams` 函数可以与 [动态路由段](/docs/app/api-reference/file-conventions/dynamic-routes) 结合使用,以定义将在构建时静态生成的路由段参数列表,而不是在请求时按需生成。 -有关详细信息,请参阅 [API 参考](/docs/app/api-reference/functions/generate-static-params)。 +有关更多详细信息,请参阅 [API 参考](/docs/app/api-reference/functions/generate-static-params)。 ## 版本历史 | 版本 | | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `v15.0.0-RC` | `export const runtime = "experimental-edge"` 已弃用。提供 [代码修改工具](/docs/app/guides/upgrading/codemods#transform-app-router-route-segment-config-runtime-value-from-experimental-edge-to-edge)。 | \ No newline at end of file +| `v15.0.0-RC` | `export const runtime = "experimental-edge"` 已弃用。提供了 [代码修改工具](/docs/app/guides/upgrading/codemods#transform-app-router-route-segment-config-runtime-value-from-experimental-edge-to-edge)。 | \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/05-api-reference/04-functions/use-report-web-vitals.mdx b/apps/docs/content/zh-hans/docs/01-app/05-api-reference/04-functions/use-report-web-vitals.mdx index cb91dbaf..8597af29 100644 --- a/apps/docs/content/zh-hans/docs/01-app/05-api-reference/04-functions/use-report-web-vitals.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/05-api-reference/04-functions/use-report-web-vitals.mdx @@ -1,23 +1,27 @@ --- -source-updated-at: 2025-05-19T22:31:51.000Z -translation-updated-at: 2025-05-20T22:23:58.229Z +source-updated-at: 2025-06-02T15:30:01.000Z +translation-updated-at: 2025-06-02T19:01:54.691Z title: useReportWebVitals -description: API 参考文档:关于 `useReportWebVitals` 函数的使用说明。 +description: 关于 useReportWebVitals 函数的 API 参考文档。 --- -{/* 本文档内容在应用路由和页面路由间共享。您可以使用 `内容` 组件添加特定于页面路由的内容。所有共享内容不应包裹在任何组件中。 */} +{/* 本文档内容在应用路由和页面路由间共享。您可以使用 `内容` 组件添加特定于页面路由的内容。任何共享内容不应包裹在组件中。 */} -`useReportWebVitals` 钩子允许您上报 [核心网页指标 (Core Web Vitals)](https://web.dev/vitals/),并可与您的分析服务结合使用。 +`useReportWebVitals` 钩子允许您报告 [核心 Web 指标 (Core Web Vitals)](https://web.dev/vitals/),并可与您的分析服务结合使用。 + +传递给 `useReportWebVitals` 的新函数会使用当前可用的指标进行调用。为防止重复报告数据,请确保回调函数引用保持不变(如下方代码示例所示)。 ```jsx filename="pages/_app.js" import { useReportWebVitals } from 'next/web-vitals' +const logWebVitals = (metric) => { + console.log(metric) +} + function MyApp({ Component, pageProps }) { - useReportWebVitals((metric) => { - console.log(metric) - }) + useReportWebVitals(logWebVitals) return } @@ -32,10 +36,12 @@ function MyApp({ Component, pageProps }) { import { useReportWebVitals } from 'next/web-vitals' +const logWebVitals = (metric) => { + console.log(metric) +} + export function WebVitals() { - useReportWebVitals((metric) => { - console.log(metric) - }) + useReportWebVitals(logWebVitals) return null } @@ -56,7 +62,7 @@ export default function Layout({ children }) { } ``` -> 由于 `useReportWebVitals` 钩子需要 `'use client'` 指令,最高效的做法是创建一个单独的组件并由根布局导入。这样可以将客户端边界严格限制在 `WebVitals` 组件内。 +> 由于 `useReportWebVitals` 钩子需要 `'use client'` 指令,最高效的方法是创建一个单独的组件,由根布局导入。这样可以将客户端边界限制在 `WebVitals` 组件内。 @@ -65,23 +71,23 @@ export default function Layout({ children }) { 作为钩子参数传递的 `metric` 对象包含以下属性: - `id`:当前页面加载上下文中该指标的唯一标识符 -- `name`:性能指标名称。可能的值包括特定于 Web 应用的 [网页指标](#web-vitals) 名称(TTFB、FCP、LCP、FID、CLS) -- `delta`:该指标当前值与先前值的差值。通常以毫秒为单位,表示指标值随时间的变化 +- `name`:性能指标名称。可能值包括特定于 Web 应用的 [Web 指标](#web-vitals) 名称(TTFB、FCP、LCP、FID、CLS) +- `delta`:该指标当前值与先前值的差异。值通常以毫秒为单位,表示指标随时间的变化 - `entries`:与指标关联的 [性能条目 (Performance Entries)](https://developer.mozilla.org/docs/Web/API/PerformanceEntry) 数组。这些条目提供与指标相关的性能事件的详细信息 -- `navigationType`:指示触发指标收集的 [导航类型](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming/type)。可能的值包括 `"navigate"`、`"reload"`、`"back_forward"` 和 `"prerender"` -- `rating`:指标值的定性评级,提供性能评估。可能的值是 `"good"`、`"needs-improvement"` 和 `"poor"`。评级通常通过将指标值与预定义阈值进行比较来确定 -- `value`:性能条目的实际值或持续时间,通常以毫秒为单位。该值提供了指标跟踪的性能方面的定量测量。值的来源取决于所测量的特定指标,可能来自各种 [性能 API (Performance API)](https://developer.mozilla.org/docs/Web/API/Performance_API) +- `navigationType`:指示触发指标收集的 [导航类型](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming/type)。可能值包括 `"navigate"`、`"reload"`、`"back_forward"` 和 `"prerender"` +- `rating`:指标值的定性评级,提供性能评估。可能值为 `"good"`(良好)、`"needs-improvement"`(需改进)和 `"poor"`(较差)。评级通常通过将指标值与预定义阈值进行比较来确定 +- `value`:性能条目的实际值或持续时间,通常以毫秒为单位。该值提供了指标跟踪的性能方面的定量测量。值的来源取决于具体测量的指标,可能来自各种 [性能 API (Performance API)](https://developer.mozilla.org/docs/Web/API/Performance_API) -## 网页指标 (Web Vitals) +## Web 指标 -[网页指标 (Web Vitals)](https://web.dev/vitals/) 是一组旨在捕捉网页用户体验的有用指标。包含以下所有核心指标: +[Web 指标 (Web Vitals)](https://web.dev/vitals/) 是一组旨在捕捉网页用户体验的有用指标。包含以下所有 Web 指标: - [首字节时间 (Time to First Byte, TTFB)](https://developer.mozilla.org/docs/Glossary/Time_to_first_byte) - [首次内容绘制 (First Contentful Paint, FCP)](https://developer.mozilla.org/docs/Glossary/First_contentful_paint) - [最大内容绘制 (Largest Contentful Paint, LCP)](https://web.dev/lcp/) - [首次输入延迟 (First Input Delay, FID)](https://web.dev/fid/) - [累积布局偏移 (Cumulative Layout Shift, CLS)](https://web.dev/cls/) -- [下次绘制交互 (Interaction to Next Paint, INP)](https://web.dev/inp/) +- [交互到下一次绘制 (Interaction to Next Paint, INP)](https://web.dev/inp/) 您可以使用 `name` 属性处理所有这些指标的结果。 @@ -90,18 +96,20 @@ export default function Layout({ children }) { ```jsx filename="pages/_app.js" import { useReportWebVitals } from 'next/web-vitals' -function MyApp({ Component, pageProps }) { - useReportWebVitals((metric) => { - switch (metric.name) { - case 'FCP': { - // 处理 FCP 结果 - } - case 'LCP': { - // 处理 LCP 结果 - } - // ... +const handleWebVitals = (metric) => { + switch (metric.name) { + case 'FCP': { + // 处理 FCP 结果 } - }) + case 'LCP': { + // 处理 LCP 结果 + } + // ... + } +} + +function MyApp({ Component, pageProps }) { + useReportWebVitals(handleWebVitals) return } @@ -116,18 +124,22 @@ function MyApp({ Component, pageProps }) { import { useReportWebVitals } from 'next/web-vitals' -export function WebVitals() { - useReportWebVitals((metric) => { - switch (metric.name) { - case 'FCP': { - // 处理 FCP 结果 - } - case 'LCP': { - // 处理 LCP 结果 - } - // ... +type ReportWebVitalsCallback = Parameters[0] + +const handleWebVitals: ReportWebVitalsCallback = (metric) => { + switch (metric.name) { + case 'FCP': { + // 处理 FCP 结果 + } + case 'LCP': { + // 处理 LCP 结果 } - }) + // ... + } +} + +export function WebVitals() { + useReportWebVitals(handleWebVitals) } ``` @@ -136,18 +148,20 @@ export function WebVitals() { import { useReportWebVitals } from 'next/web-vitals' -export function WebVitals() { - useReportWebVitals((metric) => { - switch (metric.name) { - case 'FCP': { - // 处理 FCP 结果 - } - case 'LCP': { - // 处理 LCP 结果 - } - // ... +const handleWebVitals = (metric) => { + switch (metric.name) { + case 'FCP': { + // 处理 FCP 结果 } - }) + case 'LCP': { + // 处理 LCP 结果 + } + // ... + } +} + +export function WebVitals() { + useReportWebVitals(handleWebVitals) } ``` @@ -157,72 +171,76 @@ export function WebVitals() { ## 自定义指标 -除了上述核心指标外,还有一些额外的自定义指标用于测量页面水合 (hydrate) 和渲染的时间: +除了上述核心指标外,还有一些额外的自定义指标用于测量页面水合 (hydrate) 和渲染所需时间: -- `Next.js-hydration`:页面开始和完成水合所需的时间(毫秒) -- `Next.js-route-change-to-render`:路由变更后页面开始渲染所需的时间(毫秒) -- `Next.js-render`:路由变更后页面完成渲染所需的时间(毫秒) +- `Next.js-hydration`:页面开始和完成水合所需时间(毫秒) +- `Next.js-route-change-to-render`:路由变更后页面开始渲染所需时间(毫秒) +- `Next.js-render`:路由变更后页面完成渲染所需时间(毫秒) -您可以单独处理这些指标的结果: +您可以分别处理这些指标的结果: ```jsx filename="pages/_app.js" import { useReportWebVitals } from 'next/web-vitals' +function handleCustomMetrics(metrics) { + switch (metric.name) { + case 'Next.js-hydration': + // 处理水合结果 + break + case 'Next.js-route-change-to-render': + // 处理路由变更到渲染结果 + break + case 'Next.js-render': + // 处理渲染结果 + break + default: + break + } +} + function MyApp({ Component, pageProps }) { - useReportWebVitals((metric) => { - switch (metric.name) { - case 'Next.js-hydration': - // 处理水合结果 - break - case 'Next.js-route-change-to-render': - // 处理路由变更到渲染的结果 - break - case 'Next.js-render': - // 处理渲染结果 - break - default: - break - } - }) + useReportWebVitals(handleCustomMetrics) return } ``` -这些指标在所有支持 [用户计时 API (User Timing API)](https://caniuse.com/#feat=user-timing) 的浏览器中均有效。 +这些指标在所有支持 [用户计时 API (User Timing API)](https://caniuse.com/#feat=user-timing) 的浏览器中均可工作。 -## 将结果发送至外部系统 +## 将结果发送到外部系统 -您可以将结果发送至任何端点以测量和跟踪站点上的真实用户性能。例如: +您可以将结果发送到任何端点以测量和跟踪您网站上的真实用户性能。例如: ```js -useReportWebVitals((metric) => { +function postWebVitals(metrics) { const body = JSON.stringify(metric) const url = 'https://example.com/analytics' - // 优先使用 `navigator.sendBeacon()`,回退到 `fetch()` + // 如果可用则使用 `navigator.sendBeacon()`,否则回退到 `fetch()` if (navigator.sendBeacon) { navigator.sendBeacon(url, body) } else { fetch(url, { body, method: 'POST', keepalive: true }) } -}) +} + +useReportWebVitals(postWebVitals) ``` -> **须知**:如果您使用 [Google Analytics](https://analytics.google.com/analytics/web/),利用 `id` 值可以手动构建指标分布(用于计算百分位数等) +> **须知**:如果您使用 [Google Analytics](https://analytics.google.com/analytics/web/),使用 `id` 值可以手动构建指标分布(计算百分位数等) > ```js > useReportWebVitals(metric => { -> // 如果按照此示例初始化了 Google Analytics,请使用 `window.gtag`: +> // 如果您按照此示例初始化了 Google Analytics,请使用 `window.gtag`: > // https://github.com/vercel/next.js/blob/canary/examples/with-google-analytics > window.gtag('event', metric.name, { > value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value), // 值必须为整数 -> event_label: metric.id, // 当前页面加载的唯一标识符 +> event_label: metric.id, // 当前页面加载的唯一 id > non_interaction: true, // 避免影响跳出率 > }); > } > ``` > -> 了解更多关于 [将结果发送至 Google Analytics](https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics) 的信息。 \ No newline at end of file +> 了解更多关于 [将结果发送到 Google Analytics](https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics) 的信息。 \ No newline at end of file diff --git a/apps/docs/content/zh-hans/learn/02-dashboard-app/12-mutating-data.mdx b/apps/docs/content/zh-hans/learn/02-dashboard-app/12-mutating-data.mdx index 02943d90..818fc5a8 100644 --- a/apps/docs/content/zh-hans/learn/02-dashboard-app/12-mutating-data.mdx +++ b/apps/docs/content/zh-hans/learn/02-dashboard-app/12-mutating-data.mdx @@ -1,25 +1,25 @@ --- -source-updated-at: 2025-05-29T18:05:49.000Z -translation-updated-at: 2025-05-29T19:50:40.393Z +source-updated-at: 2025-06-02T15:30:02.000Z +translation-updated-at: 2025-06-02T19:04:53.837Z title: 数据变更 -headline: '应用路由 (App Router):数据变更' +headline: 'App Router:数据变更' description: '使用 React 服务端操作 (Server Actions) 变更数据,并重新验证 Next.js 缓存。' image: 'https://nextjs.org/api/learn-og?title=Mutating%20Data&chapter=12' --- -在上一章节中,您通过 URL 搜索参数和 Next.js API 实现了搜索与分页功能。现在让我们继续完善发票页面,添加创建、更新和删除发票的功能! +在上一章中,您使用 URL 搜索参数和 Next.js API 实现了搜索和分页功能。现在让我们继续完善发票页面,添加创建、更新和删除发票的功能! [什么是服务端操作 (Server Actions)?](#what-are-server-actions) ---------------------------------------------------- -React 服务端操作允许您直接在服务端运行异步代码,无需创建专门的 API 端点来变更数据。您只需编写可在服务端执行的异步函数,即可从客户端或服务端组件调用这些函数。 +React 服务端操作允许您直接在服务器上运行异步代码。它们消除了创建 API 端点来变更数据的需要。相反,您可以编写在服务器上执行的异步函数,这些函数可以从客户端或服务端组件调用。 -安全性是 Web 应用的首要考量,因为应用可能面临多种威胁。服务端操作通过加密闭包、严格的输入检查、错误消息哈希、主机限制等特性,显著提升了应用安全性。 +安全性是 Web 应用程序的首要任务,因为它们容易受到各种威胁。这正是服务端操作的用武之地。它们包含加密闭包、严格的输入检查、错误消息哈希、主机限制等功能——所有这些共同作用,显著提升您的应用程序安全性。 [在表单中使用服务端操作](#using-forms-with-server-actions) ------------------------------------------------------------------- -在 React 中,您可以使用 `
` 元素的 `action` 属性来调用操作。操作会自动接收包含捕获数据的原生 [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) 对象。 +在 React 中,您可以使用 `` 元素中的 `action` 属性来调用操作。该操作会自动接收原生的 [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) 对象,其中包含捕获的数据。 例如: @@ -33,17 +33,17 @@ export default function Page() { // 数据变更逻辑... } - // 通过 "action" 属性调用操作 + // 使用 "action" 属性调用操作 return ...
; } ``` -在服务端组件中调用服务端操作的优势在于渐进增强 —— 即使客户端 JavaScript 尚未加载,表单仍可正常工作(例如在网络连接较慢的情况下)。 +在服务端组件中调用服务端操作的一个优势是渐进增强——即使客户端尚未加载 JavaScript,表单也能正常工作。例如,在较慢的网络连接下。 [Next.js 与服务端操作](#nextjs-with-server-actions) ---------------------------------------------------------- -服务端操作与 Next.js [缓存](https://nextjs.org/docs/app/building-your-application/caching)深度集成。通过服务端操作提交表单时,您不仅可以变更数据,还能使用 `revalidatePath` 和 `revalidateTag` 等 API 重新验证相关缓存。 +服务端操作还与 Next.js [缓存](https://nextjs.org/docs/app/building-your-application/caching)深度集成。当通过服务端操作提交表单时,您不仅可以使用该操作来变更数据,还可以使用 `revalidatePath` 和 `revalidateTag` 等 API 重新验证相关缓存。 让我们看看它们如何协同工作! @@ -52,26 +52,26 @@ export default function Page() { 以下是创建新发票的步骤: -1. 创建表单以捕获用户输入 -2. 创建服务端操作并从表单调用 -3. 在操作中从 `formData` 对象提取数据 -4. 验证并准备要插入数据库的数据 -5. 插入数据并处理可能的错误 -6. 重新验证缓存并重定向用户回发票页面 +1. 创建一个表单来捕获用户输入。 +2. 创建一个服务端操作并从表单中调用它。 +3. 在服务端操作中,从 `formData` 对象提取数据。 +4. 验证并准备要插入数据库的数据。 +5. 插入数据并处理任何错误。 +6. 重新验证缓存并将用户重定向回发票页面。 ### [1\. 创建新路由和表单](#1-create-a-new-route-and-form) -首先,在 `/invoices` 文件夹中添加名为 `/create` 的路由段,并创建 `page.tsx` 文件: +首先,在 `/invoices` 文件夹中,添加一个名为 `/create` 的新路由段,并在其中创建一个 `page.tsx` 文件: 包含嵌套 create 文件夹的 Invoices 目录,内含 page.tsx 文件 -此路由用于创建新发票。将以下代码粘贴到 `page.tsx` 文件中并仔细研究: +您将使用此路由来创建新发票。在 `page.tsx` 文件中粘贴以下代码,然后花些时间研究它: ```tsx filename="/dashboard/invoices/create/page.tsx" import Form from '@/app/ui/invoices/create-form'; @@ -99,15 +99,16 @@ export default async function Page() { } ``` -该页面是服务端组件,会获取 `customers` 数据并传递给 `
` 组件。为节省时间,我们已为您准备好 `` 组件。 +您的页面是一个服务端组件,它获取 `customers` 并将其传递给 `` 组件。为了节省时间,我们已经为您创建了 `` 组件。 -查看 `` 组件,您会发现表单包含: -* 一个带有客户列表的 `` 元素 -* 两个 `type="radio"` 的状态 `` 元素 -* 一个 `type="submit"` 的按钮 +导航到 `` 组件,您会看到该表单: -访问 [http://localhost:3000/dashboard/invoices/create](http://localhost:3000/dashboard/invoices/create) 将看到以下界面: +* 有一个包含**客户**列表的 `` 元素,类型为 `type="number"`。 +* 有两个用于状态的 `` 元素,类型为 `type="radio"`。 +* 有一个类型为 `type="submit"` 的按钮。 + +在 [http://localhost:3000/dashboard/invoices/create](http://localhost:3000/dashboard/invoices/create) 上,您应该看到以下 UI: 创建发票页面,包含面包屑导航和表单` 组件中导入 `createInvoice` 操作,为 `` 元素添加 `action` 属性并调用 `createInvoice`: +然后,在您的 `` 组件中,从 `actions.ts` 文件导入 `createInvoice`。向 `` 元素添加 `action` 属性,并调用 `createInvoice` 操作。 ```tsx {10,18} filename="/app/ui/invoices/create-form.tsx" import { CustomerField } from '@/app/lib/definitions'; @@ -165,15 +166,15 @@ export default function Form({ } ``` -> **注意**:在 HTML 中,您需要向 `action` 属性传递 URL,该 URL 是表单数据的提交目标(通常是 API 端点)。 +> **须知**:在 HTML 中,您会将 URL 传递给 `action` 属性。此 URL 是表单数据应提交到的目标(通常是 API 端点)。 > -> 但在 React 中,`action` 被视为特殊属性 —— React 在其基础上扩展,允许直接调用操作。 +> 然而,在 React 中,`action` 属性被视为一个特殊属性——这意味着 React 在其基础上构建,允许调用操作。 > -> 在底层,服务端操作会创建 `POST` API 端点,这就是为什么使用服务端操作时无需手动创建 API 端点。 +> 在幕后,服务端操作会创建一个 `POST` API 端点。这就是为什么在使用服务端操作时不需要手动创建 API 端点。 ### [3\. 从 `formData` 提取数据](#3-extract-the-data-from-formdata) -在 `actions.ts` 文件中,您需要使用 [多种方法](https://developer.mozilla.org/en-US/docs/Web/API/FormData) 从 `formData` 提取值。本例中我们使用 [`.get(name)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/get) 方法。 +回到您的 `actions.ts` 文件,您需要提取 `formData` 的值,可以使用[几种方法](https://developer.mozilla.org/en-US/docs/Web/API/FormData)。对于此示例,让我们使用 [`.get(name)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/get) 方法。 ```ts {3,4,5,6,7,8,9,10} filename="/app/lib/actions.ts" 'use server'; @@ -184,24 +185,24 @@ export async function createInvoice(formData: FormData) { amount: formData.get('amount'), status: formData.get('status'), }; - // 测试输出: + // 测试一下: console.log(rawFormData); } ``` -> **提示**:如果表单字段较多,可以考虑使用 [`entries()`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries) 方法配合 JavaScript 的 [`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)。 +> **提示**:如果您正在处理具有许多字段的表单,您可能需要考虑使用 [`entries()`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries) 方法与 JavaScript 的 [`Object.fromEntries()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)。 -提交表单后,您应该在终端(而非浏览器)中看到刚输入的表单数据。 +要检查一切是否正确连接,请尝试使用表单。提交后,您应该会在**终端**(而不是浏览器)中看到您刚刚输入到表单中的数据。 -现在数据已转换为对象格式,操作起来会更加方便。 +现在您的数据以对象的形式存在,处理起来会容易得多。 -### [4\. 验证并准备数据](#4-validate-and-prepare-the-data) +### [4\. 验证和准备数据](#4-validate-and-prepare-the-data) -将表单数据发送到数据库前,需确保其格式和类型正确。回想课程前面的内容,发票表期望以下格式的数据: +在将表单数据发送到数据库之前,您需要确保其格式正确且类型正确。如果您记得课程前面的内容,您的发票表期望以下格式的数据: ```ts filename="/app/lib/definitions.ts" export type Invoice = { - id: string; // 将在数据库创建 + id: string; // 将在数据库中创建 customer_id: string; amount: number; // 以分为单位存储 status: 'pending' | 'paid'; @@ -209,21 +210,21 @@ export type Invoice = { }; ``` -目前您只有表单中的 `customer_id`、`amount` 和 `status`。 +到目前为止,您只有来自表单的 `customer_id`、`amount` 和 `status`。 -#### [类型验证与强制转换](#type-validation-and-coercion) +#### [类型验证和强制转换](#type-validation-and-coercion) -验证表单数据是否符合数据库预期类型非常重要。例如,如果在操作中添加: +验证表单数据与数据库中预期的类型是否一致非常重要。例如,如果您在操作中添加 `console.log`: ``` console.log(typeof rawFormData.amount); ``` -您会发现 `amount` 是 `string` 类型而非 `number`。这是因为 `type="number"` 的输入元素实际返回字符串而非数字! +您会注意到 `amount` 的类型是 `string` 而不是 `number`。这是因为 `type="number"` 的 `input` 元素实际上返回的是字符串,而不是数字! -虽然可以手动验证类型,但使用类型验证库能节省时间。本例中我们将使用 [Zod](https://zod.dev/),这是一个 TypeScript 优先的验证库。 +要处理类型验证,您有几个选择。虽然您可以手动验证类型,但使用类型验证库可以节省时间和精力。对于您的示例,我们将使用 [Zod](https://zod.dev/),这是一个 TypeScript 优先的验证库,可以简化此任务。 -在 `actions.ts` 中导入 Zod 并定义与表单对象结构匹配的模式,该模式会在数据存入数据库前验证 `formData`: +在您的 `actions.ts` 文件中,导入 Zod 并定义一个与表单对象形状匹配的模式。此模式将在将 `formData` 保存到数据库之前对其进行验证。 ```ts {3,5,6,7,8,9,10,11,13} filename="/app/lib/actions.ts" 'use server'; @@ -245,9 +246,9 @@ export async function createInvoice(formData: FormData) { } ``` -`amount` 字段专门设置为从字符串强制转换为数字并验证类型。 +`amount` 字段专门设置为强制(更改)从字符串到数字,同时验证其类型。 -然后将 `rawFormData` 传递给 `CreateInvoice` 进行类型验证: +然后,您可以将 `rawFormData` 传递给 `CreateInvoice` 以验证类型: ```ts {3} filename="/app/lib/actions.ts" // ... @@ -262,9 +263,9 @@ export async function createInvoice(formData: FormData) { #### [以分为单位存储值](#storing-values-in-cents) -通常建议在数据库中以分为单位存储金额,以避免 JavaScript 浮点数错误并提高精度。 +通常,良好的做法是在数据库中以分为单位存储货币值,以消除 JavaScript 浮点错误并确保更高的准确性。 -将金额转换为分: +让我们将金额转换为分: ```ts {8} filename="/app/lib/actions.ts" // ... @@ -280,7 +281,7 @@ export async function createInvoice(formData: FormData) { #### [创建新日期](#creating-new-dates) -最后,为发票创建日期创建格式为 "YYYY-MM-DD" 的新日期: +最后,让我们为发票的创建日期创建一个格式为 "YYYY-MM-DD" 的新日期: ```ts {9} filename="/app/lib/actions.ts" // ... @@ -297,9 +298,9 @@ export async function createInvoice(formData: FormData) { ### [5\. 将数据插入数据库](#5-inserting-the-data-into-your-database) -现在您已拥有数据库所需的所有值,可以创建 SQL 查询将新发票插入数据库并传入变量: +现在您拥有了数据库所需的所有值,可以创建一个 SQL 查询来将新发票插入数据库并传递变量: -```ts {2,15,16,17,18} filename="/app/lib/actions.ts" +```ts {2,4,17,18,19,20} filename="/app/lib/actions.ts" import { z } from 'zod'; import postgres from 'postgres'; @@ -323,13 +324,13 @@ export async function createInvoice(formData: FormData) { } ``` -目前我们尚未处理任何错误,这将在下一章节讨论。现在让我们继续下一步。 +目前,我们没有处理任何错误。我们将在下一章讨论这个问题。现在,让我们继续下一步。 ### [6. 重新验证与重定向](#6-revalidate-and-redirect) -Next.js 拥有一个客户端路由缓存,可在用户浏览器中临时存储路由片段。结合[预获取](/docs/app/building-your-application/routing/linking-and-navigating#1-prefetching)功能,该缓存能确保用户在路由间快速导航,同时减少向服务器发出的请求次数。 +Next.js 有一个客户端路由缓存 (client-side router cache),它会将路由片段临时存储在用户的浏览器中。结合[预取 (prefetching)](/docs/app/building-your-application/routing/linking-and-navigating#1-prefetching) 功能,这个缓存机制能确保用户在路由间快速导航,同时减少向服务器发出的请求数量。 -由于您需要更新发票路由中显示的数据,因此需要清除此缓存并触发向服务器的新请求。您可以使用 Next.js 的 [`revalidatePath`](/docs/app/api-reference/functions/revalidatePath) 函数实现: +由于您正在更新发票路由中显示的数据,您需要清除这个缓存并触发向服务器的新请求。可以通过 Next.js 的 [`revalidatePath`](/docs/app/api-reference/functions/revalidatePath) 函数实现: ```ts {4,25} filename="/app/lib/actions.ts" 'use server'; @@ -364,7 +365,7 @@ export async function createInvoice(formData: FormData) { 此时,您还需要将用户重定向回 `/dashboard/invoices` 页面。可以使用 Next.js 的 [`redirect`](/docs/app/api-reference/functions/redirect) 函数实现: -```ts {6,14} filename="/app/lib/actions.ts" +```ts {5,16} filename="/app/lib/actions.ts" 'use server'; import { z } from 'zod'; @@ -384,37 +385,39 @@ export async function createInvoice(formData: FormData) { } ``` -恭喜!您已成功实现第一个服务端操作 (Server Action)。通过添加新发票来测试功能,如果一切正常: +恭喜!您已经实现了第一个服务器操作 (Server Action)。通过添加新发票来测试它,如果一切正常: -1. 提交后应重定向至 `/dashboard/invoices` 路由 -2. 应在表格顶部看到新增发票 +1. 提交后应重定向到 `/dashboard/invoices` 路由 +2. 应在表格顶部看到新发票 [更新发票](#updating-an-invoice) ------------------------------------------- -更新发票表单与创建发票表单类似,区别在于需要传递发票 `id` 以更新数据库记录。以下是更新发票的步骤: +更新发票表单与创建发票表单类似,不同之处在于您需要传递发票 `id` 来更新数据库中的记录。让我们看看如何获取和传递发票 `id`。 + +以下是更新发票的步骤: -1. 创建包含发票 `id` 的动态路由片段 -2. 从页面参数中读取发票 `id` -3. 从数据库获取特定发票数据 +1. 使用发票 `id` 创建新的动态路由段 (dynamic route segment) +2. 从页面参数 (page params) 中读取发票 `id` +3. 从数据库中获取特定发票 4. 用发票数据预填充表单 -5. 更新数据库中的发票数据 +5. 在数据库中更新发票数据 -### [1. 创建包含发票 `id` 的动态路由片段](#1-create-a-dynamic-route-segment-with-the-invoice-id) +### [1. 使用发票 `id` 创建动态路由段](#1-create-a-dynamic-route-segment-with-the-invoice-id) -Next.js 允许创建[动态路由片段](/docs/app/building-your-application/routing/dynamic-routes),适用于需要基于数据创建路由的场景(如博客标题、产品页面等)。通过在文件夹名称外添加方括号可创建动态路由,例如 `[id]`、`[post]` 或 `[slug]`。 +当您不知道确切的段名称并希望基于数据创建路由时,Next.js 允许您创建[动态路由段 (Dynamic Route Segments)](/docs/app/building-your-application/routing/dynamic-routes)。这适用于博客文章标题、产品页面等场景。您可以通过将文件夹名称用方括号包裹来创建动态路由段,例如 `[id]`、`[post]` 或 `[slug]`。 -在 `/invoices` 文件夹中创建名为 `[id]` 的动态路由,然后新建包含 `page.tsx` 文件的 `edit` 路由。文件结构应如下: +在您的 `/invoices` 文件夹中,创建一个名为 `[id]` 的新动态路由,然后在其中创建一个名为 `edit` 的路由并添加 `page.tsx` 文件。文件结构应如下所示: 包含嵌套[id]文件夹和edit文件夹的发票目录结构 -在 `` 组件中,注意 `` 按钮会接收来自表格记录的发票 `id`。 +在您的 `
` 组件中,注意有一个 `` 按钮,它从表格记录中接收发票的 `id`。 ```tsx {11} filename="/app/ui/invoices/table.tsx" export default async function InvoicesTable({ @@ -435,7 +438,7 @@ export default async function InvoicesTable({ } ``` -导航至 `` 组件,更新 `Link` 的 `href` 以接收 `id` 属性。可使用模板字面量链接到动态路由片段: +导航到您的 `` 组件,并更新 `Link` 的 `href` 以接受 `id` 属性。您可以使用模板字面量链接到动态路由段: ```tsx {9} filename="/app/ui/invoices/buttons.tsx" import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/24/outline'; @@ -455,9 +458,9 @@ export function UpdateInvoice({ id }: { id: string }) { } ``` -### [2. 从页面参数读取发票 `id`](#2-read-the-invoice-id-from-page-params) +### [2. 从页面参数中读取发票 `id`](#2-read-the-invoice-id-from-page-params) -在 `` 组件中粘贴以下代码: +回到您的 `` 组件,粘贴以下代码: ```tsx filename="/app/dashboard/invoices/[id]/edit/page.tsx" import Form from '@/app/ui/invoices/edit-form'; @@ -483,11 +486,11 @@ export default async function Page() { } ``` -注意此处与创建发票页面的相似性,区别在于导入了来自 `edit-form.tsx` 的不同表单。该表单应预填充客户名称、发票金额和状态的 `defaultValue`。要预填充表单字段,需使用 `id` 获取特定发票。 +注意它与您的 `/create` 发票页面类似,但它导入的是不同的表单(来自 `edit-form.tsx` 文件)。这个表单应该用客户名称、发票金额和状态的 `defaultValue` **预填充**。为了预填充表单字段,您需要使用 `id` 获取特定的发票。 -除了 `searchParams`,页面组件还接受 `params` 属性用于访问 `id`。更新 `` 组件以接收该属性: +除了 `searchParams` 外,页面组件还接受一个名为 `params` 的属性,您可以用它来访问 `id`。更新您的 `` 组件以接收这个属性: -```tsx {5,6} filename="/app/dashboard/invoices/[id]/edit/page.tsx" +```tsx {5,6,7} filename="/app/dashboard/invoices/[id]/edit/page.tsx" import Form from '@/app/ui/invoices/edit-form'; import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; import { fetchCustomers } from '@/app/lib/data'; @@ -501,11 +504,12 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { ### [3. 获取特定发票](#3-fetch-the-specific-invoice) -接着: -* 导入名为 `fetchInvoiceById` 的新函数并传递 `id` 参数 -* 导入 `fetchCustomers` 以获取下拉菜单的客户名称 +然后: -使用 `Promise.all` 并行获取发票和客户数据: +* 导入一个名为 `fetchInvoiceById` 的新函数,并将 `id` 作为参数传递 +* 导入 `fetchCustomers` 以获取下拉菜单的客户名称 + +您可以使用 `Promise.all` 并行获取发票和客户数据: ```tsx {3,8,9,10,11} filename="/dashboard/invoices/[id]/edit/page.tsx" import Form from '@/app/ui/invoices/edit-form'; @@ -523,36 +527,36 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { } ``` -终端中可能会看到关于 `invoice` 属性的临时 TypeScript 错误(因为 `invoice` 可能为 `undefined`)。暂时无需担心,后续添加错误处理时将解决此问题。 +您会在终端中看到关于 `invoice` 属性的临时 TypeScript 错误,因为 `invoice` 可能为 `undefined`。暂时不用担心,您将在下一章添加错误处理时解决这个问题。 -测试功能是否正常:访问 [http://localhost:3000/dashboard/invoices](http://localhost:3000/dashboard/invoices) 并点击铅笔图标编辑发票。导航后应看到预填充发票详情的表单: +很好!现在测试一切是否连接正确。访问 [http://localhost:3000/dashboard/invoices](http://localhost:3000/dashboard/invoices) 并点击铅笔图标编辑发票。导航后,您应该看到一个预填充了发票详情的表单: -URL 也应更新为包含 `id` 的格式:`http://localhost:3000/dashboard/invoice/uuid/edit` +URL 也应更新为包含 `id`,如下所示:`http://localhost:3000/dashboard/invoice/uuid/edit` -> **UUID 对比自增键** +> **UUID 与自增键** > -> 我们使用 UUID 而非自增键(如1、2、3等)。虽然会使 URL 变长,但 UUID 能消除 ID 冲突风险,具有全局唯一性,并减少枚举攻击风险——非常适合大型数据库。 +> 我们使用 UUID 而不是自增键(例如 1、2、3 等)。这使得 URL 更长,但 UUID 消除了 ID 冲突的风险,具有全局唯一性,并减少了枚举攻击的风险——使其成为大型数据库的理想选择。 > -> 但如果您偏好简洁的 URL,可以选择使用自增键。 +> 但是,如果您更喜欢简洁的 URL,您可能会倾向于使用自增键。 -### [4. 将 `id` 传递给服务端操作](#4-pass-the-id-to-the-server-action) +### [4. 将 `id` 传递给服务器操作](#4-pass-the-id-to-the-server-action) -最后,将 `id` 传递给服务端操作以更新数据库中的正确记录。不能直接传递 `id` 作为参数: +最后,您需要将 `id` 传递给服务器操作,以便更新数据库中的正确记录。您**不能**像这样将 `id` 作为参数传递: ```tsx filename="/app/ui/invoices/edit-form.tsx" -// 直接传递id作为参数无效 +// 将 id 作为参数传递是无效的 ``` -但可以使用 JS 的 `bind` 方法传递 `id`,确保传递给服务端操作的值被正确编码。 +相反,您可以使用 JavaScript 的 `bind` 将 `id` 传递给服务器操作。这将确保传递给服务器操作的所有值都被编码。 ```tsx {2,11,13} filename="/app/ui/invoices/edit-form.tsx" // ... @@ -571,12 +575,12 @@ export default function EditInvoiceForm({ } ``` -> **注意:** 在表单中使用隐藏输入字段也可实现(如 ``)。但值会以明文形式出现在HTML源码中,不适合敏感数据。 +> **注意:** 在表单中使用隐藏的输入字段也可以(例如 ``)。但是,这些值会以纯文本形式出现在 HTML 源代码中,对于敏感数据来说并不理想。 -然后在 `actions.ts` 文件中创建新的 `updateInvoice` 操作: +然后,在您的 `actions.ts` 文件中,创建一个新的操作 `updateInvoice`: ```ts filename="/app/lib/actions.ts" -// 使用Zod更新预期类型 +// 使用 Zod 更新预期的类型 const UpdateInvoice = FormSchema.omit({ id: true, date: true }); // ... @@ -601,20 +605,21 @@ export async function updateInvoice(id: string, formData: FormData) { } ``` -与 `createInvoice` 操作类似,此处: -1. 从 `formData` 提取数据 -2. 使用 Zod 验证类型 -3. 将金额转换为分 -4. 将变量传递给 SQL 查询 -5. 调用 `revalidatePath` 清除客户端缓存并发起新服务器请求 -6. 调用 `redirect` 重定向用户至发票页面 +与 `createInvoice` 操作类似,这里您: + +1. 从 `formData` 中提取数据 +2. 使用 Zod 验证类型 +3. 将金额转换为分 +4. 将变量传递给 SQL 查询 +5. 调用 `revalidatePath` 清除客户端缓存并发出新的服务器请求 +6. 调用 `redirect` 将用户重定向到发票页面 -通过编辑发票测试功能。提交表单后应重定向至发票页面,且发票数据应已更新。 +通过编辑发票来测试它。提交表单后,您应该被重定向到发票页面,并且发票应该已更新。 [删除发票](#deleting-an-invoice) ------------------------------------------- -要使用服务端操作删除发票,将删除按钮包裹在 `` 元素中,并通过 `bind` 将 `id` 传递给服务端操作: +要使用服务器操作删除发票,请将删除按钮包裹在 `` 元素中,并使用 `bind` 将 `id` 传递给服务器操作: ```tsx {1,6,9} filename="/app/ui/invoices/buttons.tsx" import { deleteInvoice } from '@/app/lib/actions'; @@ -635,7 +640,7 @@ export function DeleteInvoice({ id }: { id: string }) { } ``` -在 `actions.ts` 文件中创建名为 `deleteInvoice` 的新操作: +在您的 `actions.ts` 文件中,创建一个名为 `deleteInvoice` 的新操作。 ```ts filename="/app/lib/actions.ts" export async function deleteInvoice(id: string) { @@ -644,11 +649,11 @@ export async function deleteInvoice(id: string) { } ``` -由于该操作在 `/dashboard/invoices` 路径下调用,无需调用 `redirect`。调用 `revalidatePath` 将触发新服务器请求并重新渲染表格。 +由于此操作是在 `/dashboard/invoices` 路径上调用的,您不需要调用 `redirect`。调用 `revalidatePath` 将触发新的服务器请求并重新渲染表格。 [延伸阅读](#further-reading) ----------------------------------- -本章学习了如何使用服务端操作变更数据,以及如何使用 `revalidatePath` API 重新验证 Next.js 缓存,并通过 `redirect` 重定向用户至新页面。 +在本章中,您学习了如何使用服务器操作来变更数据。您还学习了如何使用 `revalidatePath` API 重新验证 Next.js 缓存,以及使用 `redirect` 将用户重定向到新页面。 -您还可以阅读[服务端操作的安全性](https://nextjs.org/blog/security-nextjs-server-components-actions)了解更多内容。 +您还可以阅读更多关于[服务器操作的安全性](https://nextjs.org/blog/security-nextjs-server-components-actions)以进行进一步学习。