跳到主要内容

路由

每个应用程序的骨架都是路由。 本页面将向您介绍 Web 路由的基本概念以及如何在 Next.js 中处理路由。

术语

首先,您将看到整个文档中使用了这些术语。这是一个快速参考: terminology component tree

  • 树:用于可视化层次结构的一种约定。例如,一个具有父组件和子组件的组件树,一个文件夹结构等。
  • 子树:树的一部分,以新的根(首个)开始,以叶子(最后)结束。
  • 根:树或子树中的第一个节点,例如根布局。
  • 叶子:子树中没有子节点的节点,例如URL路径中的最后一段。"

terminology url anatomy

  • URL 段:由斜杠分隔的 URL 路径的一部分。
  • URL 路径:域名后面的 URL 部分(由段组成)

app路由

在第13版本中,Next.js引入了一个基于 React Server Components 构建的新的App Router,支持共享布局、嵌套路由、加载状态、错误处理等功能。

App Router在一个名为app的新目录中工作。 该app目录与pages目录一起工作,以支持渐进式采用。 这使您可以将应用程序的某些路由选择为新行为,同时保留pages目录中的其他路由以保持先前的行为。 如果您的应用程序使用pages目录, 请同时查看Pages Router文档。

提示

App Router优先于Pages Router跨目录的路由不应解析为相同的URL路径, 否则将导致构建时错误以防止冲突。

默认情况下,app目录中的组件是 React Server Components这是一种性能优化,使您能够轻松采用它们, 您还可以使用Client Components

备注

如果您是Server Components的新手, 请查看Server页面。

文件夹和文件的角色

Next.js使用基于文件系统的路由器,其中:

  • 文件夹用于定义路由。路由是嵌套文件夹的单一路径, 遵循从根文件夹到包含page.js文件的最终叶文件夹的文件系统层次结构。 请参阅定义路由
  • 文件用于创建显示在路由段上的UI。 请参阅特殊文件

路由段

路由中的每个文件夹表示一个路由段。每个路由段都映射到URL路径中相应的段。 route segments to path segments

嵌套路由

要创建嵌套路由,可以将文件夹嵌套在彼此内部。 例如,您可以通过在app目录中嵌套两个新文件夹来添加一个新的/dashboard/settings路由。

/dashboard/settings路由由三个段组成:

  1. /(根段)
  2. dashboard(段)
  3. settings(叶段)

文件约定

Next.js提供了一组特殊文件,以在嵌套路由中创建具有特定行为的UI

  • layout:用于一个段及其子代的共享UI。
  • page:路由的唯一UI,使路由公开可访问。
  • loading:一个段及其子代的加载UI。
  • not-found:一个段及其子代的未找到UI。
  • error:一个段及其子代的错误UI。
  • global-error:全局错误UI。
  • route:服务器端API端点。
  • template:专门重新呈现的布局UI。
  • default:并行路由的回退UI。
备注

.js.jsx.tsx文件扩展名可用于特殊文件。

组件层次结构

路由段中特殊文件定义的React组件以特定的层次结构呈现:

  • layout.js
  • template.js
  • error.js(React错误边界)
  • loading.js(React悬停边界)
  • not-found.js(React错误边界)
  • page.js或嵌套的layout.js

file conventions component hierarchy

在嵌套路由中,段的组件将嵌套在其父段的组件内。 nested file conventions component hierarch

同地放置

除了特殊文件外,您还可以选择将您自己的文件(例如组件、样式、测试等)同地放置在app目录中的文件夹中。

这是因为虽然文件夹定义路由,但只有由`page.js`或`route.js`返回的内容是公开可寻址的project organization colocation

高级路由模式

App Router还提供了一组约定,帮助您实现更高级的路由模式。这些包括:

  • 并行路由:允许您同时在同一视图中显示两个或更多可以独立导航的页面。您可以用于具有自己子导航的分屏视图,例如仪表板。
  • 拦截路由:允许您拦截路由并在另一个路由的上下文中显示它。当保持当前页面的上下文很重要时,您可以使用这些功能。例如,在编辑一个任务时查看所有任务或在动态源中展开照片。

这些模式使您能够构建更丰富和复杂的UI,使过去对小团队和个人开发人员来说历来复杂的功能变得更加平民化。

定义路由

创建路由

Next.js使用基于文件系统的路由器,其中文件夹用于定义路由

每个文件夹表示一个路由段,对应到一个URL段。 要创建嵌套路由,您可以将文件夹嵌套在彼此内部。 route segments to path segments

使用特殊的page.js文件可以使路由段公开可访问defining routes

在这个例子中,/dashboard/analytics的URL路径是不公开可访问的, 因为它没有相应的page.js文件。 这个文件夹可以用于存储组件、样式表、图像或其他同地放置的文件。

备注

特殊文件可以使用.js.jsx.tsx文件扩展名。

创建UI

使用特殊文件约定来为每个路由段创建UI。 最常见的是用于显示路由独特UI的pages和用于显示跨多个路由共享UI的layouts

例如,要创建您的第一个页面,请在app目录中添加一个page.js文件,并导出一个React组件:

app/page.tsx

import React from 'react';

const Page: React.FC = () => {
return <h1>Hello, Next.js!</h1>;
};

export default Page;

页面和布局

Next.js 13 中的 App Router 引入了新的文件约定,可以轻松创建页面、共享布局和模板。 本页面将指导您如何在 Next.js 应用程序中使用这些特殊文件。

页面

页面是特定路由的UI。 您可以通过从page.js文件中导出组件来定义页面。 使用嵌套文件夹来定义路由,并使用page.js文件使路由公开可访问。

通过在app目录中添加page.js文件来创建您的第一个页面: page special file

app/page.tsx
// `app/page.tsx` is the UI for the `/` URL
export default function Page() {
return <h1>Hello, Home page!</h1>
}
app/dashboard/page.tsx
// `app/dashboard/page.tsx` is the UI for the `/dashboard` URL
export default function Page() {
return <h1>Hello, Dashboard Page!</h1>
}

值得知道:

  • 页面始终是路由子树的叶子。
  • 可以使用.js.jsx.tsx文件扩展名用于页面。
  • 必须使用page.js文件才能使路由段公开可访问。
  • 页面默认是Server Components,但可以设置为Client Components。
  • 页面可以获取数据。有关更多信息,请查看数据获取部分。

布局

布局是在多个页面之间共享的UI。 在导航时,布局保留状态,保持交互,并且不重新呈现。 布局也可以是嵌套的。

您可以通过从layout.js文件中默认导出一个React组件来定义布局。 组件应接受一个children属性,在渲染期间将其填充为子布局(如果存在)或子页面。 layout special file

app/dashboard/layout.tsx
export default function DashboardLayout({
children, // will be a page or nested layout
}: {
children: React.ReactNode
}) {
return (
<section>
{/* Include shared UI here e.g. a header or sidebar */}
<nav></nav>

{children}
</section>
)
}

值得知道:

  • 最上层的布局称为根布局。 这是一个必需的布局,它在应用程序中的所有页面之间共享。 根布局必须包含htmlbody标签。
  • 任何路由段都可以选择性地定义自己的布局。这些布局将在该段中的所有页面之间共享。
  • 路由中的布局默认是嵌套的。每个父布局使用React children属性包装其下方的子布局。
  • 您可以使用Route Groups选择性地将特定路由段放入和移出共享布局。
  • 布局默认是Server Components,但可以设置为Client Components。
  • 布局可以获取数据。有关更多信息,请查看数据获取部分。
  • 在父布局和其子布局之间传递数据是不可能的。 但是,您可以在路由中多次获取相同的数据,而React将自动去重请求,而不会影响性能。
  • 布局无法访问其下方的路由段。要访问所有路由段,可以在Client Component中使用useSelectedLayoutSegmentuseSelectedLayoutSegments
  • 可以使用.js.jsx.tsx文件扩展名用于布局。
  • 可以在同一文件夹中定义layout.jspage.js文件。布局将包裹页面

