# 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
目录。
# 路由
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
9
访问 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 返回
{
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
用于并行路由。