5.3 KiB
5.3 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend.ssr.nextjs-app-router | Next.js App Router:服务端组件、客户端组件、路由处理器做 AI 流式接口、缓存 | lesson | frontend |
|
|
intermediate |
|
3 |
|
|
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(npm install && npm run build 已构建通过,next start 后实际请求验证)。
路由处理器(app/api/chat/route.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、app/chat.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 + 读取流 */ }
构建输出:
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 应用的流式接口要注意什么?