Skip to content

第 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

嵌套规则

  1. Client Component 内部嵌套的子组件自动变 Client
  2. Server Component 可以渲染 Client Component。
  3. Client Component 不能直接 import Server Component,但可以通过 children prop 接受。

通俗理解: "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.tsx404 页
route.tsAPI 接口(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 路径前缀)

限制

  • 没有 fspath、原生 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.tsxpages/index.vue
app/layout.tsxlayouts/default.vue
app/api/route.tsserver/api/*.ts
middleware.tsmiddleware/*.ts
fetch 缓存策略useFetch / useAsyncData
Server Components(目前没有 RSC,默认 SSR)
<Link><NuxtLink>
<Image /><NuxtImg />
revalidate: 60routeRules: { '/': { 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 generaterouteRules: { 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 会报警告(开发环境抛红字)。

常见原因

  1. 用了 Date.now() / Math.random() —— 服务端和客户端值不同
  2. 用了 window / localStorage —— 服务端没有
  3. 第三方扩展往 DOM 里塞了节点(Grammarly、Dark Reader)
  4. 用户语言 / 时区在两端结果不同

解决方案

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 边界、代码分割)