Files
zhuangang/topics/frontend/ssr/02-nextjs-app-router.md
T

115 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: frontend.ssr.nextjs-app-router
title: Next.js App Router:服务端组件、客户端组件、路由处理器做 AI 流式接口、缓存
type: lesson
domain: frontend
stack: [ssr, react]
tags: [Next.js, App Router, Server Components, use client, Route Handlers, Server Actions, 流式响应, Turbopack]
level: intermediate
prerequisites: [frontend.react.hooks-rendering, frontend.ssr.rendering-modes]
est_hours: 3
related: [frontend.ssr.scenario.help-center, backend.nodejs.index, frontend.ssr.interview]
week: [7]
order: 2
---
# Next.js App Router:服务端组件、客户端组件、路由处理器做 AI 流式接口、缓存
> 基于 Next.js 16 编写并实际构建通过。Next.js 大版本之间变化很多(例如 16 版默认使用 Turbopack 构建,`middleware` 文件约定改名为 `proxy`,缓存机制也在调整),以官方文档为准:https://nextjs.org/docs 。BOSS直聘 2026-10-23 的调研中有岗位提到 Next.js,它也是“前端出身做全栈”最常见的选择。
## 解释说明
### App Router 的核心概念
| 概念 | 说明 |
| --- | --- |
| 文件路由 | `app/` 目录:`page.tsx` 页面、`layout.tsx` 布局、`loading.tsx` 加载态、`error.tsx` 错误边界、`route.ts` 接口 |
| **服务端组件**(默认) | 在服务器上运行,可以直接访问数据库 / 密钥,代码不会发到浏览器;不能用 `useState`、事件处理 |
| **客户端组件** | 文件顶部写 `"use client"`,可以有状态和交互;它和它导入的模块会打包到浏览器 |
| 路由处理器 | `app/api/xxx/route.ts` 导出 `GET` / `POST` 等函数,使用 Web 标准的 `Request` / `Response`,可以返回流 |
| Server Actions | `"use server"` 的异步函数,可以直接作为表单的 `action`,在服务端执行 |
| 缓存和重新验证 | 数据请求和页面的缓存策略(`revalidate`、按路径 / 标签失效等),不同版本默认值不同,以官方文档为准 |
**边界原则**:尽量让组件保持为服务端组件,只把需要交互的“叶子”做成客户端组件,减少浏览器端的 JS;不能把函数、类实例作为 props 从服务端组件传给客户端组件(必须可序列化)。
### 用 Next.js 做 AI 接口
路由处理器中调用模型的流式接口,返回 `ReadableStream`;模型密钥只在服务端;用 `request.signal` 感知用户断开并停止生成。复杂的 Agent 逻辑仍然建议放在 Python 服务中,Next.js 作为 BFF(面向前端的后端)转发和鉴权。
## 案例
帮助中心:常见问题由服务端组件渲染(构建时静态生成,SEO 好);智能客服是客户端组件,调用 `/api/chat` 路由处理器,后者流式返回答案。构建结果中 `/` 是静态页面,`/api/chat` 是按需执行的动态路由。
## 代码示例
完整项目:[examples/next-ai-chat](examples/next-ai-chat/)(`npm install && npm run build` 已构建通过,`next start` 后实际请求验证)。
路由处理器([app/api/chat/route.ts](examples/next-ai-chat/app/api/chat/route.ts)):
```ts
export async function POST(request: Request): Promise<Response> {
const { question } = (await request.json()) as { question?: unknown };
if (typeof question !== "string" || question.length === 0 || question.length > 500) {
return Response.json({ error: "question 必须是 1~500 字的字符串" }, { status: 400 });
}
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
try {
for await (const token of fakeLLM(question, request.signal)) { // signal:用户断开就停止
controller.enqueue(encoder.encode(token));
}
} finally {
controller.close();
}
},
});
return new Response(stream, { headers: { "Content-Type": "text/plain; charset=utf-8", "X-Accel-Buffering": "no" } });
}
```
服务端组件页面 + 客户端组件([app/page.tsx](examples/next-ai-chat/app/page.tsx)、[app/chat.tsx](examples/next-ai-chat/app/chat.tsx)):
```tsx
// app/page.tsx —— 服务端组件:取数和渲染都在服务器
export default async function Page() {
const faqs = await getFaqs();
return (
<main>
{faqs.map((f) => <details key={f.id}><summary>{f.q}</summary><p>{f.a}</p></details>)}
<Chat /> {/* 客户端组件 */}
</main>
);
}
// app/chat.tsx
"use client";
export function Chat() { const [answer, setAnswer] = useState(""); /* fetch + 读取流 */ }
```
构建输出:
```text
Route (app)
┌ ○ /
├ ○ /_not-found
└ ƒ /api/chat
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
```
## 易错点
- 在服务端组件中使用 `useState` 或事件处理,或者忘了写 `"use client"`。
- 把整个页面都标记为 `"use client"`,失去服务端组件的好处。
- 在客户端组件中导入了只能在服务端使用的模块(数据库客户端、密钥)。
- 以为缓存行为和上个版本一样:升级大版本前阅读升级指南。
- 部署在 Nginx 后面时流式响应被缓冲。
- 把复杂的 Agent 长任务放在路由处理器中执行,受函数超时限制(尤其是 Serverless 部署)。
## 面试常问
- 服务端组件和客户端组件的区别?边界怎么划分?
- Next.js 的路由处理器和 Server Actions 分别用在哪里?
- 用 Next.js 做 AI 应用的流式接口要注意什么?