根布局(必需)

根布局在app目录的顶层定义,并应用于所有路由。此布局使您能够修改从服务器返回的初始HTML。

app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

值得知道:

  • app目录必须包含一个根布局。
  • 根布局必须定义<html><body>标签,因为Next.js不会自动创建它们。
  • 您可以使用内置的SEO支持来管理<head> HTML元素,例如<title>元素。
  • 您可以使用路由组创建多个根布局。查看此处的示例。
  • 根布局默认是Server Components,不能设置为Client Components。
  • pages目录迁移:根布局替代了_app.js_document.js文件。查看迁移指南。

嵌套布局

在文件夹中定义的布局(例如app/dashboard/layout.js) 适用于特定的路由段(例如acme.com/dashboard), 并在这些段处于活动状态时呈现。 默认情况下,文件层次结构中的布局是嵌套的, 这意味着它们通过其children属性包装子布局。

nested layout

app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}

值得知道:

  • 只有根布局可以包含<html><body>标签。

如果将上述两个布局合并, 根布局(app/layout.js)将包装仪表板布局(app/dashboard/layout.js), 后者将包装app/dashboard/*内的路由段。

这两个布局将嵌套如下: nested layouts UI

可以使用Route Groups选择性地将特定路由段放入和移出共享布局。

模板

模板与布局相似,因为它们包装每个子布局或页面。 与布局不同的是,模板为导航中的每个子项创建一个新实例。 这意味着当用户在共享模板的路由之间导航时,该组件的新实例被挂载, DOM元素被重新创建,状态不保留,并且效果重新同步。

可能有些情况下,您需要这些特定的行为,而模板将比布局更合适。例如:

  • 依赖于useEffect(例如记录页面视图)和useState(例如每页反馈表单)的功能。
  • 更改默认框架行为。例如,布局内的Suspense边界仅在第一次加载布局时显示回退,而在切换页面时不会显示。对于模板,每次导航都会显示回退。

可以通过从template.js文件中导出一个默认的React组件来定义模板。组件应该接受一个children属性。 template special file

app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}

就嵌套而言,template.js在布局和其子布局之间呈现。以下是一个简化的输出:

Output
<Layout>
{/* 注意,模板被赋予唯一的键。 */}
<Template key={routeParam}>{children}</Template>
</Layout>

修改<head>

app目录中,您可以使用内置的SEO支持来修改<head> HTML元素,例如标题和meta。

可以通过在layout.jspage.js文件中导出一个metadata对象或 generateMetadata函数来定义元数据。

app/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Next.js',
};

export default function Page() {
return '...';
}
备注

您不应手动添加<head>标签(如<title><meta>)到根布局。 相反,您应该使用Metadata API,该API会自动处理高级要求,如流式传输和去重<head>元素。

链接和导航

在Next.js中,有两种在路由之间导航的方式

  • 使用 <Link> 组件
  • 使用 useRouter Hook

本页面将介绍如何使用 <Link>useRouter(),并深入探讨导航的工作原理。

<Link> 是一个内置组件,它扩展了 HTML 的 <a> 标签, 提供了在路由之间进行预取和客户端导航的功能。 这是在Next.js中导航之间的主要方式。

您可以通过从 next/link 中导入它,并向组件传递一个 href 属性来使用它:

app/page.tsx
import Link from 'next/link'

export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}

还可以向 <Link> 传递其他可选的属性。更多信息请参阅 API 参考。

示例

链接到动态段

在链接到动态段时,您可以使用模板文字和插值来生成链接列表。例如,要生成博客文章列表:

app/blog/PostList.js
import Link from 'next/link'

export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}

检查活动链接

您可以使用 usePathname() 来确定链接是否处于活动状态。 例如,要向活动链接添加类,您可以检查当前pathname是否与链接的 href 匹配:

app/components/links.tsx
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Links() {
const pathname = usePathname()

return (
<nav>
<ul>
<li>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
</li>
<li>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
About
</Link>
</li>
</ul>
</nav>
)
}

滚动到id

Next.js App Router 的默认行为是在导航时滚动到新路由的顶部,或者对于后退和前进导航保持滚动位置。

如果您想要在导航时滚动到特定的标识,可以在URL后附加一个 # 锚链接, 或者只是将一个 # 锚链接传递给 href 属性。这是因为 <Link> 渲染为 <a> 元素,所以是可能的。

<Link href="/dashboard#settings">Settings</Link>

// 输出
<a href="/dashboard#settings">Settings</a>

禁用滚动恢复

Next.js App Router 的默认行为是在导航时滚动到新路由的顶部,或者对于后退和前进导航保持滚动位置。 如果您想要禁用此行为,可以将 scroll={false} 传递给 <Link> 组件, 或者将 scroll: false 传递给 router.push()router.replace()

// next/link
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>

// useRouter
import { useRouter } from 'next/navigation'

const router = useRouter()

router.push('/dashboard', { scroll: false })

useRouter() Hook

useRouter 钩子允许您以编程方式更改路由。

此钩子只能在客户端组件内使用,并且是从 next/navigation 导入的。

app/page.js

'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
const router = useRouter()

return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}

有关 useRouter 方法的完整列表,请参阅 API 参考

备注

除非有使用 useRouter 的特定需求,否则请使用 <Link> 组件在路由之间进行导航。

路由和导航的工作原理

App Router 使用混合方法进行路由和导航。 在服务器上,您的应用程序代码会自动按路由段进行代码拆分。 在客户端上,Next.js 会预取和缓存路由段。 这意味着当用户导航到新路由时,浏览器不会重新加载页面,只有更改的路由段会重新渲染, 从而提高导航体验和性能。

  1. 预取 预取是在用户访问之前在后台预加载路由的一种方式。

在Next.js中,有两种路由的预取方式:

  • <Link> 组件:路由在用户视口中变得可见时会自动进行预取。预取发生在页面首次加载时或通过滚动进入视图时。
  • router.prefetch():可以使用 useRouter 钩子以编程方式进行路由的预取。

对于<Link>的预取行为在

静态和动态路由上是不同的:

  • 静态路由:prefetch 默认为 true。整个路由将被预取和缓存。
  • 动态路由:prefetch 默认为 automatic。 只有共享布局直到第一个 loading.js 文件的路由段被预取和缓存,缓存有效期为30秒。 这降低了获取整个动态路由的成本,并且您可以显示即时的加载状态,以提供更好的视觉反馈。

您可以通过将 prefetch 属性设置为 false 来禁用预取。

有关更多信息,请参阅 <Link> API 参考。

备注

prefetch在开发中未启用,只在生产中启用。

  1. 缓存 Next.js具有一个内存中的客户端端缓存, 称为Router Cache。 当用户在应用程序中导航时, 预取 路由段和访问过的路由的React Server Component Payload都存储在缓存中。

这意味着在导航时,尽可能地重用缓存,而不是向服务器发出新请求, 从而通过减少请求和数据传输的次数来提高性能。

了解有关Router Cache 如何工作以及如何配置它的更多信息。

  1. 部分渲染 部分渲染意味着在客户端上只重新渲染在导航时发生变化的路由段,而保留任何共享段。

例如,在两个兄弟路由之间导航,/dashboard/settings/dashboard/analytics, 将呈现settingsanalytics,并保留共享的仪表板布局。 partial rendering

