115 lines
5.3 KiB
Markdown
115 lines
5.3 KiB
Markdown
---
|
||
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 应用的流式接口要注意什么?
|