--- 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 { 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({ 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 (
{faqs.map((f) =>
{f.q}

{f.a}

)} {/* 客户端组件 */}
); } // 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 应用的流式接口要注意什么?