CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Next.js 实战入门/课时/数据获取与缓存
RawBlame
课时目录12 节

Next.js 实战入门

  1. 01Next.js 是什么
  2. 02项目创建与目录
  3. 03App Router 与文件路由
  4. 04Server 与 Client 组件
  5. 05数据获取与缓存
  6. 06渲染模式 SSR / SSG / ISR
  7. 07Route Handlers
  8. 08MDX 与课时渲染
  9. 09Tailwind 与样式
  10. 10认证与登录门控
  11. 11读 platform 源码
  12. 12综合练习与部署

数据获取与缓存

async Server Component fetch、revalidate、与 React Router 对比。

35 分钟

数据获取与缓存

这一课你将能做到

  1. 在 Server Component 里直接 async/await fetch 拉取后端数据并渲染,不再需要 useEffect
  2. 说出 Next.js 三层缓存的分工,会配置 revalidate、no-store 与按需失效
  3. 把「对接 Spring Boot REST API」的完整取数链路写出来(本站 platform 的真实做法)

为什么要学

老 React 取数据的标准姿势是:组件挂载 → useEffect 发请求 → 三个状态变量(data/loading/error)→ 处理竞态。每个页面重复一遍,代码又长又容易出 bug。

App Router 把这套东西砍了:组件就是 async 函数。

// 过去:useState + useEffect 三件套,20 行
// 现在:
const res = await fetch("http://localhost:8080/api/tracks");
const tracks = await res.json();

数据在服务器上取好、和 HTML 一起送达,用户端没有 loading 闪烁,爬虫直接拿到内容。代价是你要理解缓存——服务端取数意味着每次渲染都打一次后端,不打缓存的话,高流量下后端会被打爆。所以「数据获取」和「缓存策略」在本课是同一件事的两面。

面试中「Next.js 的缓存机制」「fetch 什么时候重新请求」「SSG 数据更新了怎么办」是持续高频的题目,本课给出能背的标准答案。

先建立图像

先看完整链路,了解数据从哪来到哪去(本站同款架构):

浏览器 ──请求 /tracks/xxx──→ Next.js 服务器
                              │
                              ├─ ① 查 Next.js 数据缓存(Data Cache)
                              │      命中 → 直接用,不打后端
                              │      未命中 ↓
                              ├─ ② fetch → Spring Boot /api/tracks/xxx
                              │      拿到 JSON,按策略写入缓存
                              ↓
                        渲染成 HTML → 返回浏览器

fetch 缓存策略速查表(重点背这张):

写法行为适用
fetch(url)Next 14 默认缓存;Next 15 起默认不缓存视版本
fetch(url, { cache: "force-cache" })强制缓存(构建时/首次取后复用)内容稳定:课程目录、配置
fetch(url, { next: { revalidate: 3600 } })缓存 3600 秒,过期后台刷新(ISR)定期更新:文章、榜单
fetch(url, { cache: "no-store" })每次请求都现打后端实时数据:用户进度、库存

三层缓存的分工(面试标准图):

Request Memoization 请求记忆:同一次渲染内,相同 fetch 只发一次(自动,无需配置)
Data Cache 数据缓存:跨请求持久,revalidate 控制寿命
Router Cache 路由缓存:浏览器端缓存已访问过的路由段,点返回键秒开

跟着做

步骤 1:最基础的 Server 端取数

新建 src/app/tracks-demo/page.tsx:

interface Track {
  slug: string;
  title: string;
  lessonCount: number;
}

