Next.js 新数据获取三剑客:fetch() + cache() + use —— 从引擎盖下聊到赛道上

  • 读者画像:使用 Next.js 13+/14 的开发者,想吃透 Server Components、缓存与并发。
  • 技术关键词:Server Components、Request Memoization、Full-route Cache、Revalidation、React Suspense、用 use 读取 promise。

一、时代变了:数据获取从"接口即拉"到"服务端即思考"

在 React 传统客户端时代,数据获取是"组件挂载 → useEffect → fetch → setState"。Next.js 13+ 以后,服务端组件成为主角,数据获取"前置"到了服务器渲染阶段,顺便把缓存、并发、流式、SEO 一锅端正。

新三剑客各司其职:

  • fetch:带有框架级缓存语义的"带脑子"的请求器。
  • cache:对任意纯函数做"结果记忆",并与 React 的数据获取黏合。
  • use:对 promise 说"请你等会儿",自动挂起,搭配 Suspense 实现流式与并发。

小图标提示:🧠 表示"有状态的聪明操作",⚙️ 表示"底层机制",🚀 表示"性能与并发"。


二、fetch 的真身:不仅是请求,更是声明缓存策略的"合同"

在 Next.js App Router 中,fetch 默认不是裸奔:

  • 同一路由请求内:同 URL 和相同选项的 fetch 会"请求记忆"(Request Memoization),第二次读取直接复用上次结果(🧠 节流降噪)。
  • 全路由层面:配合缓存策略,结果可被渲染层缓存(Full-route Cache),走静态化或增量再生。

核心选项:

  • cache: 'force-cache'(默认)、'no-store'
  • next: { revalidate: 秒数 } 增量再验证
  • next: { tags: ['...'] } 用于按 tag 失效
  • headers、method 等则决定"请求唯一性"。

最常见的三种姿势:

javascript 复制代码
// 1) 完全静态,可被构建或首请求缓存
export async function getPostStatic(id) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    // 默认就是 force-cache
    // cache: 'force-cache'
  });
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
}

// 2) 纯动态,永远直连后端
export async function getPostDynamic(id) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    cache: 'no-store',
  });
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
}

// 3) ISR(增量再验证):N 秒后下一次请求触发再拉取
export async function getPostISR(id) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { revalidate: 60 }, // 60 秒
  });
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
}

注意点(⚙️ 底层脾性):

  • 请求唯一性键由 URL + method + headers + body + fetch 选项共同决定。
  • 同一请求周期内,请求记忆会避免重复 IO,避免"你问一次我答两次"的尴尬。
  • 使用 cookies/session 影响静态化,Next 会将页面判定为动态(例如读取 request headers)。

三、cache:给纯函数一把"记忆锤",把数据源变成幂等接口

cache(fn) 返回一个"会记住输入→输出"的函数,缓存键由参数值决定。与 fetch 不同,它是对"函数级"层面的记忆,常用于聚合数据、组合查询、或昂贵计算。

  • 纯函数?好朋友。无副作用,输入相同,输出稳定(🧠 可安全记忆)。
  • 动态参数?仍可记忆,但注意参数可序列化和稳定性。
  • 与 revalidate/tag 搭配?可以,但需要配合 route handler 的 revalidateTag 等手段去主动失效。

例:聚合用户详情与文章列表,避免重复计算。

javascript 复制代码
import { cache } from 'react';

// 带记忆的聚合查询
export const getUserBundle = cache(async function getUserBundle(userId) {
  const [userRes, postsRes] = await Promise.all([
    fetch(`https://api.example.com/users/${userId}`, { next: { revalidate: 300 } }),
    fetch(`https://api.example.com/users/${userId}/posts`, { next: { revalidate: 300 } })
  ]);

  if (!userRes.ok || !postsRes.ok) throw new Error('Upstream error');

  const [user, posts] = await Promise.all([userRes.json(), postsRes.json()]);
  return { user, postsCount: posts.length };
});

// 相同 userId 多处调用,仅首次会命中 IO,后续来自函数级缓存

注意点(⚙️):

  • cache 作用域在 server runtime 内,与请求记忆不同,生命周期可跨请求,但实际寿命取决于部署与进程回收(在 Serverless 场景不保证长寿,像金鱼的记忆力,有时说走就走)。
  • 如果外部数据变化频繁,记得设计失效策略:例如使用 revalidateTag 并在数据变更后打标签失效。

四、use:让 promise 说人话------"我还没好,你先挂起"

React 的 use(来自 react 包,不是 React Hook)可在 Server Components 或 RSC 环境中"读取 promise"。当 promise 未完成时,组件会挂起(suspend),Suspense 辅助显示 fallback,实现并发与流式。

  • use(promise) 返回已解析的值,若未解析则触发挂起。
  • 在 Server Components 中,挂起会被流式分块传输,Out-of-order chunking 提升首屏时间。
  • 在 Client Components 中使用 use 需要 React 19+ 并受限,通常通过 Server Components 来承担数据获取更自然。

