# Learn Next.js
我之前 使用 Express 创建了一个 ToDo 的 Web 应用。Express (opens new window) 就像一张空白的画布,任你挥洒创意。你可以用水彩勾勒一幅童趣盎然的插画,也可以用毛笔描绘一卷意境深远的山水长卷。如果技艺精湛,甚至能创作出堪比《清明上河图》的传世之作。这正是 Express 所谓的 "unopinionated"——它不设限,你的作品由你定义。
然而, Express (opens new window) 仅仅是一张空白的画布,所有的工具和材料都需要我们自行准备。我们要处理模板(Pug)、样式(Sass/Less/CSS Modules)、脚本(TypeScript)、用户验证、数据库、组件等方方面面。而以我们的能力,尚无法企及张择端的造诣。
因此,我们需要一个框架来为我们打好基础,就像一位绘画老师,提前选好铅笔、调好色板,让我们能够专注于挥洒创意,将脑海中的构想变为现实。
Next.js (opens new window) 正是这样一个出色的框架。接下来,我们来学习一下 Next.js (opens new window)。
# 安装
$ npx create-next-app@latest
当前版本 15.3.0
要求 Node.js 18.18+
安装的过程中,Next.js 会提供以下选项供您选择
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`? No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
2
3
4
5
6
7
8
9
以上选项,我都选择 "Yes",因此 Next.js 自动为我配置了 Typescript、ESLint、Tailwind CSS、Turbopack、Alias 和路由机制,并且代码放入 src
目录。
# 调试
调试纯前端项目,我们知道使用浏览器(我使用 Chrome)就可以了。但是 Next.js 是前后端一体的项目,所以除了浏览器我们还需要使用 VS Code(我使用VS Code)
关于怎么使用 VS Code 调试 Node.js,可以参考我的 VS Code 调试 Node.js 这篇文章
在项目的根目录创建 .vscode/launch.json
文件
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "npm run dev"
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: debug full stack",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/next/dist/bin/next",
"runtimeArgs": ["--inspect"],
"skipFiles": ["<node_internals>/**"],
"serverReadyAction": {
"action": "debugWithChrome",
"killOnServerStop": true,
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}"
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
如果要从根目录以外的目录运行 Next.js,则需要添加 cwd
选项(表示启动程序所在的目录),例如:"cwd": "${workspaceFolder}/apps/web"
。
# 路由
Next.js 提供了两种路由机制:
- App Router:基于
app/
目录中的文件结构,引入了 布局(Layout)和嵌套路由的概念。 - Pages Router:基于
pages/
目录中的文件结构,每个.js/.tsx
文件自动成为一个路由。
App Router 自 Next.js 13 引入的,是目前 Next.js 推荐的路由机制。所以对于新应用,我们使用 App Router 即可。
App Router 以 app/
下的文件结构形成路由,并支持路由嵌套。当然不是 app/
下的所有的文件都可以用作路由,只有 page.{js,jsx,ts,tsx}
文件才可以当做路由,比如:
└── app
├── content
| ├── layout.tsx
| └── task
| └── page.tsx 🌐 /content/task/
├── components
| └── button.tsx 不是路由
├── layout.tss
├── lib
└── actions.ts 不是路由
├── page.tsx 🌐 /
└── user
├── layout.tsx
├── page.tsx 🌐 /user/
├── login
| └── page.tsx 🌐 /user/login/
└── register
└── page.tsx 🌐 /user/register/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
并且以 page.{js,jsx,ts,tsx}
文件返回的组件作为路由渲染的页面。比如
// app/user/page.tsx
export default function User() {
return (
<div>
I'm cp3hnu
</div>
);
}
2
3
4
5
6
7
8
访问 http://localhost:3000/user 时,页面显示 I'm cp3hnu
。
此外 App Router 支持布局共享和嵌套。一个目录下的所有路由共享当前目录的布局,这个布局由 layout.{js,jsx,ts,tsx}
文件定义。比如上面的 /user/login/
和 /user/register/
共享 user 的布局,即 user/layout.tsx
定义的布局。
布局共享可以用于性能优化,比如从
/user/login/
跳转到/user/register/
,user/layout.tsx
不需要重新渲染。
每一个 Next.js 项目都应该有一个根布局,由 app/layout.tsx
定义,里面应该包含 html
和 body
标签,例如:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<body
>
{children}
</body>
</html>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
同时布局支持嵌套,因此对应路由地址 /user/login/
,渲染的页面应该是这样的:
<RootLayout>
<UserLayout>
<LoginPage />
</UserLayout>
</RootLayout>
2
3
4
5
# 目录约定
一般情况下 app/
下的目录用于构建路由段 (route segment),但也存在以下几种特殊情形
# [folder]
这种是动态 route segment,比如:
└── app
└── user
└── [id]
└── page.tsx 🌐 /user/1/、/user/2/ 等
2
3
4
这种类似于 React-Router 的动态路由 (opens new window),route("user/:id", "./user.tsx"),
这个动态参数通过 params
props 传递给 page。
// app/user/[id]/page.tsx
export default async function UserDetail({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <div>{id}</div>;
}
2
3
4
5
6
7
8
9
10
📢
params
是一个 Promise
如果是 Client Component,也可以通过 useParams
(opens new window) 获取。
当导航到 /user/5/
时,params.id
为 5
# [...folder]
除了匹配单个 route segment,Next.js 还能匹配多个 route segment,即使用 [...folder]
,比如
└── app
└── user
└── [...slug]
└── page.tsx 🌐 /user/1/、/user/1/info、/user/1/info/avatar 等
2
3
4
此时 params.slug
是一个数组,对应路由地址 /user/1/info/edit
,值为 ["1", "info", "avatar"]
# [[...folder]]
这个相对于上面的 [...folder]
, route segment 可以为空,比如上面的例子,它能匹配 /user/
# (folder)
根据业务逻辑进行分组,但是不会生成 route segment,比如
└── app
├── (main)
├── layout.tsx
└── task
└── page.tsx 🌐 /task/
2
3
4
5
一般是用于根据业务逻辑进行分组,比如 (admin)
、(marketing
)等。
虽然 (folder)
不会产生 route segment,但是目录下的路由可以共享 Layout。
有一点需要注意,不要产生路由冲突,比如:``(marketing)/about/page.js和
(shop)/about/page.js都会生成
/about` 路由。
# _folder
私有文件夹。私有文件夹及其子文件夹都不会生成布局和路由,即使目录下存在 page
文件
# @folder
插槽,用于并行路由,可以同时渲染多个页面,比如下面的结构
└── app
├── layout.tsx
├── page.tsx
├── @team
└── page.tsx
├── @intro
└── page.tsx
2
3
4
5
6
7
可以这么定义 app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
intro: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{intro}
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这样就可以同时渲染 @team/page.tsx
、@intro/page.tsx
、app/page.tsx
三个页面的内容
children
其实就是一个默认的插槽
# 文件约定
除了 page
文件用于路由,layout
文件用于布局之外,Next.js 还定义了一些特殊用途的文件
下面文件的后缀跟
page
文件一样,支持.js
、.jsx
、.ts
、.tsx
# loading
显示页面的加载状态。其实就是作为 Suspense (opens new window) 组件的 fallback
<Layout>
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
</Layout>
2
3
4
5
主要是用于异步组件,更多详情请参考 Loading UI and Streaming (opens new window)
# error
显示页面的错误状态。其实就是作为 ErrorBoundary (opens new window) 组件的 fallback
。必须是 Client Component
<Layout>
<ErrorBoundary fallback={<Error />}>
<Page />
</ErrorBoundary>
</Layout>
2
3
4
5
它接收两个 props
error
: 错误信息,包含message
和digest
reset
: 刷新函数
'use client' // 必须是 Client Component
export default function Error({
error,
reset,
}: {
error: Error & { message?: string; digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# global-error
处理根布局错误,必须在 /app
目录下,必须包含 html
和 body
标签,因为它会取代根 Layout 的内容,即
<ErrorBoundary fallback={<GlobalError />}>
<RootLayout>
<RootPage />
</RootLayout>
</ErrorBoundary>
2
3
4
5
其它的和 error
一样
'use client' // 必须是 Client Component
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
// global-error must include html and body tags
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# not-found
显示 404 错误。但是需要搭配 notFound
(opens new window) 函数使用,比如对于路由 /user/:id
,如果通过 id
没有找到用户,可以使用这个文件显示错误信息。
返回的状态码为 404 Not Found
.
// app/user/[id]/page.tsx
import { notFound } from "next/navigation";
export default async function UserD({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const user = await getUserById(id);
if (!user) {
notFound();
}
return <div>{id}</div>;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/user/[id]/not-found.tsx
export default function UserNotFound() {
return <div>该用户不存在</div>;
}
2
3
4
5
not-found
文件还有一个特殊用途,根目录下的 not-found
文件(app/not-found
) 处理应用程序不匹配的 URL。即访问应用程序中不存在的路由时,显示这个文件的内容,以提示用户。
# route
类似于 Express 的路由处理程序。在 Next.js 中一般用于后台接口。同一目录下不要同时存在 route
文件和 page
文件
// app/api/user
export async function GET(request: Request) {
return Response.json({
id: 1,
name: 'John Doe',
age: 30,
email: '5Ml0D@example.com'
})
}
2
3
4
5
6
7
8
9
GET http://localhost:3000/api/user (opens new window) 返回
{
id: 1,
name: 'John Doe',
age: 30,
email: '5Ml0D@example.com'
}
2
3
4
5
6
除了GET
方法,Next.js 还支持 POST
、PUT
、PATCH
、DELETE
、HEAD
和 OPTIONS
。对于不支持的方法,返回 405 Method Not Allowed
。
Next.js 接收 NextRequest
(opens new window) 参数,返回 NextResponse
(opens new window),它们分别扩展了原生的 Request (opens new window) 和 Response (opens new window) ,以提供更多功能。
# default
用于并行路由。例如
└── app
├── @intro
| ├── default.tsx
| └── page.tsx
├── @user
| ├── info
| | └── page.tsx
| └── page.tsx
├── default.tsx
├── layout.tsx
└── page.tsx
2
3
4
5
6
7
8
9
10
11
当访问 /
路由时,同时展示 app/page.tsx
、@intro/page.tsx
、@user/page.tsx
的内容,但是通过软导航(Soft Navigation) (opens new window)到 /user/info
时,展示 @user/info/page.tsx
的内容,同时保持 app/page.tsx
、@intro/page.tsx
的内容不变,这个在 Next.js 里称为 Partial Render (opens new window)。
但是如果进行硬导航(Hard Navigation) (opens new window) ,比如浏览器刷新,@intro
目录无法匹配路由 /user/info
,这个时候就会显示 @intro/default.tsx
的内容,因为 children
是隐式插槽 @children
,因此也显示 app/default.tsx
。如果没有 default
文件,则整个页面将显示 404 错误,即 app/not-found
的内容。
# middleware
中间件文件。中间件在路由呈现之前执行,可以实现如身份验证、日志记录或重定向等功能。一个项目只能有一个中间件文件,且和 app
文件夹在同一个层级中。更多详情请参考下文的中间件
# template
模板文件。类似于layout
,但是与跨路由持久化和状态保持的 layout
不同,模板文件被赋予了一个唯一的 key,这意味着子客户端组件在导航时将重置它们的状态。
这个文件不经常使用。
# 渲染
Next.js 将组件分为服务器组件(Server Components)和客户端组件(Client Components)
为了加速初始页面的加载,Next.js 在服务器上为客户端和服务器组件生成静态 HTML,当用户第一次访问时,将立即看到页面的内容,而不必等待客户端下载、解析和执行 JavaScript 包。
Next.js 的渲染步骤如下:
在服务器端:
- React 将服务器组件渲染为一种称为 React Server Component Payload (RSC Payload) (opens new window) 的特殊数据格式,其中包括对客户端组件的引用。
- Next.js 使用 RSC Payload 和客户端组件 JavaScript 指令在服务器上生成 HTML。
然后,在客户端:
- 生成的 HTML 快速地生成非交互式初始预览。
- 然后 RSC Payload 协调客户端和服务器组件树,并更新 DOM。
- JavaScript 指令 Hydrate 客户端组件并使其具有交互性。
RSC Payload 是渲染的服务器组件树的紧凑二进制表示,它包含:
- 服务器组件的渲染结果;
- 客户端组件应该渲染的位置以及对其 JavaScript 文件的引用;
- 任何从服务器组件传递给客户端组件的 Props;
Hydration 是给 DOM 添加事件侦听器的过程,使得静态 HTML 具有交互性。在幕后,Hydration 是通过
hydrateRoot
(opens new window) React API 完成的。
# 服务器组件
Next.js 默认组件都是服务器组件,服务器组件有以下几个优势:
- 获取数据更快:服务器组件和数据源都在服务器上,因此获取数据更快。
- 更安全:服务器组件将敏感数据保存在服务器上,不会暴露给客户端,因此更安全。
- 缓存:服务器组件渲染的内容能被缓存,可以提高性能。
- 减少客户端 JavaScript 包大小:服务器组件在服务器上运行,因此客户端需要下载、解析和执行的 JavaScript 减少。
- 初始页面加载和第一次内容绘制(FCP)速度更快:服务器组件在服务器上生成 HTML 以允许用户立即查看页面,而无需等待客户端下载、解析和执行渲染页面所需的 JavaScript,因此初始页面加载和第一次内容绘制(FCP)速度更快。
- 搜索引擎优化和社交网络可共享性:服务器组件生成的 HTML 可以被搜索引擎机器人访问、被社交网络机器人用于生成社交卡。
- 流式:服务器组件可以将渲染分割成块,然后将它们流式传输到客户端,这可以使用户更早地看到页面的部分内容。
服务器组件有三种渲染方式:
- 静态
- 动态
- 流式
# 静态渲染
静态渲染,静态渲染的内容在编译的时候生成。静态渲染的内容可以被缓存,因此有好的性能优势。适用于非个性化的场景,比如静态博客文章或产品页面等。
export default function Component() {
return (
<div>
<h1>服务器组件有三种渲染方式:</h1>
<ul>
<li>静态</li>
<li>动态</li>
<li>流式</li>
</ul>
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
# 动态渲染
与静态相反,动态渲染的内容在请求的时候生成。适用于个性化的场景,比如显示用户的购物清单。
服务器组件默认是静态渲染,但是在渲染的过程中,如果 Next.js 发现服务器组件使用了 动态 API (opens new window) 或带有选项 {cache: 'no-store'}
的fetch
函数 (opens new window)时,Next.js 将使用动态渲染。
{cache: 'no-store'}
表示不缓存,讲缓存的时候会介绍
动态 API 包括:
cookies
(opens new window)headers
(opens new window)connection
(opens new window)draftMode
(opens new window)searchParams
prop (opens new window)unstable_noStore
(opens new window)
# 流式渲染
流式渲染是指将 UI 分割成块,然后流式传输给客户端,这使得用户能立即看到页面的部分内容。
流式渲染有两种方式:
- 使用
loading.js
- 使用
suspense
# 使用 loading.js
整个页面作为一个流式块,它的好处是不会阻止 Layout 的展示,同时用户可以看到页面的加载状态
# 使用 Suspense
Suspense
(opens new window) 可以更细粒度地控制页面的哪些部分进行流式传输。
更多关于 Stream 更多内容,请参考 Loading UI and Streaming (opens new window)、How to fetch data and stream (opens new window)、Server Components (opens new window)
# 客户端组件
但是服务器组件也有限制。服务器组件不能有事件处理函数、不能使用 useState
、useEffect
等 React Hook、不能使用 Web 专有的 API,比如 geolocation (opens new window)、localStorage (opens new window) 等。比如下面组件使用了事件处理函数
export default function Component() {
return (
<div>
<button onClick={() => console.log("clicked")}>Click Me</button>
</div>
);
}
2
3
4
5
6
7
运行时报错
这些都必须使用客户端组件。客户端组件使用 "use client"
(opens new window) 来标识。
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
# 服务器组件 VS 客户端组件
我应该什么时候使用服务器组件,什么时候使用客户端组件呢?一张图胜过千言万语。
# 获取数据
Next.js 中获取数据分为服务器组件获取数据和客户端组件获取数据
# 服务器组件获取数据
# 数据库
服务器组件一般都会获取数据库的数据。用法很简单,只需要将服务器组件改为异步即可。
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
12
# 第三方服务
服务器组件有可能需要获取第三方服务的数据。这个时候可以使用 Next.js 的 fetch
方法 (opens new window)。Next.js 的 fetch
方法扩展了 Web 的 fetch()
方法 (opens new window),并添加了服务器缓存。
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
# 客户端组件获取数据
# use
方法
客户端组件获取数据推荐使用 React 19+ 的 use
方法 (opens new window)。
首先参考上一节的内容,在服务器组件里获取数据,然后传递数据 Promise 给客户端组件。这种方式能阻止 Promise 阻塞服务器组件的渲染。
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// Don't await the data fetching function
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
然后客户端组件使用 use
方法 (opens new window) 接收数据 Promise
'use client'
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
注意,客户端组件需要使用 Suspense
(opens new window) 进行包裹,因为获取数据是一个异步操作。Promise 有可能会失败,因此还应该添加 ErrorBoundary,更多详情请参考 use
- Dealing with rejected Promises (opens new window)。
# 第三方库
推荐使用 SWR (opens new window) 和 React Query (opens new window),也可以使用 axios
(opens new window)。
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 中间件
Next.js 允许在请求完成之前运行中间件。它根据传入的请求,通过重写、重定向、修改请求或响应头、甚至直接返回响应来修改原请求的响应。
正如前面介绍的,通过项目根目录下的 middleware.ts
文件定义中间件,一般格式如下
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
// 匹配请求路由
export const config = {
matcher: '/about/:path*',
}
2
3
4
5
6
7
8
9
10
11
12
默认 Next.js 中间件处理每一个请求路由,但是可以通过 config.matcher
进行配置,只有匹配的路由才使用中间件,支持字符串、正则表达式、对象以及以上三种类型组成的数组。
config.matcher
支持的对象格式如下:
- source:匹配请求路由的路径或模式。可以是字符串和正则表达式
- regexp(可选):正则表达式字符串,根据
source
对匹配进行微调 - locale(可选):一个布尔值,当设置为
false
时,忽略路径匹配中基于地区的路由 - has(可选):指定路由请求中必须存在特定请求元素(如 headers、查询参数或 cookie)
- missing(可选):指定路由请求中不存在特定请求元素(如 headers、查询参数或 cookie)
一个复杂的例子如下:
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, sitemap.xml, robots.txt (metadata files)
*/
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
has: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
},
],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
中间件的应用场景有很多,比如设置响应头、登录验证与授权、CORS 等,下面以 CORS 举例说明
import { NextRequest, NextResponse } from 'next/server'
// 允许的 origin
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
// Cors Headers
const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
export function middleware(request: NextRequest) {
// Check the origin from the request
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)
// Handle preflighted requests
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}
// Handle simple requests
// `next()` 表示继续路由
const response = NextResponse.next()
if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}
Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const config = {
matcher: '/api/:path*',
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 缓存
Next.js 支持四种缓存机制
它们的运行机制如下
# Request Memoization
上面讲服务器组件获取数据时,介绍了 Next.js 的 fetch
方法。它扩展了 Web 的 fetch()
方法 (opens new window),并且对相同的请求(URL 和请求参数相同),缓存其结果。但是它只应用于 React 组件树(Layout、Page、服务器组件、generateMetadata
, generateStaticParams
),在组件树的渲染过程中,多次调用同一个请求,后续的请求使用缓存。这种缓存是自动开启的,只适用于 GET
请求,在 React 组件树渲染完成之后,缓存失效。
# Data Cache
Data Cache 也是通过 fetch
方法进行缓存的,与 Request Memoization 不同的是,Request Memoization 是自动开启的,而 Data Cache 需要进行配置,Request Memoization 的缓存只能在同一 React 组件树里有效,而 Data Cache 可以跨服务器请求和部署。
当 Next.js 发出带有 {cache: 'force-cache'}
选项的 fetch
请求时,其结果缓存到 Data Cache。如果 fetch
请求没有 cache
选项,或者带有 {cache: 'no-cache'}
选项,则请求结果不缓存。
默认 Data Cache 的缓存一直有效,但是有两种方式可以使其失效:
- 设置过期时间(Time-based Revalidation):使用
next.revalidate
选项设置过期时间
// Revalidate at most every hour
fetch('https://...', { next: { revalidate: 3600 } })
2
- 手动设置失效(On-demand Revalidation)
使用 revalidatePath
(opens new window) 或 revalidateTag
(opens new window) 设置缓存失效
// Path
import { revalidatePath } from 'next/cache'
export default async function submit() {
await addPost()
revalidatePath('posts')
}
// Tag
// fetch(`https://...`, { next: { tags: ['a', 'b', 'c'] } })
import { revalidateTag } from 'next/cache'
export default async function submit() {
await addPost()
revalidateTag('a')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Full Route Cache
上面介绍服务器组件时,曾说到服务器组件静态渲染的内容可以被缓存,因此有好的性能优势。这里的缓存就是指 Full Route Cache。它缓存 RSC Payload 和 生成的 HTML。
默认 Full Route Cache 的缓存一直有效,但是有两种方式可以使其无效:
- 使 Data Cache 失效,然后会在服务器上重新渲染组件并缓存新的渲染结果,从而使 Full Route Cache 缓存失效
- 重新部署
Full Route Cache 只能应用于服务器组件静态渲染。以下四种操作将不能利用 Full Route Cache:
- 使用 动态 API (opens new window),Next.js 将使用动态渲染
- 使用
dynamic = 'force-dynamic'
选项配置路由段,强制动态渲染 - 使用
revalidate = 0
选项配置路由端 - 带有
{cache: 'no-cache'}
选项(fetch
请求的默认值)的fetch
请求
# Client-side Router Cache
Next.js js 有一个内存的 Client-side Router Cache,它缓存路由段的 RSC Payload。当用户在路由之间导航时,Next.js 会缓存访问过的路由段,并预取用户可能导航到的路由。这导致了即时的后退/前进导航,在导航之间不需要重新加载整个页面,并且保留了 React 状态和浏览器状态。
Client-side Router Cache 将
- 缓存 Layout 和 Loading,在导航上的时候可以复用。
- 默认情况下,Page 不缓存,但可以在浏览器向后和向前导航时复用。