如果没有部分渲染,每次导航都会导致在服务器上重新渲染整个页面。 只渲染发生变化的段减少了传输的数据量和执行时间,从而提高了性能。

  1. 软导航 默认情况下,浏览器在页面之间执行硬导航。 这意味着浏览器重新加载页面并重置React状态,例如应用程序中的useState钩子和浏览器状态, 例如用户的滚动位置或焦点元素。 然而,在Next.js中,App Router 使用软导航。 这意味着React仅渲染更改的段,同时保留React和浏览器状态,而没有完全重新加载页面。

  2. 后退和前进导航 默认情况下,Next.js会保持后退和前进导航的滚动位置, 并在Router Cache 中重新使用路由段。

路由分组

app 目录中,嵌套文件夹通常映射到 URL 路径。 但是,您可以将文件夹标记为路由组,以防止将文件夹包含在路由的 URL 路径中。

这使您能够在不影响 URL 路径结构的情况下,将路由段和项目文件组织成逻辑组。

路由组对以下情况很有用:

约定

可以通过在文件夹名称周围加上括号来创建路由组:(folderName)

示例

在不影响 URL 路径的情况下组织路由

要组织路由而不影响 URL,请创建一个组以将相关路由放在一起。 括号中的文件夹将从 URL 中省略(例如 (marketing) 或 (shop))。 route group organisation

即使 (marketing) 和 (shop) 中的路由共享相同的 URL 层次结构, 您仍可以通过在其文件夹中添加 layout.js 文件为每个组创建不同的布局。 route group multiple layouts

将特定段投入布局

要将特定路由投入布局,请创建一个新的路由组(例如 (shop))并将共享相同布局的路由移到该组中(例如 accountcart)。 组外的路由将不共享布局(例如 checkout)。 route group opt in layouts

创建多个根布局

要创建多个根布局,请删除顶层 layout.js 文件,并在每个路由组中添加 layout.js 文件。 这对于将应用程序分区为具有完全不同 UI 或体验的部分非常有用。 需要在每个根布局中添加 <html><body> 标签。 route group multiple root layouts

在上面的示例中,(marketing) 和 (shop) 都有自己的根布局。

值得知道:

  • 路由组的命名除了组织外,并没有特殊的意义。它们不影响 URL 路径。
  • 包含路由组的路由不应解析为其他路由的相同 URL 路径。 例如,由于路由组不影响 URL 结构,(marketing)/about/page.js(shop)/about/page.js 都将解析为 /about,并导致错误。
  • 如果使用多个根布局而没有顶层 layout.js 文件,首页 page.js 文件应该在路由组中定义, 例如:app/(marketing)/page.js
  • 在多个根布局之间导航将导致完整页面加载(而不是客户端导航)。 例如,从使用 app/(shop)/layout.js/cart 导航到使用 app/(marketing)/layout.js/blog 将导致完整页面加载。这仅适用于多个根布局。

动态路由

当您事先不知道确切的段名称并希望从动态数据创建路由时,可以使用动态段,在请求时填充或在构建时预渲染。

约定

可以通过将文件夹的名称用方括号括起来来创建动态段:[folderName]。例如,[id][slug]

动态段作为 params 属性传递给 layoutpageroutegenerateMetadata函数。

示例

例如,博客可以包含以下路由 app/blog/[slug]/page.js,其中 [slug] 是博客文章的动态段。

app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>;
}
RouteExample URLparams
app/blog/[slug]/page.js/blog/a{ slug: 'a' }
app/blog/[slug]/page.js/blog/b{ slug: 'b' }
app/blog/[slug]/page.js/blog/c{ slug: 'c' }

了解如何生成段的参数,请参阅 generateStaticParams() 页面。

备注

动态段相当于页面目录中的动态路由。

生成静态参数

generateStaticParams 函数可以与 动态路由段 结合使用, 在构建时 静态生成 路由,而不是在请求时按需生成。

app/blog/[slug]/page.tsx

export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json());

return posts.map((post) => ({
slug: post.slug,
}));
}

generateStaticParams 函数的主要优势在于其智能的数据检索。 如果在 generateStaticParams 函数中使用 fetch 请求获取内容, 请求将自动进行记忆化。 这意味着在多个 generateStaticParamsLayoutsPages 之间使用相同参数进行的 fetch 请求只会执行一次,这有助于减少构建时间。

如果要从页面目录进行迁移, 请使用迁移指南

有关更多信息和高级用法,请参阅 generateStaticParams 服务器函数文档

捕获所有段

动态段可以通过在括号内添加省略号 [...folderName] 来扩展为捕获所有后续段。

例如,app/shop/[...slug]/page.js 将匹配 /shop/clothes, 还将匹配 /shop/clothes/tops/shop/clothes/tops/t-shirts 等。

RouteExample URLparams
app/shop/[...slug]/page.js/shop/a{ slug: ['a'] }
app/shop/[...slug]/page.js/shop/a/b{ slug: ['a', 'b'] }
app/shop/[...slug]/page.js/shop/a/b/c{ slug: ['a', 'b', 'c'] }

可选捕获所有段

可以通过在双方括号中包含参数来使捕获所有段变为可选:[[...folderName]]

例如,app/shop/[[...slug]]/page.js 也将匹配 /shop, 除了 /shop/clothes/shop/clothes/tops/shop/clothes/tops/t-shirts 之外。

捕获所有和可选捕获所有段之间的区别在于,在可选情况下,还将匹配不带参数的路由(在上述示例中为 /shop)。

RouteExample URLparams
app/shop/[[...slug]]/page.js/shop{}
app/shop/[[...slug]]/page.js/shop/a{ slug: ['a'] }
app/shop/[[...slug]]/page.js/shop/a/b{ slug: ['a', 'b'] }
app/shop/[[...slug]]/page.js/shop/a/b/c{ slug: ['a', 'b', 'c'] }

TypeScript

在使用 TypeScript 时,可以根据配置的路由段为 params 添加类型。

app/blog/[slug]/page.tsx
export default function Page({ params }: { params: { slug: string } }) {
return <h1>My Page</h1>;
}
Routeparams 类型定义
app/blog/[slug]/page.js{ slug: string }
app/shop/[...slug]/page.js{ slug: string[] }
app/[categoryId]/[itemId]/page.js{ categoryId: string, itemId: string }
备注

将来 TypeScript 插件 可能会自动完成这个操作。

加载 UI 和流式处理

特殊文件 loading.js 可以帮助您使用 React Suspense 创建有意义的加载 UI。 通过这种约定,您可以在路由段的内容加载时显示 即时加载状态。 一旦渲染完成,新内容就会自动替换进来。 loading UI

即时加载状态

即时加载状态是在导航后立即显示的回退 UI。 您可以预渲染加载指示器,如骨架屏和旋转器,或者未来屏幕的一个小但有意义的部分,例如封面照片、标题等。 这有助于用户了解应用正在响应并提供更好的用户体验。

通过在文件夹内添加 loading.js 文件来创建加载状态。 loading special file

app/dashboard/loading.tsx

import LoadingSkeleton from './LoadingSkeleton';

export default function Loading() {
// 可以在 Loading 中添加任何 UI,包括骨架屏。
return <LoadingSkeleton />;
}

在同一文件夹中,loading.js 将嵌套在 layout.js 内。 它将自动将 page.js 文件和下面的任何子元素包装在 <Suspense> 边界内。 loading overview

值得知道:

  • 导航是立即的,即使是在以服务器为中心的路由中。
  • 导航是可中断的,这意味着在完全加载路由内容之前,不需要等待更改路由以导航到另一个路由。
  • 共享布局在加载新路由段时仍然可交互。
提示

在 Next.js 中,建议使用 loading.js 约定为路由段(布局和页面),因为 Next.js 优化了这一功能。