例:并发拉取与 use 协同:

javascript 复制代码
// app/users/[id]/page.js
import { Suspense } from 'react';
import { use } from 'react';

function UserView({ userPromise }) {
  const user = use(userPromise); // 如果还没来,就挂起
  return (
    <section>
      <h2>👤 {user.name}</h2>
      <p>Email: {user.email}</p>
    </section>
  );
}

function PostsView({ postsPromise }) {
  const posts = use(postsPromise);
  return (
    <section>
      <h3>📝 Posts</h3>
      <ul>
        {posts.map(p => <li key={p.id}>{p.title}</li>)}
      </ul>
    </section>
  );
}

export default function Page({ params }) {
  const { id } = params;

  // 并发启动请求:避免串行瀑布
  const userPromise = fetch(`https://api.example.com/users/${id}`, { next: { revalidate: 120 } }).then(r => r.json());
  const postsPromise = fetch(`https://api.example.com/users/${id}/posts`, { cache: 'no-store' }).then(r => r.json());

  return (
    <>
      <h1>🚀 用户信息</h1>
      <Suspense fallback={<p>加载用户...</p>}>
        <UserView userPromise={userPromise} />
      </Suspense>
      <Suspense fallback={<p>加载文章列表...</p>}>
        <PostsView postsPromise={postsPromise} />
      </Suspense>
    </>
  );
}

亮点:

  • 两个请求并发启动,组件内用 use 消费。
  • 每个分块独立 Suspense,谁先好谁先渲染,用户先看到头像再看列表,首屏更快。

五、三剑客联手:从"拉数据"到"声明数据生命周期"

一套实战示范:产品页,需求如下

  • 详情相对稳定:可 ISR。
  • 库存变化频繁:要动态。
  • 推荐列表可缓存 5 分钟。
  • 用户点击"刷新推荐"后,手动失效。

目录:app/products/[id]/page.js

javascript 复制代码
import { Suspense } from 'react';
import { cache } from 'react';
import { revalidateTag } from 'next/cache';
import { use } from 'react';

// 1) 详情:ISR 60 秒
const getProduct = cache(async (id) => {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { revalidate: 60, tags: [`product:${id}`] }
  });
  if (!res.ok) throw new Error('Failed to fetch product');
  return res.json();
});

// 2) 库存:纯动态
const getStock = cache(async (id) => {
  const res = await fetch(`https://api.example.com/stock/${id}`, { cache: 'no-store' });
  if (!res.ok) throw new Error('Failed to fetch stock');
  return res.json();
});

// 3) 推荐:5 分钟缓存,可标签失效
const getRecommendations = cache(async (id) => {
  const res = await fetch(`https://api.example.com/reco?pid=${id}`, {
    next: { revalidate: 300, tags: [`reco:${id}`] }
  });
  if (!res.ok) throw new Error('Failed to fetch recommendations');
  return res.json();
});

// 小小 action:允许用户触发标签失效(需要 Next.js 的 Server Actions)
export async function refreshRecommendations(formData) {
  'use server';
  const id = formData.get('id');
  revalidateTag(`reco:${id}`);
}

function ProductSection({ productP }) {
  const product = use(productP);
  return (
    <section>
      <h2>📦 {product.title}</h2>
      <p>{product.description}</p>
      <p>价格:¥{product.price}</p>
    </section>
  );
}

function StockSection({ stockP }) {
  const stock = use(stockP);
  return (
    <section>
      <h3>📊 库存</h3>
      <p>剩余:{stock.remaining}</p>
    </section>
  );
}

function RecoSection({ recoP, id }) {
  const reco = use(recoP);
  return (
    <section>
      <h3>✨ 为你推荐</h3>
      <ul>
        {reco.items.map(r => <li key={r.id}>{r.title}</li>)}
      </ul>
      <form action={refreshRecommendations}>
        <input type="hidden" name="id" value={id} />
        <button type="submit">🔄 刷新推荐</button>
      </form>
    </section>
  );
}

export default function Page({ params }) {
  const { id } = params;

  // 并发触发 promise,搭配 use 消费
  const productP = getProduct(id);
  const stockP = getStock(id);
  const recoP = getRecommendations(id);

  return (
    <>
      <h1>🛒 商品详情</h1>
      <Suspense fallback={<p>加载商品详情...</p>}>
        <ProductSection productP={productP} />
      </Suspense>

      <Suspense fallback={<p>查询库存...</p>}>
        <StockSection stockP={stockP} />
      </Suspense>

      <Suspense fallback={<p>计算推荐...</p>}>
        <RecoSection recoP={recoP} id={id} />
      </Suspense>
    </>
  );
}

这里,我们把生命周期"声明"出来:

  • 详情:60 秒自动再生。
  • 库存:永远直连,不缓存。
  • 推荐:5 分钟缓存,可手动失效(标签模式)。

