Part 4 Next.js 初体验
约 2068 字大约 7 分钟
2025-07-08
我们注意到 React 官网中更加推荐 Next.js 的方式编写 React 应用。Next.js 的开发体验完全不同于 Vite、Webpack 等静态打包器,独特的路由和服务端渲染机制让其无愧于全栈框架之名。
1 Next.js 项目
1.1 创建一个 Next.js 项目
使用你喜欢的包管理器初始化一个 Next.js 仓库。
npx create-next-app@latest
pnpx create-next-app@latest
按照你的要求选取依赖:
❯ pnpx create-next-app@latest
√ What is your project named? ... nextjsdemo
√ 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
1.2 Next.js 项目结构
进入项目后,你将看到以下文件结构:
node_modules
...
public
...
src
app
layout.tsx
page.tsx
...
...
.gitignore
eslint.config.mjs
next-env.d.ts
next-config.ts
package.json
pnpm-lock.yaml
postcss.config.mjs
README.md
tsconfig.json
同样的,我们主要关注src
文件夹。所有的源代码文件都置于此处。Next.js 默认提供了layout.tsx
和page.tsx
的示例。layout.tsx
和page.tsx
相当于 Next.js 中的“保留字”,它们有着特殊的含义。
执行pnpm dev
即可启动开发服务器。
1.3 page.tsx
page.tsx
是页面入口,本质上是一个 React 组件。一个最简单的page.tsx
可以是这样的:
export default function Home() {
return <div>Hello Next.js!</div>
}
Next.js 只会选择名为page
的文件作为路由,而其他文件则会被当做普通组件。
app
page.tsx
layout.tsx
Button.tsx
import Button from "./Button"
export default function Home() {
return (
<>
<Button />
<div>Hello Next.js!</div>
</>
)
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="zh-cn">
<body>{children}</body>
</html>
)
}
"use client"
export default function Button() {
const handleClick = () => {
alert("Button clicked!")
}
return <button onClick={handleClick}>Click Me</button>
}
你可能已经注意到在Button.tsx
中出现了"use client"
一行代码,这是因为在 Next.js 13+ 的 App Router 中,组件默认是服务器组件(Server Component),而服务器组件不能包含客户端交互逻辑(如事件处理器)。要解决这个问题,你需要将组件转换为客户端组件,在文件顶部添加 "use client" 指令。这其中的原因我们后面再讲。
1.4 layout.tsx
layout.tsx
管理页面的布局,使得不同页面之间共享一致的 UI,例如顶栏、底栏、导航栏、侧边栏等。
一个最简单的layout.tsx
是这样的:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="zh-cn">
<body>{children}</body>
</html>
)
}
在body
元素中配置布局,例如:
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="zh-cn">
<Header />
<SideBar />
<body>{children}</body>
<Footer />
</html>
)
}
2 路由
Next.js 拥有 Pages Router 和 App Router 两套完全独立的路由系统。最新技术中推荐使用 App Router,因此 Pages Router 这里不做阐述。
2.1 App Router 基本使用
App Router 基于文件结构自动生成路由,而无需配置繁琐的路由表。例如这样一个文件结构:
app
layout.tsx
page.tsx
这使得app/page.tsx
成为我们的主页,浏览器访问https://localhost:3000/
即可。但如果我想添加一个关于页面该如何做?Next.js 的 App Router 提供了一种优雅的解决方案:
app
layout.tsx
page.tsx
about
page.tsx
...
...
是的,在 App Router 的帮助下,我们访问https://localhost:3000/about
即可到达app/about/page.tsx
,免去了配置路由表的麻烦。也就是说,page.tsx
充当了index.html
或者main.ts
的作用。
而如果要有一个https://localhost:3000/about/personal/
的页面,只需要再创建新的文件夹即可。
app
layout.tsx
page.tsx
about
page.tsx
personal
page.tsx
...
...
...
2.2 动态路由
对于非静态页面,往往要从服务器中取得实时数据。例如商品 ID,我们期望访问如下页面https://localhost:3000/goods/{good_id}
即可访问good_id
对应的商品。如果商品有成百上千,我们必不可能硬编码所有的商品页面。在使用路由表配置路由时我们可以使用动态路由动态获取,而 App Router 中,你可以这样做:
app
layout.tsx
page.tsx
goods
[ null ]
page.tsx
...
这样当我们访问https://localhost:3000/goods/123
时,App Router 将提供[good_id]
中的page.tsx
文件,而在页面内部我们可以捕获good_id
作为参数,我们可以利用这个参数做非常多的事情。
export default function ({ params }: { params: { good_id: string } }) {
return (
<div>
<h1>商品详情页</h1>
<p>这里是商品{params.good_id}的详细信息。</p>
</div>
)
}
3 服务端渲染
在 Next.js 中,主要有两种类型的组件:服务器组件和客户端组件。Next.js 默认将组件设置为服务器组件。
服务器组件会在服务器上渲染,并向客户端发送渲染完成的静态 HTML 文件。服务器组件对于非交互内容是非常理想的,例如图片展示、文章展示等。
而需要按钮点击、文本输入等交互,或者需要管理状态时,我们就可以使用客户端组件。需要注意的是,仅在必要情况下使用客户端组件。要显式声明一个客户端组件,只需要在 TSX 文件第一行使用"use client"
即可。
场景 | 服务端组件 | 客户端组件 |
---|---|---|
获取数据 | ✅ | ❌ |
直接访问后端资源 | ✅ | ❌ |
涉及敏感数据(Access Key、Token 等) | ✅ | ❌ |
减少客户端体积 | ✅ | ❌ |
交互与事件监听 | ❌ | ✅ |
状态、生命周期、副作用(useState()``useReducer()``useEffect() 等) | ❌ | ✅ |
浏览器 API | ❌ | ✅ |
4 API Routes
API Routes 是 Next.js 全栈能力的最直接体现。借助 Next.js,我们可以在 App Router 中构建 API 端点,使得 Next.js 应用更加容易处理后端逻辑。
4.1 API Routes 基本使用
在app
目录中,创建一个api
文件夹用于 API 的路由。和页面的路由一样,API 的路由也基于文件结构生成:
app
layout.tsx
page.tsx
api
getData
router.ts
...
...
我们可以访问https://localhost:3000/api/getData/
访问到router.ts
中的逻辑:
export async function GET(request: Request) {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1")
if (!res.ok) {
return new Response("Failed to fetch data", { status: 500 })
}
const data = await res.json()
return Response.json(data)
}
然后在组件中,我们就可以使用fetch
来访问这个 API:
const fetchData = async () => {
try {
const response = await fetch("/api/getData")
const data = await response.json()
console.log(data)
} catch (error) {
console.error("Error fetching data:", error)
}
}
API Routes 支持常见的 HTTP 方法:GET
POST
PUT
DELETE
PATCH
。只需要通过不同的命名导出即可:
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ received: body })
}
4.2 动态 API Routes
API Routes 同样支持动态路由,且使用方法几乎相同:
app
api
[ null ]
route.ts
...
...
export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
return Response.json({ userId: params.id })
}
4.3 为什么不能在组件中直接 fetch 后端资源?
你当然可以在组件中直接访问后端资源(比如第三方 API),但在以下场景下,建议通过 Next.js 的 API Routes 中转一层。
4.3.1 客户端组件运行在浏览器中
如果你在客户端组件中写:
const res = await fetch("https://mybackend.com/secret/api")
这段代码会在浏览器中执行,直接暴露真实后端地址,请求也能被抓包看到(如 token、header 等),任何用户都能看到你请求的 URL、参数,甚至访问令牌。
4.3.2 服务端资源需要访问数据库、使用密钥、调用本地服务等
这些行为只能在服务端环境中完成。你不能在客户端组件中执行这些操作。
你应当这样写:
export async function GET() {
const data = await db.query(...) // 数据库操作
return Response.json(data)
}
客户端再调用这个 API 路由:
const res = await fetch("/api/data")
这样,真正的资源访问只发生在服务器上,前端访问的是你暴露出来的“网关”。
4.3.3 分层架构与复用逻辑
如果你将所有后端请求逻辑封装到 API Routes,那么客户端与服务端之间通信统一走/api/...
,后续你可以在 API 中加入鉴权、中间件、节流等逻辑。再比如你要为多个页面复用同一个数据源,只需要写一个 API route。
这就是一种常见的后端网关封装模式(Backend for Frontend, BFF)。