Skip to content

第 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.jsReactVercelReact 阵营事实标准,App Router + RSC
NuxtVueNuxtLabsVue 阵营标杆,DX(开发体验)好
RemixReactShopify强调 Web 标准、表单 / Loader / Action
SvelteKitSvelteSvelte 团队跟 Svelte 一样小巧
SolidStartSolidSolid 社区性能极致
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 区 CSR

3. 渲染策略详解(重点!面试必考)

这是元框架的灵魂。先记住一句话:"渲染" = 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.tsx404
template.tsx类似 layout,但每次切换都重挂载
route.tsAPI 接口(替代 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" → SSR

4.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.tsxpages/index.vue页面
app/layout.tsxlayouts/default.vue布局
app/loading.tsx<NuxtLoadingIndicator>加载条
app/api/users/route.tsserver/api/users.tsAPI 路由
middleware.tsmiddleware/auth.ts中间件(路由级)
Server Components(默认 SSR,无 RSC 概念)Vue 还没原生 RSC
fetch(...) 缓存useFetch / useAsyncDataComposable
next.config.jsnuxt.config.ts配置
<Link href><NuxtLink to>路由跳转
<Image /><NuxtImg />(@nuxt/image)图片优化
revalidate: 60routeRules: { '/': { 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/123params.id = "123"
[...slug]/page.tsx/post/a/b/cparams.slug = ["a","b","c"]
[[...slug]]/page.tsx/post/post/a/bslug 可选

坑 7:在 Edge Middleware 里用 Node API

middleware.ts 跑在 Edge Runtime(V8 isolate),没有 fspath、原生 Buffer。需要这些就只能放到 API Route 里。


9. 一句话总结

元框架(Next.js / Nuxt)= 框架 + 路由 + 服务端渲染 + 数据获取 + 部署优化;选哪种渲染策略(CSR/SSR/SSG/ISR/Streaming)取决于"数据有多动态"和"是否需要 SEO",搞不清楚的时候默认 Server Components + ISR 准没错。


10. 延伸阅读