六、底层原理串讲:从字节、管线到缓存层

  • 请求记忆(Request Memoization):同一请求周期内的相同 fetch 调用,框架在内存里存放 promise,复用其结果,避免重复触发 IO。像同车厢的人合并点餐。
  • 渲染管线(React Flight + Suspense):Server Components 渲染成特殊 payload 流,客户端逐块接收并复用缓存树。use 遇到未完成 promise 就挂起,Suspense 提供局部 fallback,达成"水龙头先出清水,后出冰块"的体验。
  • 缓存边界(Full-route Cache vs. Per-fetch):页面整体可被缓存为产物,也可在每个数据源级别设定 revalidate/标签。两者叠加时,以更"动态"的一方决定最终是否能静态化。
  • 失效策略:时间(revalidate 秒数)是一把日晷,标签失效是一把按钮。按钮在你手里,别乱按。

七、易踩坑清单(带幽默警示牌)

  • 混用请求头导致缓存键剧增:headers 里放入无用的随机值,就像给每份外卖改名字,快递员迷路了。
  • 在 Client Component 里乱用 fetch:你以为在省油,其实在前端点火自燃。数据获取尽量交给 Server Components。
  • cache 包装非纯函数:今天返回 42,明天返回"香蕉",记忆系统直接人格分裂。
  • 忘了并发:顺序 await 导致"瀑布",用户在瀑布下冲了个冷水澡。
  • 忘了错误边界:Suspense 负责等待,ErrorBoundary 负责兜底;别让用户只看到永恒的"加载中"。

八、性能调优小抄

  • 能静态,尽量静态;能 ISR,就给个合理 revalidate。
  • 高频变化的接口用 cache: 'no-store',避免陈旧。
  • 并发拉取 + use 消费,搭配多个 Suspense,首屏更快。
  • 用 tags 组织失效域:按资源 ID、按类型归类。
  • 在 route handlers 内进行服务端合并请求,减少下游次数。

九、最小可运行样例:Next.js App Router

目录结构(简化):

  • app/page.js 首页,拉三段数据并发显示
  • app/actions.js Server Action 示例

app/page.js

javascript 复制代码
import { Suspense } from 'react';
import { cache } from 'react';
import { use } from 'react';

const getA = cache(async () => {
  const r = await fetch('https://api.example.com/a', { next: { revalidate: 30 } });
  return r.json();
});

const getB = cache(async () => {
  const r = await fetch('https://api.example.com/b', { cache: 'no-store' });
  return r.json();
});

const getC = cache(async () => {
  const r = await fetch('https://api.example.com/c', { next: { revalidate: 300, tags: ['c'] } });
  return r.json();
});

function A({ p }) { const d = use(p); return <div>🅰️ {d.value}</div>; }
function B({ p }) { const d = use(p); return <div>🅱️ {d.value}</div>; }
function C({ p }) { const d = use(p); return <div>🆑 {d.value}</div>; }

export default function Page() {
  const aP = getA();
  const bP = getB();
  const cP = getC();

  return (
    <main style={{ display: 'grid', gap: 12 }}>
      <h1>🚦 三路并发流</h1>
      <Suspense fallback={<p>加载 A...</p>}><A p={aP} /></Suspense>
      <Suspense fallback={<p>加载 B...</p>}><B p={bP} /></Suspense>
      <Suspense fallback={<p>加载 C...</p>}><C p={cP} /></Suspense>
    </main>
  );
}

十、收尾:写给未来的你

  • 把数据当"素材",把缓存当"工艺",把 use 当"流水线控制器"。
  • 能在 Server Components 里解决的事,不要放到客户端"加班"。
  • 记忆不是目的,响应才是目的;缓存不是玄学,是一门工程学。
相关推荐
Juchecar4 小时前
Vite = 让 Vue 开发像写 HTML 一样快的现代工具
前端·vue.js
coding随想4 小时前
前端设备方向监听全解析:从orientationchange到实战技巧大揭秘
前端
支撑前端荣耀4 小时前
这个工具让AI真正理解你的需求,告别反复解释!
前端
Juchecar4 小时前
如何实现Node.js查看 Vue3 项目请求日志
前端·vue.js
扑克中的黑桃A4 小时前
飞算JavaAI家庭记账系统:从收支记录到财务分析的全流程管理方案
前端
我的写法有点潮4 小时前
IOS下H5 index层级混乱
前端·vue.js
一枚前端小能手4 小时前
🔥 闭包又把我坑了!这5个隐藏陷阱,90%的前端都中过招
前端·javascript
纯JS甘特图_MZGantt4 小时前
让你的甘特图"活"起来!mzgantt事件处理与数据同步实战指南
前端·javascript
鹏程十八少4 小时前
7. Android <卡顿七>优化动画导致卡顿:一套基于 Matrix 监控、Systrace/Perfetto 标准化排查流程(卡顿实战)
前端