主题
第 20 章 · Next.js / Nuxt 入门(元框架)
一句话开篇:本章带你理解什么是「元框架(Meta-Framework)」,搞懂 CSR / SSR / SSG / ISR / Streaming SSR 五种渲染策略,并通过 Next.js(App Router)和 Nuxt 3 把这些抽象概念落地到真实项目里。
0. 生活类比(先建立直觉)
想象你要开一家餐厅卖饭:
- React / Vue 就好比你买齐了所有厨具:锅、铲、菜刀、灶台、调料……该有的工具都给你了,但是怎么开餐厅、菜单怎么排、怎么招呼客人、怎么收银,你得自己琢磨。
- Next.js / Nuxt 就是「整套餐厅加盟方案」:除了厨具(React / Vue),还提供了:
- 装修好的店面(路由、页面结构)
- 菜单系统(文件即路由)
- 中央厨房(服务端渲染)
- 配送系统(API 路由、中间件)
- 预制菜方案(SSG / ISR)
- 收银结算(部署优化)
一句话:框架(React/Vue)让你能写组件,元框架(Next/Nuxt)让你能交付产品。
再换一个角度:
| 角色 | 类比 | 干啥的 |
|---|---|---|
| HTML/CSS/JS | 砖瓦水泥 | 最原始的建材 |
| React / Vue | 装修好的房间 | 解决组件化、状态管理 |
| Next.js / Nuxt | 精装修的整栋楼+物业服务 | 路由、SSR、缓存、部署一条龙 |
1. 什么是「元框架(Meta-Framework)」
1.1 定义
元框架 = 在前端框架(React、Vue、Svelte)之上,提供完整应用解决方案的"框架的框架"。
它把以下能力默认打包给你:
- 路由(不用再装 React Router / Vue Router)
- 服务端渲染(SSR)/ 静态生成(SSG)
- 数据获取与缓存
- API 接口(同一个项目里写后端接口)
- 构建优化、代码分割、图片优化
- 部署友好(与 Vercel / Netlify / Node Server / 边缘节点深度集成)
1.2 主流元框架一览
| 元框架 | 底层 | 公司 | 特点 |
|---|---|---|---|
| Next.js | React | Vercel | React 阵营事实标准,App Router + RSC |
| Nuxt | Vue | NuxtLabs | Vue 阵营标杆,DX(开发体验)好 |
| Remix | React | Shopify | 强调 Web 标准、表单 / Loader / Action |
| SvelteKit | Svelte | Svelte 团队 | 跟 Svelte 一样小巧 |
| SolidStart | Solid | Solid 社区 | 性能极致 |
| Astro | 多框架 | Astro | 内容站点为主,岛屿架构 |
本章主讲 Next.js(App Router 13+),并在末尾给出 Nuxt 3 的概念映射表。
2. 为什么需要元框架(vs 纯 React/Vue)
不用 Next/Nuxt,你直接用 CRA / Vite + React 行不行?能跑,但很多场景会很痛。
2.1 痛点对比表
| 维度 | 纯 React (CSR/SPA) | Next.js (元框架) |
|---|---|---|
| 首屏速度 | 白屏等 JS 加载完 → 渲染 | HTML 直接返回,毫秒级看到内容 |
| SEO | 爬虫看到空 <div id="root"></div> | 爬虫看到完整 HTML,搜索引擎友好 |
| 路由 | 自己装 React Router,配置一堆 | 文件即路由,开箱即用 |
| 代码分割 | 手动 React.lazy | 路由级自动分割 |
| 数据获取 | useEffect + fetch,加载态自己写 | 服务端组件 async 直接拿数据 |
| API 接口 | 单独起一个后端项目 | app/api/* 同项目里写接口 |
| 图片优化 | 自己改 <img>、自己写 lazyload | <Image> 自动 webp、lazy、CDN |
| 部署 | Nginx + 静态文件 + 反代 | 一键部署到 Vercel / Node / Edge |
2.2 三类典型场景,需求决定选型
场景 A:内部后台管理系统 / Dashboard
└─ 不在乎 SEO、不要白屏 → 纯 React + Vite 就够
场景 B:电商 / 媒体 / 落地页 / 博客
└─ 要 SEO、要首屏快、要分享可见
→ 必须 SSR / SSG → Next.js / Nuxt
场景 C:SaaS 产品(既要营销页又要 App 区)
└─ Next.js 一把梭,营销页 SSG,App 区 CSR3. 渲染策略详解(重点!面试必考)
这是元框架的灵魂。先记住一句话:"渲染" = HTML 在哪里生成、什么时候生成。
3.1 五种策略一览
浏览器请求
│
┌────────────┼────────────┐
│ │ │
CSR SSR SSG (+ ISR)
(客户端渲染) (服务端渲染) (构建时生成)
│ │ │
返回空 HTML 返回完整 HTML 返回静态 HTML
+ JS Bundle + 客户端水合 (CDN 直接给)3.2 五种策略对比表
| 策略 | HTML 何时生成 | 在哪生成 | 首屏 | SEO | 适用场景 |
|---|---|---|---|---|---|
| CSR | 浏览器运行时 | 浏览器 | 慢(白屏) | 差 | 后台、Dashboard |
| SSR | 每次请求 | 服务器 | 快 | 好 | 实时数据、个性化页面 |
| SSG | 构建时一次 | 构建机器 | 极快 | 好 | 博客、文档、营销页 |
| ISR | 构建时 + 后台增量 | 服务器 | 极快 | 好 | 商品页、新闻列表(半动态) |
| Streaming SSR | 服务器边算边吐 | 服务器 | 渐进式快 | 好 | 复杂页面拆 Suspense 分块返回 |
3.3 CSR:客户端渲染(传统 SPA)
浏览器 CDN/服务器
│ │
│── 请求 / ─────>│
│<── 空白 HTML ──│ (只有 <div id="root"></div> 和 main.js)
│ │
│── 请求 main.js ─>│
│<── JS Bundle ──│ (几百 KB)
│ 执行 JS │
│ 渲染 DOM │
│── 请求 API ────>│
│<── 数据 ───────│
│ 渲染数据 │
│ ✅ 用户看到内容 │特点:用户从打开链接到看到内容,要等 HTML + JS + API + 渲染 4 步。慢、白屏、爬虫看不到。
3.4 SSR:服务端渲染(每次请求都渲染)
浏览器 Node 服务器 数据库/API
│ │ │
│── 请求 / ─────>│ │
│ │── 取数据 ──────>│
│ │<── 数据 ────────│
│ │ 渲染 HTML │
│<── 完整 HTML ──│ │
│ ✅ 立即看到内容 │ │
│── 请求 hydrate.js ─>│ │
│<── JS ──────────│ │
│ hydrate 绑定事件│ │
│ ✅ 可交互 │ │特点:服务器干活,每次请求都跑一遍。用户首屏快,但服务器压力大。
⚠️ 注意:SSR 后浏览器还要做 hydration(水合)——把静态 HTML "激活"成可交互的 React 应用。
3.5 SSG:静态生成(构建时一次性产出 HTML)
构建时(一次):
Next build
│
│── 取数据 ──> API
│── 渲染所有页面 ──> /index.html, /about.html, /post/1.html ...
│── 上传到 CDN
用户运行时:
浏览器 ──> CDN ──> 直接返回静态 HTML(毫秒级)特点:极致速度、零运行时成本、CDN 友好。但数据更新需要重新构建。
3.6 ISR:增量静态再生(SSG 升级版)
SSG 的问题:博客新增一篇文章就得重新构建?太蠢了。
ISR 的解法:先返回旧版,后台静悄悄重建新版。
第 1 次请求 /post/1:
──> CDN 返回旧 HTML ──> 用户看到旧内容
│
└──> 服务器后台重建 /post/1 ──> 更新 CDN
第 2 次请求 /post/1:
──> CDN 返回新 HTML ──> 用户看到新内容关键参数:revalidate: 60(秒),表示"距离上次构建超过 60 秒就触发重建"。
3.7 Streaming SSR:流式渲染(React 18+)
痛点:传统 SSR 必须等所有数据都拿到才能返回 HTML。
解法:服务器边算边吐,先返回页面骨架,慢的部分用<Suspense>包起来后到后吐。
传统 SSR:等齐数据 → 一次性吐 30KB HTML(首屏 1500ms)
Streaming SSR:先吐头部 + 骨架(200ms)→ 边查数据边补 → 完成时间轴:
0ms ─── 收到请求
100ms ─── 吐 <head> + <header>
300ms ─── 吐文章主体
800ms ─── 吐评论列表(Suspense fallback 先撑着)
1200ms ─── 吐推荐位4. Next.js App Router 核心(13+)
Next.js 13+ 引入了 App Router(基于
app/目录)和 React Server Components(RSC),是当前的官方推荐方式。老的pages/目录叫 Pages Router,新项目别用了。
4.1 文件系统路由
app/
├─ layout.tsx # 根布局(所有页面共享)
├─ page.tsx # 首页 → /
├─ loading.tsx # 全局 loading UI
├─ error.tsx # 全局错误边界
├─ not-found.tsx # 404 页
├─ about/
│ └─ page.tsx # /about
├─ blog/
│ ├─ layout.tsx # /blog/* 共享布局
│ ├─ page.tsx # /blog
│ └─ [slug]/
│ └─ page.tsx # /blog/:slug 动态路由
├─ (marketing)/ # () 路由组,不影响 URL
│ ├─ pricing/page.tsx # /pricing
│ └─ contact/page.tsx # /contact
└─ api/
└─ users/route.ts # /api/users(API 接口)特殊文件名速查表
| 文件 | 作用 |
|---|---|
page.tsx | 路由的页面组件 |
layout.tsx | 嵌套布局(嵌套时不重新挂载) |
loading.tsx | 自动包 <Suspense>,加载中显示 |
error.tsx | 错误边界,必须是 Client Component |
not-found.tsx | 404 |
template.tsx | 类似 layout,但每次切换都重挂载 |
route.ts | API 接口(替代 pages/api) |
middleware.ts(项目根) | 中间件(在 Edge 上运行) |
4.2 Server Components vs Client Components(核心!)
这是 React 18 后最大的变化,也是面试官最爱问的。
类比:服务端组件 = 后厨预制菜,客户端组件 = 餐桌现做菜
| 类型 | 类比 | 特点 |
|---|---|---|
| Server Component(默认) | 后厨预制菜 | 在服务器做好,端到桌上就是成品。没有 JS 发到客户端,包体积小。可直接读数据库、用 Node API。不能用 useState / useEffect / 浏览器 API。 |
Client Component("use client") | 餐桌现做菜(火锅) | 食材发到客户端,客户自己交互。JS 会打包到 Bundle,可以用 hooks、事件、浏览器 API。 |
怎么用
tsx
// app/page.tsx —— 默认是 Server Component
import { db } from "@/lib/db";
export default async function HomePage() {
// ✅ Server Component 可以 async 直接拿数据
const posts = await db.post.findMany();
return (
<div>
<h1>文章列表</h1>
<PostList posts={posts} />
{/* 嵌入一个客户端组件 */}
<LikeButton />
</div>
);
}tsx
// app/components/like-button.tsx
"use client"; // 👈 必须显式声明
import { useState } from "react";
export function LikeButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>👍 {count}</button>;
}心智模型:组件树的"边界"
<RootLayout> ← Server
<HomePage> ← Server
<PostList> ← Server(直接读数据)
<LikeButton> ← Client(交互)
<Avatar> ← Client(在 Client 内部,自动 Client)规则:Client Component 内部嵌套的子组件自动也是 Client。所以
"use client"的位置要尽量"下沉"到叶子节点,把上层留给 Server。
4.3 Data Fetching(数据获取)
tsx
// app/posts/page.tsx
async function getPosts() {
// ✅ Next.js 扩展了原生 fetch,加了缓存语义
const res = await fetch("https://api.example.com/posts", {
// 默认就是 force-cache(SSG),下面是几种策略:
// cache: "force-cache", // SSG(构建时缓存,永久)
// cache: "no-store", // SSR(每次请求都重新拿)
next: { revalidate: 60 }, // ISR(60 秒内复用)
// next: { tags: ["posts"] }, // 配合 revalidateTag 主动失效
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}缓存策略选择决策树
数据是否会经常变?
├─ 几乎不变(如官网文案)
│ └─ cache: "force-cache" → SSG
├─ 半小时一变(如商品列表)
│ └─ next: { revalidate: 1800 } → ISR
└─ 实时(如订单详情、用户余额)
└─ cache: "no-store" → SSR4.4 API Routes(route.ts)
ts
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(req: NextRequest) {
const body = await req.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}Next.js 让你前后端同项目,特别适合做 BFF(Backend for Frontend)。
4.5 中间件(middleware.ts)
在请求到达页面之前拦截,跑在 Edge Runtime 上(V8 isolate,不是完整 Node)。
ts
// middleware.ts(放在项目根)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const token = req.cookies.get("token");
// 没登录访问 /dashboard 就跳到 /login
if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};典型用途:鉴权、A/B 测试、地理重定向、添加请求头、改写 URL。
5. Nuxt 3 简介(对照 Next 的概念映射)
如果你团队用 Vue,那就用 Nuxt。它的设计理念跟 Next 高度相似,学完 Next 看 Nuxt 几乎不费力。
5.1 概念映射表
| Next.js (App Router) | Nuxt 3 | 说明 |
|---|---|---|
app/page.tsx | pages/index.vue | 页面 |
app/layout.tsx | layouts/default.vue | 布局 |
app/loading.tsx | <NuxtLoadingIndicator> | 加载条 |
app/api/users/route.ts | server/api/users.ts | API 路由 |
middleware.ts | middleware/auth.ts | 中间件(路由级) |
| Server Components | (默认 SSR,无 RSC 概念) | Vue 还没原生 RSC |
fetch(...) 缓存 | useFetch / useAsyncData | Composable |
next.config.js | nuxt.config.ts | 配置 |
<Link href> | <NuxtLink to> | 路由跳转 |
<Image /> | <NuxtImg />(@nuxt/image) | 图片优化 |
revalidate: 60 | routeRules: { '/': { isr: 60 } } | ISR 配置 |
5.2 Nuxt 最小示例
vue
<!-- pages/index.vue -->
<script setup lang="ts">
const { data: posts } = await useFetch("/api/posts");
</script>
<template>
<div>
<h1>文章列表</h1>
<ul>
<li v-for="p in posts" :key="p.id">{{ p.title }}</li>
</ul>
</div>
</template>ts
// server/api/posts.ts
export default defineEventHandler(async () => {
return await db.post.findMany();
});Nuxt 的
useFetch在 SSR 时跑在服务端、CSR 时跑在客户端,自动避免重复请求(服务端拿到的数据会序列化到 HTML 里,客户端直接复用)。
6. 数据获取与缓存策略(深入)
6.1 Next.js 的多级缓存
┌──────────────────────────────────┐
│ 浏览器(Router Cache) │ 客户端路由切换缓存
└──────────────────────────────────┘
│
┌──────────────────────────────────┐
│ Full Route Cache │ 整页 HTML/RSC payload 缓存
└──────────────────────────────────┘
│
┌──────────────────────────────────┐
│ Data Cache(fetch 默认开启) │ API 响应缓存
└──────────────────────────────────┘
│
┌──────────────────────────────────┐
│ Request Memoization │ 同一次渲染中相同 fetch 去重
└──────────────────────────────────┘6.2 主动失效缓存
ts
import { revalidatePath, revalidateTag } from "next/cache";
// 提交评论后,刷新文章页
export async function addComment(formData: FormData) {
"use server";
await db.comment.create({ data: { /* ... */ } });
revalidatePath("/posts/[slug]"); // 按路径
revalidateTag("comments"); // 按 tag
}7. 实战案例
详见 examples/ 目录:
next-app-route.tsx—— App Router 路由 + 动态路由示例next-server-component.tsx—— Server / Client 组件配合next-api-route.ts—— API 接口写法nuxt-page.vue—— Nuxt 页面 + useFetch
8. ⚠️ 易踩的坑
坑 1:在 Server Component 里用 useState / useEffect
tsx
// ❌ 报错:useState is not a function
export default function Page() {
const [count, setCount] = useState(0); // 服务端没有 useState
return <div>{count}</div>;
}
// ✅ 改为客户端组件
"use client";
import { useState } from "react";
export default function Page() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}坑 2:"use client" 写在最顶层就把整棵树都标成 Client
错误做法:在 app/layout.tsx 顶部写 "use client",导致整个 App 失去 Server Components 的优势。
正确:把 "use client" 推到最叶子的交互组件(按钮、输入框)上。
坑 3:在 Client Component 里 import 服务端模块
tsx
"use client";
import { db } from "@/lib/db"; // ❌ 服务端代码会被打到客户端 Bundle,泄露密钥还报错坑 4:fetch 默认有缓存,调试时怎么也刷新不到新数据
ts
fetch(url); // 默认 cache: "force-cache",永久缓存
// 调试 / 实时数据要显式:
fetch(url, { cache: "no-store" });坑 5:把 Next.js 当成"自带 SEO"——但 Server Component 拼出来的 HTML 才有 SEO
如果你在 Client Component 里 useEffect 异步拉数据,那部分内容爬虫还是看不到,等于 CSR。
坑 6:动态路由 [id] 和 catch-all [...slug] 弄混
| 文件 | 匹配 | 取值 |
|---|---|---|
[id]/page.tsx | /post/123 | params.id = "123" |
[...slug]/page.tsx | /post/a/b/c | params.slug = ["a","b","c"] |
[[...slug]]/page.tsx | /post 或 /post/a/b | slug 可选 |
坑 7:在 Edge Middleware 里用 Node API
middleware.ts 跑在 Edge Runtime(V8 isolate),没有 fs、path、原生 Buffer。需要这些就只能放到 API Route 里。
9. 一句话总结
元框架(Next.js / Nuxt)= 框架 + 路由 + 服务端渲染 + 数据获取 + 部署优化;选哪种渲染策略(CSR/SSR/SSG/ISR/Streaming)取决于"数据有多动态"和"是否需要 SEO",搞不清楚的时候默认 Server Components + ISR 准没错。
10. 延伸阅读
- Next.js 官方文档:https://nextjs.org/docs
- Nuxt 官方文档:https://nuxt.com/docs
- React Server Components 原文:https://react.dev/reference/rsc/server-components
- Vercel 渲染策略详解:https://vercel.com/docs/frameworks/nextjs
- Patterns.dev(前端架构模式):https://www.patterns.dev/