diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/01-installation.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/01-installation.mdx index affe04e2..06069df3 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/01-installation.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/01-installation.mdx @@ -1,29 +1,29 @@ --- -source-updated-at: 2025-05-21T18:33:43.000Z -translation-updated-at: 2025-05-21T19:18:02.138Z +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:11:51.399Z title: 如何新建 Next.js 项目 nav_title: 安装指南 description: 使用 `create-next-app` CLI 创建新的 Next.js 应用,并配置 TypeScript、ESLint 和模块路径别名。 --- -{/* 本文档内容在应用路由和页面路由间共享。如需添加仅适用于页面路由的内容,可使用 `内容` 组件。共享内容不应包裹在任何组件中。*/} +{/* 本文档内容在应用路由和页面路由间共享。如需添加仅适用于页面路由的内容,可使用 `内容` 组件。共享内容不应包裹在任何组件中。 */} ## 系统要求 -开始前请确保系统满足以下要求: +开始前请确保您的系统满足以下要求: - [Node.js 18.18](https://nodejs.org/) 或更高版本 - macOS、Windows(含 WSL)或 Linux 系统 ## 自动安装 -创建 Next.js 应用最快的方式是使用 [`create-next-app`](/docs/app/api-reference/cli/create-next-app),它会自动完成所有配置。运行以下命令创建项目: +创建 Next.js 应用最快的方式是使用 [`create-next-app`](/docs/app/api-reference/cli/create-next-app),它会自动为您完成所有设置。运行以下命令创建项目: ```bash filename="终端" npx create-next-app@latest ``` -安装过程中会看到以下提示: +安装过程中会显示以下提示: ```txt filename="终端" 请输入项目名称?my-app @@ -31,23 +31,23 @@ npx create-next-app@latest 是否使用 ESLint?否 / 是 是否使用 Tailwind CSS?否 / 是 是否将代码放在 `src/` 目录下?否 / 是 -是否使用应用路由(推荐)?否 / 是 +是否使用 App Router?(推荐)否 / 是 是否在 `next dev` 中使用 Turbopack?否 / 是 是否自定义导入别名(默认为 `@/*`)?否 / 是 请输入配置的导入别名?@/* ``` -完成提示后,[`create-next-app`](/docs/app/api-reference/cli/create-next-app) 会创建项目文件夹并安装所需依赖。 +完成提示后,[`create-next-app`](/docs/app/api-reference/cli/create-next-app) 将创建以项目名命名的文件夹并安装所需依赖。 ## 手动安装 -要手动创建 Next.js 应用,请先安装必要包: +要手动创建 Next.js 应用,请先安装必要依赖包: ```bash filename="终端" npm install next@latest react@latest react-dom@latest ``` -然后在 `package.json` 中添加以下脚本: +然后在 `package.json` 文件中添加以下脚本: ```json filename="package.json" { @@ -71,9 +71,9 @@ npm install next@latest react@latest react-dom@latest ### 创建 `app` 目录 -Next.js 使用文件系统路由,意味着应用路由由文件结构决定。 +Next.js 使用文件系统路由,意味着应用中的路由由文件结构决定。 -创建 `app` 文件夹,然后在其中创建 `layout.tsx` 文件作为[根布局](/docs/app/api-reference/file-conventions/layout#root-layouts)。该文件必须包含 `` 和 `` 标签。 +创建 `app` 文件夹后,在其中添加 `layout.tsx` 文件作为[根布局](/docs/app/api-reference/file-conventions/layout#root-layout)。该文件必须包含 `` 和 `` 标签。 ```tsx filename="app/layout.tsx" switcher export default function RootLayout({ @@ -113,7 +113,7 @@ export default function Page() { } ``` -当用户访问应用根路径 (`/`) 时,`layout.tsx` 和 `page.tsx` 将同时渲染。 +当用户访问应用根路径(`/`)时,`layout.tsx` 和 `page.tsx` 将同时被渲染。 应用目录结构 **须知**: > -> - 如果忘记创建根布局,运行 `next dev` 启动开发服务器时 Next.js 会自动生成 -> - 可选择使用项目根目录下的 [`src` 文件夹](/docs/app/api-reference/file-conventions/src-folder) 来分离应用代码和配置文件 +> - 如果忘记创建根布局,运行 `next dev` 启动开发服务器时 Next.js 会自动生成此文件 +> - 可选择在项目根目录使用 [`src` 文件夹](/docs/app/api-reference/file-conventions/src-folder) 来分离应用代码与配置文件 @@ -134,9 +134,9 @@ export default function Page() { ### 创建 `pages` 目录 -Next.js 使用文件系统路由,意味着应用路由由文件结构决定。 +Next.js 使用文件系统路由,意味着应用中的路由由文件结构决定。 -在项目根目录创建 `pages` 文件夹,然后添加 `index.tsx` 作为首页 (`/`): +在项目根目录创建 `pages` 文件夹,然后添加 `index.tsx` 文件作为首页(`/`): ```tsx filename="pages/index.tsx" switcher export default function Page() { @@ -150,7 +150,7 @@ export default function Page() { } ``` -接着在 `pages/` 下添加 `_app.tsx` 定义全局布局。了解更多关于[自定义 App 文件](/docs/pages/building-your-application/routing/custom-app)。 +接着在 `pages/` 下添加 `_app.tsx` 文件定义全局布局。了解更多关于[自定义 App 文件](/docs/pages/building-your-application/routing/custom-app)的信息。 ```tsx filename="pages/_app.tsx" switcher import type { AppProps } from 'next/app' @@ -166,7 +166,7 @@ export default function App({ Component, pageProps }) { } ``` -最后在 `pages/` 下添加 `_document.tsx` 控制服务器初始响应。了解更多关于[自定义 Document 文件](/docs/pages/building-your-application/routing/custom-document)。 +最后在 `pages/` 下添加 `_document.tsx` 文件控制服务器初始响应。了解更多关于[自定义 Document 文件](/docs/pages/building-your-application/routing/custom-document)的信息。 ```tsx filename="pages/_document.tsx" switcher import { Html, Head, Main, NextScript } from 'next/document' @@ -204,7 +204,7 @@ export default function Document() { ### 创建 `public` 文件夹(可选) -在项目根目录创建 [`public` 文件夹](/docs/app/api-reference/file-conventions/public-folder) 存放静态资源(如图片、字体等)。`public` 中的文件可通过根路径 (`/`) 引用。 +在项目根目录创建 [`public` 文件夹](/docs/app/api-reference/file-conventions/public-folder) 存放静态资源如图片、字体等。`public` 中的文件可通过根路径(`/`)引用。 例如 `public/profile.png` 可引用为 `/profile.png`: @@ -228,23 +228,23 @@ export default function Page() { 1. 执行 `npm run dev` 启动开发服务器 2. 访问 `http://localhost:3000` 查看应用 -3. 编辑 `app/page.tsx``pages/index.tsx` 文件并保存,浏览器中即可看到更新结果 +3. 编辑 `app/page.tsx``pages/index.tsx` 文件并保存,浏览器中将实时更新 ## 配置 TypeScript -> TypeScript 最低版本要求:`v4.5.2` +> 最低 TypeScript 版本要求:`v4.5.2` -Next.js 内置 TypeScript 支持。要将 TypeScript 添加到项目,只需将文件重命名为 `.ts`/`.tsx` 并运行 `next dev`。Next.js 会自动安装必要依赖并创建包含推荐配置的 `tsconfig.json` 文件。 +Next.js 内置 TypeScript 支持。要将 TypeScript 添加到项目,只需将文件重命名为 `.ts` / `.tsx` 并运行 `next dev`。Next.js 会自动安装必要依赖并生成包含推荐配置的 `tsconfig.json` 文件。 ### IDE 插件 -Next.js 包含自定义 TypeScript 插件和类型检查器,可供 VSCode 等编辑器实现高级类型检查和自动补全。 +Next.js 包含自定义 TypeScript 插件和类型检查器,可供 VSCode 等代码编辑器实现高级类型检查和自动补全。 在 VS Code 中启用插件: -1. 打开命令面板 (`Ctrl/⌘` + `Shift` + `P`) +1. 打开命令面板(`Ctrl/⌘` + `Shift` + `P`) 2. 搜索 "TypeScript: 选择 TypeScript 版本" 3. 选择 "使用工作区版本" @@ -258,13 +258,13 @@ Next.js 包含自定义 TypeScript 插件和类型检查器,可供 VSCode 等 -更多信息请参考 [TypeScript 配置文档](/docs/app/api-reference/config/next-config-js/typescript)。 +更多信息请参考 [TypeScript 参考文档](/docs/app/api-reference/config/next-config-js/typescript)。 ## 配置 ESLint -Next.js 内置 ESLint 支持。使用 `create-next-app` 创建项目时会自动安装必要包并配置正确设置。 +Next.js 内置 ESLint 支持。使用 `create-next-app` 创建新项目时会自动安装必要包并配置正确设置。 -要为现有项目手动添加 ESLint,请在 `package.json` 中添加 `next lint` 脚本: +要为现有项目手动添加 ESLint,在 `package.json` 中添加 `next lint` 脚本: ```json filename="package.json" { @@ -274,7 +274,7 @@ Next.js 内置 ESLint 支持。使用 `create-next-app` 创建项目时会自动 } ``` -然后运行 `npm run lint`,安装向导将引导完成配置过程: +然后运行 `npm run lint`,系统将引导您完成安装和配置流程: ```bash filename="终端" npm run lint @@ -282,27 +282,27 @@ npm run lint 您将看到如下提示: -> ? 希望如何配置 ESLint? +> ? 您希望如何配置 ESLint? > > ❯ 严格模式(推荐) > 基础模式 > 取消 -- **严格模式**:包含 Next.js 基础 ESLint 配置及更严格的 Core Web Vitals 规则集。首次配置 ESLint 时推荐使用 +- **严格模式**:包含 Next.js 基础 ESLint 配置及更严格的 Core Web Vitals 规则集。首次设置 ESLint 时推荐此配置 - **基础模式**:仅包含 Next.js 基础 ESLint 配置 -- **取消**:跳过配置。如需自定义 ESLint 配置请选择此项 +- **取消**:跳过配置。如需自定义 ESLint 配置可选择此项 -选择"严格"或"基础"后,Next.js 会自动安装 `eslint` 和 `eslint-config-next` 依赖,并在项目根目录创建包含所选配置的 `.eslintrc.json` 文件。 +选择"严格"或"基础"模式后,Next.js 会自动安装 `eslint` 和 `eslint-config-next` 依赖,并在项目根目录创建包含所选配置的 `.eslintrc.json` 文件。 -此后可通过运行 `next lint` 执行 ESLint 检查。ESLint 配置完成后,每次构建 (`next build`) 时也会自动运行。错误会导致构建失败,而警告不会。 +此后可随时运行 `next lint` 进行错误检查。ESLint 设置完成后,每次构建(`next build`)时也会自动运行。错误会导致构建失败,而警告则不会。 更多信息请参考 [ESLint 插件文档](/docs/app/api-reference/config/next-config-js/eslint)。 ## 配置绝对导入和模块路径别名 -Next.js 原生支持 `tsconfig.json` 和 `jsconfig.json` 中的 `"paths"` 和 `"baseUrl"` 选项。 +Next.js 原生支持 `tsconfig.json` 和 `jsconfig.json` 文件中的 `"paths"` 和 `"baseUrl"` 选项。 -这些选项允许将项目目录映射为绝对路径,使模块导入更清晰简洁。例如: +这些选项允许您将项目目录映射为绝对路径,使模块导入更简洁清晰。例如: ```jsx // 之前 @@ -312,7 +312,7 @@ import { Button } from '../../../components/button' import { Button } from '@/components/button' ``` -要配置绝对导入,请在 `tsconfig.json` 或 `jsconfig.json` 中添加 `baseUrl` 配置: +要配置绝对导入,在 `tsconfig.json` 或 `jsconfig.json` 中添加 `baseUrl` 配置选项。例如: ```json filename="tsconfig.json 或 jsconfig.json" { @@ -322,7 +322,7 @@ import { Button } from '@/components/button' } ``` -除配置 `baseUrl` 外,还可使用 `"paths"` 选项设置模块路径"别名"。 +除配置 `baseUrl` 路径外,还可使用 `"paths"` 选项设置模块路径"别名"。 例如以下配置将 `@/components/*` 映射到 `components/*`: @@ -338,4 +338,4 @@ import { Button } from '@/components/button' } ``` -每个 `"paths"` 都相对于 `baseUrl` 指定的位置。 \ No newline at end of file +每个 `"paths"` 都相对于 `baseUrl` 位置进行解析。 \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/02-project-structure.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/02-project-structure.mdx index 3139b620..29392379 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/02-project-structure.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/02-project-structure.mdx @@ -1,12 +1,12 @@ --- -source-updated-at: 2025-05-16T04:52:11.000Z -translation-updated-at: 2025-05-17T02:38:02.762Z -title: 项目结构与组织方式 +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:13:22.972Z +title: 项目结构与组织 nav_title: 项目结构 -description: 概述 Next.js 中的文件夹和文件约定,以及如何组织项目。 +description: 概述 Next.js 中的文件夹与文件约定,以及如何组织项目结构。 --- -本页概述了 Next.js 中**所有**文件夹和文件的约定,并提供了项目组织建议。 +本页全面介绍了 Next.js 中**所有**文件夹与文件约定,并提供项目组织建议。 ## 文件夹与文件约定 @@ -15,7 +15,7 @@ description: 概述 Next.js 中的文件夹和文件约定,以及如何组织 顶级文件夹用于组织应用程序代码和静态资源。 路由段到路径段的映射 @@ -136,54 +136,54 @@ description: 概述 Next.js 中的文件夹和文件约定,以及如何组织 | | | | | ----------------------------------------------------------------------------------------------------------- | ------------------- | ----------------- | -| [`_app`](/docs/pages/building-your-application/routing/custom-app) | `.js` `.jsx` `.tsx` | 自定义应用 (Custom App) | -| [`_document`](/docs/pages/building-your-application/routing/custom-document) | `.js` `.jsx` `.tsx` | 自定义文档 (Custom Document) | -| [`_error`](/docs/pages/building-your-application/routing/custom-error#more-advanced-error-page-customizing) | `.js` `.jsx` `.tsx` | 自定义错误页面 (Custom Error Page) | -| [`404`](/docs/pages/building-your-application/routing/custom-error#404-page) | `.js` `.jsx` `.tsx` | 404 错误页面 (404 Error Page) | -| [`500`](/docs/pages/building-your-application/routing/custom-error#500-page) | `.js` `.jsx` `.tsx` | 500 错误页面 (500 Error Page) | +| [`_app`](/docs/pages/building-your-application/routing/custom-app) | `.js` `.jsx` `.tsx` | 自定义 App 组件 | +| [`_document`](/docs/pages/building-your-application/routing/custom-document) | `.js` `.jsx` `.tsx` | 自定义 Document | +| [`_error`](/docs/pages/building-your-application/routing/custom-error#more-advanced-error-page-customizing) | `.js` `.jsx` `.tsx` | 自定义错误页面 | +| [`404`](/docs/pages/building-your-application/routing/custom-error#404-page) | `.js` `.jsx` `.tsx` | 404 错误页面 | +| [`500`](/docs/pages/building-your-application/routing/custom-error#500-page) | `.js` `.jsx` `.tsx` | 500 错误页面 | ### 路由 | | | | | ---------------------------------------------------------------------------------------------- | ------------------- | ----------- | -| **文件夹约定** | | | -| [`index`](/docs/pages/building-your-application/routing/pages-and-layouts#index-routes) | `.js` `.jsx` `.tsx` | 首页 (Home page) | -| [`folder/index`](/docs/pages/building-your-application/routing/pages-and-layouts#index-routes) | `.js` `.jsx` `.tsx` | 嵌套页面 (Nested page) | -| **文件约定** | | | -| [`index`](/docs/pages/building-your-application/routing/pages-and-layouts#index-routes) | `.js` `.jsx` `.tsx` | 首页 (Home page) | -| [`file`](/docs/pages/building-your-application/routing/pages-and-layouts) | `.js` `.jsx` `.tsx` | 嵌套页面 (Nested page) | +| **文件夹约定** | | | +| [`index`](/docs/pages/building-your-application/routing/pages-and-layouts#index-routes) | `.js` `.jsx` `.tsx` | 首页 | +| [`folder/index`](/docs/pages/building-your-application/routing/pages-and-layouts#index-routes) | `.js` `.jsx` `.tsx` | 嵌套页面 | +| **文件约定** | | | +| [`index`](/docs/pages/building-your-application/routing/pages-and-layouts#index-routes) | `.js` `.jsx` `.tsx` | 首页 | +| [`file`](/docs/pages/building-your-application/routing/pages-and-layouts) | `.js` `.jsx` `.tsx` | 嵌套页面 | ### 动态路由 | | | | | ----------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------- | -| **文件夹约定** | | | -| [`[folder]/index`](/docs/pages/building-your-application/routing/dynamic-routes) | `.js` `.jsx` `.tsx` | 动态路由段 (Dynamic route segment) | -| [`[...folder]/index`](/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments) | `.js` `.jsx` `.tsx` | 全捕获路由段 (Catch-all route segment) | -| [`[[...folder]]/index`](/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments) | `.js` `.jsx` `.tsx` | 可选全捕获路由段 (Optional catch-all route segment) | -| **文件约定** | | | -| [`[file]`](/docs/pages/building-your-application/routing/dynamic-routes) | `.js` `.jsx` `.tsx` | 动态路由段 (Dynamic route segment) | -| [`[...file]`](/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments) | `.js` `.jsx` `.tsx` | 全捕获路由段 (Catch-all route segment) | -| [`[[...file]]`](/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments) | `.js` `.jsx` `.tsx` | 可选全捕获路由段 (Optional catch-all route segment) | +| **文件夹约定** | | | +| [`[folder]/index`](/docs/pages/building-your-application/routing/dynamic-routes) | `.js` `.jsx` `.tsx` | 动态路由段 | +| [`[...folder]/index`](/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments) | `.js` `.jsx` `.tsx` | 全捕获路由段 | +| [`[[...folder]]/index`](/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments) | `.js` `.jsx` `.tsx` | 可选全捕获路由段 | +| **文件约定** | | | +| [`[file]`](/docs/pages/building-your-application/routing/dynamic-routes) | `.js` `.jsx` `.tsx` | 动态路由段 | +| [`[...file]`](/docs/pages/building-your-application/routing/dynamic-routes#catch-all-segments) | `.js` `.jsx` `.tsx` | 全捕获路由段 | +| [`[[...file]]`](/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-segments) | `.js` `.jsx` `.tsx` | 可选全捕获路由段 | -## 组织项目 +## 组织项目结构 -Next.js **不强制**项目的组织和文件共置方式,但提供了多种功能来帮助您组织项目。 +Next.js 对项目文件组织方式**不做强制要求**,但提供了多种功能来帮助您管理项目。 -### 组件层级结构 +### 组件层级 -特殊文件中定义的组件会按照特定层级结构渲染: +特殊文件中定义的组件会按照特定层级渲染: -- `layout.js` -- `template.js` -- `error.js` (React 错误边界) -- `loading.js` (React 悬念边界) -- `not-found.js` (React 错误边界) -- `page.js` 或嵌套的 `layout.js` +1. `layout.js` +2. `template.js` +3. `error.js` (React 错误边界) +4. `loading.js` (React Suspense 边界) +5. `not-found.js` (React 错误边界) +6. `page.js` 或嵌套的 `layout.js` 文件约定的组件层级结构 -在嵌套路由中,组件会递归渲染,这意味着路由段的组件会嵌套在其父段组件的**内部**。 +在嵌套路由中,组件会递归渲染,这意味着子路由段的组件会嵌套在其父路由段组件**内部**。 嵌套文件约定的组件层级结构 -### 共置 + + +### 文件同置 (Colocation) -在 `app` 目录中,嵌套文件夹定义了路由结构。每个文件夹代表一个路由段,映射到 URL 路径中的相应段。 +在 `app` 目录中,嵌套文件夹定义了路由结构。每个文件夹代表一个路由片段,对应 URL 路径中的相应片段。 -然而,尽管路由结构是通过文件夹定义的,但只有在路由段中添加 `page.js` 或 `route.js` 文件后,该路由才会**公开可访问**。 +但需要注意的是,尽管路由结构通过文件夹定义,**只有当路由片段中添加了 `page.js` 或 `route.js` 文件时**,该路由才会**对外可访问**。 示意图展示路由在添加 page.js 或 route.js 文件前不会公开可访问 -即使路由公开可访问,也只有 `page.js` 或 `route.js` 返回的**内容**会发送到客户端。 +即使路由对外可访问后,也仅有 `page.js` 或 `route.js` 返回的**内容**会被发送到客户端。 示意图展示 page.js 和 route.js 文件使路由公开可访问 -这意味着**项目文件**可以**安全地共置**在 `app` 目录的路由段中,而不会意外变为可路由。 +这意味着**项目文件**可以**安全地同置**在 `app` 目录的路由片段中,而不会意外成为可访问路由。 示意图展示共置的项目文件即使路由段包含 page.js 或 route.js 文件也不会变为可路由 -> **须知**:虽然您**可以**将项目文件共置在 `app` 中,但您**不必**这样做。如果愿意,您可以[将它们保留在 `app` 目录之外](#store-project-files-outside-of-app)。 +> **须知**:虽然你**可以**将项目文件同置在 `app` 目录中,但并非**必须**这样做。如果愿意,你也可以[将它们保留在 `app` 目录外](#store-project-files-outside-of-app)。 -### 私有文件夹 +### 私有文件夹 (Private folders) -可以通过在文件夹名前添加下划线来创建私有文件夹:`_folderName` +通过给文件夹添加下划线前缀可以创建私有文件夹:`_folderName` -这表示该文件夹是私有实现细节,不应被路由系统考虑,从而**将该文件夹及其所有子文件夹**排除在路由之外。 +这表示该文件夹是私有实现细节,路由系统不应考虑它,从而**将该文件夹及其所有子文件夹**排除在路由之外。 使用私有文件夹的示例文件夹结构 -由于 `app` 目录中的文件默认可以[安全共置](#colocation),私有文件夹并非共置所必需。但它们可用于: +由于 `app` 目录中的文件默认可以[安全同置](#colocation),私有文件夹并非同置所必需。但它们可用于: - 将 UI 逻辑与路由逻辑分离。 -- 在项目和 Next.js 生态系统中一致地组织内部文件。 +- 在项目和 Next.js 生态系统中一致组织内部文件。 - 在代码编辑器中排序和分组文件。 - 避免与未来 Next.js 文件约定的潜在命名冲突。 > **须知**: > -> - 虽然不是框架约定,但您也可以考虑使用相同的下划线模式标记私有文件夹之外的文件为“私有”。 -> - 可以通过在文件夹名前添加 `%5F`(下划线的 URL 编码形式)来创建以下划线开头的 URL 段:`%5FfolderName`。 -> - 如果不使用私有文件夹,了解 Next.js [特殊文件约定](/docs/app/getting-started/project-structure#routing-files)有助于避免意外的命名冲突。 +> - 虽然不是框架约定,你也可以考虑使用相同的下划线模式标记私有文件夹外的文件为“私有”。 +> - 可以通过给文件夹名添加 `%5F`(下划线的 URL 编码形式)来创建以下划线开头的 URL 片段:`%5FfolderName`。 +> - 如果不使用私有文件夹,了解 Next.js 的[特殊文件约定](/docs/app/getting-started/project-structure#routing-files)有助于避免意外的命名冲突。 -### 路由组 +### 路由组 (Route groups) -可以通过将文件夹用括号包裹来创建路由组:`(folderName)` +通过将文件夹包裹在括号中可以创建路由组:`(folderName)` -这表示该文件夹用于组织目的,**不应包含**在路由的 URL 路径中。 +这表示该文件夹仅用于组织目的,**不应包含**在路由的 URL 路径中。 使用路由组的示例文件夹结构 -路由组可用于: +路由组适用于: -- 按站点部分、意图或团队组织路由。例如营销页面、管理页面等。 -- 在同一路由段级别启用嵌套布局: - - [在同一段中创建多个嵌套布局,包括多个根布局](#creating-multiple-root-layouts) - - [将布局添加到公共段中的路由子集](#opting-specific-segments-into-a-layout) +- 按网站部分、意图或团队组织路由。例如营销页面、管理页面等。 +- 在同一路由片段级别启用嵌套布局: + - [在同一片段中创建多个嵌套布局,包括多个根布局](#creating-multiple-root-layouts) + - [将布局添加到公共片段中的路由子集](#opting-specific-segments-into-a-layout) ### `src` 文件夹 -Next.js 支持将应用程序代码(包括 `app`)存储在可选的 [`src` 文件夹](/docs/app/api-reference/file-conventions/src-folder)中。这将应用程序代码与主要位于项目根目录的项目配置文件分开。 +Next.js 支持将应用代码(包括 `app`)存储在可选的 [`src` 文件夹](/docs/app/api-reference/file-conventions/src-folder)中。这可以将应用代码与主要位于项目根目录的配置文件分开。 带有 `src` 文件夹的示例文件夹结构 **须知**:在下面的示例中,我们使用 `components` 和 `lib` 文件夹作为通用占位符,它们的命名没有特殊的框架意义,您的项目可能会使用其他文件夹,如 `ui`、`utils`、`hooks`、`styles` 等。 +> **须知**:在下面的示例中,我们使用 `components` 和 `lib` 文件夹作为通用占位符,它们的命名没有特殊的框架意义,你的项目可能会使用其他文件夹如 `ui`、`utils`、`hooks`、`styles` 等。 -### 将项目文件存储在 `app` 之外 +### 将项目文件存储在 `app` 外 -此策略将所有应用程序代码存储在**项目根目录**的共享文件夹中,并保持 `app` 目录纯粹用于路由目的。 +此策略将所有应用代码存储在**项目根目录**的共享文件夹中,并保持 `app` 目录纯粹用于路由目的。 项目文件存储在 app 之外的示例文件夹结构 -### 将项目文件存储在 `app` 内的顶级文件夹中 +### 将项目文件存储在 `app` 内的顶层文件夹中 -此策略将所有应用程序代码存储在 `app` 目录**根目录**的共享文件夹中。 +此策略将所有应用代码存储在 `app` 目录**根目录**的共享文件夹中。 项目文件存储在 app 内的示例文件夹结构 -尽管 `(marketing)` 和 `(shop)` 中的路由共享相同的 URL 层次结构,但您可以通过在其文件夹中添加 `layout.js` 文件为每个组创建不同的布局。 +尽管 `(marketing)` 和 `(shop)` 中的路由共享相同的 URL 层次结构,但你可以通过在其文件夹中添加 `layout.js` 文件为每个组创建不同的布局。 具有多个布局的路由组 -### 将特定段加入布局 +### 将特定片段加入布局 要将特定路由加入布局,可以创建一个新的路由组(例如 `(shop)`),并将共享相同布局的路由移动到该组中(例如 `account` 和 `cart`)。组外的路由不会共享该布局(例如 `checkout`)。 @@ -377,7 +379,7 @@ Next.js 支持将应用程序代码(包括 `app`)存储在可选的 [`src` ### 为特定路由选择加载骨架 -要通过 `loading.js` 文件将[加载骨架](/docs/app/building-your-application/routing/loading-ui-and-streaming)应用于特定路由,可以创建一个新的路由组(例如 `/(overview)`),然后将 `loading.tsx` 移动到该路由组中。 +要通过 `loading.js` 文件将[加载骨架](/docs/app/building-your-application/routing/loading-ui-and-streaming)应用到特定路由,可以创建一个新的路由组(例如 `/(overview)`),然后将 `loading.tsx` 移动到该路由组中。 文件夹结构展示路由组中的 loading.tsx 和 page.tsx -现在,`loading.tsx` 文件将仅应用于您的仪表盘 → 概览页面,而不是所有仪表盘页面,同时不影响 URL 路径结构。 +现在,`loading.tsx` 文件将仅应用于你的 dashboard → overview 页面,而不是所有 dashboard 页面,且不影响 URL 路径结构。 ### 创建多个根布局 -要创建多个[根布局](/docs/app/api-reference/file-conventions/layout#root-layouts),可以删除顶级 `layout.js` 文件,并在每个路由组中添加一个 `layout.js` 文件。这对于将应用程序划分为具有完全不同 UI 或体验的部分非常有用。每个根布局中都需要添加 `` 和 `` 标签。 +要创建多个[根布局](/docs/app/api-reference/file-conventions/layout#root-layout),可以移除顶层的 `layout.js` 文件,并在每个路由组中添加一个 `layout.js` 文件。这对于将应用划分为具有完全不同 UI 或体验的部分非常有用。每个根布局都需要添加 `` 和 `` 标签。 具有多个根布局的路由组 {/* 布局 UI */} - {/* 在此处渲染页面或嵌套布局 */} + {/* 将 children 放置在你希望渲染页面或嵌套布局的位置 */}
{children}
@@ -79,7 +80,7 @@ export default function DashboardLayout({ children }) { {/* 布局 UI */} - {/* 在此处渲染页面或嵌套布局 */} + {/* 将 children 放置在你希望渲染页面或嵌套布局的位置 */}
{children}
@@ -87,25 +88,25 @@ export default function DashboardLayout({ children }) { } ``` -上述布局称为 [根布局 (root layout)](/docs/app/api-reference/file-conventions/layout#root-layouts),因为它定义在 `app` 目录的根目录下。根布局是 **必需的**,且必须包含 `html` 和 `body` 标签。 +上面的布局被称为[根布局](/docs/app/api-reference/file-conventions/layout#root-layout),因为它定义在 `app` 目录的根目录下。根布局是**必需的**,并且必须包含 `html` 和 `body` 标签。 ## 创建嵌套路由 -**嵌套路由 (nested route)** 是由多个 URL 段组成的路由。例如,`/blog/[slug]` 路由由以下三个段组成: +嵌套路由是由多个 URL 段组成的路由。例如,`/blog/[slug]` 路由由三个段组成: -- `/`(根段) -- `blog`(段) -- `[slug]`(叶段) +- `/` (根段) +- `blog` (段) +- `[slug]` (叶段) 在 Next.js 中: -- **文件夹** 用于定义映射到 URL 段的路由段。 -- **文件**(如 `page` 和 `layout`)用于为路由段创建用户界面。 +- **文件夹**用于定义映射到 URL 段的路由段。 +- **文件**(如 `page` 和 `layout`)用于创建为段显示的用户界面。 -要创建嵌套路由,您可以将文件夹相互嵌套。例如,要为 `/blog` 添加路由,请在 `app` 目录中创建一个名为 `blog` 的文件夹。然后,为了使 `/blog` 可公开访问,添加一个 `page.tsx` 文件: +要创建嵌套路由,你可以将文件夹相互嵌套。例如,要添加 `/blog` 路由,在 `app` 目录中创建一个名为 `blog` 的文件夹。然后,为了使 `/blog` 可公开访问,添加一个 `page.tsx` 文件: 文件层次结构展示 blog 文件夹和 page.js 文件 ```tsx filename="app/blog/page.tsx" switcher -// 示例导入 +// 虚拟导入 import { getPosts } from '@/lib/posts' import { Post } from '@/ui/post' @@ -131,7 +132,7 @@ export default async function Page() { ``` ```jsx filename="app/blog/[slug]/page.js" switcher -// 示例导入 +// 虚拟导入 import { getPosts } from '@/lib/posts' import { Post } from '@/ui/post' @@ -148,10 +149,10 @@ export default async function Page() { } ``` -您可以继续嵌套文件夹以创建更深层次的路由。例如,要为特定博客文章创建路由,请在 `blog` 文件夹中新建一个 `[slug]` 文件夹并添加 `page` 文件: +你可以继续嵌套文件夹以创建嵌套路由。例如,要为特定博客文章创建路由,在 `blog` 文件夹内创建一个新的 `[slug]` 文件夹并添加一个 `page` 文件: 文件层次结构展示 blog 文件夹内嵌套的 slug 文件夹和 page.js 文件` 组件](/docs/app/api-reference/components/link) 在路由之间导航。`` 是 Next.js 内置组件,它扩展了 HTML `` 标签的功能,提供 [预取 (prefetching)](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 和 [客户端导航 (client-side navigation)](/docs/app/building-your-application/routing/linking-and-navigating#5-soft-navigation)。 +[动态段](/docs/app/api-reference/file-conventions/dynamic-routes)允许你从数据生成路由。例如,与其手动为每篇博客文章创建路由,不如创建一个动态段来基于博客文章数据生成路由。 -例如,要生成博客文章列表,请从 `next/link` 导入 `` 并向组件传递 `href` 属性: +要创建动态段,将段(文件夹)名称用方括号包裹:`[segmentName]`。例如,在 `app/blog/[slug]/page.tsx` 路由中,`[slug]` 就是动态段。 + +```tsx filename="app/blog/[slug]/page.tsx" switcher +export default async function BlogPostPage({ + params, +}: { + params: Promise<{ slug: string }> +}) { + const { slug } = await params + const post = await getPost(slug) + + return ( +
+

{post.title}

+

{post.content}

+
+ ) +} +``` + +```jsx filename="app/blog/[slug]/page.js" switcher +export default async function BlogPostPage({ params }) { + const { slug } = await params + const post = await getPost(slug) + + return ( +
+

{post.title}

+

{post.content}

+
+ ) +} +``` + +了解更多关于[动态段](/docs/app/api-reference/file-conventions/dynamic-routes)的信息。 + +## 在页面之间链接 + +你可以使用 [`` 组件](/docs/app/api-reference/components/link) 在路由之间导航。`` 是 Next.js 内置的组件,它扩展了 HTML `
` 标签,提供了[预取](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching)和[客户端导航](/docs/app/building-your-application/routing/linking-and-navigating#5-soft-navigation)功能。 + +例如,要生成博客文章列表,从 `next/link` 导入 `` 并向组件传递 `href` 属性: ```tsx filename="app/ui/post.tsx" highlight={1,10} switcher import Link from 'next/link' @@ -250,4 +291,4 @@ export default async function Post({ post }) { } ``` -`` 是 Next.js 应用中导航路由的主要推荐方式。但对于更高级的导航需求,您也可以使用 [`useRouter` 钩子](/docs/app/api-reference/functions/use-router)。 +`` 是在 Next.js 应用程序中导航路由的主要和推荐方式。不过,你也可以使用 [`useRouter` 钩子](/docs/app/api-reference/functions/use-router) 进行更高级的导航。 \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/05-fonts.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/05-fonts.mdx index 23b6e7b7..06e66a06 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/05-fonts.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/05-fonts.mdx @@ -1,6 +1,6 @@ --- -source-updated-at: 2025-05-16T04:52:11.000Z -translation-updated-at: 2025-05-17T02:11:20.017Z +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:10:32.521Z title: 如何使用字体 nav_title: 字体 description: 学习如何在 Next.js 中使用字体 @@ -11,11 +11,11 @@ related: - app/api-reference/components/font --- -[`next/font`](/docs/app/api-reference/components/font) 模块会自动优化您的字体,并移除外部网络请求以提升隐私性和性能。 +[`next/font`](/docs/app/api-reference/components/font) 模块能自动优化您的字体,并移除外部网络请求以提升隐私性和性能。 它包含**内置的自托管功能**,适用于任何字体文件。这意味着您可以无布局偏移地最优加载网页字体。 -要开始使用 `next/font`,请从 [`next/font/local`](#local-fonts) 或 [`next/font/google`](#google-fonts) 导入,调用该函数并传入适当的选项,然后为您想要应用字体的元素设置 `className`。例如: +要开始使用 `next/font`,请从 [`next/font/local`](#本地字体) 或 [`next/font/google`](#google-字体) 导入,调用该函数并传入适当的选项,然后为您想要应用字体的元素设置 `className`。例如: ```tsx filename="app/layout.tsx" highlight={1,3-5,9} switcher import { Geist } from 'next/font/google' @@ -49,13 +49,13 @@ export default function Layout({ children }) { } ``` -字体作用域仅限于使用它们的组件。要将字体应用到整个应用程序,请将其添加到 [根布局 (Root Layout)](/docs/app/api-reference/file-conventions/layout#root-layouts)。 +字体作用域限定于它们所使用的组件。要将字体应用于整个应用程序,请将其添加到 [根布局 (Root Layout)](/docs/app/api-reference/file-conventions/layout#root-layout)。 ## Google 字体 您可以自动自托管任何 Google 字体。字体作为静态资源存储,并从与您的部署相同的域名提供服务,这意味着用户访问您的网站时,浏览器不会向 Google 发送任何请求。 -要开始使用 Google 字体,从 `next/font/google` 导入您选择的字体: +要开始使用 Google 字体,请从 `next/font/google` 导入您选择的字体: ```tsx filename="app/layout.tsx" switcher import { Geist } from 'next/font/google' @@ -93,7 +93,7 @@ export default function RootLayout({ children }) { } ``` -我们建议使用 [可变字体 (variable fonts)](https://fonts.google.com/variablefonts) 以获得最佳性能和灵活性。但如果无法使用可变字体,您需要指定一个权重: +我们推荐使用 [可变字体 (variable fonts)](https://fonts.google.com/variablefonts) 以获得最佳性能和灵活性。但如果无法使用可变字体,您需要指定一个权重: ```tsx filename="app/layout.tsx" highlight={4} switcher import { Roboto } from 'next/font/google' @@ -135,7 +135,7 @@ export default function RootLayout({ children }) { ## 本地字体 -要使用本地字体,从 `next/font/local` 导入您的字体,并指定本地字体文件的 [`src`](/docs/app/api-reference/components/font#src)。字体可以存储在 [`public`](/docs/app/api-reference/file-conventions/public-folder) 文件夹中。例如: +要使用本地字体,请从 `next/font/local` 导入您的字体,并指定本地字体文件的 [`src`](/docs/app/api-reference/components/font#src)。字体可以存储在 [`public`](/docs/app/api-reference/file-conventions/public-folder) 文件夹中。例如: ```tsx filename="app/layout.tsx" switcher import localFont from 'next/font/local' @@ -173,7 +173,7 @@ export default function RootLayout({ children }) { } ``` -如果希望为单个字体系列使用多个文件,`src` 可以是一个数组: +如果想为单个字体系列使用多个文件,`src` 可以是一个数组: ```js const roboto = localFont({ @@ -200,4 +200,4 @@ const roboto = localFont({ }, ], }) -``` +``` \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/08-fetching-data.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/08-fetching-data.mdx index a8ad28dd..8861028c 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/08-fetching-data.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/08-fetching-data.mdx @@ -1,12 +1,12 @@ --- -source-updated-at: 2025-05-22T15:18:56.000Z -translation-updated-at: 2025-05-23T16:46:50.155Z +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:12:44.080Z title: 如何获取数据并实现流式传输 nav_title: 数据获取 description: 开始在您的应用中获取数据并流式传输内容。 related: title: API 参考 - description: 通过阅读 API 参考文档了解更多本页提到的功能特性。 + description: 通过阅读 API 参考文档,了解更多本页提到的功能特性。 links: - app/api-reference/functions/fetch - app/api-reference/file-conventions/loading @@ -14,20 +14,19 @@ related: - app/api-reference/config/next-config-js/taint --- -本页将引导您了解如何在 [服务端与客户端组件 (Server and Client Components)](/docs/app/getting-started/server-and-client-components) 中获取数据,以及如何对依赖数据的组件进行 [流式传输 (streaming)](#streaming)。 +本页将引导您了解如何在[服务端与客户端组件](/docs/app/getting-started/server-and-client-components)中获取数据,以及如何流式传输依赖数据的组件。 ## 数据获取 ### 服务端组件 您可以通过以下方式在服务端组件中获取数据: - -1. 使用 [`fetch` API](#with-the-fetch-api) -2. 使用 [ORM 或数据库](#with-an-orm-or-database) +1. 使用 [`fetch` API](#使用-fetch-api) +2. 使用 [ORM 或数据库](#使用-orm-或数据库) #### 使用 `fetch` API -要通过 `fetch` API 获取数据,请将组件转换为异步函数,并等待 `fetch` 调用完成。例如: +要通过 `fetch` API 获取数据,将组件转换为异步函数并等待 `fetch` 调用。例如: ```tsx filename="app/blog/page.tsx" switcher export default async function Page() { @@ -59,12 +58,12 @@ export default async function Page() { > **须知:** > -> - `fetch` 响应默认不会被缓存。但 Next.js 会 [预渲染 (prerender)](/docs/app/getting-started/partial-prerendering#static-rendering) 路由,其输出会被缓存以提高性能。如需启用 [动态渲染 (dynamic rendering)](/docs/app/getting-started/partial-prerendering#dynamic-rendering),请使用 `{ cache: 'no-store' }` 选项。详见 [`fetch` API 参考](/docs/app/api-reference/functions/fetch)。 -> - 开发环境下,您可以记录 `fetch` 调用以便更好地调试和观察。参见 [`logging` API 参考](/docs/app/api-reference/config/next-config-js/logging)。 +> - `fetch` 响应默认不会被缓存。但 Next.js 会[预渲染](/docs/app/getting-started/partial-prerendering#static-rendering)路由,输出会被缓存以提高性能。如需启用[动态渲染](/docs/app/getting-started/partial-prerendering#dynamic-rendering),请使用 `{ cache: 'no-store' }` 选项。详见 [`fetch` API 参考](/docs/app/api-reference/functions/fetch)。 +> - 开发过程中,您可以记录 `fetch` 调用以便更好地调试和观察。参见 [`logging` API 参考](/docs/app/api-reference/config/next-config-js/logging)。 #### 使用 ORM 或数据库 -由于服务端组件在服务器端渲染,您可以安全地使用 ORM 或数据库客户端进行查询。将组件转换为异步函数并等待调用完成: +由于服务端组件在服务器上渲染,您可以安全地使用 ORM 或数据库客户端进行查询。将组件转换为异步函数并等待调用: ```tsx filename="app/blog/page.tsx" switcher import { db, posts } from '@/lib/db' @@ -98,14 +97,13 @@ export default async function Page() { ### 客户端组件 -在客户端组件中获取数据有两种方式: - +在客户端组件中有两种获取数据的方式: 1. 使用 React 的 [`use` hook](https://react.dev/reference/react/use) 2. 使用社区库如 [SWR](https://swr.vercel.app/) 或 [React Query](https://tanstack.com/query/latest) #### 使用 `use` hook 流式传输数据 -您可以使用 React 的 [`use` hook](https://react.dev/reference/react/use) 将数据从服务器 [流式传输 (streaming)](#streaming) 到客户端。首先在服务端组件中获取数据,然后将 Promise 作为 prop 传递给客户端组件: +您可以使用 React 的 [`use` hook](https://react.dev/reference/react/use) 将数据从服务器[流式传输](#streaming)到客户端。首先在服务端组件中获取数据,然后将 Promise 作为 prop 传递给客户端组件: ```tsx filename="app/blog/page.tsx" switcher import Posts from '@/app/ui/posts @@ -179,11 +177,11 @@ export default function Posts({ posts }) { } ``` -在上例中,`` 组件被包裹在 [`` 边界](https://react.dev/reference/react/Suspense) 内。这意味着在 Promise 解析期间会显示 fallback 内容。了解更多关于 [流式传输 (streaming)](#streaming) 的信息。 +在上面的示例中,`` 组件被包裹在 [`` 边界](https://react.dev/reference/react/Suspense)中。这意味着在 Promise 解析期间会显示 fallback 内容。了解更多关于[流式传输](#streaming)的内容。 #### 社区库 -您可以使用社区库如 [SWR](https://swr.vercel.app/) 或 [React Query](https://tanstack.com/query/latest) 在客户端组件中获取数据。这些库有自己的缓存、流式传输等特性语义。例如使用 SWR: +您可以使用社区库如 [SWR](https://swr.vercel.app/) 或 [React Query](https://tanstack.com/query/latest) 在客户端组件中获取数据。这些库有自己的缓存、流式传输和其他特性的语义。例如,使用 SWR: ```tsx filename="app/blog/page.tsx" switcher 'use client' @@ -236,13 +234,44 @@ export default function BlogPage() { } ``` +## 使用 `React.cache` 去重请求 + +去重是指在渲染过程中防止对同一资源发出重复请求的过程。它允许您在不同的组件中获取相同的数据,同时防止向数据源发出多个网络请求。 + +如果使用 `fetch`,可以通过添加 `cache: 'force-cache'` 来去重请求。这意味着您可以安全地使用相同的 URL 和选项调用,只会发出一个请求。 + +如果不使用 `fetch`,而是直接使用 ORM 或数据库,可以使用 [React `cache`](https://react.dev/reference/react/cache) 函数包装数据获取。 + +```tsx filename="app/lib/data.ts" switcher +import { cache } from 'react' +import { db, posts, eq } from '@/lib/db' + +export const getPost = cache(async (id: string) => { + const post = await db.query.posts.findFirst({ + where: eq(posts.id, parseInt(id)), + }) +}) +``` + +```jsx filename="app/lib/data.js" switcher +import { cache } from 'react' +import { db, posts, eq } from '@/lib/db' +import { notFound } from 'next/navigation' + +export const getPost = cache(async (id) => { + const post = await db.query.posts.findFirst({ + where: eq(posts.id, parseInt(id)), + }) +}) +``` + ## 流式传输 -> **警告:** 以下内容假设您的应用已启用 [`dynamicIO` 配置选项](/docs/app/api-reference/config/next-config-js/dynamicIO)。该标志在 Next.js 15 canary 版本中引入。 +> **警告:** 以下内容假设您的应用程序启用了 [`dynamicIO` 配置选项](/docs/app/api-reference/config/next-config-js/dynamicIO)。该标志在 Next.js 15 canary 版本中引入。 -在服务端组件中使用 `async/await` 时,Next.js 会启用 [动态渲染 (dynamic rendering)](/docs/app/getting-started/partial-prerendering#dynamic-rendering)。这意味着数据将在服务器端为每个用户请求获取并渲染。如果有任何慢速数据请求,整个路由的渲染将被阻塞。 +在服务端组件中使用 `async/await` 时,Next.js 会启用[动态渲染](/docs/app/getting-started/partial-prerendering#dynamic-rendering)。这意味着数据将在服务器上为每个用户请求获取和渲染。如果有任何慢速数据请求,整个路由的渲染将被阻塞。 -为了改善初始加载时间和用户体验,您可以使用流式传输将页面的 HTML 拆分为较小的块,并逐步将这些块从服务器发送到客户端。 +为了改善初始加载时间和用户体验,您可以使用流式传输将页面的 HTML 分成较小的块,并逐步将这些块从服务器发送到客户端。 服务端渲染与流式传输的工作原理 有两种方式可以在应用中实现流式传输: - -1. 使用 [`loading.js` 文件](#with-loadingjs) 包裹页面 -2. 使用 [``](#with-suspense) 包裹组件 +1. 使用 [`loading.js` 文件](#使用-loadingjs)包裹页面 +2. 使用 [``](#使用-suspense) 包裹组件 ### 使用 `loading.js` -您可以在页面所在文件夹中创建 `loading.js` 文件,在数据获取期间流式传输 **整个页面**。例如,要流式传输 `app/blog/page.js`,请在 `app/blog` 文件夹中添加该文件。 +您可以在页面所在文件夹中创建 `loading.js` 文件,以便在获取数据时流式传输整个页面。例如,要流式传输 `app/blog/page.js`,请在 `app/blog` 文件夹中添加该文件。 包含 loading.js 文件的博客文件夹结构Loading... } ``` ```jsx filename="app/blog/loading.js" switcher export default function Loading() { - // 在此定义加载状态 UI + // 在此定义加载 UI return
Loading...
} ``` -导航时,用户会立即看到布局和 [加载状态](#creating-meaningful-loading-states),同时页面正在渲染。渲染完成后,新内容会自动替换显示。 +在导航时,用户会立即看到布局和[加载状态](#创建有意义的加载状态),同时页面正在渲染。一旦渲染完成,新内容会自动替换。 加载状态 UI -在底层,`loading.js` 会被嵌套在 `layout.js` 中,并自动将 `page.js` 文件及其子组件包裹在 `` 边界内。 +在幕后,`loading.js` 将被嵌套在 `layout.js` 中,并自动将 `page.js` 文件及其子内容包裹在 `` 边界中。 loading.js 概览 -这种方法适用于路由段(布局和页面),如需更细粒度的流式传输,可以使用 ``。 +这种方法适用于路由段(布局和页面),但对于更细粒度的流式传输,可以使用 ``。 ### 使用 `` -`` 允许您更精细地控制页面的哪些部分需要流式传输。例如,您可以立即显示 `` 边界外的页面内容,而边界内的博客列表则进行流式传输。 +`` 允许您更精细地控制页面的哪些部分需要流式传输。例如,您可以立即显示 `` 边界外的任何页面内容,并流式传输边界内的博客文章列表。 ```tsx filename="app/blog/page.tsx" switcher import { Suspense } from 'react' @@ -323,7 +351,7 @@ export default function BlogPage() {

阅读以下最新文章。

- {/* 任何包裹在 边界内的内容都将被流式传输 */} + {/* 任何包裹在 边界中的内容将被流式传输 */} }> @@ -347,7 +375,7 @@ export default function BlogPage() {

阅读以下最新文章。

- {/* 任何包裹在 边界内的内容都将被流式传输 */} + {/* 任何包裹在 边界中的内容将被流式传输 */} }> @@ -359,15 +387,15 @@ export default function BlogPage() { ### 创建有意义的加载状态 -即时加载状态是导航后立即向用户显示的 fallback UI。为了最佳用户体验,我们建议设计能帮助用户理解应用正在响应的有意义加载状态。例如,可以使用骨架屏和旋转器,或未来屏幕的一小部分有意义内容如封面图片、标题等。 +即时加载状态是在导航后立即向用户显示的 fallback UI。为了获得最佳用户体验,我们建议设计有意义的加载状态,帮助用户理解应用正在响应。例如,可以使用骨架屏和旋转器,或未来屏幕的一小部分但有意义的内容,如封面照片、标题等。 -开发时,您可以使用 [React Devtools](https://react.dev/learn/react-developer-tools) 预览和检查组件的加载状态。 +在开发过程中,您可以使用 [React Devtools](https://react.dev/learn/react-developer-tools) 预览和检查组件的加载状态。 ## 示例 ### 顺序数据获取 -当树中的嵌套组件各自获取数据且请求未被 [去重 (deduplicated)](/docs/app/deep-dive/caching#request-memoization) 时,会发生顺序数据获取,导致响应时间延长。 +顺序数据获取发生在树中的嵌套组件各自获取自己的数据且请求未被[去重](/docs/app/deep-dive/caching#request-memoization)时,导致响应时间更长。 顺序与并行数据获取 -某些情况下您可能需要这种模式,因为一次获取依赖于另一次的结果。 +有时您可能需要这种模式,因为一个获取依赖于另一个的结果。 -例如,`` 组件只有在 `` 组件完成数据获取后才会开始获取数据,因为 `` 依赖 `artistID` prop: +例如,`` 组件只有在 `` 组件完成数据获取后才会开始获取数据,因为 `` 依赖于 `artistID` prop: ```tsx filename="app/artist/[username]/page.tsx" switcher export default async function Page({ @@ -449,21 +477,21 @@ async function Playlists({ artistID }) { } ``` -为了改善用户体验,您应该使用 [React ``](/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense) 在数据获取期间显示 `fallback`。这将启用 [流式传输 (streaming)](#streaming) 并防止整个路由被顺序数据请求阻塞。 +为了改善用户体验,您应该使用 [React ``](/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense) 在数据获取时显示 `fallback`。这将启用[流式传输](#streaming)并防止整个路由被顺序数据请求阻塞。 ### 并行数据获取 当路由中的数据请求被主动发起并同时开始时,就会发生并行数据获取。 -默认情况下,[布局和页面](/docs/app/getting-started/layouts-and-pages)是并行渲染的。因此每个路由段都会尽可能早地开始获取数据。 +默认情况下,[布局和页面 (layouts and pages)](/docs/app/getting-started/layouts-and-pages) 是并行渲染的。因此每个路由段会尽可能早地开始获取数据。 -然而,在_任何_组件内部,如果将多个 `async`/`await` 请求顺序排列,它们仍会按顺序执行。例如,`getAlbums` 将一直阻塞,直到 `getArtist` 解析完成: +然而,在_任意_组件中,如果多个 `async`/`await` 请求是先后放置的,它们仍可能是顺序执行的。例如,`getAlbums` 会阻塞直到 `getArtist` 解析完成: ```tsx filename="app/artist/[username]/page.tsx" switcher import { getArtist, getAlbums } from '@/app/lib/data' export default async function Page({ params }) { - // 这些请求将按顺序执行 + // 这些请求将是顺序执行的 const { username } = await params const artist = await getArtist(username) const albums = await getAlbums(username) @@ -537,13 +565,13 @@ export default async function Page({ params }) { } ``` -> **须知:** 使用 `Promise.all` 时,如果其中一个请求失败,整个操作都会失败。为了处理这种情况,可以使用 [`Promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) 方法替代。 +> **须知:** 当使用 `Promise.all` 时,如果其中一个请求失败,整个操作都会失败。为了处理这种情况,你可以改用 [`Promise.allSettled`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) 方法。 ### 预加载数据 -你可以通过创建一个工具函数来预加载数据,并在阻塞请求之前主动调用它。`` 会根据 `checkIsAvailable()` 函数的结果条件性渲染。 +你可以通过创建一个工具函数来预加载数据,并在阻塞请求之前主动调用它。`` 会根据 `checkIsAvailable()` 函数的结果有条件地渲染。 -你可以在 `checkIsAvailable()` 之前调用 `preload()` 来主动发起 `` 的数据依赖请求。当 `` 渲染时,其数据已经获取完成。 +你可以在 `checkIsAvailable()` 之前调用 `preload()` 来主动发起 `` 的数据依赖请求。当 `` 渲染时,它的数据已经被获取。 ```tsx filename="app/item/[id]/page.tsx" switcher import { getItem } from '@/lib/data' @@ -563,7 +591,7 @@ export default async function Page({ } export const preload = (id: string) => { - // void 运算符会执行给定表达式并返回 undefined + // void 会执行给定表达式并返回 undefined // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void void getItem(id) } @@ -587,7 +615,7 @@ export default async function Page({ params }) { } export const preload = (id) => { - // void 运算符会执行给定表达式并返回 undefined + // void 会执行给定表达式并返回 undefined // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void void getItem(id) } @@ -596,7 +624,7 @@ export async function Item({ id }) { // ... ``` -此外,你还可以使用 React 的 [`cache` 函数](https://react.dev/reference/react/cache) 和 [`server-only` 包](https://www.npmjs.com/package/server-only) 来创建一个可复用的工具函数。这种方法可以缓存数据获取函数,并确保它只在服务器端执行。 +此外,你可以使用 React 的 [`cache` 函数](https://react.dev/reference/react/cache) 和 [`server-only` 包](https://www.npmjs.com/package/server-only) 来创建一个可复用的工具函数。这种方法可以缓存数据获取函数并确保它只在服务器端执行。 ```ts filename="utils/get-item.ts" switcher import { cache } from 'react' diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/09-caching-and-revalidating.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/09-caching-and-revalidating.mdx new file mode 100644 index 00000000..161b781e --- /dev/null +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/09-caching-and-revalidating.mdx @@ -0,0 +1,252 @@ +--- +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:10:00.706Z +title: 数据缓存与重新验证方法 +nav_title: 缓存与重新验证 +description: 学习如何在应用程序中缓存和重新验证数据。 +related: + title: API 参考 + description: 通过阅读 API 参考文档了解更多本页提到的功能。 + links: + - app/api-reference/functions/fetch + - app/api-reference/functions/unstable_cache + - app/api-reference/functions/revalidatePath + - app/api-reference/functions/revalidateTag +--- + +缓存是一种存储数据获取结果和其他计算的技术,使得未来对相同数据的请求可以更快响应,而无需重复执行相同工作。而重新验证则允许您更新缓存条目,无需重建整个应用程序。 + +Next.js 提供了几个 API 来处理缓存和重新验证。本指南将带您了解何时以及如何使用它们。 + +- [`fetch`](#fetch) +- [`unstable_cache`](#unstable_cache) +- [`revalidatePath`](#revalidatepath) +- [`revalidateTag`](#revalidatetag) + +## `fetch` + +默认情况下,[`fetch`](/docs/app/api-reference/functions/fetch) 请求不会被缓存。您可以通过将 `cache` 选项设置为 `'force-cache'` 来缓存单个请求。 + +```tsx filename="app/page.tsx" switcher +export default async function Page() { + const data = await fetch('https://...', { cache: 'force-cache' }) +} +``` + +```jsx filename="app/page.jsx" switcher +export default async function Page() { + const data = await fetch('https://...', { cache: 'force-cache' }) +} +``` + +> **须知**:虽然 `fetch` 请求默认不缓存,但 Next.js 会对包含 `fetch` 请求的路由进行[预渲染](/docs/app/getting-started/partial-prerendering#static-rendering)并缓存 HTML。如果您希望确保路由是[动态的](/docs/app/getting-started/partial-prerendering#dynamic-rendering),请使用 [`connection` API](/docs/app/api-reference/functions/connection)。 + +要重新验证 `fetch` 请求返回的数据,可以使用 `next.revalidate` 选项。 + +```tsx filename="app/page.tsx" switcher +export default async function Page() { + const data = await fetch('https://...', { next: { revalidate: 3600 } }) +} +``` + +```jsx filename="app/page.jsx" switcher +export default async function Page() { + const data = await fetch('https://...', { next: { revalidate: 3600 } }) +} +``` + +这将按指定的秒数间隔重新验证数据。 + +查看 [`fetch` API 参考文档](/docs/app/api-reference/functions/fetch)了解更多。 + +## `unstable_cache` + +`unstable_cache` 允许您缓存数据库查询和其他异步函数的结果。要使用它,请用 `unstable_cache` 包裹函数。例如: + +```tsx filename="app/lib/data.ts swichter +import { db } from '@/lib/db' +export async function getUserById(id: string) { + return db + .select() + .from(users) + .where(eq(users.id, id)) + .then((res) => res[0]) +} +``` + +```jsx filename="app/lib/data.js" switcher +import { db } from '@/lib/db' + +export async function getUserById(id) { + return db + .select() + .from(users) + .where(eq(users.id, id)) + .then((res) => res[0]) +} +``` + +```tsx filename="app/page.tsx" highlight={2,11,13} switcher +import { unstable_cache } from 'next/cache' +import { getUserById } from '@/app/lib/data' + +export default async function Page({ + params, +}: { + params: Promise<{ userId: string }> +}) { + const { userId } = await params + + const getCachedUser = unstable_cache( + async () => { + return getUserById(userId) + }, + [userId] // 将用户 ID 添加到缓存键 + ) +} +``` + +```jsx filename="app/page.jsx" highlight={2,7,9} switcher +import { unstable_cache } from 'next/cache'; +import { getUserById } from '@/app/lib/data'; + +export default async function Page({ params } }) { + const { userId } = await params + + const getCachedUser = unstable_cache( + async () => { + return getUserById(userId) + }, + [userId] // 将用户 ID 添加到缓存键 + ); +} +``` + +该函数接受第三个可选对象来定义缓存应如何重新验证。它包含: + +- `tags`:Next.js 用于重新验证缓存的标签数组。 +- `revalidate`:缓存应重新验证的秒数。 + +```tsx filename="app/page.tsx" highlight={6-9} switcher +const getCachedUser = unstable_cache( + async () => { + return getUserById(userId) + }, + [userId], + { + tags: ['user'], + revalidate: 3600, + } +) +``` + +```jsx filename="app/page.js" highlight={6-9} switcher +const getCachedUser = unstable_cache( + async () => { + return getUserById(userId) + }, + [userId], + { + tags: ['user'], + revalidate: 3600, + } +) +``` + +查看 [`unstable_cache` API 参考文档](/docs/app/api-reference/functions/unstable_cache)了解更多。 + +## `revalidateTag` + +`revalidateTag` 用于基于标签在事件发生后重新验证缓存条目。要与 `fetch` 一起使用,首先使用 `next.tags` 选项标记函数: + +```tsx filename="app/lib/data.ts" highlight={3-5} switcher +export async function getUserById(id: string) { + const data = await fetch(`https://...`, { + next: { + tags: ['user'], + }, + }) +} +``` + +```jsx filename="app/lib/data.js" highlight={3-5} switcher +export async function getUserById(id) { + const data = await fetch(`https://...`, { + next: { + tags: ['user'], + }, + }) +} +``` + +或者,您可以使用 `tags` 选项标记 `unstable_cache` 函数: + +```tsx filename="app/lib/data.ts" highlight={6-8} switcher +export const getUserById = unstable_cache( + async (id: string) => { + return db.query.users.findFirst({ where: eq(users.id, id) }) + }, + ['user'], // 如果变量未作为参数传递则需要 + { + tags: ['user'], + } +) +``` + +```jsx filename="app/lib/data.js" highlight={6-8} switcher +export const getUserById = unstable_cache( + async (id) => { + return db.query.users.findFirst({ where: eq(users.id, id) }) + }, + ['user'], // 如果变量未作为参数传递则需要 + { + tags: ['user'], + } +) +``` + +然后,在[路由处理器](/docs/app/api-reference/file-conventions/route)或服务器操作中调用 `revalidateTag`: + +```tsx filename="app/lib/actions.ts" highlight={1} switcher +import { revalidateTag } from 'next/cache' + +export async function updateUser(id: string) { + // 修改数据 + revalidateTag('user') +} +``` + +```jsx filename="app/lib/actions.js" highlight={1} switcher +import { revalidateTag } from 'next/cache' + +export async function updateUser(id) { + // 修改数据 + revalidateTag('user') +} +``` + +您可以在多个函数中重复使用相同的标签,以便一次性重新验证它们。 + +查看 [`revalidateTag` API 参考文档](/docs/app/api-reference/functions/revalidateTag)了解更多。 + +## `revalidatePath` + +`revalidatePath` 用于在事件发生后重新验证路由。要使用它,请在[路由处理器](/docs/app/api-reference/file-conventions/route)或服务器操作中调用: + +```tsx filename="app/lib/actions.ts" highlight={1} switcher +import { revalidatePath } from 'next/cache' + +export async function updateUser(id: string) { + // 修改数据 + revalidatePath('/profile') +``` + +```jsx filename="app/lib/actions.js" highlight={1} switcher +import { revalidatePath } from 'next/cache' + +export async function updateUser(id) { + // 修改数据 + revalidatePath('/profile') +``` + +查看 [`revalidatePath` API 参考文档](/docs/app/api-reference/functions/revalidatePath)了解更多。 \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/11-error-handling.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/11-error-handling.mdx index aba6db4c..d978ff35 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/11-error-handling.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/11-error-handling.mdx @@ -1,9 +1,9 @@ --- -source-updated-at: 2025-05-22T15:18:55.000Z -translation-updated-at: 2025-05-22T15:18:55.000Z +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:10:12.334Z title: 错误处理指南 nav_title: 错误处理 -description: 学习如何显示预期错误并处理未捕获的异常。 +description: 了解如何显示预期错误并处理未捕获异常。 related: title: API 参考 description: 通过阅读 API 参考文档深入了解本页提到的功能。 @@ -14,17 +14,17 @@ related: - app/api-reference/file-conventions/not-found --- -错误可分为两类:[预期错误](#handling-expected-errors) 和 [未捕获异常](#handling-uncaught-exceptions)。本指南将介绍如何在 Next.js 应用中处理这些错误。 +错误可分为两类:[预期错误](#handling-expected-errors) 和 [未捕获异常](#handling-uncaught-exceptions)。本文将指导您如何在 Next.js 应用中处理这些错误。 ## 处理预期错误 -预期错误指应用正常运行过程中可能出现的错误,例如来自 [服务端表单验证 (server-side form validation)](/docs/app/building-your-application/data-fetching/server-actions-and-mutations#server-side-form-validation) 的验证错误或失败的请求。这类错误应被显式处理并返回给客户端。 +预期错误是指在应用正常运行过程中可能发生的错误,例如来自[服务端表单验证](/docs/app/guides/forms)的错误或失败的请求。这些错误应被显式处理并返回给客户端。 ### 服务端函数 -您可以使用 [`useActionState`](https://react.dev/reference/react/useActionState) 钩子来处理 [服务端函数 (Server Functions)](https://react.dev/reference/rsc/server-functions) 中的预期错误。 +您可以使用 [`useActionState`](https://react.dev/reference/react/useActionState) 钩子来处理[服务端函数](https://react.dev/reference/rsc/server-functions)中的预期错误。 -对于这类错误,应避免使用 `try`/`catch` 代码块和抛出错误,而应将预期错误建模为返回值。 +对于这些错误,应避免使用 `try`/`catch` 代码块和抛出错误。相反,应将预期错误建模为返回值。 ```ts filename="app/actions.ts" switcher 'use server' @@ -64,7 +64,7 @@ export async function createPost(prevState, formData) { } ``` -您可以将操作传递给 `useActionState` 钩子,并使用返回的 `state` 来显示错误信息。 +您可以将操作传递给 `useActionState` 钩子,并使用返回的 `state` 来显示错误消息。 ```tsx filename="app/ui/form.tsx" highlight={11,19} switcher 'use client' @@ -120,7 +120,7 @@ export function Form() { ### 服务端组件 -在服务端组件中获取数据时,您可以使用响应来条件渲染错误信息或执行 [`redirect`](/docs/app/api-reference/functions/redirect) 重定向。 +在服务端组件中获取数据时,您可以使用响应来有条件地渲染错误消息或执行 [`redirect`](/docs/app/api-reference/functions/redirect)。 ```tsx filename="app/page.tsx" switcher export default async function Page() { @@ -150,7 +150,7 @@ export default async function Page() { ### 未找到页面 -您可以在路由段中调用 [`notFound`](/docs/app/api-reference/functions/not-found) 函数,并使用 [`not-found.js`](/docs/app/api-reference/file-conventions/not-found) 文件来显示 404 UI。 +您可以在路由段内调用 [`notFound`](/docs/app/api-reference/functions/not-found) 函数,并使用 [`not-found.js`](/docs/app/api-reference/file-conventions/not-found) 文件来显示 404 UI。 ```tsx filename="app/blog/[slug]/page.tsx" switcher import { getPostBySlug } from '@/lib/posts' @@ -196,11 +196,11 @@ export default function NotFound() { ## 处理未捕获异常 -未捕获异常是意外错误,表明应用中存在不应在正常流程中出现的错误或问题。这类错误应通过抛出错误来处理,然后由错误边界捕获。 +未捕获异常是指意外错误,表明在应用正常流程中不应出现的错误或问题。这些错误应通过抛出错误来处理,然后由错误边界捕获。 ### 嵌套错误边界 -Next.js 使用错误边界来处理未捕获异常。错误边界会捕获其子组件中的错误,并显示备用 UI 而非崩溃的组件树。 +Next.js 使用错误边界来处理未捕获异常。错误边界会捕获其子组件中的错误,并显示回退 UI 而不是崩溃的组件树。 通过在路由段内添加 [`error.js`](/docs/app/api-reference/file-conventions/error) 文件并导出 React 组件来创建错误边界: @@ -264,7 +264,7 @@ export default function Error({ error, reset }) { } ``` -错误会冒泡到最近的父级错误边界。通过在 [路由层级结构 (route hierarchy)](/docs/app/getting-started/project-structure#component-hierarchy) 的不同层级放置 `error.tsx` 文件,可以实现细粒度的错误处理。 +错误会冒泡到最近的父级错误边界。通过在[路由层级结构](/docs/app/getting-started/project-structure#component-hierarchy)的不同级别放置 `error.tsx` 文件,可以实现细粒度的错误处理。 嵌套错误组件层级结构` 和 `` 标签,因为它在激活时会替换根布局或模板。 +虽然不太常见,但您可以使用位于根应用目录中的 [`global-error.js`](/docs/app/api-reference/file-conventions/error#global-error) 文件来处理根布局中的错误,即使在使用[国际化](/docs/app/guides/internationalization)时也是如此。全局错误 UI 必须定义自己的 `` 和 `` 标签,因为它在激活时会替换根布局或模板。 ```tsx filename="app/global-error.tsx" switcher 'use client' // 错误边界必须是客户端组件 diff --git a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/13-metadata-and-og-images.mdx b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/13-metadata-and-og-images.mdx index 5b04b582..798a275c 100644 --- a/apps/docs/content/zh-hans/docs/01-app/01-getting-started/13-metadata-and-og-images.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/01-getting-started/13-metadata-and-og-images.mdx @@ -1,12 +1,12 @@ --- -source-updated-at: 2025-05-19T22:31:51.000Z -translation-updated-at: 2025-05-19T22:31:51.000Z -title: 如何添加元数据并生成 OG 图片 +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:10:24.865Z +title: 如何添加元数据并创建 OG 图片 nav_title: 元数据与 OG 图片 -description: 学习如何为页面添加元数据并创建动态 OG 图片。 +description: 了解如何为页面添加元数据并创建动态 OG 图片。 related: title: API 参考 - description: 了解本页提到的元数据 API 的更多信息。 + description: 详细了解本页提到的元数据 API。 links: - app/api-reference/functions/generate-metadata - app/api-reference/functions/generate-viewport @@ -18,37 +18,37 @@ related: - app/api-reference/file-conventions/metadata/sitemap --- -元数据 (Metadata) API 可用于定义应用元数据以提升 SEO 和网页可分享性,包括: +元数据 API 可用于定义应用程序元数据以提升 SEO 和网页可分享性,包括: 1. [静态 `metadata` 对象](#static-metadata) 2. [动态 `generateMetadata` 函数](#generated-metadata) -3. 特殊的 [文件约定](/docs/app/api-reference/file-conventions/metadata),可用于添加静态或动态生成的 [favicon](#favicons) 和 [OG 图片](#static-open-graph-images)。 +3. 特殊的[文件约定](/docs/app/api-reference/file-conventions/metadata),可用于添加静态或动态生成的 [favicons](#favicons) 和 [OG 图片](#static-open-graph-images)。 -使用上述所有选项时,Next.js 会自动为页面生成相关的 `` 标签,可通过浏览器开发者工具查看。 +使用以上所有选项时,Next.js 会自动为页面生成相关的 `` 标签,您可以在浏览器的开发者工具中查看。 ## 默认字段 即使路由未定义元数据,也会始终添加两个默认的 `meta` 标签: -- [meta charset 标签](https://developer.mozilla.org/docs/Web/HTML/Element/meta#attr-charset) 设置网站的字符编码。 -- [meta viewport 标签](https://developer.mozilla.org/docs/Web/HTML/Viewport_meta_tag) 设置网站的视口宽度和缩放比例以适应不同设备。 +- [meta charset 标签](https://developer.mozilla.org/docs/Web/HTML/Element/meta#attr-charset) 设置网站字符编码 +- [meta viewport 标签](https://developer.mozilla.org/docs/Web/HTML/Viewport_meta_tag) 设置网站视口宽度和缩放比例以适应不同设备 ```html ``` -其他元数据字段可通过 `Metadata` 对象(用于 [静态元数据](#static-metadata))或 `generateMetadata` 函数(用于 [生成元数据](#generated-metadata))定义。 +其他元数据字段可以通过 `Metadata` 对象(用于[静态元数据](#static-metadata))或 `generateMetadata` 函数(用于[生成元数据](#generated-metadata))定义。 ## 静态元数据 -要定义静态元数据,可从静态的 [`layout.js`](/docs/app/api-reference/file-conventions/layout) 或 [`page.js`](/docs/app/api-reference/file-conventions/page) 文件中导出一个 [`Metadata` 对象](/docs/app/api-reference/functions/generate-metadata#metadata-object)。例如,为博客路由添加标题和描述: +要定义静态元数据,可以从静态的 [`layout.js`](/docs/app/api-reference/file-conventions/layout) 或 [`page.js`](/docs/app/api-reference/file-conventions/page) 文件中导出一个 [`Metadata` 对象](/docs/app/api-reference/functions/generate-metadata#metadata-object)。例如,为博客路由添加标题和描述: ```tsx filename="app/blog/layout.tsx" switcher import type { Metadata } from 'next' export const metadata: Metadata = { - title: 'My Blog', + title: '我的博客', description: '...', } @@ -57,24 +57,24 @@ export default function Page() {} ```jsx filename="app/blog/layout.tsx" switcher export const metadata = { - title: 'My Blog', + title: '我的博客', description: '...', } export default function Page() {} ``` -完整可用选项列表可在 [`generateMetadata` 文档](/docs/app/api-reference/functions/generate-metadata#metadata-fields) 中查看。 +您可以在 [`generateMetadata` 文档](/docs/app/api-reference/functions/generate-metadata#metadata-fields) 中查看完整可用选项列表。 ## 生成元数据 -可使用 [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata) 函数来 `fetch` 依赖于数据的元数据。例如,获取特定博客文章的标题和描述: +您可以使用 [`generateMetadata`](/docs/app/api-reference/functions/generate-metadata) 函数来 `fetch` 依赖于数据的元数据。例如,获取特定博客文章的标题和描述: ```tsx filename="app/blog/[slug]/page.tsx" switcher import type { Metadata, ResolvingMetadata } from 'next' type Props = { - params: Promise<{ id: string }> + params: Promise<{ slug: string }> searchParams: Promise<{ [key: string]: string | string[] | undefined }> } @@ -116,17 +116,17 @@ export async function generateMetadata({ params, searchParams }, parent) { export default function Page({ params, searchParams }) {} ``` -在幕后,Next.js 会从 UI 中单独流式传输元数据,并在解析完成后立即将其注入 HTML。 +在后台,Next.js 会从 UI 中单独流式传输元数据,并在解析后立即将元数据注入 HTML。 -### 数据请求的缓存 +### 数据请求记忆化 -有时可能需要为元数据和页面本身获取 **相同** 的数据。为避免重复请求,可使用 React 的 [`cache` 函数](https://react.dev/reference/react/cache) 来缓存返回值,仅获取一次数据。例如,为元数据和页面获取博客文章信息: +某些情况下,您可能需要为元数据和页面本身获取**相同**的数据。为避免重复请求,可以使用 React 的 [`cache` 函数](https://react.dev/reference/react/cache) 来记忆返回值,仅获取一次数据。例如,同时获取博客文章的元数据和页面内容: ```tsx filename="app/lib/data.ts" highlight={5} switcher import { cache } from 'react' import { db } from '@/app/lib/db' -// getPost 会被使用两次,但仅执行一次 +// getPost 会被使用两次,但只执行一次 export const getPost = cache(async (slug: string) => { const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) }) return res @@ -137,7 +137,7 @@ export const getPost = cache(async (slug: string) => { import { cache } from 'react' import { db } from '@/app/lib/db' -// getPost 会被使用两次,但仅执行一次 +// getPost 会被使用两次,但只执行一次 export const getPost = cache(async (slug) => { const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) }) return res @@ -186,16 +186,16 @@ export default async function Page({ params }) { 以下特殊文件可用于元数据: -- [favicon.ico、apple-icon.jpg 和 icon.jpg](/docs/app/api-reference/file-conventions/metadata/app-icons) +- [favicon.ico, apple-icon.jpg 和 icon.jpg](/docs/app/api-reference/file-conventions/metadata/app-icons) - [opengraph-image.jpg 和 twitter-image.jpg](/docs/app/api-reference/file-conventions/metadata/opengraph-image) - [robots.txt](/docs/app/api-reference/file-conventions/metadata/robots) - [sitemap.xml](/docs/app/api-reference/file-conventions/metadata/sitemap) -这些文件可用于静态元数据,也可通过代码动态生成。 +您可以使用这些文件进行静态元数据设置,也可以通过代码编程方式生成这些文件。 -## Favicon +## 网站图标 -Favicon 是代表网站在书签和搜索结果中的小图标。要为应用添加 favicon,可在应用文件夹的根目录创建 `favicon.ico` 文件。 +网站图标 (Favicons) 是代表您网站的小图标,显示在书签和搜索结果中。要为应用程序添加网站图标,请创建 `favicon.ico` 并添加到应用文件夹的根目录。 应用文件夹中的 Favicon 特殊文件,与布局和页面文件并列 -> 也可通过代码动态生成 favicon。更多信息请参阅 [favicon 文档](/docs/app/api-reference/file-conventions/metadata/app-icons)。 +> 您也可以使用代码编程方式生成网站图标。更多信息请参阅 [网站图标文档](/docs/app/api-reference/file-conventions/metadata/app-icons)。 ## 静态 Open Graph 图片 -Open Graph (OG) 图片是代表网站在社交媒体中的图片。要为应用添加静态 OG 图片,可在应用文件夹的根目录创建 `opengraph-image.png` 文件。 +Open Graph (OG) 图片是在社交媒体上代表您网站的图片。要为应用程序添加静态 OG 图片,请在应用文件夹根目录创建 `opengraph-image.png` 文件。 应用文件夹中的 OG 图片特殊文件,与布局和页面文件并列 -还可通过文件夹结构中更深层次的 `opengraph-image.png` 为特定路由添加 OG 图片。例如,要为 `/blog` 路由创建特定的 OG 图片,可在 `blog` 文件夹内添加 `opengraph-image.jpg` 文件。 +您还可以通过更深入文件夹结构创建 `opengraph-image.png` 来为特定路由添加 OG 图片。例如,要为 `/blog` 路由创建特定的 OG 图片,请在 `blog` 文件夹内添加 `opengraph-image.jpg` 文件。 blog 文件夹中的 OG 图片特殊文件 还支持其他图片格式,如 `jpeg`、`png` 和 `webp`。更多信息请参阅 [Open Graph 图片文档](/docs/app/api-reference/file-conventions/metadata/opengraph-image)。 +> 还支持其他图像格式,如 `jpeg`、`png` 和 `webp`。更多信息请参阅 [Open Graph 图片文档](/docs/app/api-reference/file-conventions/metadata/opengraph-image)。 ## 动态生成 Open Graph 图片 -[`ImageResponse` 构造函数](/docs/app/api-reference/functions/image-response) 允许使用 JSX 和 CSS 生成动态图片。这对于依赖于数据的 OG 图片非常有用。 +[`ImageResponse` 构造函数](/docs/app/api-reference/functions/image-response) 允许您使用 JSX 和 CSS 生成动态图片。这对于依赖于数据的 OG 图片非常有用。 -例如,要为每篇博客文章生成唯一的 OG 图片,可在 `blog` 文件夹内添加 `opengraph-image.ts` 文件,并从 `next/og` 导入 `ImageResponse` 构造函数: +例如,要为每篇博客文章生成唯一的 OG 图片,请在 `blog` 文件夹内添加 `opengraph-image.ts` 文件,并从 `next/og` 导入 `ImageResponse` 构造函数: ```tsx filename="app/blog/[slug]/opengraph-image.ts" switcher import { ImageResponse } from 'next/og' @@ -313,10 +313,10 @@ export default async function Image({ params }) { } ``` -`ImageResponse` 支持常见的 CSS 属性,包括 flexbox 和绝对定位、自定义字体、文本换行、居中和嵌套图片。[查看支持的完整 CSS 属性列表](/docs/app/api-reference/functions/image-response)。 +`ImageResponse` 支持常见的 CSS 属性,包括 flexbox 和绝对定位、自定义字体、文本换行、居中和嵌套图片。[查看支持的 CSS 属性完整列表](/docs/app/api-reference/functions/image-response)。 > **须知**: > -> - 示例可在 [Vercel OG Playground](https://og-playground.vercel.app/) 中查看。 -> - `ImageResponse` 使用 [`@vercel/og`](https://vercel.com/docs/og-image-generation)、[`satori`](https://github.com/vercel/satori) 和 `resvg` 将 HTML 和 CSS 转换为 PNG。 -> - 仅支持 flexbox 和部分 CSS 属性。高级布局(如 `display: grid`)无效。 +> - 示例可在 [Vercel OG Playground](https://og-playground.vercel.app/) 查看 +> - `ImageResponse` 使用 [`@vercel/og`](https://vercel.com/docs/og-image-generation)、[`satori`](https://github.com/vercel/satori) 和 `resvg` 将 HTML 和 CSS 转换为 PNG +> - 仅支持 flexbox 和部分 CSS 属性。高级布局(如 `display: grid`)将不起作用 \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/02-guides/authentication.mdx b/apps/docs/content/zh-hans/docs/01-app/02-guides/authentication.mdx index 9e28f071..4af1d2da 100644 --- a/apps/docs/content/zh-hans/docs/01-app/02-guides/authentication.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/02-guides/authentication.mdx @@ -1,30 +1,30 @@ --- -source-updated-at: 2025-05-19T22:31:51.000Z -translation-updated-at: 2025-05-19T23:12:24.557Z +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:16:21.788Z title: 如何在 Next.js 中实现身份验证 nav_title: 身份验证 -description: 学习如何在您的 Next.js 应用中实现身份验证功能。 +description: 学习如何在你的 Next.js 应用中实现身份验证功能。 --- -理解身份验证机制对保护应用数据至关重要。本文将指导您使用 React 和 Next.js 的哪些特性来实现身份验证。 +理解身份验证机制对于保护应用数据至关重要。本页将指导你使用 React 和 Next.js 的哪些功能来实现认证流程。 -开始之前,建议将流程分解为三个核心概念: +在开始之前,我们可以将整个流程分解为三个核心概念: -1. **[身份验证 (Authentication)](#authentication)**:验证用户是否为其声称的身份。要求用户通过用户名密码等凭证证明身份。 +1. **[身份验证 (Authentication)](#authentication)**:验证用户是否与其声称的身份一致。要求用户通过用户名密码等方式证明身份。 2. **[会话管理 (Session Management)](#session-management)**:跨请求跟踪用户的认证状态。 -3. **[授权 (Authorization)](#authorization)**:决定用户可访问的路由和数据。 +3. **[授权 (Authorization)](#authorization)**:决定用户可以访问哪些路由和数据。 -下图展示了使用 React 和 Next.js 特性的身份验证流程: +下图展示了使用 React 和 Next.js 功能的认证流程: 展示 React 和 Next.js 身份验证流程的示意图 -本文示例出于教学目的演示基础的用户名密码验证。虽然您可以实现自定义方案,但为了安全性和简便性,我们推荐使用身份验证库。这些库提供开箱即用的解决方案,涵盖身份验证、会话管理、授权功能,以及社交登录、多因素认证、基于角色的访问控制等特性。您可以在[身份验证库](#auth-libraries)章节查看推荐列表。 +本文示例出于教学目的演示了基础的用户名密码认证。虽然你可以实现自定义认证方案,但为了更高的安全性和简便性,我们推荐使用认证库。这些库提供了开箱即用的认证、会话管理和授权解决方案,以及社交登录、多因素认证和基于角色的访问控制等附加功能。你可以在[认证库](#auth-libraries)部分找到相关列表。 ## 身份验证 @@ -32,15 +32,15 @@ description: 学习如何在您的 Next.js 应用中实现身份验证功能。 ### 注册与登录功能 -您可以使用 [`
`](https://react.dev/reference/react-dom/components/form) 元素配合 React 的[服务端操作 (Server Actions)](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) 和 `useActionState` 来捕获用户凭证、验证表单字段并调用身份验证提供商的 API 或数据库。 +你可以使用 [``](https://react.dev/reference/react-dom/components/form) 元素配合 React 的[服务端操作 (Server Actions)](/docs/app/building-your-application/data-fetching/server-actions-and-mutations) 和 `useActionState` 来捕获用户凭证、验证表单字段并调用认证提供商的 API 或数据库。 -由于服务端操作始终在服务器执行,这为处理身份验证逻辑提供了安全环境。 +由于服务端操作始终在服务器端执行,它们为处理认证逻辑提供了安全环境。 以下是实现注册/登录功能的步骤: #### 1. 捕获用户凭证 -创建提交时触发服务端操作的表单。例如接收用户名、邮箱和密码的注册表单: +要捕获用户凭证,创建一个表单并在提交时触发服务端操作。例如,一个接收用户名、邮箱和密码的注册表单: ```tsx filename="app/ui/signup-form.tsx" switcher import { signup } from '@/app/actions/auth' @@ -98,11 +98,11 @@ export async function signup(formData: FormData) {} export async function signup(formData) {} ``` -#### 2. 服务端表单验证 +#### 2. 在服务端验证表单字段 -在服务端操作中使用验证库如 [Zod](https://zod.dev/) 或 [Yup](https://github.com/jquense/yup) 验证表单字段。 +使用服务端操作在服务端验证表单字段。如果你的认证提供商不提供表单验证,可以使用 [Zod](https://zod.dev/) 或 [Yup](https://github.com/jquense/yup) 等模式验证库。 -以 Zod 为例,定义带错误提示的表单结构: +以 Zod 为例,你可以定义带有适当错误信息的表单模式: ```ts filename="app/lib/definitions.ts" switcher import { z } from 'zod' @@ -110,16 +110,16 @@ import { z } from 'zod' export const SignupFormSchema = z.object({ name: z .string() - .min(2, { message: '姓名至少需要 2 个字符' }) + .min(2, { message: '姓名长度至少为 2 个字符。' }) .trim(), - email: z.string().email({ message: '请输入有效邮箱地址' }).trim(), + email: z.string().email({ message: '请输入有效的邮箱地址。' }).trim(), password: z .string() - .min(8, { message: '至少 8 个字符长度' }) - .regex(/[a-zA-Z]/, { message: '至少包含一个字母' }) - .regex(/[0-9]/, { message: '至少包含一个数字' }) + .min(8, { message: '长度至少为 8 个字符' }) + .regex(/[a-zA-Z]/, { message: '必须包含至少一个字母。' }) + .regex(/[0-9]/, { message: '必须包含至少一个数字。' }) .regex(/[^a-zA-Z0-9]/, { - message: '至少包含一个特殊字符', + message: '必须包含至少一个特殊字符。', }) .trim(), }) @@ -142,22 +142,22 @@ import { z } from 'zod' export const SignupFormSchema = z.object({ name: z .string() - .min(2, { message: '姓名至少需要 2 个字符' }) + .min(2, { message: '姓名长度至少为 2 个字符。' }) .trim(), - email: z.string().email({ message: '请输入有效邮箱地址' }).trim(), + email: z.string().email({ message: '请输入有效的邮箱地址。' }).trim(), password: z .string() - .min(8, { message: '至少 8 个字符长度' }) - .regex(/[a-zA-Z]/, { message: '至少包含一个字母' }) - .regex(/[0-9]/, { message: '至少包含一个数字' }) + .min(8, { message: '长度至少为 8 个字符' }) + .regex(/[a-zA-Z]/, { message: '必须包含至少一个字母。' }) + .regex(/[0-9]/, { message: '必须包含至少一个数字。' }) .regex(/[^a-zA-Z0-9]/, { - message: '至少包含一个特殊字符', + message: '必须包含至少一个特殊字符。', }) .trim(), }) ``` -若表单验证失败,可提前终止流程避免调用身份验证接口: +为了避免不必要的认证提供商 API 或数据库调用,如果任何表单字段不符合定义的模式,可以在服务端操作中提前 `return`。 ```ts filename="app/actions/auth.ts" switcher import { SignupFormSchema, FormState } from '@/app/lib/definitions' @@ -170,14 +170,14 @@ export async function signup(state: FormState, formData: FormData) { password: formData.get('password'), }) - // 若验证失败则提前返回 + // 如果任何字段无效,提前返回 if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, } } - // 调用提供商接口创建用户... + // 调用提供商或数据库创建用户... } ``` @@ -192,18 +192,18 @@ export async function signup(state, formData) { password: formData.get('password'), }) - // 若验证失败则提前返回 + // 如果任何字段无效,提前返回 if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, } } - // 调用提供商接口创建用户... + // 调用提供商或数据库创建用户... } ``` -在 `` 中使用 React 的 `useActionState` 钩子展示验证错误: +回到你的 `` 组件,可以使用 React 的 `useActionState` 钩子在表单提交时显示验证错误: ```tsx filename="app/ui/signup-form.tsx" switcher highlight={7,15,21,27-36} 'use client' @@ -297,12 +297,12 @@ export default function SignupForm() { > **须知:** > -> - 在 React 19 中,`useFormStatus` 返回对象包含 data、method、action 等额外字段。若未使用 React 19,则仅有 `pending` 字段可用。 -> - 执行数据变更前,应始终验证用户权限。详见[身份验证与授权](#authorization)。 +> - 在 React 19 中,`useFormStatus` 返回对象包含额外键值如 data、method 和 action。如果未使用 React 19,则只有 `pending` 键可用。 +> - 在修改数据前,应始终确保用户有执行该操作的权限。参见[认证与授权](#authorization)。 -#### 3. 创建用户或验证用户凭据 +#### 3. 创建用户或验证用户凭证 -验证表单字段后,您可以通过调用认证提供商的 API 或数据库来创建新用户账户或检查用户是否存在。 +在验证表单字段后,您可以通过调用认证提供商的 API 或数据库来创建新用户账户或检查用户是否存在。 继续之前的示例: @@ -313,7 +313,7 @@ export async function signup(state: FormState, formData: FormData) { // 2. 准备插入数据库的数据 const { name, email, password } = validatedFields.data - // 例如:存储前对用户密码进行哈希处理 + // 例如:在存储前对用户密码进行哈希处理 const hashedPassword = await bcrypt.hash(password, 10) // 3. 将用户插入数据库或调用认证库的 API @@ -347,7 +347,7 @@ export async function signup(state, formData) { // 2. 准备插入数据库的数据 const { name, email, password } = validatedFields.data - // 例如:存储前对用户密码进行哈希处理 + // 例如:在存储前对用户密码进行哈希处理 const hashedPassword = await bcrypt.hash(password, 10) // 3. 将用户插入数据库或调用库的 API @@ -374,12 +374,12 @@ export async function signup(state, formData) { } ``` -成功创建用户账户或验证用户凭据后,您可以创建一个会话来管理用户的认证状态。根据您的会话管理策略,会话可以存储在 cookie 或数据库中,或两者兼有。继续阅读[会话管理](#session-management)部分以了解更多。 +成功创建用户账户或验证用户凭证后,您可以创建一个会话来管理用户的认证状态。根据您的会话管理策略,会话可以存储在 cookie 或数据库中,或两者兼有。继续阅读[会话管理](#session-management)部分了解更多。 > **提示:** > -> - 上述示例较为详细,目的是为了教学而分解了认证步骤。这突显了实现自己的安全解决方案可能很快变得复杂。考虑使用[认证库](#auth-libraries)来简化流程。 -> - 为了提升用户体验,您可能希望在注册流程的早期检查重复的电子邮件或用户名。例如,当用户输入用户名或输入框失去焦点时。这可以帮助避免不必要的表单提交,并向用户提供即时反馈。您可以使用诸如 [use-debounce](https://www.npmjs.com/package/use-debounce) 这样的库来防抖请求,以管理这些检查的频率。 +> - 上面的示例较为详细,目的是为了教学而分解了认证步骤。这表明实现自己的安全解决方案可能很快变得复杂。考虑使用[认证库](#auth-libraries)来简化流程。 +> - 为了改善用户体验,您可能希望在注册流程的早期检查重复的电子邮件或用户名。例如,当用户输入用户名或输入框失去焦点时。这可以帮助防止不必要的表单提交,并立即向用户提供反馈。您可以使用诸如 [use-debounce](https://www.npmjs.com/package/use-debounce) 这样的库来管理这些检查的频率。 @@ -387,12 +387,12 @@ export async function signup(state, formData) { 以下是实现注册和/或登录表单的步骤: -1. 用户通过表单提交凭据。 +1. 用户通过表单提交他们的凭证。 2. 表单发送一个由 API 路由处理的请求。 3. 验证成功后,流程完成,表示用户已成功认证。 4. 如果验证失败,则显示错误消息。 -考虑一个用户可以输入凭据的登录表单: +考虑一个用户可以输入其凭证的登录表单: ```tsx filename="pages/login.tsx" switcher import { FormEvent } from 'react' @@ -468,7 +468,7 @@ export default function LoginPage() { } ``` -上述表单有两个输入字段,用于捕获用户的电子邮件和密码。提交时,它会触发一个函数,向 API 路由 (`/api/auth/login`) 发送 POST 请求。 +上面的表单有两个输入字段,用于捕获用户的电子邮件和密码。提交时,它会触发一个函数,向 API 路由 (`/api/auth/login`) 发送 POST 请求。 然后,您可以在 API 路由中调用认证提供商的 API 来处理认证: @@ -487,9 +487,9 @@ export default async function handler( res.status(200).json({ success: true }) } catch (error) { if (error.type === 'CredentialsSignin') { - res.status(401).json({ error: '无效凭据。' }) + res.status(401).json({ error: '无效凭证。' }) } else { - res.status(500).json({ error: '出错了。' }) + res.status(500).json({ error: '发生错误。' }) } } } @@ -506,9 +506,9 @@ export default async function handler(req, res) { res.status(200).json({ success: true }) } catch (error) { if (error.type === 'CredentialsSignin') { - res.status(401).json({ error: '无效凭据。' }) + res.status(401).json({ error: '无效凭证。' }) } else { - res.status(500).json({ error: '出错了。' }) + res.status(500).json({ error: '发生错误。' }) } } } @@ -518,16 +518,16 @@ export default async function handler(req, res) { ## 会话管理 -会话管理确保用户的认证状态在多个请求之间保持。它涉及创建、存储、刷新和删除会话或令牌。 +会话管理确保用户的认证状态在多个请求之间得以保持。它涉及创建、存储、刷新和删除会话或令牌。 有两种类型的会话: -1. [**无状态会话 (Stateless)**](#stateless-sessions):会话数据(或令牌)存储在浏览器的 cookie 中。每次请求时都会发送该 cookie,允许在服务器上验证会话。这种方法更简单,但如果实现不当可能不太安全。 -2. [**数据库会话 (Database)**](#database-sessions):会话数据存储在数据库中,用户的浏览器仅接收加密的会话 ID。这种方法更安全,但可能更复杂且占用更多服务器资源。 +1. [**无状态会话**](#stateless-sessions):会话数据(或令牌)存储在浏览器的 cookie 中。每次请求都会发送 cookie,允许在服务器上验证会话。这种方法更简单,但如果实现不当可能不太安全。 +2. [**数据库会话**](#database-sessions):会话数据存储在数据库中,用户的浏览器仅接收加密的会话 ID。这种方法更安全,但可能更复杂且占用更多服务器资源。 > **须知:** 虽然您可以使用其中一种方法或两者兼用,但我们建议使用会话管理库,如 [iron-session](https://github.com/vvo/iron-session) 或 [Jose](https://github.com/panva/jose)。 -### 无状态会话 (Stateless Sessions) +### 无状态会话 @@ -537,13 +537,13 @@ export default async function handler(req, res) { 2. 使用会话管理库编写加密/解密会话数据的逻辑。 3. 使用 Next.js [`cookies`](/docs/app/api-reference/functions/cookies) API 管理 cookie。 -除了上述内容,还可以考虑添加功能以在用户返回应用程序时[更新(或刷新)](#updating-or-refreshing-sessions)会话,并在用户注销时[删除](#deleting-the-session)会话。 +除了上述内容,还可以考虑添加功能,在用户返回应用程序时[更新(或刷新)](#updating-or-refreshing-sessions)会话,并在用户注销时[删除](#deleting-the-session)会话。 > **须知:** 检查您的[认证库](#auth-libraries)是否包含会话管理功能。 #### 1. 生成密钥 -您可以通过几种方式生成用于签名会话的密钥。例如,您可以选择在终端中使用 `openssl` 命令: +有几种方法可以生成用于签名会话的密钥。例如,您可以选择在终端中使用 `openssl` 命令: ```bash filename="terminal" openssl rand -base64 32 @@ -563,7 +563,7 @@ const secretKey = process.env.SESSION_SECRET #### 2. 加密和解密会话 -接下来,您可以使用您首选的[会话管理库](#session-management-libraries)来加密和解密会话。继续之前的示例,我们将使用 [Jose](https://www.npmjs.com/package/jose)(兼容 [Edge Runtime](/docs/app/api-reference/edge))和 React 的 [`server-only`](https://www.npmjs.com/package/server-only) 包,以确保您的会话管理逻辑仅在服务器上执行。 +接下来,您可以使用您首选的[会话管理库](#session-management-libraries)来加密和解密会话。继续前面的示例,我们将使用 [Jose](https://www.npmjs.com/package/jose)(与 [Edge Runtime](/docs/app/api-reference/edge) 兼容)和 React 的 [`server-only`](https://www.npmjs.com/package/server-only) 包,以确保您的会话管理逻辑仅在服务器上执行。 ```tsx filename="app/lib/session.ts" switcher import 'server-only' @@ -622,11 +622,11 @@ export async function decrypt(session) { > **提示:** > -> - 载荷应包含在后续请求中使用的**最小**、唯一的用户数据,例如用户 ID、角色等。不应包含个人身份信息,如电话号码、电子邮件地址、信用卡信息等,或敏感数据如密码。 +> - 负载应包含在后续请求中使用的**最小**、唯一的用户数据,例如用户的 ID、角色等。不应包含个人身份信息,如电话号码、电子邮件地址、信用卡信息等,或敏感数据,如密码。 #### 3. 设置 cookie(推荐选项) -要将会话存储在 cookie 中,请使用 Next.js [`cookies`](/docs/app/api-reference/functions/cookies) API。Cookie 应在服务器上设置,并包括以下推荐选项: +要将会话存储在 cookie 中,请使用 Next.js [`cookies`](/docs/app/api-reference/functions/cookies) API。cookie 应在服务器上设置,并包括推荐的选项: - **HttpOnly**:防止客户端 JavaScript 访问 cookie。 - **Secure**:使用 https 发送 cookie。 @@ -634,7 +634,7 @@ export async function decrypt(session) { - **Max-Age 或 Expires**:在一定时间后删除 cookie。 - **Path**:定义 cookie 的 URL 路径。 -请参阅 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) 以获取有关这些选项的更多信息。 +请参阅 [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) 获取有关这些选项的更多信息。 ```ts filename="app/lib/session.ts" switcher import 'server-only' @@ -674,7 +674,7 @@ export async function createSession(userId) { } ``` -回到您的服务器操作中,您可以调用 `createSession()` 函数,并使用 [`redirect()`](/docs/app/building-your-application/routing/redirecting) API 将用户重定向到适当的页面: +回到您的服务器操作中,您可以调用 `createSession()` 函数,并使用 [`redirect()`](/docs/app/guides/redirecting) API 将用户重定向到适当的页面: ```ts filename="app/actions/auth.ts" switcher import { createSession } from '@/app/lib/session' @@ -712,12 +712,12 @@ export async function signup(state, formData) { > **提示:** > -> - **Cookie 应在服务器上设置**以防止客户端篡改。 +> - **应在服务器上设置 cookie**,以防止客户端篡改。 > - 🎥 观看:了解更多关于无状态会话和 Next.js 认证的内容 → [YouTube (11 分钟)](https://www.youtube.com/watch?v=DJvM2lSPn6w)。 #### 更新(或刷新)会话 -您还可以延长会话的过期时间。这对于用户在再次访问应用程序时保持登录状态非常有用。例如: +您还可以延长会话的过期时间。这对于在用户再次访问应用程序时保持其登录状态非常有用。例如: ```ts filename="app/lib/session.ts" switcher import 'server-only' @@ -869,11 +869,11 @@ export default function handler(req, res) { ### 数据库会话 -要创建和管理数据库会话,你需要遵循以下步骤: +要创建和管理数据库会话,需要遵循以下步骤: -1. 在数据库中创建表来存储会话和数据(或检查你的认证库是否已处理此功能)。 -2. 实现插入、更新和删除会话的功能。 -3. 将会话 ID 加密后再存储到用户的浏览器中,并确保数据库和 cookie 保持同步(此步骤可选,但建议用于 [中间件](#optimistic-checks-with-middleware-optional) 中的乐观认证检查)。 +1. 在数据库中创建表来存储会话数据(或检查你的认证库是否已处理此功能) +2. 实现插入、更新和删除会话的功能 +3. 将会话 ID 加密后再存储到用户浏览器中,并确保数据库和 cookie 保持同步(这是可选的,但推荐用于 [中间件](#optimistic-checks-with-middleware-optional) 中的乐观认证检查) @@ -902,7 +902,7 @@ export async function createSession(id: number) { // 2. 加密会话 ID const session = await encrypt({ sessionId, expiresAt }) - // 3. 将会话存储在 cookie 中以便进行乐观认证检查 + // 3. 将会话存储在 cookie 中以供乐观认证检查 const cookieStore = await cookies() cookieStore.set('session', session, { httpOnly: true, @@ -937,7 +937,7 @@ export async function createSession(id) { // 2. 加密会话 ID const session = await encrypt({ sessionId, expiresAt }) - // 3. 将会话存储在 cookie 中以便进行乐观认证检查 + // 3. 将会话存储在 cookie 中以供乐观认证检查 const cookieStore = await cookies() cookieStore.set('session', session, { httpOnly: true, @@ -951,10 +951,10 @@ export async function createSession(id) { > **提示**: > -> - 为了更快的访问速度,你可以考虑为会话的生命周期添加服务端缓存。你也可以将会话数据保留在主数据库中,并合并数据请求以减少查询次数。 -> - 对于更高级的用例,例如跟踪用户上次登录时间、活动设备数量或允许用户从所有设备登出,你可以选择使用数据库会话。 +> - 为了更快访问,可以考虑在会话生命周期内添加服务端缓存。你也可以将会话数据保留在主数据库中,并合并数据请求以减少查询次数。 +> - 对于更高级的用例,如跟踪用户最后登录时间、活动设备数量或让用户能够注销所有设备,可以选择使用数据库会话。 -在实现会话管理后,你需要添加授权逻辑来控制用户在应用中可以访问和执行的内容。继续阅读 [授权](#authorization) 部分以了解更多。 +实现会话管理后,需要添加授权逻辑来控制用户可以在应用中访问和执行的操作。继续阅读 [授权](#authorization) 部分了解更多。 @@ -1010,27 +1010,27 @@ export default async function handler(req, res) { ## 授权 -一旦用户通过认证并创建了会话,你可以实现授权逻辑来控制用户在应用中可以访问和执行的内容。 +用户认证并创建会话后,可以实现授权来控制用户在应用中可以访问和执行的操作。 -授权检查主要有两种类型: +主要有两种授权检查: -1. **乐观检查**:使用存储在 cookie 中的会话数据检查用户是否有权访问路由或执行操作。这些检查适用于快速操作,例如根据权限或角色显示/隐藏 UI 元素或重定向用户。 -2. **安全检查**:使用存储在数据库中的会话数据检查用户是否有权访问路由或执行操作。这些检查更安全,适用于需要访问敏感数据或执行敏感操作的场景。 +1. **乐观检查**:使用存储在 cookie 中的会话数据检查用户是否有权访问路由或执行操作。这些检查适用于快速操作,如根据权限或角色显示/隐藏 UI 元素或重定向用户。 +2. **安全检查**:使用存储在数据库中的会话数据检查用户是否有权访问路由或执行操作。这些检查更安全,用于需要访问敏感数据或操作的情况。 对于这两种情况,我们建议: -- 创建一个 [数据访问层 (DAL)](#creating-a-data-access-layer-dal) 来集中管理授权逻辑。 -- 使用 [数据传输对象 (DTO)](#using-data-transfer-objects-dto) 仅返回必要的数据。 -- 可选地使用 [中间件](#optimistic-checks-with-middleware-optional) 执行乐观检查。 +- 创建 [数据访问层 (DAL)](#creating-a-data-access-layer-dal) 来集中授权逻辑 +- 使用 [数据传输对象 (DTO)](#using-data-transfer-objects-dto) 仅返回必要的数据 +- 可选使用 [中间件](#optimistic-checks-with-middleware-optional) 执行乐观检查。 ### 使用中间件进行乐观检查(可选) 在某些情况下,你可能希望使用 [中间件 (Middleware)](/docs/app/building-your-application/routing/middleware) 并根据权限重定向用户: -- 执行乐观检查。由于中间件在每个路由上运行,因此它是集中重定向逻辑和预过滤未授权用户的好方法。 -- 保护在用户之间共享数据的静态路由(例如付费内容)。 +- 执行乐观检查。由于中间件在每个路由上运行,这是集中重定向逻辑和预过滤未授权用户的好方法。 +- 保护用户间共享数据的静态路由(如付费内容)。 -然而,由于中间件在每个路由上运行,包括 [预取 (prefetched)](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 的路由,因此重要的是仅从 cookie 中读取会话(乐观检查),并避免数据库检查以防止性能问题。 +然而,由于中间件在每个路由上运行,包括 [预取 (prefetched)](/docs/app/building-your-application/routing/linking-and-navigating#2-prefetching) 路由,重要的是仅从 cookie 中读取会话(乐观检查),避免数据库检查以防止性能问题。 例如: @@ -1039,12 +1039,12 @@ import { NextRequest, NextResponse } from 'next/server' import { decrypt } from '@/app/lib/session' import { cookies } from 'next/headers' -// 1. 指定受保护和公开的路由 +// 1. 指定受保护和公开路由 const protectedRoutes = ['/dashboard'] const publicRoutes = ['/login', '/signup', '/'] export default async function middleware(req: NextRequest) { - // 2. 检查当前路由是受保护还是公开的 + // 2. 检查当前路由是受保护还是公开 const path = req.nextUrl.pathname const isProtectedRoute = protectedRoutes.includes(path) const isPublicRoute = publicRoutes.includes(path) @@ -1053,12 +1053,12 @@ export default async function middleware(req: NextRequest) { const cookie = (await cookies()).get('session')?.value const session = await decrypt(cookie) - // 4. 如果用户未认证,则重定向到 /login + // 4. 如果用户未认证,重定向到 /login if (isProtectedRoute && !session?.userId) { return NextResponse.redirect(new URL('/login', req.nextUrl)) } - // 5. 如果用户已认证,则重定向到 /dashboard + // 5. 如果用户已认证,重定向到 /dashboard if ( isPublicRoute && session?.userId && @@ -1081,12 +1081,12 @@ import { NextResponse } from 'next/server' import { decrypt } from '@/app/lib/session' import { cookies } from 'next/headers' -// 1. 指定受保护和公开的路由 +// 1. 指定受保护和公开路由 const protectedRoutes = ['/dashboard'] const publicRoutes = ['/login', '/signup', '/'] export default async function middleware(req) { - // 2. 检查当前路由是受保护还是公开的 + // 2. 检查当前路由是受保护还是公开 const path = req.nextUrl.pathname const isProtectedRoute = protectedRoutes.includes(path) const isPublicRoute = publicRoutes.includes(path) @@ -1095,12 +1095,12 @@ export default async function middleware(req) { const cookie = (await cookies()).get('session')?.value const session = await decrypt(cookie) - // 5. 如果用户未认证,则重定向到 /login + // 5. 如果用户未认证,重定向到 /login if (isProtectedRoute && !session?.userId) { return NextResponse.redirect(new URL('/login', req.nextUrl)) } - // 6. 如果用户已认证,则重定向到 /dashboard + // 6. 如果用户已认证,重定向到 /dashboard if ( isPublicRoute && session?.userId && @@ -1118,7 +1118,7 @@ export const config = { } ``` -虽然中间件对于初始检查很有用,但它不应该是保护数据的唯一防线。大多数安全检查应尽可能靠近数据源执行,更多信息请参阅 [数据访问层](#creating-a-data-access-layer-dal)。 +虽然中间件可用于初始检查,但它不应该是保护数据的唯一防线。大多数安全检查应尽可能靠近数据源执行,详见 [数据访问层 (DAL)](#creating-a-data-access-layer-dal)。 > **提示**: > @@ -1130,9 +1130,9 @@ export const config = { ### 创建数据访问层 (DAL) -我们建议创建一个 DAL 来集中管理数据请求和授权逻辑。 +我们建议创建 DAL 来集中数据请求和授权逻辑。 -DAL 应包含一个函数,用于在用户与应用交互时验证用户的会话。至少,该函数应检查会话是否有效,然后重定向或返回用户信息以进行进一步的请求。 +DAL 应包含一个函数,在用户与应用交互时验证用户的会话。至少,该函数应检查会话是否有效,然后重定向或返回进一步请求所需的用户信息。 例如,为你的 DAL 创建一个单独的文件,其中包含 `verifySession()` 函数。然后使用 React 的 [cache](https://react.dev/reference/react/cache) API 在 React 渲染过程中记忆函数的返回值: @@ -1172,7 +1172,7 @@ export const verifySession = cache(async () => { }) ``` -然后你可以在数据请求、服务端操作 (Server Actions)、路由处理器 (Route Handlers) 中调用 `verifySession()` 函数: +然后你可以在数据请求、服务器操作、路由处理程序中调用 `verifySession()` 函数: ```tsx filename="app/lib/dal.ts" switcher export const getUser = cache(async () => { @@ -1182,7 +1182,7 @@ export const getUser = cache(async () => { try { const data = await db.query.users.findMany({ where: eq(users.id, session.userId), - // 显式返回你需要的列,而不是整个用户对象 + // 明确返回你需要的列而不是整个用户对象 columns: { id: true, name: true, @@ -1208,7 +1208,7 @@ export const getUser = cache(async () => { try { const data = await db.query.users.findMany({ where: eq(users.id, session.userId), - // 显式返回你需要的列,而不是整个用户对象 + // 明确返回你需要的列而不是整个用户对象 columns: { id: true, name: true, @@ -1228,15 +1228,15 @@ export const getUser = cache(async () => { > **提示**: > -> - DAL 可用于保护在请求时获取的数据。然而,对于在用户之间共享数据的静态路由,数据将在构建时获取,而不是在请求时获取。使用 [中间件](#optimistic-checks-with-middleware-optional) 来保护静态路由。 -> - 对于安全检查,你可以通过将会话 ID 与数据库进行比较来检查会话是否有效。使用 React 的 [cache](https://react.dev/reference/react/cache) 函数避免在渲染过程中重复请求数据库。 -> - 你可能希望将相关的数据请求整合到一个 JavaScript 类中,该类在任何方法之前运行 `verifySession()`。 +> - DAL 可用于保护请求时获取的数据。然而,对于用户间共享数据的静态路由,数据将在构建时获取而不是请求时获取。使用 [中间件](#optimistic-checks-with-middleware-optional) 保护静态路由。 +> - 对于安全检查,可以通过将会话 ID 与数据库进行比较来检查会话是否有效。使用 React 的 [cache](https://react.dev/reference/react/cache) 函数避免在渲染过程中对数据库进行不必要的重复请求。 +> - 你可能希望将相关数据请求合并到一个 JavaScript 类中,该类在任何方法之前运行 `verifySession()`。 ### 使用数据传输对象 (DTO) -在检索数据时,建议仅返回应用程序中需要使用的必要数据,而非整个对象。例如,获取用户数据时,可以只返回用户 ID 和姓名,而不是包含密码、电话号码等敏感信息的完整用户对象。 +获取数据时,建议仅返回应用程序所需的必要数据,而非完整对象。例如,获取用户数据时,可以只返回用户 ID 和姓名,而非包含密码、电话号码等敏感信息的完整用户对象。 -但如果无法控制返回的数据结构,或团队协作中需要避免完整对象传递到客户端,可以采用以下策略:明确指定哪些字段可以安全暴露给客户端。 +若无法控制返回的数据结构,或需要避免完整对象传递至客户端,可采用以下策略:明确指定哪些字段可安全暴露给客户端。 ```tsx filename="app/lib/dto.ts" switcher import 'server-only' @@ -1259,7 +1259,7 @@ export async function getProfileDTO(slug: string) { const currentUser = await getUser(user.id) - // 或在此处仅返回查询相关的数据 + // 或在此处仅返回查询所需的字段 return { username: canSeeUsername(currentUser) ? user.username : null, phonenumber: canSeePhoneNumber(currentUser, user.team) @@ -1290,7 +1290,7 @@ export async function getProfileDTO(slug) { const currentUser = await getUser(user.id) - // 或在此处仅返回查询相关的数据 + // 或在此处仅返回查询所需的字段 return { username: canSeeUsername(currentUser) ? user.username : null, phonenumber: canSeePhoneNumber(currentUser, user.team) @@ -1300,12 +1300,12 @@ export async function getProfileDTO(slug) { } ``` -通过在数据访问层 (DAL) 集中管理数据请求和授权逻辑,并使用 DTO,可以确保所有数据请求的安全性和一致性,从而更易于维护、审计和调试,适应应用程序的扩展需求。 +通过在数据访问层 (DAL) 集中管理数据请求和授权逻辑,并使用 DTO,可以确保所有数据请求安全且一致,便于应用扩展时的维护、审计和调试。 -> **须知**: +> **须知**: > -> - 定义 DTO 有几种不同方式:使用 `toJSON()`、如示例中的独立函数或 JS 类。由于这些是 JavaScript 模式而非 React 或 Next.js 特性,建议研究最适合您应用的方案。 -> - 了解更多安全最佳实践,请参阅 [Next.js 安全指南](/blog/security-nextjs-server-components-actions)。 +> - 定义 DTO 有多种方式:使用 `toJSON()`、如示例中的独立函数或 JS 类。由于这些是 JavaScript 模式而非 React 或 Next.js 特性,建议研究选择最适合应用的方案。 +> - 了解更多安全实践,请参阅 [Next.js 安全指南](/blog/security-nextjs-server-components-actions)。 ### 服务端组件 (Server Components) @@ -1345,15 +1345,13 @@ export default function Dashboard() { } ``` -示例中,我们使用 DAL 的 `verifySession()` 函数检查 'admin'、'user' 和未授权角色。这种模式确保每个用户仅访问与其角色匹配的组件。 +此示例使用 DAL 中的 `verifySession()` 函数检查 'admin'、'user' 和未授权角色,确保用户仅访问与其角色匹配的组件。 ### 布局与权限检查 -由于[部分渲染 (Partial Rendering)](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering),在[布局 (Layouts)](/docs/app/building-your-application/routing/layouts-and-templates) 中进行检查需谨慎,因为导航时布局不会重新渲染,意味着每次路由变更时不会检查用户会话。 +由于[部分渲染 (Partial Rendering)](/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering),在[布局 (Layouts)](/docs/app/api-reference/file-conventions/layout) 中进行检查需谨慎,因其不会在导航时重新渲染,用户会话不会在每次路由变更时被验证。 -应将检查逻辑放在靠近数据源或条件渲染组件的位置。例如,共享布局获取用户数据并在导航栏显示用户头像时,应在布局中获取用户数据 (`getUser()`),而在 DAL 中进行权限检查。 - -这确保无论 `getUser()` 在应用何处调用,都会执行权限检查,避免开发者忘记验证用户是否有权访问数据。 +应将检查逻辑靠近数据源或条件渲染的组件。例如,共享布局获取用户数据并在导航栏显示头像时,应在布局中调用 `getUser()`,而在 DAL 中执行权限检查。这确保无论 `getUser()` 在何处调用,都会执行权限检查,避免开发者遗漏授权验证。 ```tsx filename="app/layout.tsx" switcher export default async function Layout({ @@ -1397,15 +1395,13 @@ export const getUser = cache(async () => { }) ``` -> **须知**: +> **须知**: > -> - 单页应用 (SPA) 中常见的模式是在布局或顶层组件中 `return null` 以拒绝未授权用户。**不推荐**此模式,因为 Next.js 应用有多个入口点,无法阻止嵌套路由段和服务端操作 (Server Actions) 被访问。 +> - 单页应用 (SPA) 中常见模式是在布局或顶层组件中 `return null` 以拒绝未授权用户。此模式**不推荐**,因为 Next.js 应用有多个入口点,无法阻止嵌套路由段和服务端操作 (Server Actions) 被访问。 ### 服务端操作 (Server Actions) -对待[服务端操作](/docs/app/building-your-application/data-fetching/server-actions-and-mutations)需与对外 API 端点相同的安全考量,验证用户是否有权执行操作。 - -以下示例在执行操作前检查用户角色: +对待[服务端操作](/docs/app/building-your-application/data-fetching/server-actions-and-mutations)需与对外 API 端点同等安全,验证用户是否有权执行操作。下例检查用户角色后才允许操作继续: ```ts filename="app/lib/actions.ts" switcher 'use server' @@ -1415,7 +1411,7 @@ export async function serverAction(formData: FormData) { const session = await verifySession() const userRole = session?.user?.role - // 如果用户无权执行操作,则提前返回 + // 若用户无权则提前返回 if (userRole !== 'admin') { return null } @@ -1432,7 +1428,7 @@ export async function serverAction() { const session = await verifySession() const userRole = session.user.role - // 如果用户无权执行操作,则提前返回 + // 若用户无权则提前返回 if (userRole !== 'admin') { return null } @@ -1443,18 +1439,16 @@ export async function serverAction() { ### 路由处理器 (Route Handlers) -对待[路由处理器](/docs/app/building-your-application/routing/route-handlers)需与对外 API 端点相同的安全考量,验证用户是否有权访问路由处理器。 - -例如: +对待[路由处理器](/docs/app/building-your-application/routing/route-handlers)需与对外 API 端点同等安全,验证用户是否有权访问。例如: ```ts filename="app/api/route.ts" switcher import { verifySession } from '@/app/lib/dal' export async function GET() { - // 用户认证和角色验证 + // 用户认证与角色验证 const session = await verifySession() - // 检查用户是否已认证 + // 检查用户是否认证 if (!session) { // 用户未认证 return new Response(null, { status: 401 }) @@ -1474,10 +1468,10 @@ export async function GET() { import { verifySession } from '@/app/lib/dal' export async function GET() { - // 用户认证和角色验证 + // 用户认证与角色验证 const session = await verifySession() - // 检查用户是否已认证 + // 检查用户是否认证 if (!session) { // 用户未认证 return new Response(null, { status: 401 }) @@ -1493,13 +1487,13 @@ export async function GET() { } ``` -上述示例展示了具有双重安全检查的路由处理器:首先检查有效会话,然后验证登录用户是否为 'admin'。 +上例展示了两层安全检查的路由处理器:先检查活跃会话,再验证登录用户是否为 'admin'。 ## 上下文提供器 (Context Providers) 由于[交错渲染 (interleaving)](/docs/app/getting-started/server-and-client-components#examples#interleaving-server-and-client-components),上下文提供器可用于认证。但 React `context` 不支持服务端组件,仅适用于客户端组件。 -这种方式有效,但任何子服务端组件会先在服务端渲染,无法访问上下文提供器的会话数据: +此方式有效,但任何子服务端组件会先在服务端渲染,无法访问上下文提供器的会话数据: ```tsx filename="app/layout.ts" switcher import { ContextProvider } from 'auth-lib' @@ -1545,7 +1539,7 @@ export default function Profile() { } ``` -如果客户端组件需要会话数据(如客户端数据获取),使用 React 的 [`taintUniqueValue`](https://react.dev/reference/react/experimental_taintUniqueValue) API 防止敏感会话数据暴露给客户端。 +若客户端组件需要会话数据(如客户端数据获取),可使用 React 的 [`taintUniqueValue`](https://react.dev/reference/react/experimental_taintUniqueValue) API 防止敏感会话数据暴露至客户端。 @@ -1555,9 +1549,9 @@ export default function Profile() { #### 保护 API 路由 -Next.js 中的 API 路由对处理服务端逻辑和数据管理至关重要。需确保仅授权用户能访问特定功能,通常包括验证用户认证状态和基于角色的权限。 +Next.js 中的 API 路由对处理服务端逻辑和数据管理至关重要。需确保这些路由的安全,仅允许授权用户访问特定功能,通常包括验证用户认证状态和基于角色的权限。 -以下是保护 API 路由的示例: +以下为保护 API 路由的示例: ```ts filename="pages/api/route.ts" switcher import { NextApiRequest, NextApiResponse } from 'next' @@ -1568,7 +1562,7 @@ export default async function handler( ) { const session = await getSession(req) - // 检查用户是否已认证 + // 检查用户是否认证 if (!session) { res.status(401).json({ error: '用户未认证', @@ -1579,7 +1573,7 @@ export default async function handler( // 检查用户是否为 'admin' 角色 if (session.user.role !== 'admin') { res.status(401).json({ - error: '未授权访问:用户无管理员权限。', + error: '未经授权的访问:用户无管理员权限。', }) return } @@ -1593,7 +1587,7 @@ export default async function handler( export default async function handler(req, res) { const session = await getSession(req) - // 检查用户是否已认证 + // 检查用户是否认证 if (!session) { res.status(401).json({ error: '用户未认证', @@ -1604,7 +1598,7 @@ export default async function handler(req, res) { // 检查用户是否为 'admin' 角色 if (session.user.role !== 'admin') { res.status(401).json({ - error: '未授权访问:用户无管理员权限。', + error: '未经授权的访问:用户无管理员权限。', }) return } @@ -1614,13 +1608,13 @@ export default async function handler(req, res) { } ``` -此示例展示了具有双重安全检查的 API 路由:认证和授权。首先检查有效会话,然后验证登录用户是否为 'admin',确保仅限认证和授权用户安全访问,保持请求处理的强安全性。 +此例展示了两层安全检查的 API 路由:先检查活跃会话,再验证登录用户是否为 'admin',确保仅限认证和授权用户安全访问,保障请求处理的稳健安全。 ## 资源 -了解 Next.js 认证后,以下是与 Next.js 兼容的库和资源,帮助实现安全的认证和会话管理: +了解 Next.js 认证后,以下兼容库和资源可帮助实现安全的认证和会话管理: ### 认证库 @@ -1643,9 +1637,9 @@ export default async function handler(req, res) { ## 延伸阅读 -继续学习认证和安全相关资源: +继续学习认证与安全,可查阅以下资源: - [Next.js 安全思考](/blog/security-nextjs-server-components-actions) - [理解 XSS 攻击](https://vercel.com/guides/understanding-xss-attacks) - [理解 CSRF 攻击](https://vercel.com/guides/understanding-csrf-attacks) -- [The Copenhagen Book](https://thecopenhagenbook.com/) +- [哥本哈根手册](https://thecopenhagenbook.com/) diff --git a/apps/docs/content/zh-hans/docs/01-app/02-guides/css-in-js.mdx b/apps/docs/content/zh-hans/docs/01-app/02-guides/css-in-js.mdx index 8e6a7407..aa701b35 100644 --- a/apps/docs/content/zh-hans/docs/01-app/02-guides/css-in-js.mdx +++ b/apps/docs/content/zh-hans/docs/01-app/02-guides/css-in-js.mdx @@ -1,16 +1,16 @@ --- -source-updated-at: 2025-05-19T22:31:51.000Z -translation-updated-at: 2025-05-19T23:05:28.721Z +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:09:26.737Z title: 如何使用 CSS-in-JS 库 nav_title: CSS-in-JS description: 在 Next.js 中使用 CSS-in-JS 库 --- -{/* 本文档内容在应用路由和页面路由间共享。您可以使用 `内容` 组件添加特定于页面路由的内容。任何共享内容不应包裹在组件中。 */} +{/* 本文档内容在应用路由和页面路由之间共享。您可以使用 `内容` 组件添加特定于页面路由的内容。任何共享内容不应包裹在组件中。 */} -> **警告**:在较新的 React 功能(如服务端组件和流式渲染)中使用 CSS-in-JS 需要库作者支持最新版本的 React,包括 [并发渲染 (concurrent rendering)](https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react)。 +> **警告**:在 Next.js 中使用 CSS-in-JS 库配合 React 新特性(如服务端组件和流式渲染)需要库作者支持最新版本的 React,包括 [并发渲染](https://react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react)。 以下库在 `app` 目录的客户端组件中受支持(按字母顺序排列): @@ -28,17 +28,17 @@ description: 在 Next.js 中使用 CSS-in-JS 库 - [`tss-react`](https://tss-react.dev/) - [`vanilla-extract`](https://vanilla-extract.style) -以下库正在开发支持中: +以下库正在努力支持中: - [`emotion`](https://github.com/emotion-js/emotion/issues/2928) -> **须知**:我们正在测试不同的 CSS-in-JS 库,并将为支持 React 18 功能和/或 `app` 目录的库添加更多示例。 +> **须知**:我们正在测试不同的 CSS-in-JS 库,并将为支持 React 18 特性和/或 `app` 目录的库添加更多示例。 -## 在 `app` 中配置 CSS-in-JS +## 在 `app` 目录中配置 CSS-in-JS 配置 CSS-in-JS 是一个三步选择加入的过程,包括: -1. 一个 **样式注册表 (style registry)** 用于收集渲染中的所有 CSS 规则。 +1. 一个**样式注册表**用于收集渲染中的所有 CSS 规则。 2. 新的 `useServerInsertedHTML` 钩子,用于在可能使用这些规则的内容之前注入规则。 3. 一个客户端组件,在初始服务端渲染期间用样式注册表包裹您的应用。 @@ -58,8 +58,8 @@ export default function StyledJsxRegistry({ }: { children: React.ReactNode }) { - // 仅使用惰性初始状态创建样式表一次 - // 参考:https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + // 使用惰性初始状态只创建一次样式表 + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state const [jsxStyleRegistry] = useState(() => createStyleRegistry()) useServerInsertedHTML(() => { @@ -80,8 +80,8 @@ import { useServerInsertedHTML } from 'next/navigation' import { StyleRegistry, createStyleRegistry } from 'styled-jsx' export default function StyledJsxRegistry({ children }) { - // 仅使用惰性初始状态创建样式表一次 - // 参考:https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + // 使用惰性初始状态只创建一次样式表 + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state const [jsxStyleRegistry] = useState(() => createStyleRegistry()) useServerInsertedHTML(() => { @@ -94,7 +94,7 @@ export default function StyledJsxRegistry({ children }) { } ``` -然后,用注册表包裹您的 [根布局 (root layout)](/docs/app/building-your-application/routing/layouts-and-templates#root-layout-required): +然后,用该注册表包裹您的[根布局](/docs/app/api-reference/file-conventions/layout#root-layout): ```tsx filename="app/layout.tsx" switcher import StyledJsxRegistry from './registry' @@ -158,8 +158,8 @@ export default function StyledComponentsRegistry({ }: { children: React.ReactNode }) { - // 仅使用惰性初始状态创建样式表一次 - // 参考:https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + // 使用惰性初始状态只创建一次样式表 + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()) useServerInsertedHTML(() => { @@ -186,8 +186,8 @@ import { useServerInsertedHTML } from 'next/navigation' import { ServerStyleSheet, StyleSheetManager } from 'styled-components' export default function StyledComponentsRegistry({ children }) { - // 仅使用惰性初始状态创建样式表一次 - // 参考:https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + // 使用惰性初始状态只创建一次样式表 + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet()) useServerInsertedHTML(() => { @@ -244,9 +244,9 @@ export default function RootLayout({ children }) { > **须知**: > -> - 在服务端渲染期间,样式将被提取到全局注册表并刷新到 HTML 的 `` 中。这确保样式规则放置在可能使用它们的内容之前。未来,我们可能会使用即将推出的 React 功能来确定注入样式的位置。 -> - 在流式渲染期间,每个块的样式将被收集并附加到现有样式中。客户端水合完成后,`styled-components` 将像往常一样接管并注入任何进一步的动态样式。 -> - 我们特意在树的顶层使用客户端组件作为样式注册表,因为这种方式提取 CSS 规则更高效。它避免了在后续服务端渲染时重新生成样式,并防止它们被发送到服务端组件负载中。 +> - 在服务端渲染期间,样式将被提取到全局注册表中并刷新到 HTML 的 `` 中。这确保了样式规则在使用它们的任何内容之前放置。将来,我们可能会使用即将推出的 React 功能来确定注入样式的位置。 +> - 在流式传输期间,每个块的样式将被收集并附加到现有样式中。客户端水合完成后,`styled-components` 将像往常一样接管并注入任何进一步的动态样式。 +> - 我们特意在树的顶层使用客户端组件作为样式注册表,因为这样提取 CSS 规则更高效。它避免了在后续服务端渲染时重新生成样式,并防止它们被发送到服务端组件负载中。 > - 对于需要配置 styled-components 编译的各个属性的高级用例,您可以阅读我们的 [Next.js styled-components API 参考](/docs/architecture/nextjs-compiler#styled-components) 了解更多信息。 @@ -277,9 +277,9 @@ function HiThere() { export default HiThere ``` -我们捆绑了 [styled-jsx](https://github.com/vercel/styled-jsx) 以提供对隔离作用域 CSS 的支持。目标是支持类似于 Web 组件的 "影子 CSS",但遗憾的是 [不支持服务端渲染且仅限 JS](https://github.com/w3c/webcomponents/issues/71)。 +我们捆绑了 [styled-jsx](https://github.com/vercel/styled-jsx) 以提供对隔离作用域 CSS 的支持。目标是支持类似于 Web 组件的"影子 CSS",但遗憾的是 [不支持服务端渲染且仅限 JS](https://github.com/w3c/webcomponents/issues/71)。 -查看上述示例以了解其他流行的 CSS-in-JS 解决方案(如 Styled Components)。 +请参阅上面的示例了解其他流行的 CSS-in-JS 解决方案(如 Styled Components)。 使用 `styled-jsx` 的组件如下所示: @@ -318,6 +318,6 @@ export default HelloWorld ### 禁用 JavaScript -是的,如果您禁用 JavaScript,CSS 仍会在生产构建 (`next start`) 中加载。在开发期间,我们需要启用 JavaScript 以提供最佳的开发者体验,包括 [快速刷新 (Fast Refresh)](https://nextjs.org/blog/next-9-4#fast-refresh)。 +是的,如果您禁用 JavaScript,CSS 仍将在生产构建 (`next start`) 中加载。在开发过程中,我们需要启用 JavaScript 以提供最佳的开发者体验,包括 [快速刷新](https://nextjs.org/blog/next-9-4#fast-refresh)。 \ No newline at end of file diff --git a/apps/docs/content/zh-hans/docs/01-app/02-guides/forms.mdx b/apps/docs/content/zh-hans/docs/01-app/02-guides/forms.mdx new file mode 100644 index 00000000..34292bea --- /dev/null +++ b/apps/docs/content/zh-hans/docs/01-app/02-guides/forms.mdx @@ -0,0 +1,493 @@ +--- +source-updated-at: 2025-06-01T01:32:20.000Z +translation-updated-at: 2025-06-01T22:09:41.632Z +title: 如何使用 Server Actions 创建表单 +nav_title: 表单 +description: 学习如何在 Next.js 中使用 React Server Actions 创建表单。 +--- + +React Server Actions 是运行在服务端的[服务端函数 (Server Functions)](https://react.dev/reference/rsc/server-functions),可以在服务端和客户端组件中调用以处理表单提交。本指南将介绍如何在 Next.js 中使用 Server Actions 创建表单。 + +## 工作原理 + +React 扩展了 HTML [``](https://developer.mozilla.org/docs/Web/HTML/Element/form) 元素,允许通过 [`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/form#action) 属性调用 Server Actions。 + +在表单中使用时,函数会自动接收 [`FormData`](https://developer.mozilla.org/docs/Web/API/FormData/FormData) 对象。您可以使用原生 [`FormData 方法`](https://developer.mozilla.org/en-US/docs/Web/API/FormData#instance_methods)提取数据: + +```tsx filename="app/invoices/page.tsx" switcher +export default function Page() { + async function createInvoice(formData: FormData) { + 'use server' + + const rawFormData = { + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + } + + // mutate data + // revalidate the cache + } + + return ... +} +``` + +```jsx filename="app/invoices/page.js" switcher +export default function Page() { + async function createInvoice(formData) { + 'use server' + + const rawFormData = { + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + } + + // mutate data + // revalidate the cache + } + + return
...
+} +``` + +> **须知:** 处理多字段表单时,可以使用 [`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)。例如:`const rawFormData = Object.fromEntries(formData)`。 + +## 传递额外参数 + +除了表单字段外,您可以使用 JavaScript 的 [`bind`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind) 方法向服务端函数传递额外参数。例如,向 `updateUser` 服务端函数传递 `userId` 参数: + +```tsx filename="app/client-component.tsx" highlight={6} switcher +'use client' + +import { updateUser } from './actions' + +export function UserProfile({ userId }: { userId: string }) { + const updateUserWithId = updateUser.bind(null, userId) + + return ( +
+ + +
+ ) +} +``` + +```jsx filename="app/client-component.js" highlight={6} switcher +'use client' + +import { updateUser } from './actions' + +export function UserProfile({ userId }) { + const updateUserWithId = updateUser.bind(null, userId) + + return ( +
+ + +
+ ) +} +``` + +服务端函数将接收 `userId` 作为额外参数: + +```ts filename="app/actions.ts" switcher +'use server' + +export async function updateUser(userId: string, formData: FormData) {} +``` + +```js filename="app/actions.js" switcher +'use server' + +export async function updateUser(userId, formData) {} +``` + +> **须知:** +> +> - 另一种方法是将参数作为隐藏输入字段传递(例如 ``)。但这种方式会使值成为渲染 HTML 的一部分且不会被编码。 +> - `bind` 方法在服务端和客户端组件中都适用,并支持渐进增强。 + +## 表单验证 + +表单可以在客户端或服务端进行验证。 + +- **客户端验证**:可以使用 HTML 属性如 `required` 和 `type="email"` 进行基本验证。 +- **服务端验证**:可以使用 [zod](https://zod.dev/) 等库验证表单字段。例如: + +```tsx filename="app/actions.ts" switcher +'use server' + +import { z } from 'zod' + +const schema = z.object({ + email: z.string({ + invalid_type_error: '无效的邮箱格式', + }), +}) + +export default async function createUser(formData: FormData) { + const validatedFields = schema.safeParse({ + email: formData.get('email'), + }) + + // 如果表单数据无效则提前返回 + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + // 变更数据 +} +``` + +```jsx filename="app/actions.js" switcher +'use server' + +import { z } from 'zod' + +const schema = z.object({ + email: z.string({ + invalid_type_error: '无效的邮箱格式', + }), +}) + +export default async function createsUser(formData) { + const validatedFields = schema.safeParse({ + email: formData.get('email'), + }) + + // 如果表单数据无效则提前返回 + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + } + } + + // 变更数据 +} +``` + +## 验证错误 + +要显示验证错误或消息,可以将定义 `
` 的组件转换为客户端组件,并使用 React 的 [`useActionState`](https://react.dev/reference/react/useActionState)。 + +使用 `useActionState` 时,服务端函数签名会发生变化,第一个参数将接收新的 `prevState` 或 `initialState` 参数。 + +```tsx filename="app/actions.ts" highlight={4} switcher +'use server' + +import { z } from 'zod' + +export async function createUser(initialState: any, formData: FormData) { + const validatedFields = schema.safeParse({ + email: formData.get('email'), + }) + // ... +} +``` + +```jsx filename="app/actions.ts" highlight={4} switcher +'use server' + +import { z } from 'zod' + +// ... + +export async function createUser(initialState, formData) { + const validatedFields = schema.safeParse({ + email: formData.get('email'), + }) + // ... +} +``` + +然后可以根据 `state` 对象条件渲染错误消息。 + +```tsx filename="app/ui/signup.tsx" highlight={11,18-20} switcher +'use client' + +import { useActionState } from 'react' +import { createUser } from '@/app/actions' + +const initialState = { + message: '', +} + +export function Signup() { + const [state, formAction, pending] = useActionState(createUser, initialState) + + return ( + + + + {/* ... */} +

{state?.message}

+ +
+ ) +} +``` + +```jsx filename="app/ui/signup.js" highlight={11,18-20} switcher +'use client' + +import { useActionState } from 'react' +import { createUser } from '@/app/actions' + +const initialState = { + message: '', +} + +export function Signup() { + const [state, formAction, pending] = useActionState(createUser, initialState) + + return ( +
+ + + {/* ... */} +

{state?.message}

+ +
+ ) +} +``` + +## 等待状态 + +[`useActionState`](https://react.dev/reference/react/useActionState) 钩子暴露了一个 `pending` 布尔值,可用于在执行操作时显示加载指示器或禁用提交按钮。 + +```tsx filename="app/ui/signup.tsx" highlight={7,12} switcher +'use client' + +import { useActionState } from 'react' +import { createUser } from '@/app/actions' + +export function Signup() { + const [state, formAction, pending] = useActionState(createUser, initialState) + + return ( +
+ {/* 其他表单元素 */} + +
+ ) +} +``` + +```jsx filename="app/ui/signup.js" highlight={7,12} switcher +'use client' + +import { useActionState } from 'react' +import { createUser } from '@/app/actions' + +export function Signup() { + const [state, formAction, pending] = useActionState(createUser, initialState) + + return ( +
+ {/* 其他表单元素 */} + +
+ ) +} +``` + +或者,您可以使用 [`useFormStatus`](https://react.dev/reference/react-dom/hooks/useFormStatus) 钩子在操作执行时显示加载指示器。使用此钩子时,需要创建一个单独的组件来渲染加载指示器。例如,在操作等待时禁用按钮: + +```tsx filename="app/ui/button.tsx" highlight={6} switcher +'use client' + +import { useFormStatus } from 'react-dom' + +export function SubmitButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +```jsx filename="app/ui/button.js" highlight={6} switcher +'use client' + +import { useFormStatus } from 'react-dom' + +export function SubmitButton() { + const { pending } = useFormStatus() + + return ( + + ) +} +``` + +然后可以在表单中嵌套 `SubmitButton` 组件: + +```tsx filename="app/ui/signup.tsx" switcher +import { SubmitButton } from './button' +import { createUser } from '@/app/actions' + +export function Signup() { + return ( +
+ {/* 其他表单元素 */} + + + ) +} +``` + +```jsx filename="app/ui/signup.js" switcher +import { SubmitButton } from './button' +import { createUser } from '@/app/actions' + +export function Signup() { + return ( +
+ {/* 其他表单元素 */} + + + ) +} +``` + +> **须知:** 在 React 19 中,`useFormStatus` 包含返回对象上的额外键,如 data、method 和 action。如果您未使用 React 19,则只有 `pending` 键可用。 + +## 乐观更新 + +您可以使用 React 的 [`useOptimistic`](https://react.dev/reference/react/useOptimistic) 钩子在服务端函数执行完成前乐观地更新 UI,而不是等待响应: + +```tsx filename="app/page.tsx" switcher +'use client' + +import { useOptimistic } from 'react' +import { send } from './actions' + +type Message = { + message: string +} + +export function Thread({ messages }: { messages: Message[] }) { + const [optimisticMessages, addOptimisticMessage] = useOptimistic< + Message[], + string + >(messages, (state, newMessage) => [...state, { message: newMessage }]) + + const formAction = async (formData: FormData) => { + const message = formData.get('message') as string + addOptimisticMessage(message) + await send(message) + } + + return ( +
+ {optimisticMessages.map((m, i) => ( +
{m.message}
+ ))} +
+ + +
+
+ ) +} +``` + +```jsx filename="app/page.js" switcher +'use client' + +import { useOptimistic } from 'react' +import { send } from './actions' + +export function Thread({ messages }) { + const [optimisticMessages, addOptimisticMessage] = useOptimistic( + messages, + (state, newMessage) => [...state, { message: newMessage }] + ) + + const formAction = async (formData) => { + const message = formData.get('message') + addOptimisticMessage(message) + await send(message) + } + + return ( +
+ {optimisticMessages.map((m) => ( +
{m.message}
+ ))} +
+ + +
+
+ ) +} +``` + +## 嵌套表单元素 + +可以在 `
` 内的嵌套元素(如 `