主题
第 20 章 · 面试题集(Next.js / Nuxt)
Q1:什么是元框架(Meta-Framework)?为什么不直接用 React?
考察点:对前端工程演进的理解。
标准答案: 元框架是构建在 React / Vue 等框架之上、提供完整应用解决方案的"框架的框架"。它把路由、SSR / SSG、数据获取、API 接口、构建优化、部署等能力开箱集成。
React 自身只解决组件化和 UI 渲染,剩下的"如何组织一个应用"全靠用户自己拼装(要装 React Router、要写 webpack 配置、要做 SSR 时还得自己搞 Express + renderToString)。元框架把这些工程化决策都帮你做好了。
通俗理解: React 像"乐高积木",你能拼任何东西,但你得自己设计图纸;Next.js 像"乐高城堡套装",城堡已经给你设计好了,你只要按说明拼。
加分回答:
- Next.js 13+ 引入 App Router 和 React Server Components,进一步把"组件该在哪渲染"的决定权交给框架来优化。
- 元框架的兴起背景:SPA 首屏慢、SEO 差、前后端割裂的问题,需要在框架层面解决。
- Vercel、Shopify、Cloudflare 等平台也在推动元框架(Edge Runtime、Streaming、ISR)。
追问:
- Next.js 跟 Remix 有什么区别?(Remix 强调 Web 标准、表单/Loader/Action;Next 偏向 React 生态最佳实践)
- Astro 是元框架吗?(是,但偏内容站点,岛屿架构)
Q2:CSR、SSR、SSG、ISR、Streaming SSR 的区别?分别用在什么场景?
考察点:渲染策略——必考题。
标准答案:
| 策略 | HTML 何时生成 | 在哪生成 | 典型场景 |
|---|---|---|---|
| CSR | 浏览器运行时 | 浏览器 | 后台管理系统、Dashboard |
| SSR | 每次请求 | 服务器 | 个性化页面、实时数据(订单详情) |
| SSG | 构建时 | 构建机器 | 博客、文档、营销页(数据几乎不变) |
| ISR | 构建时 + 后台增量 | 服务器 | 商品详情、新闻列表(半动态) |
| Streaming SSR | 服务器边算边吐 | 服务器 | 复杂页面,希望首屏更快 |
通俗理解:
- CSR = 客人来了再炒菜(慢)
- SSR = 客人下单后中央厨房现做(每次都做)
- SSG = 提前把饭做好放冷柜(最快但只能吃存货)
- ISR = 提前做好但定期重做(兼顾速度与新鲜度)
- Streaming = 上菜不必等全套,先上前菜,后上主菜
代码示例:
ts
// Next.js App Router 用一个 fetch 就能切换
fetch(url); // 默认 SSG
fetch(url, { cache: "no-store" }); // SSR
fetch(url, { next: { revalidate: 60 } }); // ISR(60 秒)加分回答:
- ISR 第一次请求看到的是旧版(stale),后台触发重建后才返回新版(while-revalidate)。
- Streaming SSR 依赖 React 18 的
<Suspense>和renderToPipeableStream。 - SSG 在大型站点(10w+ 页面)构建时间会爆炸,这时 ISR / On-Demand ISR 是救命方案。
追问:
- SSR 完成后浏览器还要做什么?(hydration 水合)
- SSG 怎么解决用户登录后"个性化部分"?(外壳 SSG + 客户端动态拉取)
Q3:什么是 React Server Components?跟 SSR 有什么区别?
考察点:RSC 是 React 18 后最大的概念变化。
标准答案:
- SSR:在服务器把 React 组件渲染成 HTML 字符串返回,浏览器拿到后还要下载完整的 React + 组件代码进行水合,JS 包不会变小。
- RSC(Server Components):组件本身只在服务器执行,输出的不是 HTML 字符串而是一种特殊的"RSC 序列化格式",组件源码不会打包到客户端 Bundle。可以直接读数据库、用 Node 模块。
通俗理解:
- SSR 像"快递把组装好的家具送到家",但家具说明书(JS 源码)也得给你一份。
- RSC 像"宜家给你直接送装好的样品柜",连说明书都不给你(也不需要),更轻。
代码示例:
tsx
// 默认就是 Server Component
export default async function Posts() {
const data = await db.post.findMany(); // ✅ 直接访问数据库
return <ul>{data.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
// 需要交互时显式声明客户端
"use client";
function Like() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}加分回答:
- RSC 解决了"组件库越来越大、客户端 Bundle 越来越胖"的问题。
- RSC 与 SSR 不是互斥的:实际项目里 RSC 输出 + 经过 SSR 渲染成 HTML,再到客户端水合,三者一起协作。
- RSC 的传输格式是逐行的 JSON-like 流,可以与 Streaming SSR 天然结合。
追问:
- Server Component 里能用 useState 吗?(不能,会报错)
- Client Component 里能 import Server Component 吗?(不能直接 import,但可以作为 children prop 传入)
Q4:Server Components 和 Client Components 怎么选?嵌套规则是什么?
考察点:RSC 实战心智模型。
标准答案: 默认所有组件都是 Server Component,需要以下能力时才用 "use client":
- 浏览器 API(
window/localStorage) - React Hook(
useState/useEffect/useReducer/useContext) - 事件监听(
onClick/onChange) - 第三方库依赖浏览器环境(如
react-leaflet)
嵌套规则:
- Client Component 内部嵌套的子组件自动变 Client。
- Server Component 可以渲染 Client Component。
- Client Component 不能直接 import Server Component,但可以通过
childrenprop 接受。
通俗理解: "use client" 像在组件树上画了一条边界线,线之下全部是 Client,所以这条线要尽量画到叶子上。
代码示例:
tsx
// ❌ 反模式:在顶层 Layout 加 "use client"
"use client";
export default function Layout({ children }) {
return <div>{children}</div>; // 整棵树失去 RSC 优势
}
// ✅ 推荐:用 children prop 把 Server 内容当作槽传入
"use client";
export function ClientShell({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return <div>{open && children}</div>;
}
// 父级(Server Component)
<ClientShell>
<ServerOnlyHeavyComponent />
</ClientShell>加分回答:
- Next.js 的
<Link>、<Image>都是 Client Components,但它们体积很小,可以放心用。 - 通过
children模式可以让 Server Component 嵌入 Client 边界之下——这是绕过限制的核心技巧。
追问:
- 在 Server Component 里能
import "some-css.css"吗?(可以,CSS 在两侧都能用) - 怎么把数据从 Server Component 传给 Client Component?(用 props,但必须是可序列化的)
Q5:Next.js 的文件系统路由有哪些特殊文件名?各自作用?
考察点:App Router 基础。
标准答案:
| 文件 | 作用 |
|---|---|
page.tsx | 路由的 UI |
layout.tsx | 嵌套布局,切路由不重挂 |
template.tsx | 跟 layout 类似,但每次都重挂载 |
loading.tsx | 自动包 Suspense 的 fallback |
error.tsx | 错误边界(必须 "use client") |
not-found.tsx | 404 页 |
route.ts | API 接口(GET/POST/PUT…) |
default.tsx | 并行路由(Parallel Routes)的兜底 |
middleware.ts | 项目根的中间件 |
通俗理解: 就像 Linux 文件系统里的特殊文件名——.htaccess / index.html 一样,Next.js 给你一组保留文件名,文件名等于功能。
代码示例:
app/
blog/
layout.tsx ← 所有 /blog/* 共享
page.tsx ← /blog
loading.tsx ← 加载中 UI
error.tsx ← 出错时 UI
[slug]/
page.tsx ← /blog/:slug加分回答:
- 路由组
(group)/:括号包裹,不影响 URL,只用于组织布局。 - 私有目录
_components/:下划线开头,不会成为路由。 - 并行路由
@modal/:@开头,可以同时渲染多个 slot 到同一 layout。 - 拦截路由
(.)photo/、(..)photo/:用于"模态框 + URL 路由"模式(如 Instagram 点击图片弹层但 URL 也变化)。
追问:
layout.tsx切路由会不会重新执行?(不会,状态保留)- 为什么
error.tsx必须是客户端组件?(错误边界依赖 Client 生命周期)
Q6:Next.js 的 fetch 缓存策略是怎样的?怎么主动失效?
考察点:Next.js 的缓存系统——大型应用必懂。
标准答案:
Next.js 扩展了原生 fetch,默认开启 Data Cache:
ts
fetch(url); // 默认 cache: "force-cache"(永久缓存,SSG 行为)
fetch(url, { cache: "no-store" }); // 不缓存,每次请求都 fetch(SSR 行为)
fetch(url, { next: { revalidate: 60 } }); // 60 秒内复用,过期后台重建(ISR 行为)
fetch(url, { next: { tags: ["posts"] } }); // 打 tag,配合 revalidateTag 失效主动失效:
ts
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath("/posts"); // 按路径失效
revalidateTag("posts"); // 按 tag 失效通俗理解: Next.js 的缓存像一个"超大冰柜",默认所有 fetch 来的数据都进冰柜永久保存。你可以告诉它"这个东西 60 秒就要换新"(revalidate),或者"这个根本别进冰柜"(no-store),也可以事后"清掉这一批"(revalidateTag)。
加分回答:
- 实际有 4 层缓存:Router Cache、Full Route Cache、Data Cache、Request Memoization。
revalidateTag适合跨页面的相关数据失效(一个评论提交后,文章页和列表页都要刷新)。- Server Action 配合
revalidatePath是最常见的"提交后刷新"模式。
追问:
Cache-Control跟 Next.js Data Cache 是同一个东西吗?(不是,前者是 HTTP 浏览器/CDN 缓存,后者是服务端内置)- 如何禁用整个路由的缓存?(导出
export const dynamic = "force-dynamic")
Q7:Next.js 的 middleware 能做什么?跑在哪?有什么限制?
考察点:边缘计算与中间件。
标准答案: middleware.ts 在请求到达页面/API 之前执行,跑在 Edge Runtime(V8 isolate,不是完整 Node)。
典型用途:
- 鉴权重定向(未登录 → /login)
- A/B 测试(按 cookie 重写到不同变体)
- 地理重定向(按 geo 重写到 /cn 或 /en)
- 添加请求头 / 改写 URL
- 国际化(i18n 路径前缀)
限制:
- 没有
fs、path、原生 Node API - 只能用 Web 标准 API(
Request/Response/URL/Headers) - 体积上限(Vercel 默认 1MB)
代码示例:
ts
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
export function middleware(req: NextRequest) {
if (!req.cookies.get("token") && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/admin/:path*"],
};加分回答:
- Middleware 跑在 CDN 边缘节点,延迟极低(≈ 用户最近的城市)。
matcher越精准越好,避免每个请求都过中间件浪费成本。- 与
cookies()/headers()配合可以做"按用户分桶 + ISR"。
追问:
- middleware 能查询数据库吗?(理论上可以但不推荐,应放到 API Route)
- middleware 和 API Route 的区别?(middleware 在路由前拦截,API Route 是路由本身)
Q8:Next.js 跟 Nuxt 的核心概念怎么映射?
考察点:跨框架知识迁移能力。
标准答案:
| Next.js (App Router) | Nuxt 3 |
|---|---|
app/page.tsx | pages/index.vue |
app/layout.tsx | layouts/default.vue |
app/api/route.ts | server/api/*.ts |
middleware.ts | middleware/*.ts |
fetch 缓存策略 | useFetch / useAsyncData |
| Server Components | (目前没有 RSC,默认 SSR) |
<Link> | <NuxtLink> |
<Image /> | <NuxtImg /> |
revalidate: 60 | routeRules: { '/': { isr: 60 } } |
通俗理解: 两者就像安卓和 iOS——目标一致(开元框架体验),细节用语不同。学完 Next 看 Nuxt 几乎是"换语言不换思想"。
加分回答:
- Nuxt 的
useFetch在 SSR 时跑在服务端、客户端 hydration 时自动复用服务端拿到的数据,避免重复请求。 - Nuxt 的
routeRules可以把缓存策略写在nuxt.config.ts里集中管理(Next.js 是分散在每个 fetch)。 - Nuxt 自带 Nitro Server,可以输出到 Node、Cloudflare Workers、Deno Deploy 等多种 Runtime。
追问:
- Nuxt 里如何实现客户端组件?(默认就是同构,不需要 "use client",Vue 没有 RSC 边界)
- Nuxt 怎么做 SSG?(
nuxi generate或routeRules: { prerender: true })
Q9(加分题):什么时候必须 SSR?什么时候 SSG 更好?
考察点:场景化决策——架构师思维。
标准答案:
必须 SSR 的场景:
- 个性化数据(用户头像、订单、余额)
- 高时效内容(直播、股票、订单状态)
- 受用户 Cookie / Session 影响的内容
- 数据量极大、不可能全量预生成(百万级商品页)
SSG 更好的场景:
- 内容站点(博客、文档、营销页)
- 营销活动页(一段时间内不变)
- 全站搜索引擎要爬的页面
- 数据来源稳定且变更频率低
加分回答:
- "动态部分"和"静态外壳"可以拆分:外壳 SSG + 动态部分客户端 fetch / Server Component 流式渲染。
- ISR 是 SSG 与 SSR 的折中,95% 场景下是更好的选择。
- 大型电商常用:分类列表 ISR + 商品详情 ISR(短 revalidate)+ 购物车 SSR + 个人中心 CSR。
追问:
- 一个电商首页应该用什么策略?(外壳 ISR + 用户相关组件 Streaming + 个性化推荐 SSR)
Q10(加分题):什么是 hydration?hydration mismatch 怎么排查?
考察点:SSR 经典坑。
标准答案: Hydration(水合):服务端返回的 HTML 是静态的,浏览器拿到后还要执行 React 代码"复活"它——把事件监听、组件状态绑回 DOM。整个过程叫水合。
Hydration Mismatch:服务端渲染的 HTML 跟客户端首次渲染的虚拟 DOM 结构不一致,React 会报警告(开发环境抛红字)。
常见原因:
- 用了
Date.now()/Math.random()—— 服务端和客户端值不同 - 用了
window/localStorage—— 服务端没有 - 第三方扩展往 DOM 里塞了节点(Grammarly、Dark Reader)
- 用户语言 / 时区在两端结果不同
解决方案:
tsx
"use client";
import { useEffect, useState } from "react";
function Time() {
const [t, setT] = useState<string | null>(null);
useEffect(() => setT(new Date().toLocaleString()), []);
return <span>{t ?? "—"}</span>; // 服务端和客户端首屏都返回 "—"
}
// 或用 dynamic import 关闭 SSR
const NoSSRComp = dynamic(() => import("./Comp"), { ssr: false });加分回答:
- React 18 的部分水合(Selective Hydration)允许优先水合用户交互的部分。
- Next.js 的
suppressHydrationWarning可以单点抑制(如<time>标签),但不要滥用。
追问:
- 水合慢会带来什么问题?(FID 高、用户点了没反应)
- 怎么减少水合成本?(多用 Server Components、减少 Client 边界、代码分割)