使用 Suspense 进行流式处理

除了 loading.js 外,您还可以为自己的 UI 组件手动创建 Suspense 边界。 App Router 支持在 Node.js 和 Edge 运行时 使用 Suspense 进行流式处理。

什么是流式处理?

要了解 React 和 Next.js 中流式处理的工作原理,理解**服务器端渲染(SSR)**及其局限性是有帮助的。

使用 SSR,需要完成一系列步骤,用户才能看到并与页面交互:

  1. 首先,在服务器上获取给定页面的所有数据。
  2. 然后,服务器渲染页面的 HTML。
  3. 将页面的 HTML、CSS 和 JavaScript 发送到客户端。
  4. 使用生成的 HTML 和 CSS 显示一个非交互式用户界面。
  5. 最后,React 对用户界面进行水合。

server rendering without streaming chart

这些步骤是顺序和阻塞的,这意味着服务器只能在获取所有数据之后才能渲染页面的 HTML。 并且在客户端,React 只能在下载页面中所有组件的代码之后才能进行水合。

结合 React 和 Next.js 的 SSR 通过尽快向用户显示非交互式页面来帮助提高感知加载性能。 server rendering without streaming

流式处理允许您将页面的 HTML 拆分为较小的块,并逐渐将这些块从服务器发送到客户端。

server rendering with streaming

这使页面的某些部分可以更早地显示,而无需等待所有数据加载完毕才能渲染任何 UI。

流式处理与 React 的组件模型配合得很好,因为可以将每个组件视为一个块。 具有较高优先级(例如产品信息)或不依赖于数据的组件(例如布局)可以首先发送,React 可以更早地开始水合。 具有较低优先级(例如评论、相关产品)的组件可以在数据获取后在同一服务器请求中发送。 server rendering with streaming chart

当你想防止长时间的数据请求阻塞页面渲染时,流媒体尤其有用, 因为它可以缩短时间优先于字节(TTFB和 第一个优先绘制内容(FCP。 它还有助于缩短交互时间(TTI, 尤其是在速度较慢的设备上。

示例

<Suspense> 的工作原理是包装一个执行异步操作(例如获取数据)的组件, 在操作发生时显示回退 UI(例如骨架、旋转器),然后在操作完成后交换组件。

app/dashboard/page.tsx
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'

export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}

通过使用 Suspense,您可以获得以下好处:

  • 流式服务器渲染 - 逐步将 HTML 从服务器渲染到客户端。
  • 选择性水合 - React 根据用户交互优先考虑哪些组件首先进行交互。

有关更多 Suspense 示例和用例,请参阅 React 文档

SEO

Next.js 将等待 generateMetadata 中的数据获取完成, 然后再将 UI 流式处理到客户端。 这确保了流式响应的第一部分包含 <head> 标签。

由于流式处理是服务器渲染的,它不会影响 SEO。 您可以使用 Google 的 Mobile Friendly Test 工具查看页面对 Google 网络爬虫的外观, 并查看序列化的 HTML(源代码)。

状态码

在流式处理时,将返回 200 状态码,以表示请求成功。

服务器仍然可以通过流式内容本身向客户端通报错误或问题,例如在使用redirectnotFound 时。 由于响应头已经发送到客户端,响应的状态码无法更新。这不会影响 SEO。

错误处理

error.js 文件约定允许您在 嵌套路由 中优雅地处理意外的运行时错误。

  • 自动将路由段及其嵌套子项包装在 React 错误边界中。
  • 使用文件系统层次结构调整细粒度,为特定段定制错误 UI。
  • 将错误隔离到受影响的段,同时保持应用程序的其余部分正常运作。
  • 添加尝试从错误中恢复而无需完全重新加载页面的功能。

通过在路由段内部添加 error.js 文件并导出一个 React 组件来创建错误 UI:

error special file

app/dashboard/error.tsx

import { useEffect } from 'react';

export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// 将错误记录到错误报告服务
console.error(error);
}, [error]);

return (
<div>
<h2>出了点问题!</h2>
<button
onClick={() =>
// 尝试通过尝试重新渲染段来恢复
reset()
}
>
重试
</button>
</div>
);
}

error.js 的工作原理

error overview

  • error.js 自动创建一个 React 错误边界, 包装一个嵌套的子段或 page.js 组件。
  • error.js 文件导出的 React 组件用作回退组件。
  • 如果在错误边界内引发错误,则错误被包含,将渲染回退组件。
  • 当回退错误组件处于活动状态时,位于错误边界上方的布局保持其状态并保持交互性,错误组件可以显示从错误中恢复的功能。

从错误中恢复

错误的原因有时可能是临时的。在这些情况下,简单地重试可能会解决问题。

错误组件可以使用 reset() 函数提示用户尝试从错误中恢复。 执行该函数时,将尝试重新渲染错误边界的内容。 如果成功,回退错误组件将被重新渲染的结果替换。

app/dashboard/error.tsx
'use client'

export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}

嵌套路由

通过特殊文件创建的 React 组件以特定的嵌套层次结构进行呈现。

例如,具有两个包含 layout.jserror.js 文件的段的嵌套路由以以下简化的组件层次结构进行呈现:

nested error component hierarchy

嵌套组件层次结构对于跨嵌套路由的 error.js 文件的行为有影响:

  • 错误会冒泡到最近的父错误边界。这意味着 error.js 文件将处理所有其嵌套子段的错误。 通过在嵌套路由的不同级别放置 error.js 文件,可以实现更或更少粒度的错误 UI。
  • 错误边界不会处理在同一段中的 layout.js 组件中引发的错误,因为错误边界嵌套在该布局的组件内部。

处理布局中的错误

error.js 边界不会捕获在同一段的 layout.jstemplate.js 组件中引发的错误。 这种有意的层次结构保持了在发生错误时共享的重要 UI(如导航)可见且可交互。

要处理特定布局或模板中的错误,请在布局的父段中放置一个 error.js 文件。

要在根布局或模板中处理错误,请使用名为 global-error.jserror.js 变体。

处理根布局中的错误

app/error.js 边界不会捕获在根 app/layout.jsapp/template.js 组件中引发的错误。

要专门处理这些根组件中的错误,请使用名为 app/global-error.jserror.js 变体,位于根 app 目录中。

与根 error.js 不同,global-error.js 错误边界包装整个应用程序,并在活动时用其回退组件替换根布局。 因此,请注意,global-error.js 必须定义自己的 <html><body> 标签。

global-error.js 是最不粒度的错误 UI,可以被视为整个应用程序的“全能处理”错误处理。 由于根组件通常不太动态,而其他 error.js边界将捕获大多数错误,因此很少触发。

即使定义了 global-error.js,仍建议定义一个根 error.js,其回退组件将在根布局中呈现,其中包括全局共享的 UI 和品牌。

app/global-error.tsx
'use client'

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}

处理服务器错误

如果在 Server Component 内引发错误,Next.js 将将 Error 对象(在生产中剥离了敏感错误信息的对象) 转发到最近的 error.js 文件,作为 error 属性。

保护敏感错误信息

在生产中,转发到客户端的 Error 对象仅包含一个关于错误的通用message和一个名为 digest 的属性。

这是为了防止将错误中包含的潜在敏感细节泄漏给客户端的安全预防措施。

message 属性包含有关错误的通用消息,而 digest 属性包含错误的自动生成哈希,可用于在服务器端日志中匹配相应的错误。

在开发期间,转发到客户端的 Error 对象将被序列化,并包含原始错误的message,以便更轻松地进行调试。

并行路由

并行路由允许您在相同布局中同时或有条件地渲染一个或多个页面。对于应用程序的高度动态部分,例如社交网站上的仪表板和动态信息流,可以使用并行路由来实现复杂的路由模式。

