Next.js 学习笔记(三)——路由
路由
路由基础知识
每个应用程序的骨架都是路由。本页将向你介绍互联网路由的基本概念以及如何在 Next.js 中处理路由。
术语
首先,你将在整个文档中看到这些术语的使用情况。以下是一个快速参考:
- 树(Tree):用于可视化层次结构的约定。例如:具有父组件和子组件的组件树、文件夹结构等
- 子树(Subtree):树的一部分,从新的根开始(第一个),到叶子结束(最后一个)
- 根(Root):树或子树中的第一个节点,例如:根布局(root layout)
- 叶(Leaf):子树中没有子级的节点,例如:URL 路径中的最后一段
- 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 页面。
文件夹和文件的角色
Next.js 使用基于文件系统的路由器,其中:
- 文件夹用于定义路由。路由是嵌套文件夹的单一路径,遵循文件系统层次结构,从根文件夹向下到包括
page.js
文件的最终叶文件夹。请参见Defining Routers。 - 文件用于创建为路由段显示的 UI。请参阅特殊文件。
路由段
路由中的每个文件夹都表示一个路由段。每个路由段都映射到 URL 路径中的相应段。
嵌套路由
若要创建嵌套路由,可以将文件夹嵌套在彼此内部。例如,你可以通过在 app
目录中嵌套两个新文件夹来添加新的 /dashboard/settings
路由。
dashboard/settings
路由由三个部分组成:
/
(根段)dashboard
(段)settings
(叶段)
文件约定
Next.js 提供了一组特殊的文件来创建嵌套路由中具有特定行为的 UI:
文件名 | 说明 |
---|---|
layout | 段及其子级的共享 UI |
page | 路由的唯一 UI,并使路由可公开访问 |
loading | Loading 段及其子级的 UI |
not-found | 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 suspense 边界)not-found.js
(React 错误边界)page.js
或嵌套layout.js
在嵌套路由中,段的组件将嵌套在其父段的组件内。
合并
除了特殊文件外,你还可以选择将自己的文件(如:组件、样式、测试等)合并到 app
目录中的文件夹中。
这是因为当文件夹定义路由时,只有 page.js
或 route.js
返回的内容是可公开寻址的。
了解有关项目组织和托管的更多信息。
高级路由模式
App Router 还提供了一组约定,以帮助你实现更高级的路由模式。其中包括:
- 并行路由:允许你在同一视图中同时显示两个或多个可以独立导航的页面。你可以将它们用于具有自己的子导航的拆分视图。例如:Dashboards。
- 拦截路由:允许你拦截一条路由,并将其显示在另一条路由的上下文中。当保留当前页面的上下文很重要时,可以使用这些内容。例如:在编辑一个任务或在提要中展开照片时查看所有任务。
这些模式允许你构建更丰富、更复杂的 UI,使小团队和个人开发人员实现的历史上复杂的功能民主化。
定义路由
本页将指导你如何在 Next.js 应用程序中定义和组织路由。
创建路由
Next.js 使用基于文件系统的路由器,其中文件夹用于定义路由。
每个文件夹代表一个映射到 URL 段的路由段。若要创建嵌套路由,可以将文件夹嵌套在彼此内部。
一个特殊的 page.js
文件用于使路由段可以公开访问。
在本例中,/dashboard/analytics
URL 路径不可公开访问,因为它没有相应的 page.js
文件。此文件夹可用于存储组件、样式表、图像或其他共有文件。
b:
.js
、.jsx
或.tsx
文件扩展名可以用于特殊文件。
创建 UI
特殊的文件约定用于为每个路由段创建 UI。最常见的是显示路由唯一 UI 的页面,以及显示跨多个路由共享的 UI 的布局。
例如:要创建第一个页面,请在 app
目录中添加 page.js
文件,然后导出 React 组件:
// app/page.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
页面与布局
Next.js 13 中的 App Router 引入了新的文件约定,可以轻松创建页面、共享布局和模板。本页将指导你如何在 Next.js 应用程序中使用这些特殊文件。
页面
页面是路由唯一的 UI。你可以通过从 page.js
文件导出组件来定义页面。使用嵌套文件夹定义路由,并使用 page.js
文件使路由可以公开访问。
通过在 app
目录中添加 page.js
文件来创建您的第一个页面:
// app/page.tsx
// app/page.tsx 是 / 路径的 UI
export default function Page() {
return <h1>Hello, Home page!</h1>
}
// app/dashboard/page.tsx
// app/dashboard/page.tsx 是 /dashboard 路径的 UI
export default function Page() {
return <h1>Hello, Dashboard Page!</h1>
}
需要知道:
- 页面始终是路由子树的叶子
- 页面的文件类型应该为
.js
、.jsx
、.tsx
- 需要一个
page.js
文件才能公开一个路由段- 默认情况下,页面是 Server Components,但可以设置为 Client Component
- 页面可以获取数据,请查看 Data Fetching 部分了解更多信息
布局
布局是在多个页面之间共享的 UI。在导航时,布局会保留状态,保持交互式,并且不会重新渲染。布局也可以嵌套。
你可以通过 default
导出从 layout.js
文件导出 React 组件来定义布局。组件应接收 children
prop,该 prop 将在渲染过程中填充子布局(如果存在)或子页面。
// app/dashboard/layout.tsx
export default function DashboardLayout({
children, // children 是一个页面或嵌套布局
}: {
children: React.ReactNode
}) {
return (
<section>
{/* 在此处包括共享 UI,例如:header 或 sidebar */}
<nav></nav>
{children}
</section>
)
}
需要知道:
- 最顶层的布局叫做根布局,这个必需的布局被一个应用中的所有页面共享,根布局必须包含
html
和body
标签- 任何路由段可以定义它自己的布局,这些布局将会在该路由的所有页面中共享
- 默认情况下,一个路由中的布局可以被嵌套,每个父布局都使用 React 的
children
prop 将子布局包裹在其下方- 可以使用路由组(Route Groups)来选择特定的路由进和出共享布局
- 默认情况下,布局是服务器组件,但可以设置为客户端组件
- 布局可以获取数据,查看 Data Fetching 部分了解更多信息
- 无法在父、子布局之间传递数据,但是可以在一个路由中多次请求相同的数据,并且 React 会自动对请求进行重复数据删除,而不会影响性能
- 布局无法访问自身路径下的路由,为了访问所有的路由,可以在客户端组件中使用
useSelectedLayoutSegment
或者useSelectedLayoutSegments
- 布局的文件类型应该为
.js
、.jsx
或.tsx
- 可以在同一个文件夹中定义
layout.js
和page.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>
元素- 可以使用路由组来创建多个根布局,在这查看一个例子
- 默认情况下,根布局是一个服务器组件,并且不能设置为客户端组件
从
pages
目录迁移:根布局替换_app.js
和_document.js
文件。查看迁移导游。
嵌套布局
文件夹内定义的布局(例如:app/dashboard/layout.js
)应用于特定的路由段(例如:acme.com/dashboard
),并在这些段处于活动状态时进行渲染。默认情况下,文件层次结构中的布局是嵌套的,这意味着它们通过其 children
prop 包装子布局。
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
需要知道:
- 仅仅根布局可以包含
<html>
和<body>
标签
如果你将上面的两个布局组合在一起,根布局(app/layout.js
)将包裹 dashboard 布局(app/dashboard/layout.js
),它将包裹 app/dashboard/*
内的路由段。
这两种布局将按如下方式嵌套:
你可以使用路由组在共享布局中选择特定的路由,也可以选择不选择。
模板
模板与布局的相似之处在于,它们包裹每个子布局或页面。与在路由上保持不变并保持状态的布局不同,模板在导航中为其每个子级创建一个新实例。这意味着,当用户在共享模板的路由之间导航时,将挂载组件的新实例,重新创建 DOM 元素,不保留状态,并重新同步效果。
在某些情况下,你可能需要这些特定的行为,模板将是比布局更合适的选择。例如:
-
依赖
useEffect
(例如:记录页面视图)和useState
(例如:每页反馈表单)的功能。 -
更改默认框架行为。例如:布局内的 Suspense Boundaries 仅在第一次加载布局时显示回退,而在切换页面时不显示回退。对于模板,回退显示在每个导航上。
可以通过从 template.js
文件导出默认的 React 组件来定义模板。这个组件应该接收一个 children
prop。
// app/template.tsx
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
在嵌套方面,template.js
将会在布局及其子节点之间渲染。
// Output
<Layout>
{/* 注意这个模板存在一个唯一的 key */}
<Template key={routeParam}>{children}</Template>
</Layout>
修饰 <head>
在 app
目录,可以使用内置的 SEO 支持修饰 <head>
HTML 元素,例如:title
和 meta
标签。
元数据可以在 layout.js
或者 page.js
文件中通过导出一个 metadata
对象或 generateMetadata
函数而被定义。
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Next.js',
}
export default function Page() {
return '...'
}
需要知道:不应该手动将
<head>
标签添加到根布局中,相反,应该使用 Metadata API,它可以自动处理高级请求,例如:流式和消除重复<head>
元素。
页面和布局的区别
- 页面是独有的
- 布局是公共的,可以被后代组件访问
链接和导航
在 Next.js 中,有两种方法可以在路由之间导航:
- 使用
<Link>
组件 - 使用
useRouter
hook
本页将介绍如何使用 <Link>
、useRouter()
,并深入了解导航的工作原理。
<link>
组件
<Link>
是一个内置组件,它扩展了 HTML <a>
标签,以提供路由之间的预获取和客户端导航。这是 Next.js 中在路由之间导航的主要方式。
你可以通过从 next/link
导入并将 href
prop 传递给组件来使用它:
// app/page.tsx
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}
你还可以将其他可选 props 传递给 <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 的默认行为是滚动到新路线的顶部,或者保持滚动位置以进行前后导航。
如果你想在导航中滚动到特定的 id
,你可以在 URL 后面添加一个 #
哈希链接,或者只将一个哈希链接传递给 href
prop。这是可能的,因为 <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()
hook 允许以编程方式更改路由。
此 hook 只能在客户端组件中使用,并从 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 参考资料。
建议:使用
<Link>
组件在路由之间导航,除非对使用useRouter
有特定要求。
路由和导航的工作原理
App Router 使用混合方法进行路由和导航。在服务器上,你的应用程序代码会自动按路由段进行代码分割。在客户端上,Next.js 预获取并缓存路由段。这意味着,当用户导航到新路由时,浏览器不会重新加载页面,只会重新渲染更改的路由段,从而提高导航体验和性能。
1. 预获取
预获取是在用户访问前在后台预加载路由的一个方式。
Next.js 中有两种预获取路由的方式:
-
<Link>
组件:路由在出现在用户的视图前会自动被预获取。预获取发生在页面第一次被加载或者通过滚动进入视图时。 -
router.prefetch()
:useRouter
hook 可以被用来以编程方式预获取路由。
<Link>
组件的预获取行为在静态路由和动态路由之间的表现不同:
- 静态路由:
prefetch
默认为true
。整个路由将会被预获取和缓存。 - 动态路由:
prefetch
默认为 automatic(自动)。只有共享布局向下直到第一个loading.js
文件被预获取并缓存了30s
。这降低了获取整个动态路由的成本,并且意味着可以显示即时加载状态,以便向用户提供更好的视觉反馈。
可以通过将 prefetch
prop 设置为 false
来禁用预获取。
有关更多信息,请参阅 <link>
API 参考资料。
需要知道:预取在开发中不启用,只在生产中启用。
2. 缓存
Next.js 有一个内存中的客户端缓存,称为 Router Cache。当用户在应用中导航时,预获取路由段和访问路由的 React 服务器组件有效负载存储在缓存中。
这意味着在导航时,将尽可能地重用缓存,而不是向服务器发起一个新的请求——通过减少请求和数据传输的数量来提高性能。
了解有关 Router Cache 如何工作以及如何配置它的更多信息。
3. 局部渲染
局部渲染意味着只有在导航时发生更改的路由段才会在客户端上重新渲染,并且任何共享段都会保留。
例如,当在两个同级路由(/dashboard/settings
和 /dashboard/analytics
)之间导航时,将渲染 settings
和 analytics
页面,并保留共享的 dashboard
布局。
如果没有局部渲染,每个导航都会导致整个页面在服务器上重新渲染。仅渲染发生更改的段可以减少传输的数据量和执行时间,从而提高性能。
4. 软导航
默认情况下,浏览器在页面之间执行硬导航。这意味着浏览器会重新加载页面并重置 React 状态,如:应用程序中的 useState
hook,以及浏览器状态,如:用户的滚动位置或聚焦元素。然而,在 Next.js 中,App Router 使用软导航。这意味着 React 只渲染已更改的片段,同时保留 React 和浏览器状态,并且不重新加载整个页面。
5. 前进/后退导航
默认情况下,Next.js 会保留前进和后退导航的滚动位置,并重复使用 Router Cache 中的路由。
路由组
在 app
目录中,嵌套文件夹通常是映射到 URL 路径。然而,可以标记文件作为路由组,以防止文件夹包含在路由的 URL 路径中。
这允许将路由段和项目文件组织到逻辑组中,而不会影响 URL 路径结构。
路由组可用于:
约定
可以通过将文件夹的名称括在括号中来创建路由组,例如:(folderName)
。
例子
在不影响 URL 路径的情况下组织路由
要在不影响 URL 的情况下组织路由,请创建一个组将相关路由放在一起。括号中的文件夹将从 URL 中省略(例如:(marketing)
或 (shop)
)。
即使 (marketing)
和 (shop)
内部的路由共享相同的 URL 层次结构,你也可以通过在每个组的文件夹中添加 layout.js
文件来为它们创建不同的布局。
将特定段选择到布局中
要将特定路由选择到布局中,请创建一个新的路由组(例如:(shop)
)并将共享相同布局的路由移动到路由组中(例如:account
和 cart
)。路由组外的路由将不会共享这些布局(例如:checkout
)。
创建多个根布局
要创建多个根布局,请删除顶层的 layout.js
文件,并在每个路由组中添加一个 layout.js
文件。这对于将应用程序划分为具有完全不同的 UI 或体验的部分非常有用。<html>
和 <body>
标签需要添加到每个根布局中。
在上面的例子中,(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
将导致整个页面加载,这仅适用于多个根布局。
动态路由
当你事先不知道确切的段名称,并且希望根据动态数据创建路由,你可以使用在请求时填写或在构建时预渲染的动态段(Dynamic Segments)。
约定
可以通过将文件夹名称括在方括号中来创建动态段(Dynamic Segments),形如 [folderName]
,例如:[id]
或 [slug]
。
动态段作为 params
prop 传递给 layout
、 page
、route
和 generateMetadata
函数。
例子
例如:博客可以包括以下路由 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>
}
路由 | 示例 URL | params |
---|---|---|
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()
页面,了解如何为段生成参数(params)。
需要知道:动态段相当于
pages
目录的动态路由。
生成静态参数
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
函数的主要优点是它可以智能地检索数据。如果使用 fetch
请求在 generateStaticParams
函数中获取内容,那么这个请求会自动保存。这意味着在多个 generateStaticParams
、布局(Layouts)、页面(Pages)中具有相同参数的 fetch
请求将只执行一次,这减少了构建时间。
如果要从 pages
目录进行迁移,请使用迁移指南。
有关更多信息和高级用例,请参阅 generateStaticParams
服务器功能文档。
捕获所有段
通过在括号 [...folderName]
内添加省略号,可以扩展动态段以捕获所有后续分段。
例如,app/shop/[...slug]/page.js
会匹配 /shop/clothes
、/shop/clothes/tops
和 /shop/clothes/tops/t-shirts
等等。
路由 | 示例 URL | params |
---|---|---|
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
)
路由 | 示例 URL | params |
---|---|---|
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>
}
路由 | params 类型定义 |
---|---|
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 创建有意义的 Loading UI。使用此约定,你可以在加载路由段的内容时从服务器显示即使加载状态。初次渲染完成后,新内容将自动交换。
即时加载状态
即时加载状态是导航时立即显示的回退 UI。你可以预渲染加载指示器,例如:骨架和旋转器,或未来屏幕中一小部分但有意义的部分,例如封面照片、标题等。这有助于用户了解应用程序的响应,并提供更好的用户体验。
通过在文件夹添加 loading.js
文件来创建加载状态。
// app/dashboard/loading.tsx
export default function Loading() {
// 可以在 Loading 中添加任何 UI,包括:骨架
return <LoadingSkeleton />
}
在同一个文件夹中,loading.js
将嵌套在 layout.js
中。它会自动将 page.js
文件和下面的任何子级包装在 <Suspense>
边界中。
需要知道:
- 即使是以服务器为中心的路由,导航也是即时的
- 导航是可中断的,这意味着更改路由不需要等到路由内容完全加载后再导航到另一条路由
- 加载新的路由段时,共享布局保持交互式
建议:将
loading.js
约定用于路由段(布局和页面),因为 Next.js 优化了此功能。
通过 Suspense 进行流式传输(Streaming)
除了 loading.js
,你还可为自己的 UI 组件手动创建 Suspense 范围。App Router 支持带有 Suspense 的流式传输用于 Node.js 和 Edge 运行时。
什么是流式传输?
要了解流式传输是如何在 React 和 Next.js 中工作的,首先理解服务端渲染(SSR) 及其局限性是很有帮助的。
使用 SSR,在用户可以查看页面并与之交互之前,需要完成一系列步骤:
- 首先,在服务器上获取给定页面的所有数据
- 然后,服务器为页面渲染 HTML
- 页面的 HTML、CSS 和 JavaScript 被发送到客户端
- 使用生成的 HTML 和 CSS 显示非交互式用户界面
- 最后,React 水合(hydrates)用户界面,使其具有交互性
这些步骤是有顺序并且分块的,意味着服务器只能在请求完所有数据后才能渲染 HTML。而且,在客户端上,React 只能在下载页面中所有组件的代码后对 UI 进行水合。
带有 React 和 Next.js 的 SSR 通过尽快向用户展示非交互式页面,有助于提高感知加载性能。
然后,它仍然可能很慢,因为在向用户显示页面之前需要完成服务器上的所有数据提取。
流式传输允许你将 HTML 分解成更小的块,并逐步将这些块从服务器发送到客户端。
这使得页面的部分内容能够更快地显示,而无需等待所有数据加载后才能渲染 UI 界面。
流式传输与 React 的组件模型能很好的一起工作是因为每个组件可以视为一个块。具有更高优先级(如:产品信息)或不依赖数据的组件可以先发送(如:布局),并且 React 可以更早开始水合。优先级较低的组件(如:评论、相关产品)可以在获取其数据后在同一服务器请求中发送。
当你希望防止长数据请求阻塞页面渲染时,流式传输尤其有用,因为它可以减少打开页面到获取第一个字节(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。你可以使用来自谷歌的工具 Mobile Friendly Test,查看你的页面在谷歌网络爬虫上的显示方式,并查看序列化的 HTML(来源)。
状态码
当流式传输时,将返回一个 200
状态码,表示请求成功。
例如,当使用 redirect
或 notFound
时,服务器仍然可以在流式内容本身内向客户端传递错误或问题。由于响应头已经被送到了客户端,因此无法修改响应的状态码。这不会影响 SEO。
错误处理
error.js
文件约定允许你优雅的处理嵌套路由中的意外运行时错误。
- 在 React 错误边界中自动包裹路由段及其嵌套子节点。
- 使用文件系统层次结构来调整粒度,创建针对特定段量身定制的错误 UI。
- 将错误隔离到受影响的段,同时保持应用程序的其余部分正常工作。
- 添加功能,尝试在不重新加载整页的情况下从错误中恢复。
通过在路由段内添加 error.js
文件并导出 React 组件来创建错误的 UI:
// app/dashboard/error.tsx
'use client' // 错误组件必须是客户端组件
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// 尝试通过重新渲染来恢复
() => reset()
}
>
Try again
</button>
</div>
)
}
error.js
的工作原理
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.js
和 error.js
文件的段的嵌套路由将在以下简化的组件层次结构中渲染:
嵌套组件层次结构对 error.js
文件在嵌套路由中的行为有影响:
- 错误冒泡到最近的父级错误边界。这意味着
error.js
文件将会处理其所有嵌套子段的错误。通过在路由的嵌套文件夹中放置不同级别的error.js
文件,可以实现或多或少的细粒度错误 UI error.js
边界不会处理同一段中layout.js
组件中抛出的错误,因为错误边界嵌套在该布局的组件中
处理布局中的错误
error.js
边界不会捕获同一段中 layout.js
或者 template.js
组件中的抛出的错误。这种有意的层次结构使同级路由(如:导航栏)之间共享的重要 UI 在发生错误时保持可见和可用。
要处理特点布局或模板中的错误,请在布局的父级中添加 error.js
文件。
要处理根布局或模板中的错误,请使用名为 global-error.js
的 error.js
变体。
在根布局中处理错误
根 app/error.js
边界不会捕获在根 app/layout.js
或 app/template.js
组件中排除的错误。
要专门处理这些根组件中的错误,请使用位于根 app
目录中的名为 app/global-error.js
的 error.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>
)
}
捕获服务器错误
如果在服务器组件内部引发错误,Next.js 将把一个 Error
对象(在生产中去掉了敏感的错误信息)作为 error
prop 转发到最近的 error.js
文件。
保护敏感错误信息
在生产过程中,转发到客户端的 Error
对象只包括一个通用 message
和 digest
属性。
这是一种安全预防措施,可避免将错误中包含的潜在敏感细节泄漏给客户端。
message
属性包含关于错误的通用信息,digest
属性包含可用于匹配服务器端日志中相应错误的自动生成的错误哈希。
在开发过程中,转发到客户端的 Error
对象将被序列化,并包含原始错误的 message
,以便于调试。
并行路由
并行路由允许你同时或有条件地渲染同一布局中的一个或更多页面。对于应用的高度动态部分,例如:社交网站上的仪表盘和提要,并行路由可用于实现复杂的路由模式。
例如,你可以同时渲染团队和分析页面。
并行路由允许你为每个路由定义独立的错误和加载状态,因为他们是独立流入的。
并行路由还允许你根据某些条件(如:身份验证状态)有条件地渲染插槽。这将在同一 URL 上启用完全分离的代码。
约定
并行路由是通过命名插槽创建的。插槽使用 @folder
约定定义的,并且将会作为 props 被传递到同级的布局。
插槽不是路由段,不会影响 URL 结构。文件路径
/@team/members
可在/members
中访问。
例如,以下文件结构定义了两个显式插槽:@analytics
和 @team
。
上面的文件夹结构意味着 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
当 Next.js 无法根据当前 URL 恢复插槽的活动状态时,你可以定义 default.js
文件来作为后备文件进行渲染。
请考虑以下文件夹结构。@team
插槽有一个 settings
目录,但是 @analytics
没有。
导航
在导航时,Next.js 将渲染插槽以前的活动状态,即使它与当前 URL 不匹配。
重新加载
重新加载时,Next.js 首先将尝试渲染不匹配插槽的 default.js
文件。如果不可用,则会渲染 404。
不匹配路由的 404 有助于确保不会意外地渲染不应该并行渲染地路由。
useSelectedLayoutSegment(s)
useSelectedLayoutSegment
和 useSelectedLayoutSegments
都接受 paralleRoutesKey
,它允许你读取该插槽中的活动路由段。
// app/layout.tsx
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
export default async function Layout(props: {
//...
auth: React.ReactNode
}) {
const loginSegments = useSelectedLayoutSegment('auth')
// ...
}
当用户导航到 URL 栏中的 @auth/login
或 /login
时,loginSegments
将等于字符串 login
。
例子
模态框
并行路由可以用于渲染模态框。
@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>
)
}
为了确保模态框的内容在它不活动时不会被渲染,你可以创建 default.js
文件来返回 null
。
// 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” 路由。
// 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
}
拦截路由
拦截路由允许你在当前布局中从应用程序的另一部分加载路由。当你希望显示路由的内容无需用户切换到不同上下文时,这种路由规范可能很有用。
例如,当点击提要中的图片时,你可以在模态框中显示图片,覆盖提要。在这种情况下,Next.js 会拦截 /photo/123
路由,屏蔽 URL,并且将其覆盖在 /feed
上。
然而,当通过点击可共享 URL 或刷新页面导航到图片时,应该渲染整个图片,而不是模态框。不应发生路由拦截。
约定
拦截路由可以用 (..)
约定来定义,这与相对路径约定 ../
类似,区别在于拦截路由作用于片段。
你可以使用:
(.)
- 匹配同级片段(..)
- 匹配上级片段(..)(..)
- 匹配上上级片段(...)
- 匹配app
目录片段
例如,你可以通过创建 (..)photo
目录从 feed
片段拦截 photo
片段。
注意,
(..)
约定基于路由段,而不是文件系统。
例子
模态框
拦截路由可以与并行路由一起使用来创建模态框。
使用此模式来创建模态框克服了使用模态框时的一些常见挑战,允许你:
- 通过 URL 使模态框可共享
- 刷新页面时保留上下文,而不是关闭模态框
- 通过回退路由关闭模态框,而不是跳转上一个路由
- 在前进路由时重新打开模态框
在上面的例子中,
photo
片段的路径可以使用(..)
匹配器,因为@modal
是一个插槽而非片段。这意味着photo
路由只高出一个片段级别,尽管它高出两个文件系统级别。
其他例子可能包括在顶部导航栏打开登录模态框,同时还具有专用的 /login
页面,或者在侧边栏中打开购物车。
查看示例具有拦截和平行路由的模式。
路由处理器
路由处理器允许你使用 Web Request 和 Response API 为给定路由创建自定义请求处理器。
需要知道:路由处理器仅在
app
目录中可用。他们相当于pages
目录中的 API 路由,这意味着你不需要同时使用 API 路由和路由处理器。
约定
路由处理器在 app
目录的 route.js|ts
文件中定义。
// app/api/route.ts
export const dynamic = 'force-dynamic' // 默认
export async function GET(request: Request) {}
路由处理器可以嵌套在 app
目录中,与 page.js
和 layout.js
类似。但不能有与 page.js
文件相同路由段级别的 route.js
文件。
支持 HTTP 方法
支持以下的 HTTP 方法:GET
、POST
、PUT
、PATCH
、DELETE
、HEAD
和 OPTIONS
。如果调用了不支持的方法,Next.js 会返回 405 Method Not Allowed
响应。
扩展 NextRequest
和 NextResponse
API
除了支持本地 Request 和 Response 外。Next.js 通过 NextRequest
和 NextResponse
来进行扩展,为高级用例提供了方便的助手。
行为
缓存
路由处理器在使用 GET
方法来获取 Response
对象时默认会被缓存。
// 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 })
}
TypeScript 警告:
Response.json()
仅在 TypeScript5.2 中有效。如果使用了更低版本的 TypeScript,你可以对类型化响应使用NextResponse.json()
。
选择退出缓存
你可以选择退出缓存通过:
例如:
// 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
文件
Page | Route | Result |
---|---|---|
app/page.js | app/route.js | ?Conflict |
app/page.js | app/api/route.js | ?Valid |
app/[user]/page.js | app/api/route.js | ?Valid |
每个 route.js
或 page.js
文件都会接管该路由的所有 HTTP 行为。
// app/page.js
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
// ? Conflict
// `app/route.js`
export async function POST(request) {}
例子
以下示例展示了如何将 Route Handlers 与其他 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 中的动态函数一起使用,如 cookies
和 handlers
。
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 之上使用抽象来读取 cookie(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
的新响应。
// 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', or 'c'
}
Route | Example URL | params |
---|---|---|
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 中 query 是 "hello"
}
流式传输
流式传输通常与大型语言模型(LLM)(如:OpenAI)结合使用,用于人工智能生产的内容。了解有关 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 })
}
请求体表单数据
你可以通过 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 方法来设置响应的 CORS 头。
// app/api/route.ts
export const dynamic = 'force-dynamic' // defaults to force-static
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.xml
、robots.txt
、app icons
和打开的图形图像都有内置的支持。
// app/rss.xml/route.ts
export const dynamic = 'force-dynamic' // 默认是 'force-static'
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 参考资料。
中间件
中间件允许你在请求完成之前运行代码。然后,根据传入的请求,你可以通过重写、重定向、修改请求或响应头或直接响应来修改响应。
中间件在缓存内容和路由匹配之前运行。有关详细信息,请参阅 Matching Paths。
约定
使用根目录中的 middleware.ts|js
文件来定义中间件。例如,在 pages
或 app
同级,或在 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*',
}
匹配路径
中间件将为项目中的每条路由调用。以下是执行顺序:
next.config.js
中的headers
next.config.js
中的redirects
- 中间件(
rewrites
、redirects
等等) next.config.js
中的beforeFiles(rewrites)
- 文件系统路由(
public/
、_next/static/
、pages/
、app/
等等) next.config.js
中的afterFiles(rewrites)
- 动态路由(
/blog/[slug]
) next.config.js
中的fallback(rewrites)
有两种方法可以定义中间件将在哪些路径上运行:
匹配器
matcher
允许你过滤中间件以在特定路径上运行。
// middleware.js
export const config = {
matcher: '/about/:path*',
}
你可以使用数组语法匹配单个路径或多个路径:
// middleware.js
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}
matcher
配置允许使用完整的 regex,因此支持像负前瞻性或字符匹配这样的匹配。此处可以看到一个负前瞻性示例,用于匹配除特定路径之外的所有路径:
// middleware.js
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}
需要知道:
matcher
值必须是常量,这样才能在构建时对其进行静态分析,动态值(如:变量)将被忽略
配置的匹配器:
- 必须以
/
开头 - 可以包括命名参数:
/about/:path
可以匹配/about/a
和/about/b
,但不能匹配/about/b/c
- 命名参数上可以有修饰符(以
:
开头):/about/:path*
匹配/about/a/b/c
,因为*
表示零或更多,?
表示零或一,+
表示一或更多 - 可以使用括号中的正则表达式:
/about/(.*)
与/about/:path*
相同
需要知道:为了相后兼容,Next.js 总是将
/public
视为/public/index
,因此,/public
会被/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 路由,
getServerSideProps
和rewrite
目标的请求头 - 设置响应 cookies
- 设置响应 headers
要从中间件生成响应,你可以:
使用 cookies
Cookies 是常规头。在 Request
时,它们存储在 Cookie
头中。在 Response
时,它们存储在 Set-Cookie
头中。Next.js 提供了一种方便的方式来访问和操作这些 cookies,即通过 NextRequest
和 NextResponse
上的 cookies
扩展。
- 对于传入请求,
cookies
具有以下方法:get
、getAll
、set
和delete
cookies,你可以使用clear
检查是否存在 cookie,或者使用删除所有 cookie。 - 对于传出响应,
cookies
具有以下方法:get
、getAll
、set
和delete
。
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 假设传入请求中存在 “Cookie:nextjs=fast” 头
// 使用 `RequestCookies` API 从请求中获取 cookie
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 在响应上设置 cookie
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
}
设置头
你可以使用 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-midleware1`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')
// 你也可以在 NextResponse.write 中设置请求头
const response = NextResponse.next({
request: {
// 新的请求头
headers: requestHeaders,
},
})
// 设置新的响应头 `x-hello-from-midleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
需要知道:避免设置大量的头,因为这可能导致 431 Request Header Fields Too Large 错误,取决于后端 web 服务器配置
产生响应
你可以通过返回 Response
或 NextResponse
实例直接从中间件进行响应。(这从 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 }
)
}
}
高级中间件标志
在 Next.js v13.1 中为中间件引入了两个额外的标志:skipMiddlewareUrlNormalize
和 skipTrailingSlashRedirect
,以处理高级用例。
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
允许禁用 URL 规范化 Next.js 所做的操作,以使处理直接访问和客户端转换相同。在一些高级情况下,你需要使用解锁的原始 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)
// 带有 this now/next/data/build-id/hello.json 标志
// 如果没有标志,这将被规范化为 /hello
}
运行时
中间件目前仅支持 Edge 运行时。Node.js 运行时无法使用。
历史版本
Version | Changes |
---|---|
v13.1.0 | 添加高级中间件标志 |
v13.0.0 | 中间件可以修改请求头、响应头和发送响应 |
v12.2.0 | 中间件稳定,请参阅升级指南 |
v12.0.9 | 在 Edge 运行时中强制执行绝对 URL(PR) |
v12.0.0 | 添加中间件(测试版) |
项目组织与文件托管
除了路由文件夹和文件约定外,Next.js 对如何组织和并置项目文件没有任何偏见。
此页面共享默认行为和特点,你可以用来组织你的项目
默认情况下安全主机代管
在 app
目录中,嵌套文件夹层次结构定义了路由结构。
每个文件夹表示映射到 URL 路径中相应段的路由段。
然后,即使路由结构是通过文件夹定义的,在将 page.js
或 route.js
文件添加到路由段之前,路由是不可公开访问的。
而且,即使路由被公开访问,也只有 page.js
或 route.js
返回的内容被发送到客户端。
这意味着项目文件可以在 app
目录中的路由段安全地内置,而不会意外地变为可路由的。
需要知道:
- 这与
pages
目录不同,在pages
目录中,页面中的任何文件都被视为路由。- 虽然你可以在
app
中对你的项目文件进行并置,但你不必这么做,如果你愿意,你可以将它们保存在app
目录之外。
项目组织特点
Next.js 提供了几个功能来帮助你组织项目。
私有文件夹
可以通过在文件夹前加下划线来创建私有文件夹:_folderName
。
这表示文件夹是一个私有的实现细节,路由系统不应考虑它,从而选择跳出路由文件夹及其所有子文件夹。
由于默认情况下 app
目录中的文件可以安全地进行主机代管,因此主机代管不需要私人文件夹。然而,它们可以用于:
- 将 UI 逻辑从路由逻辑中分离
- 在项目和 Next.js 生态系统中持续组织内部文件
- 在代码编辑器中对文件进行排序和分组
- 避免与未来的 Next.js 文件约定发生潜在的命名冲突
需要知道:
- 虽然不是框架约定,但您也可以考虑使用相同的下划线模式将私有文件夹外的文件标记为 “private”
- 你可以创建以下划线开头的 URL 段,方法是在文件夹名称前加上
%5F
(下划线的 URL 编码形式):%5FolderName
- 如果你不使用私有文件夹,了解 Next.js 的特殊文件约定会很有帮助,以防止意外的命名冲突
路由组
路由组可以通过讲文件夹括在括号中来创建:(folderNmae)
。
这表示文件夹用于组织目的,不因包含在路由的 URL 路径中。
路由组可用于:
- 将路由分组,例如,按位置、意图或团队
- 在同一的路由段级别中启用嵌套布局
src
目录
Next.js 支持将应用程序代码(包括 app
)存储在可选的 src
目录中。这将应用程序代码与项目配置文件分离,这些文件大多位于项目的根目录中。
模块路径别名
Next.js 支持模块路径别名,使其更容易读取和维护深度嵌套项目文件中的导入。
// app/dashboard/settings/analytics/page.js
// before
import { Button } from '../../../components/button'
// after
import { Button } from '@/components/button'
项目组织策略
在 Next.js 项目中组织自己的文件和文件夹时,没有 “正确” 或 “错误” 的方法。
下一节列出了共同战略的高级概述。最简单的做法是选择一种适合你和你的团队策略,并在整个项目中保持一致。
需要知道:在下面的例子中,我们使用
components
和lib
文件夹作为通用占位符,它们的命名没有特殊的框架意义,您的项目可能会使用其他文件夹,如ui
、utils
、hooks
、styles
等。
将项目文件存储在 app
外
此策略将所有应用程序代码存储在项目根目录中的共享文件夹中,并保留应用程序目录纯粹用于路由目的。
将项目文件存储在 app
内部的顶级文件夹中
此策略将所有应用程序代码存储在 app
目录的根目录的共享文件夹中。
按特点或路由拆分项目文件
此策略将全局共享的应用程序代码存储在根 app
目录中,并将更具体的应用程序编码拆分为使用它们的路由段。
国际化
Next.js 使你能够配置内容的路由和呈现,以支持多种语言。使你的网站适应不同的地区包括翻译内容(本地化)和国际化路由。
专业术语
- 区域设置:一组语言和格式首选项的标识符。这通常包括用户的首选语言以及可能的地理区域
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/productsa
)或域(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}`
// 例如:传入的请求是 /product
// 新的 URL 现在是 /en-US/products
return Response.redirect(request.nextUrl)
}
export const config = {
matcher: [
// Skip all internal paths (_next)
'/((?!_next).*)',
// Optional: only run on root (/) URL
// '/'
],
}
最后,确保 app/
中的所有特殊文件都嵌套在 app/[lang]
下。这使 Next.js 路由器能够动态处理路由中的不同区域设置,并将 lang
参数转发到每个布局和页面。例如:
// app/[lang]/page.js
// 你现在可以访问当前区域设置
// 例如:/en-US/products -> `lang` is "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/en.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/
目录中的所有布局和页面都默认为服务器组件,所以我们不需要担心翻译文件的大小会影响客户端 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>
)
}
资源
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!