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

5.3 KiB
Raw Blame History

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
ssr
react
Next.js
App Router
Server Components
use client
Route Handlers
Server Actions
流式响应
Turbopack
intermediate
frontend.react.hooks-rendering
frontend.ssr.rendering-modes
3
frontend.ssr.scenario.help-center
backend.nodejs.index
frontend.ssr.interview
7
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 应用的流式接口要注意什么?