例如,您可以同时渲染团队和分析页面。

parallel routes

并行路由允许您为每个路由在其独立流入时定义独立的错误和加载状态。

parallel routes cinematic universe

并行路由还允许您基于特定条件(如身份验证状态)有条件地渲染一个插槽。这使得可以在相同的 URL 上完全分离的代码。

conditional routes UI

约定

使用命名插槽创建并行路由。插槽使用 @folder 约定定义,并作为 props 传递给相同级别的布局。

插槽不是路由段,不会影响 URL 结构。文件路径 /@team/members 将在 /members 处可访问。

例如,以下文件结构定义了两个明确的插槽:@analytics@team

parallel routes file system

上面的文件结构意味着 app/layout.js 中的组件现在接受 @analytics@team 插槽的 props, 并可以与 children prop 一起并行渲染:

app/layout.tsx
export default function Layout(props: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{props.children}
{props.team}
{props.analytics}
</>
)
}
备注

children prop 是一个隐式插槽,无需映射到文件夹。 这意味着 app/page.js 等同于 app/@children/page.js

不匹配的路由

默认情况下,插槽内部呈现的内容将匹配当前 URL。

对于不匹配的插槽,Next.js 渲染的内容会根据路由技术和文件夹结构而异。

default.js

可以定义一个 default.js 文件,作为 Next.js 无法基于当前 URL 恢复插槽的活动状态时的回退呈现。

考虑以下文件结构。@team 插槽有一个 settings 目录,但 @analytics 没有。

parallel routes unmatched routes

导航

在导航时,Next.js 将呈现插槽先前的活动状态,即使它与当前 URL 不匹配。

重新加载

在重新加载时,Next.js 将首先尝试渲染不匹配插槽的 default.js 文件。如果不可用,则呈现一个 404。

备注

不匹配路由的 404 有助于确保您不会意外地呈现不应该并行呈现的路由。

useSelectedLayoutSegment(s)

useSelectedLayoutSegmentuseSelectedLayoutSegments 都接受一个 parallelRoutesKey,它允许您读取该插槽中的活动路由段。

app/layout.tsx
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default async function Layout(props: {
//...
auth: React.ReactNode
}) {
const loginSegments = useSelectedLayoutSegment('auth')
// ...
}

当用户导航到 @auth/login 或 URL 栏中的 /login 时,loginSegments 将等于字符串 "login"

示例

模态框

并行路由可用于呈现模态框。

parallel routes auth modal

@auth 插槽呈现一个 <Modal> 组件,可以通过导航到匹配的路由(例如 /login)来显示。

app/layout.tsx
export default async function Layout(props: {
// ...
auth: React.ReactNode
}) {
return (
<>
{/* ... */}
{props.auth}
</>
)
}
app/@auth/login/page.tsx
import { Modal } from 'components/modal'

export default function Login() {
return (
<Modal>
<h1>Login</h1>
{/* ... */}
</Modal>
)
}

为了确保在模态框不活动时不渲染其内容,您可以创建一个返回 nulldefault.js 文件。

app/@auth/default.tsx
export default function Default() {
return null
}

关闭模态框

如果模态框是通过客户端导航启动的,例如使用 <Link href="/login">, 您可以通过调用 router.back() 或使用 Link 组件来关闭模态框。

app/@auth/login/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import { Modal } from 'components/modal'

export default async function Login() {
const router = useRouter()
return (
<Modal>
<span onClick={() => router.back()}>Close modal</span>
<h1>Login</h1>
...
</Modal>
)
}
备注

有关模态框的更多信息请参见截获路由 一节。

如果要导航到其他地方并关闭模态框,还可以使用 catch-all 路由。

parallel routes catchal

app/@auth/[...catchAll]/page.tsx
export default function CatchAll() {
return null
}
备注

catch-all 路由优先于 default.js

条件路由

并行路由可用于实现条件路由。例如,您可以根据身份验证状态呈现 @dashboard@login 路由。

app/layout.tsx
import { getUser } from '@/lib/auth'

export default function Layout({
dashboard,
login,
}: {
dashboard: React.ReactNode
login: React.ReactNode
}) {
const isLoggedIn = getUser()
return isLoggedIn ? dashboard : login
}

conditional routes UI

拦截路由

拦截路由允许您在当前布局中从应用程序的另一部分加载路由。 当您希望在不切换到不同上下文的情况下显示路由内容时,这种路由范例可能很有用。

例如,当单击源中的照片时,您可以在模式中显示照片,覆盖源。 在本例中,Next.js 拦截 /photo/123 路由,屏蔽 URL,并将其覆盖在 /feed 上。

intercepting routes soft navigate

但是,通过单击可共享的URL或刷新页面导航到照片时,应该渲染整个照片页面而不是模态框。 不应发生路由拦截。

intercepting routes hard navigate

约定

拦截路由可以使用 (..) 约定定义,类似于相对路径约定 ../,但用于段。

您可以使用:

  • (.) 匹配同一级别上的段
  • (..) 匹配上一级别的段
  • (..)(..) 匹配上两级别的段
  • (...) 匹配来自根 app 目录的段

例如,您可以通过创建 (..)photo 目录从信息流段内部拦截photo段。

intercepted routes files

备注

请注意,(..) 约定基于路由段,而不是文件系统。

示例

模态框

拦截路由可以与并行路由一起使用,创建模态框。

使用此模式创建模态框克服了在处理模态框时的一些常见挑战,使您能够:

  • 通过URL共享模态框内容
  • 在刷新页面时保留上下文,而不是关闭模态框
  • 在向后导航时关闭模态框,而不是转到上一个路由
  • 在向前导航时重新打开模态框

intercepted routes modal example

在上面的示例中,路径到照片段可以使用 (..) 匹配器, 因为 @modal 是一个插槽而不是一个段。 这意味着尽管在文件系统中有两个级别,但照片路由仅高出一个段级别。

其他示例可能包括在顶部导航栏中打开登录模态框, 同时还有一个专用的 /login 页面,或在侧边模态框中打开购物车。

查看带有拦截和并行路由的模态框的示例

路由处理程序

路由处理程序允许您使用 Web 请求响应 API 为给定的路由创建自定义请求处理程序。

route special file

备注

路由处理程序仅在 app 目录内可用。 它们相当于页面目录内的 API 路由,这意味着您不需要同时使用 API 路由和路由处理程序。

约定

路由处理程序在 app 目录内的 route.js|ts 文件中定义:

app/api/route.ts

export const dynamic = 'force-dynamic'; // 默认为 auto
export async function GET(request: Request) {}

路由处理程序可以嵌套在 app 目录内,类似于 page.jslayout.js。 但是,在与 page.js 相同的路由段级别上不能有 route.js 文件。

支持的 HTTP 方法

支持以下 HTTP 方法GETPOSTPUTPATCHDELETEHEADOPTIONS。如果调用了不受支持的方法,Next.js 将返回 405 Method Not Allowed 响应。

扩展的 NextRequest 和 NextResponse API

除了支持原生的 RequestResponse 外,Next.js 还通过 NextRequestNextResponse 对它们进行了扩展, 以提供高级用例的便利助手。

行为

缓存

使用 Response 对象的 GET 方法时,路由处理程序默认会进行缓存。

app/items/route.ts

export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
});
const data = await res.json();

return Response.json({ data });
}
备注

Response.json() 仅在 TypeScript 5.2 及更高版本中有效。 如果使用较低版本的 TypeScript,可以使用 NextResponse.json() 代替, 以获取类型化的响应。

退出缓存

