# 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
1

当前版本 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? @/*
1
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/
1
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>
  );
}

1
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 定义,里面应该包含 htmlbody 标签,例如:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="zh-CN">
      <body
      >
        {children}
      </body>
    </html>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

同时布局支持嵌套,因此对应路由地址 /user/login/ ,渲染的页面应该是这样的:

<RootLayout>
  <UserLayout>
    <LoginPage />
  </UserLayout>
</RootLayout>
1
2
3
4
5

# 目录约定

一般情况下 app/ 下的目录用于构建路由段 (route segment),但也存在以下几种特殊情形

# [folder]

这种是动态 route segment,比如:

└── app
   └── user   
      └── [id]
         └── page.tsx    🌐 /user/1/、/user/2/ 等
1
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>;
}
1
2
3
4
5
6
7
8
9
10

📢 params 是一个 Promise

如果是 Client Component,也可以通过 useParams (opens new window) 获取。

当导航到 /user/5/ 时,params.id5

# [...folder]

除了匹配单个 route segment,Next.js 还能匹配多个 route segment,即使用 [...folder],比如

└── app
   └── user    
      └── [...slug]
         └── page.tsx    🌐 /user/1/、/user/1/info、/user/1/info/avatar 等
1
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/
1
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
1
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}
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这样就可以同时渲染 @team/page.tsx@intro/page.tsxapp/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>
1
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>
1
2
3
4
5

它接收两个 props

  • error: 错误信息,包含 messagedigest

  • 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>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# global-error

处理根布局错误,必须在 /app 目录下,必须包含 htmlbody 标签,因为它会取代根 Layout 的内容,即

<ErrorBoundary fallback={<GlobalError />}>
  <RootLayout>
		<RootPage />
  </RootLayout>
</ErrorBoundary>
1
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>
  )
}
1
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>;
}
1
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>;
}
1
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'
  })
}
1
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'
  }
1
2
3
4
5
6

除了GET 方法,Next.js 还支持 POSTPUTPATCHDELETEHEADOPTIONS。对于不支持的方法,返回 405 Method Not Allowed

Next.js 接收 NextRequest (opens new window) 参数,返回 NextResponse (opens new window),它们分别扩展了原生的 Request (opens new window)Response (opens new window) ,以提供更多功能。

# default

用于并行路由。

# template

# References