export default async function TracksPage() {
  const res = await fetch("http://localhost:8080/api/tracks");
  if (!res.ok) throw new Error("后端接口异常");
  const tracks: Track[] = await res.json();

  return (
    <main className="p-6">
      <h1 className="text-2xl font-bold">课程列表</h1>
      <ul className="mt-4 space-y-2">
        {tracks.map((t) => (
          <li key={t.slug} className="rounded border p-3">
            <span className="font-medium">{t.title}</span>
            <span className="ml-2 text-sm text-gray-500">{t.lessonCount} 课时</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

启动一个能返回 /api/tracks 的后端(或先用假接口),访问页面。没有 loading、没有 useEffect——数据是「渲染时就有」的。

步骤 2:像本站一样封装 API 层

本站 platform 用 src/lib/api.ts 统一封装所有后端调用,页面永远不裸写 fetch。仿写一份 src/lib/api.ts:

const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080";

async function request<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${API_BASE}${path}`, {
    headers: { "Content-Type": "application/json" },
    ...init,
  });
  if (!res.ok) {
    throw new Error(`API ${res.status}: ${path}`);
  }
  return res.json() as Promise<T>;
}

export function getTracks() {
  return request<{ slug: string; title: string }[]>("/api/tracks", {
    next: { revalidate: 300 }, // 目录类数据:缓存 5 分钟
  });
}

export function getTrackDetail(slug: string, token?: string) {
  return request(`/api/tracks/${slug}`, {
    headers: token ? { Authorization: `Bearer ${token}` } : undefined,
    cache: "no-store", // 带登录态的数据:绝不缓存
  });
}

页面里变成一行:

import { getTracks } from "@/lib/api";

export default async function TracksPage() {
  const tracks = await getTracks();
  // ...渲染
}

好处:缓存策略集中在 lib 层管理、后端地址换环境不用改页面、TS 类型一处定义。第 11 课读 platform 源码时你会看到一模一样的模式。

步骤 3:验证缓存效果

给接口加个计数(模拟后端)观察命中情况:

// 服务端打印,注意别在 build 时误触发
export default async function Page() {
  console.log("页面渲染于", new Date().toISOString());
  const tracks = await getTracks();
  return <p>共 {tracks.length} 门课</p>;
}

带 revalidate: 300 的版本:反复刷新,后端 5 分钟内只会收到一次请求;no-store 版本:每次刷新后端都收到请求。肉眼验证一次,缓存概念就落地了。

步骤 4:按需失效——revalidateTag

场景:管理员改了课程数据,希望立刻生效,而不是等 5 分钟。给请求打标签:

export function getTracks() {
  return request("/api/tracks", {
    next: { revalidate: 300, tags: ["tracks"] },
  });
}

在变更发生的地方(通常是 Route Handler 或 Server Action)触发失效:

import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";

// src/app/api/admin/refresh/route.ts
export async function POST() {
  revalidateTag("tracks"); // 所有带 tags:["tracks"] 的缓存立即作废
  return NextResponse.json({ ok: true });
}

下一次访问页面就会重新打后端。revalidatePath("/tracks") 则是按路径失效整个页面。这一对 API 是「SSG 页面如何更新」的标准答案。

步骤 5:请求记忆(Request Memoization)的实战意义

同一个渲染周期里多个组件取同一份数据:

// layout.tsx 里
const user = await getUser(token);
// page.tsx 里(同一个请求周期)
const user2 = await getUser(token); // 不会重复发请求

Next 自动对同一渲染内的相同 GET fetch去重。所以你可以在 layout 和 page 各自声明自己需要的数据,不用层层传 props——去重交给框架。注意:它只作用于单次渲染,和 Data Cache 是两个机制。

讲清楚要点

  1. 服务端取数的默认心智:数据获取发生在渲染时(服务器上),直接 await,拿到即可渲染;loading 态交给 loading.tsx 骨架页,而不是组件内的布尔值。
  2. 缓存策略必须在写代码时就明确:默认行为随版本变过(14 缓存、15 不缓存),所以工程惯例是显式声明——要么 revalidate 要么 no-store,不要裸 fetch 赌默认值。本站对「公开课程数据」用缓存、对「用户进度」用 no-store,就是按数据新鲜度分级。
  3. 带 Cookie/Token 的请求一律 no-store:缓存是「按 URL」共享的,如果 A 用户的登录态响应被缓存给了 B 用户,就是数据泄露事故。个人数据、鉴权后数据、强实时数据(支付、库存)都用 cache: "no-store"。
  4. 失效三板斧:时间到自动失效(revalidate: N);代码主动失效(revalidateTag/revalidatePath);部署即全量失效(每次 npm run build 产物全新)。生产排错先问「这页的缓存哪一层没失效」。
  5. 错误处理:服务端取数失败用 throw + 上层 error.tsx 兜底,或对非关键数据 try/catch 给默认值降级。notFound() 用于「数据不存在」的 404 语义。

你可能会懵的地方

  • Next 15 默认不缓存了,那这门课讲的缓存还有意义吗? 有。15 只是改了「默认值」,force-cache/revalidate/revalidateTag 全部可用且仍是性能主力。面试问的是机制,写代码时显式声明策略即可无视版本默认值差异。

  • revalidate 到期后,下一个用户要等刷新吗? 不等。Next 用 stale-while-revalidate 策略:过期后第一个请求立即返回旧缓存,同时后台异步重新拉取,拿到新数据后替换缓存——后续请求者见新数据。用户几乎无感。

  • useEffect 取数据是不是彻底没用了? Server 组件里用不了它;但纯 Client 场景仍然用:依赖用户输入的搜索联想、轮询、下拉分页这类「浏览器事件驱动」的取数,依然走 useEffect 或 SWR/React Query。原则:首屏需要的数据服务端取,交互后需要的数据客户端取。

  • 缓存会缓存我的登录态请求吗? 只要你显式 no-store 就不会。另外 Next 会自动给带 Cookie/Authorization 头的 fetch 关闭缓存(动态渲染),但别依赖自动行为,显式声明最安全。

  • 为什么本站首页要区分 NEXT_PUBLIC_API_URL 和 API_INTERNAL_URL? 浏览器端代码(Client 组件)只能走公网域名;服务端 SSR 取数时走容器内网地址,少一层 Nginx/SSL 开销且更稳。platform 的 src/lib/apiBase.ts 判断 typeof window === "undefined" 来分流——服务端环境变量体系的一个真实应用。

错法对照

错法问题正解
在 Client 组件里 await fetchClient 不能 async首屏数据上移到 Server 组件
带 token 的请求用了默认缓存可能串号:A 的数据给 Bcache: "no-store"
改了后端数据,页面死活不更新被缓存挡住加 revalidate 或用 revalidateTag 主动失效
fetch 后不检查 res.ok后端 500 时拿到的是错误 JSON,页面渲染出诡异空白if (!res.ok) throw new Error(...)
页面里到处裸写 fetch("http://localhost:8080")环境一换全炸集中封装到 lib/api.ts,用环境变量

卡点急救

  1. 开发环境数据改了页面不更新:dev 模式也有缓存(尤其加了 revalidate)。临时办法:重启 dev;正式办法:该请求改 no-store 或用 revalidateTag。
  2. 报错 fetch failed(ECONNREFUSED):后端没起或地址错。检查 Spring Boot 是否跑在 8080、环境变量是否写了 http:// 前缀、容器里是否能访问该主机名。
  3. 页面 TS 报错 Type 'any' is not assignable:fetch 的返回默认 any。给接口写 interface(步骤 1 的 Track),或让 request<T> 泛型兜住——顺便这也是面试里「你们项目怎么做类型安全」的素材。

面试常问

  • Next.js 有哪几层缓存?分别解决什么问题? 四层:Request Memoization 在单次渲染内去重相同 GET 请求;Data Cache 跨请求缓存 fetch 结果,由 revalidate 控制寿命,解决「别把后端打爆」;Full Route Cache 缓存整页渲染结果,配合 ISR 更新,解决「别每次都重新渲染」;Router Cache 在浏览器缓存路由段,解决「前进后退与导航秒开」。失效手段统一是 revalidateTag / revalidatePath。

  • fetch 的缓存怎么配置?数据更新了如何让页面立刻刷新? 显式传 cache: "force-cache" | "no-store" 或 next: { revalidate: 秒数, tags: [...] }。立即刷新用 revalidateTag("tag") 精确失效某类数据,或 revalidatePath("/path") 失效整页。revalidate 过期后走 SWR 语义:先回旧值、后台刷新。

  • 你们项目前后端怎么配合取数?缓存怎么分级? Next.js 作为前端层通过 REST 调 Spring Boot;lib 层统一封装 fetch 与类型。公开且低频变化的数据(课程目录)加 revalidate 缓存;用户维度数据(进度、权限、练习记录)带 token 且 no-store;SSR 服务端走内网 API 地址降低延迟。这个回答直接对应本站 platform 的真实实现。

自测三问

  1. revalidate: 300 和 cache: "no-store" 分别用在什么数据上?各举一个本站例子。
  2. 管理员改完课程标题,有哪些手段让前台立刻生效?
  3. Request Memoization 和 Data Cache 的区别是什么?

小结一句话

Server Component 让取数变成一行 await,而工程难点转移到了缓存:显式声明策略、公开数据缓存、个人数据不缓存、用 revalidateTag 精确失效。

延伸

  • Next.js 官方文档:Data Fetching and Caching
  • 下一课:渲染模式总览——SSR / SSG / ISR 到底怎么选。
加载本节练习…
← 上一课Server 与 Client 组件下一课 →渲染模式 SSR / SSG / ISR