您可以通过以下方式退出缓存:

  • 使用带有 GET 方法的 Request 对象。
  • 使用任何其他 HTTP 方法。
  • 使用动态函数,如 cookiesheaders
  • 通过手动指定动态模式的 Segment Config 选项

例如:

app/products/api/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const res = await fetch(`https://data.mongodb-api.com/product/${id}`, {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY!,
},
});
const product = await res.json();

return Response.json({ product });
}

类似地,使用 POST 方法将导致动态评估路由处理程序。

app/items/route.ts
export async function POST() {
const res = await fetch('https://data.mongodb-api.com/...', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY!,
},
body: JSON.stringify({ time: new Date().toISOString() }),
});

const data = await res.json();

return Response.json(data);
}
备注

与 API 路由一样,路由处理程序可用于处理表单提交等情况。 正在开发一个新的用于处理表单和变更的抽象,与 React 深度集成。

路由解析

您可以将路由视为最低级别的route原语。

  • 它们不参与布局或客户端导航,如 page
  • 在与 page.js 相同的路由上不能有 route.js 文件。
PageRouteResult
app/page.jsapp/route.jsConflict
app/page.jsapp/api/route.jsValid
app/[user]/page.jsapp/api/route.jsValid

每个route.jspage.js 文件都会接管该路由的所有HTTP 动词。

app/page.js
export default function Page() {
return <h1>Hello, Next.js!</h1>;
}

// ❌ 冲突
// `app/route.js`
export async function POST(request) {}

示例

以下示例显示了如何将路由处理程序与其他 Next.js API 和功能结合使用。

重新验证缓存数据

您可以使用 next.revalidate 选项重新验证缓存的数据:

app/items/route.ts
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
next: { revalidate: 60 }, // 每 60 秒重新验证一次
});
const data = await res.json();

return Response.json(data);
}

或者,您可以使用 revalidate 段配置选项

export const revalidate = 60;

动态函数

路由处理程序可以与 Next.js 的动态函数一起使用,如 cookiesheaders

Cookies

您可以使用 next/headers 中的 cookies 读取 cookies。 此服务器函数可以直接在路由处理程序中调用,也可以嵌套在另一个函数中。

这个 cookies 实例是只读的。 要设置 cookies,您需要使用 Set-Cookie 头返回一个新的 Response

app/api/route.ts

import { cookies } from 'next/headers';

export async function GET(request: Request) {
const cookieStore = cookies();
const token = cookieStore.get('token');

return new Response('Hello, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${token.value}` },
});
}

或者,您可以使用底层 Web API 上的抽象来读取 cookies (NextRequest):

app/api/route.ts
import type { NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
const token = request.cookies.get('token');
}
Headers

您可以使用 next/headers 中的 headers 读取 headers。 此服务器函数可以直接在路由处理程序中调用,也可以嵌套在另一个函数中。

这个 headers 实例是只读的。 要设置 headers,您需要使用新的 headers 返回一个新的 Response

app/api/route.ts
import { headers } from 'next/headers';

export async function GET(request: Request) {
const headersList = headers();
const referer = headersList.get('referer');

return new Response('Hello, Next.js!', {
status: 200,
headers: { referer: referer },
});
}

或者,您可以使用底层 Web API 上的抽象来读取 headers (NextRequest):

app/api/route.ts
import type { NextRequest } from 'next/server';

export async function GET(request: NextRequest) {
const requestHeaders = new Headers(request.headers);
}

重定向

app/api/route.ts
import { redirect } from 'next/navigation';

export async function GET(request: Request) {
redirect('https://nextjs.org/');
}

动态路由段

备注

我们建议在继续之前阅读定义路由页面。

路由处理程序可以使用 动态段 创建来自动态数据的请求处理程序。

app/items/[slug]/route.ts
export async function GET(
request: Request,
{ params }: { params: { slug: string } }
) {
const slug = params.slug; // 'a', 'b', 或 'c'
}
RouteExample URLparams
app/items/[slug]/route.js/items/a{ slug: 'a' }
app/items/[slug]/route.js/items/b{ slug: 'b' }
app/items/[slug]/route.js/items/c{ slug: 'c' }

URL 查询参数

传递给路由处理程序的请求对象是一个 NextRequest 实例, 它具有 一些附加的方便方法, 包括更轻松地处理查询参数。

app/api/search/route.ts
import type { NextRequest } from 'next/server';

export function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('query');
// 对于 /api/search?query=hello,查询是 "hello"
}

流式传输

流式传输通常与大型语言模型(LLM)结合使用,例如 OpenAI,用于生成的 AI 内容。 了解有关 AI SDK 的更多信息。

app/api/chat/route.ts
import OpenAI from 'openai';
import { OpenAIStream, StreamingTextResponse } from 'ai';

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

export const runtime = 'edge';

export async function POST(req: Request) {
const { messages } = await req.json();
const response = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
stream: true,
messages,
});

const stream = OpenAIStream(response);

return new StreamingTextResponse(stream);
}

这些抽象使用 Web API 创建流。您也可以直接使用底层 Web API。

app/api/route.ts

// https://developer.mozilla.org/docs/Web/API/ReadableStream#convert_async_iterator_to_stream
function iteratorToStream(iterator: any) {
return new ReadableStream({
async pull(controller) {
const { value, done } = await iterator.next();

if (done) {
controller.close();
} else {
controller.enqueue(value);
}
},
});
}

function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
}

const encoder = new TextEncoder();

async function* makeIterator() {
yield encoder.encode('<p>One</p>');
await sleep(200);
yield encoder.encode('<p>Two</p>');
await sleep(200);
yield encoder.encode('<p>Three</p>');
}

export async function GET() {
const iterator = makeIterator();
const stream = iteratorToStream(iterator);

return new Response(stream);
}

请求主体

您可以使用标准的 Web API 方法读取请求主体:

app/items/route.ts
export async function POST(request: Request) {
const res = await request.json();
return Response.json({ res });
}

请求主体 FormData

您可以使用 request.formData() 函数读取 FormData

app/items/route.ts
export async function POST(request: Request) {
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
return Response.json({ name, email });
}

由于 formData 数据都是字符串, 您可能希望使用 zod-form-data 来验证请求并以您喜欢的格式检索数据(例如,number)。

CORS

您可以使用标准的 Web API 方法在 Response 上设置 CORS 头:

app/api/route.ts
export const dynamic = 'force-dynamic'; // 默认为 auto

export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}

Edge 和 Node.js 运行时

路由处理程序具有同构的 Web API,以无缝支持 Edge 和 Node.js 运行时,包括对流的支持。 由于路由处理程序使用与页面和布局相同的路由段配置, 因此它们支持期待已久的通用静态重新生成路由处理程序的功能。

您可以使用 runtime 段配置选项指定运行时:

export const runtime = 'edge'; // 'nodejs' 是默认值

非 UI 响应

您可以使用路由处理程序返回非 UI 内容。 请注意,sitemap.xmlrobots.txtapp icons和开放图像都具有内置支持。

app/rss.xml/route.ts

export const dynamic = 'force-dynamic'; // 默认为 auto

export async function GET() {
return new Response(`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">

<channel>
<title>Next.js Documentation</title>
<link>https://nextjs.org/docs</link>
<description>The React Framework for the Web</description>
</channel>

</rss>`);
}

段配置选项

路由处理程序使用与页面和布局相同的路由段配置。

app/items/route.ts
export const dynamic = 'auto';
export const dynamicParams = true;
export const revalidate = false;
export const fetchCache = 'auto';
export const runtime = 'nodejs';
export const preferredRegion = 'auto';

有关更多详细信息,请参阅 API 参考

中间件

中间件允许您在请求完成之前运行代码。 然后,基于传入的请求,您可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。

中间件在缓存内容和路由匹配之前运行。有关更多详细信息, 请参阅匹配路径

约定

在项目的根目录中使用 middleware.ts(或 .js) 文件来定义中间件。 例如,在与 pagesapp 相同的级别,或者如果适用,内部使用 src

示例

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 如果在内部使用 await,则此函数可以标记为 `async`
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}

// 请查看下面的“匹配路径”以了解更多信息
export const config = {
matcher: '/about/:path*',
}

匹配路径

中间件将为项目中的每个路由调用。以下是执行顺序:

  1. 来自 next.config.jsheaders
  2. 来自 next.config.jsredirects
  3. 中间件(rewritesredirects等)
  4. 来自 next.config.jsbeforeFilesrewrites
  5. 文件系统路由(public/_next/static/pages/app/ 等)
  6. 来自 next.config.jsafterFilesrewrites
  7. 动态路由(/blog/[slug]
  8. 来自 next.config.jsfallbackrewrites

有两种定义中间件将在哪些路径上运行的方法:

  1. 自定义匹配器配置
  2. 条件语句

匹配器

matcher 允许您筛选要在特定路径上运行的中间件。

middleware.js
export const config = {
matcher: '/about/:path*',
}

您可以使用数组语法匹配单个路径或多个路径:

middleware.js
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher 配置允许使用完整的正则表达式,因此支持负向先行断言或字符匹配等匹配。 负向先行断言的示例可见于此:

middleware.js
export const config = {
matcher: [
/*
* 匹配除以下路径之外的所有请求路径:
* - api(API 路由)
* - _next/static(静态文件)
* - _next/image(图像优化文件)
* - favicon.ico(favicon 文件)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
备注

matcher 的值需要是常量,以便在构建时进行静态分析。动态值,如变量,将被忽略。

配置的匹配器:

  • 必须以 / 开头
  • 可包含命名参数:/about/:path 匹配 /about/a/about/b,但不匹配 /about/a/c
  • 可对命名参数(以 : 开头)进行修改:/about/:path* 匹配 /about/a/b/c, 因为 * 是零个或多个。? 是零个或一个,+ 是一个或多个
  • 可使用括号括起的正则表达式:/about/(.*)/about/:path* 相同
  • 阅读有关 path-to-regexp 文档的更多详细信息。
备注

出于向后兼容性考虑,Next.js 总是将 /public 视为 /public/index。 因此,/public/:path 的匹配器将匹配。

条件语句

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}

if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}

NextResponse

NextResponse API 允许您:

  • 将传入请求redirect到不同的 URL
  • 通过显示给定的 URL rewrite响应
  • 为 API 路由、getServerSidePropsrewrite目标设置请求头
  • 设置响应 cookies
  • 设置响应头

要从中间件生成响应,您可以:

  • rewrite到生成响应的路由(Page 或 Route Handler)
  • 直接返回一个 NextResponse。查看生成响应

使用 Cookies

Cookies 是常规头。在Request中,它们存储在 Cookie 头中。 在Response中,它们在 Set-Cookie 头中。 Next.js 通过 NextRequestNextResponse 上的 cookies 扩展提供了一种方便的方法来访问和操作这些 cookies。

  1. 对于传入请求,cookies 具有以下方法:getgetAllsetdelete cookies。 您可以使用 has 检查是否存在 cookie,使用 clear 删除所有 cookie。

  2. 对于输出响应,cookies 具有以下方法:getgetAllsetdelete

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
// 假设“Cookie:nextjs=fast”头在传入请求中存在
// 使用 `RequestCookies` API 从请求中获取 cookies
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]

request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false

// 使用 `ResponseCookies` API 在响应中设置 cookies
const response = NextResponse.next()


response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// 出站响应将具有 `Set-Cookie:vercel=fast;path=/test` 头。

return response
}

设置 Headers

您可以使用 NextResponse API 设置请求和响应头(设置请求头在 Next.js v13.0.0+ 中可用)。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
// 克隆请求头并设置新头 `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')

// 您还可以在 NextResponse.rewrite 中设置请求头
const response = NextResponse.next({
request: {
// 新的请求头
headers: requestHeaders,
},
})

// 设置新的响应头 `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
备注

避免设置大型头, 因为这可能会导致 431 Request Header Fields Too Large 错误, 具体取决于您的后端 Web 服务器配置。

生成响应

您可以直接从中间件中返回 ResponseNextResponse 实例来直接响应(从 Next.js v13.1.0+ 开始可用)。

middleware.ts
import { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// 限制中间件路径以 `/api/` 开头
export const config = {
matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
// 调用我们的身份验证函数来检查请求
if (!isAuthenticated(request)) {
// 使用 JSON 响应指示错误消息
return Response.json(
{ success: false, message: 'authentication failed' },
{ status: 401 }
)
}
}

waitUntilNextFetchEvent

NextFetchEvent 对象扩展了本机 FetchEvent对象, 并包括 waitUntil() 方法。

waitUntil() 方法接受一个 Promise 作为参数,并扩展中间件的生命周期,直到 Promise 结束。这对于在后台执行工作非常有用。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export function middleware(req: NextRequest, event: NextFetchEvent) {
event.waitUntil(
fetch('https://my-analytics-platform.com', {
method: 'POST',
body: JSON.stringify({ pathname: req.nextUrl.pathname }),
})
)

return NextResponse.next()
}

高级中间件标志

在 Next.js v13.1 中, 引入了两个用于中间件的附加标志, skipMiddlewareUrlNormalizeskipTrailingSlashRedirect, 用于处理高级用例。

skipTrailingSlashRedirect 允许禁用 Next.js 默认的添加或删除尾随斜杠的重定向, 允许在中间件内自定义处理,从而允许保留某些路径的尾随斜杠,但不允许保留其他路径的斜杠, 从而更容易进行渐进式迁移。

next.config.js
module.exports = {
skipTrailingSlashRedirect: true,
}
middleware.js
const legacyPrefixes = ['/docs', '/blog']

export default async function middleware(req) {
const { pathname } = req.nextUrl

if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}

// 应用尾随斜杠处理
if (
!pathname.endsWith('/') &&
!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
) {
req.nextUrl.pathname += '/'
return NextResponse.redirect(req.nextUrl)
}
}

skipMiddlewareUrlNormalize 允许禁用 Next.js 执行的 URL 规范化, 以使直接访问和客户端转换的处理相同。 在一些高级情况下,您需要使用原始 URL 进行完全控制,这个标志允许这样做。

next.config.js
module.exports = {
skipMiddlewareUrlNormalize: true,
}
middleware.js
export default async function middleware(req) {
const { pathname } = req.nextUrl

// GET /_next/data/build-id/hello.json

console.log(pathname)
// 使用该标志,现在这是 /_next/data/build-id/hello.json
// 没有该标志,这将被规范化为 /hello
}

运行时

中间件目前仅支持 Edge 运行时。 不能使用 Node.js 运行时。

版本历史

  • v13.1.0:引入了高级中间件标志
  • v13.0.0:中间件可以修改请求头、响应头,并发送响应
  • v12.2.0:中间件稳定,请参阅升级指南
  • v12.0.9:在 Edge 运行时强制使用绝对 URL(PR)
  • v12.0.0:添加中间件(Beta)

项目组织和文件共存

除了路由文件夹和文件的约定 之外,Next.js 不对项目文件的组织和共存方式提出意见。

本页分享了项目组织的默认行为和可用于组织项目的特性。

默认情况下的安全共存

通过默认设置,在 app 目录中,嵌套的文件夹层次结构定义了路由结构。

每个文件夹表示一个路由片段,映射到 URL 路径中的相应部分。

但是,尽管通过文件夹定义了路由结构, 但只有在向路由片段添加 page.jsroute.js 文件后,路由才是公开可访问的。 project organization not routable

即使将路由公开,也只有 page.jsroute.js 返回的内容会发送到客户端。 project organization routable

这意味着项目文件可以安全地共存在 app 目录中的路由片段中,而不会意外成为可路由的。 project organization colocation

了解更多:

  • 这与 pages 目录不同,其中 pages 中的任何文件都被视为路由。
  • 虽然可以在 app 中共存项目文件,但不是必须的。如果愿意,可以将它们保留在 app 目录之外。

项目组织的特性

Next.js 提供了几个功能来帮助您组织项目。

私有文件夹

通过在文件夹名称前加下划线(_folderName)创建私有文件夹。

这表示该文件夹是一个私有实现细节,不应被路由系统考虑, 从而将该文件夹及其所有子文件夹排除在路由之外。

project organization private folders

由于默认情况下可以安全地在 app 目录中共存文件,因此通常不需要私有文件夹。 但是,它们对于以下情况可能会有用:

  • 将 UI 逻辑与路由逻辑分离。
  • 在整个项目和 Next.js 生态系统中一致地组织内部文件。
  • 在代码编辑器中排序和分组文件。
  • 避免与未来 Next.js 文件约定的潜在命名冲突。

了解更多:

  • 虽然不是框架约定,但您可能还考虑使用相同的下划线模式将私有文件夹之外的文件标记为“private”。
  • 您可以通过在文件夹名称前缀为 %5F(下划线的 URL 编码形式:%5FfolderName)来创建以下划线开头的 URL 片段。
  • 如果不使用私有文件夹,了解 Next.js 的特殊文件约定将有助于防止意外的命名冲突。

路由组

通过用括号括起来的方式创建路由组:(folderName)。

这表示该文件夹用于组织目的,不应包含在路由的 URL 路径中。

project organization route groups

路由组对于以下情况很有用:

  • 将路由组织成组,例如按站点部分、意图或团队组织。
  • 在同一路由片段级别启用嵌套布局:
    • 在同一片段中创建多个嵌套布局,包括多个根布局。
    • 在常见片段中的一组路由中添加布局。

src 目录

Next.js 支持将应用代码(包括 app)存储在可选的 src 目录中。 这将应用代码与主要存储在项目根目录中的项目配置文件分离。

project organization src directorydex.js

模块路径别名

Next.js 支持模块路径别名,使得在深层嵌套的项目文件中更易于阅读和维护导入。

示例:

app/dashboard/settings/analytics/page.js
// before
import { Button } from '../../../components/button'

// after
import { Button } from '@/components/button'

项目组织策略

在组织 Next.js 项目中的文件和文件夹时,没有“正确”或“错误”的方式。

以下部分概述了常见策略的高层次概述。最简单的方法是选择适合您和您的团队的策略,并在整个项目中保持一致。

备注

在下面的示例中,我们使用 componentslib 文件夹作为通用占位符, 它们的命名没有特殊的框架意义,您的项目可能使用其他文件夹, 如 uiutilshooksstyles 等。

将项目文件存储在 app 之外

此策略将所有应用程序代码存储在项目根目录中的共享文件夹中, 并将 app 目录纯粹用于路由目的。

project organization project root

app 内的顶级文件夹中存储项目文件

此策略将所有应用程序代码存储在 app 目录的共享文件夹中。

project organization app root

按功能或路由拆分项目文件

此策略在根 app 目录中存储全局共享的应用程序代码, 并将更具体的应用程序代码拆分到使用它们的路由片段中。

project organization app root split

国际化

Next.js 可以配置路由和内容的渲染,以支持多语言。 使您的站点适应不同区域设置包括翻译内容(本地化)和国际化路由。

术语

  • Locale(语言环境):语言和格式首选项的标识符。通常包括用户的首选语言和可能的地理区域。
    • en-US:美国英语
    • nl-NL:荷兰语(荷兰)
    • nl:荷兰语,无特定区域

路由概览

推荐使用浏览器中用户的语言首选项来选择要使用的语言环境。 更改首选语言将修改应用程序接收的 Accept-Language 标头。

例如,使用以下库,您可以查看传入的Request,以确定要选择的语言环境, 基于 Headers、您计划支持的语言环境和默认语言环境。

middleware.js
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

let headers = { 'accept-language': 'en-US,en;q=0.5' }
let languages = new Negotiator({ headers }).languages()
let locales = ['en-US', 'nl-NL', 'nl']
let defaultLocale = 'en-US'

match(languages, locales, defaultLocale) // -> 'en-US'

路由可以通过子路径(/fr/products)或域名(my-site.fr/products)进行国际化。 有了这些信息,现在可以根据语言环境在中间件中重定向用户。

middleware.js
let locales = ['en-US', 'nl-NL', 'nl']

// 获取首选语言环境,类似于上面的方法或使用库
function getLocale(request) { ... }

export function middleware(request) {
// 检查路径名中是否有任何受支持的语言环境
const { pathname } = request.nextUrl
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)

if (pathnameHasLocale) return

// 如果没有语言环境,则重定向
const locale = getLocale(request)
request.nextUrl.pathname = `/${locale}${pathname}`
// 例如,传入请求为 /products
// 新 URL 现在是 /en-US/products
return Response.redirect(request.nextUrl)
}

export const config = {
matcher: [
// 跳过所有内部路径(_next)
'/((?!_next).*)',
// 可选:仅在根目录(/)URL 上运行
// '/'
],
}

最后,确保 app/ 内的所有特殊文件都嵌套在 app/[lang] 下。 这样,Next.js 路由器可以动态处理路由中的不同语言环境, 并将 lang 参数传递到每个布局和页面。例如:

app/[lang]/page.js
// 现在可以访问当前的语言环境
// 例如 /en-US/products -> “lang” 是 "en-US"
export default async function Page({ params: { lang } }) {
return ...
}

根布局也可以嵌套在新文件夹中(例如 app/[lang]/layout.js)。

本地化

根据用户首选语言环境更改显示的内容,或本地化,不是 Next.js 特有的事物。 下面描述的模式在任何 Web 应用程序中都可以使用。

假设我们想在应用程序内支持英语和荷兰语内容。 我们可能会维护两个不同的“字典”, 这些字典是为我们提供从某个键到本地化字符串的映射的对象。 例如:

dictionaries/en.json
{
"products": {
"cart": "Add to Cart"
}
}
dictionaries/nl.json
{
"products": {
"cart": "Toevoegen aan Winkelwagen"
}
}

然后,我们可以创建一个 getDictionary 函数来加载请求的语言环境的翻译:

app/[lang]/dictionaries.js
import 'server-only'

const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
}

export const getDictionary = async (locale) => dictionaries[locale]()

给定当前选择的语言,我们可以在布局或页面中获取字典。

app/[lang]/page.js
import { getDictionary } from './dictionaries'

export default async function Page({ params: { lang } }) {
const dict = await getDictionary(lang) // en
return <button>{dict.products.cart}</button> // Add to Cart
}

由于 app/ 目录中的所有布局和页面都默认为 Server Components, 我们不需要担心翻译文件的大小会影响客户端 JavaScript 包的大小。 此代码将仅在服务器上运行,并且只会将生成的 HTML 发送到浏览器。

静态生成

为给定的一组语言环境生成静态路由,可以使用 generateStaticParams 与任何页面或布局。 这可以是全局的,例如,在根布局中:

app/[lang]/layout.js
export async function generateStaticParams() {
return [{ lang: 'en-US' }, { lang: 'de' }]
}

export default function Root({ children, params }) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
}

资源