Node.js Fetch API:教程里没讲的那些事(2026 指南)

最后更新于 May 14, 2026
AI 总结
别再在生产环境里依赖基础 fetch 调用了。了解如何在 2026 年的 Node.js 中处理超时、HTTP 错误、重试逻辑和连接池。

每篇 Node.js fetch 教程几乎都会先教你 await fetch(url),然后就戛然而止。等真到了生产环境,你的应用悄悄吞掉一个 500 错误,请求卡了 90 秒都不超时,而你却只能在周五晚上排查一个本来一眼就该看出来的问题。

我在 做内部工具和数据管道已经有一阵子了,可以很肯定地说:从“教程里能跑”到“生产环境能跑”之间的落差,恰恰是大多数痛苦的来源。Reddit 上有位开发者说得很到位:“一旦进入生产环境,你就会发现自己需要比原生 fetch 更可靠的东西。”

还有人坦白说:“做了 3 年 Web 开发,才知道 fetch API 的 catch 块根本不是用来处理 HTTP 错误的。” 这篇指南会覆盖大多数教程会跳过的五件事——错误陷阱、AbortController 超时、重试逻辑、连接复用,以及什么时候该超越 fetch,转向结构化数据提取。如果你也曾在生产环境里遇到 fetch 调用静默失败,这篇就是给你的。

nodejs-fetch-error-handling-flowchart.png

什么是 Node.js Fetch API?

Node.js Fetch API 是 Node.js 内置、并且和浏览器兼容的 HTTP 请求方式(GET、POST、PUT、DELETE 等),不需要安装 Axios、node-fetch 或其他任何包。只要你在浏览器里用过 fetch(),你就已经熟悉它的语法了。现在,同样的 API 也能直接在服务端使用。

先快速看一下版本演进:

里程碑Node 版本发生了什么
实验性 fetch 标志v17.5.0 / v16.15.0--experimental-fetch 后添加了 fetch
默认全局 fetchv18.0.0实验性 fetch 可全局使用,由 Undici 驱动
稳定版 fetchv21.0.0不再是实验特性
2026 生产基线v22 LTS / v24 LTS生产环境推荐使用;v20 现已 EOL

在底层,Node 的 fetch 由 Undici 提供支持——这是一个专为 Node.js 打造的高性能 HTTP 客户端。它不依赖更老的内置 http 模块。实际收益很直接:你会得到一个现代、基于 Promise 的 HTTP API,而且在浏览器代码、Express 后端、无服务器函数和 CLI 脚本里都能用同一套写法。

为什么 Node.js Fetch API 对你的项目很重要

在 Node 18 之前,每个新项目的开局都差不多:npm install axiosnpm install node-fetch。到了 2026 年,如果你的项目跑在受维护的 Node LTS 上,基础 HTTP 请求已经不需要任何依赖。这对包体积、供应链安全和新成员上手体验来说,都是实打实的提升(前后端开发者终于可以共享同一个 API 了)。

原生 fetch 的优势主要体现在这些场景:

场景为什么原生 fetch 很适合生产环境注意事项
Express/Fastify 后端调用 REST API熟悉的 async/await 写法,无需依赖添加超时和 response.ok 检查
无服务器函数(Lambda、Vercel 等)冷启动开销小,无需安装包超时要低于平台最大执行时长
CLI 脚本和自动化任务简单 GET/POST,无需项目初始化为不稳定 API 添加重试/退避
Webhook 发送或转发标准 HTTP 方法和请求头不要对非幂等 POST 盲目重试
报表和仪表盘很适合从 API 拉取 JSON循环场景要使用分页和连接池
微服务通信适合简单的内部 HTTP 调用如需重试、钩子或 HTTP/2,可考虑直接用 Got 或 Undici

对于新的 Node 22+ 项目,原生 fetch 是最合理的默认选择——除非你明确需要它不提供的功能(拦截器、内置重试、HTTP/2 等)。npm 下载量也说明了行业正在变化:,但其中很多来自历史项目和传递依赖。。趋势很清楚:原生 fetch 正在成为新基线,而第三方客户端则更多用于特定需求。

原生 Fetch vs node-fetch vs Axios vs Got vs Ky:2026 选型矩阵

开发者论坛里最常见的问题之一就是:“在 Node.js 里我该用哪个 HTTP 客户端?” 有位 Reddit 用户总结得很精辟:“语言/框架已经内置了功能,为什么还要再导入一个库?” 这是个合理的问题——但答案取决于你到底需要什么。

http-client-libraries-comparison.png

功能原生 fetchnode-fetch v3axiosgot v15ky v2
Node.js 版本≥18(推荐 22/24 LTS)≥12.20广泛支持≥22≥22
是否需要安装
ESM + CJS 支持两者都支持(全局)仅 ESM(v3)两者都支持仅 ESM仅 ESM
4xx/5xx 自动拒绝
内置重试
请求拦截器是(hooks)是(hooks)
流式支持Web ReadableStream有限强大的 Node streams基于 fetch
包体积/安装开销0 KB约 107 KB,3 个依赖约 2.8 MB,4 个依赖约 355 KB,12 个依赖约 405 KB,0 个依赖
HTTP/2 支持通过 Undici dispatcher否(fetch 包装器)

关于 ESM/CJS 的麻烦,再补一句:node-fetch v3 只支持 ESM,这让很多使用 require() 的项目直接受影响。原生 fetch 是全局可用的——无论是 CJS 还是 ESM 文件都能直接用,不需要额外的导入处理。如果你因为 CommonJS 被迫停留在 node-fetch v2,原生 fetch 可以一次性把这个问题解决掉。

至于早期稳定性的担忧:确实,Node 18 初版 fetch 实现里曾经有过真实 bug。有位开发者在 Reddit 上说:“最近原生 node 18 fetch 遇到一个离谱的 bug,只好把我们的应用改掉了。” 那是 2023 年。到了 2026 年,在 Node 22 和 24 LTS 下,这些问题已经解决。原生 fetch 已经可以用于生产环境。

什么时候坚持用原生 Fetch

当满足以下情况时,优先用原生 fetch:

  • 你的项目运行在 Node 22 LTS 或 Node 24 LTS。
  • 请求很简单,就是标准 REST 调用(GET、POST、PUT、DELETE)。
  • 你愿意自己加一个小封装来处理 response.ok、JSON 解析、超时和重试。
  • 你希望零依赖,尽量减少供应链风险。
  • 你重视浏览器/服务端 API 的一致性。
  • 你在无服务器或边缘环境中开发,倾向使用内置 API。

什么时候 Axios、Got 或 Ky 更合适

Axios 适合团队依赖请求/响应拦截器的场景(例如自动刷新认证令牌、租户头信息、统一日志记录),或者你希望 HTTP 错误默认就会拒绝 Promise,或者需要兼容更老的 Node 运行时。

Got 面向高吞吐的 Node 服务,内置重试、hooks、进阶超时阶段、流、分页辅助、Unix socket、代理/缓存工作流或 HTTP/2 支持时尤其合适。它是 Node-only HTTP 工作流里的瑞士军刀。

Ky 则是你喜欢 fetch 的简洁性、又想少写样板代码时的甜点位——它以很小的包体积和零依赖,提供重试、超时、hooks 和 HTTPError

如何使用 Node.js Fetch API 发起 GET 请求

用 async/await 发起 GET 请求,大概是这样:

1const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
2const post = await response.json();
3console.log(post.title);
4// → "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"

如果你更喜欢 .then() 链式写法,也可以这样:

1fetch('https://jsonplaceholder.typicode.com/posts/1')
2  .then(response => response.json())
3  .then(post => console.log(post.title))
4  .catch(error => console.error(error));

两种都能用。但它们都还不够适合生产环境(马上就会讲原因)。

你应该了解的响应读取方式:

方法适用场景
response.json()服务端返回 JSON
response.text()服务端返回 HTML、纯文本、CSV、Markdown
response.arrayBuffer()你需要二进制数据(图片、文件)
response.body你需要流式/分块处理

更好的模式——真正会检查错误的写法:

1async function getPost(id) {
2  const response = await fetch(`https://jsonplaceholder.typicode.com/posts/$\{id\}`);
3  if (!response.ok) {
4    throw new Error(`HTTP $\{response.status\} $\{response.statusText\}`);
5  }
6  return response.json();
7}
8const post = await getPost(1);
9console.log(post.title);

这行 if (!response.ok),就是教程代码和生产代码之间的分界线。也正是这点,把我们带到最大的坑。

如何使用 Node.js Fetch API 发送 POST 请求

POST 请求的结构是一样的——只是你需要设置 method、headers 和 body:

1const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
2  method: 'POST',
3  headers: {
4    'Content-Type': 'application/json',
5  },
6  body: JSON.stringify({
7    title: 'Node fetch 指南',
8    body: '生产环境中的 fetch 需要错误处理。',
9    userId: 1,
10  }),
11});
12if (!response.ok) {
13  throw new Error(`HTTP $\{response.status\}`);
14}
15const created = await response.json();
16console.log(created.id); // → 101

发送其他请求类型(PUT、DELETE、PATCH)

PUT、PATCH 和 DELETE 只需要把 method 改掉,结构完全一样:

1// PUT — 完整替换
2await fetch('https://jsonplaceholder.typicode.com/posts/1', {
3  method: 'PUT',
4  headers: { 'Content-Type': 'application/json' },
5  body: JSON.stringify({ id: 1, title: '已替换', body: '完整替换', userId: 1 }),
6});
7// PATCH — 部分更新
8await fetch('https://jsonplaceholder.typicode.com/posts/1', {
9  method: 'PATCH',
10  headers: { 'Content-Type': 'application/json' },
11  body: JSON.stringify({ title: '部分更新' }),
12});
13// DELETE
14await fetch('https://jsonplaceholder.typicode.com/posts/1', {
15  method: 'DELETE',
16});

Express body-parser 陷阱: 如果你正在向 Express 服务器 POST JSON,而 req.body 变成了 undefined,通常的修复办法只有一个:用 express.json(),不要只用 express.urlencoded()。服务器必须在路由之前加上 express.json() 中间件,才能解析 Content-Type: application/json 的请求体。这是 上关于 Express 最常见的问题之一,而且每次都能把人坑到。

1import express from 'express';
2const app = express();
3app.use(express.json()); // ← JSON POST 请求体需要这个
4app.post('/api/posts', (req, res) => {
5  res.json({ received: req.body });
6});

会让生产应用出问题的 fetch() 错误陷阱

fetch-error-handling-flowchart.png

这就是大多数生产环境 fetch bug 的来源。

fetch() 不会在 HTTP 4xx 或 5xx 错误时拒绝 Promise。 它只会在网络层失败时拒绝——比如 DNS 错误、没有网络、请求被中止。如果服务器返回的是 403 Forbidden 或 500 Internal Server Error,fetch 仍然会把它当成一次 成功 的响应。你的 .catch() 根本不会执行。你的 try/catch 也捕获不到。你的代码会愉快地处理服务器返回的任何内容。

说得很清楚,但大多数教程都一笔带过。结果就是?下面这样的代码看起来没问题,实际上却会静默吞掉错误:

1try {
2  const response = await fetch('https://api.example.com/private');
3  const data = await response.json(); // ← 即使是 403,这里也会执行
4  console.log('看起来成功了:', data);
5} catch (error) {
6  // 只有网络层失败才会进来
7  console.error('捕获到:', error);
8}

下面快速拆解一下,每种模式到底能捕获什么:

模式能捕获网络错误能捕获 4xx/5xx能安全解析 JSON可复用
原始的 .then(res => res.json())是(通过 .catch()没有内容类型保护
await fetch()try/catch没有内容类型保护
每次调用都手写 if (!res.ok)取决于每次调用部分
自定义 fetchJSON() 封装

构建可复用的 fetchJSON() 封装

做一个封装,哪里都引入它。别再把 if (!response.ok) 复制到每个文件里:

1export class HTTPError extends Error {
2  constructor(message, { status, statusText, url, body }) {
3    super(message);
4    this.name = 'HTTPError';
5    this.status = status;
6    this.statusText = statusText;
7    this.url = url;
8    this.body = body;
9  }
10}
11export async function fetchJSON(url, options = {}) {
12  const response = await fetch(url, {
13    headers: {
14      Accept: 'application/json',
15      ...options.headers,
16    },
17    ...options,
18  });
19  const contentType = response.headers.get('content-type') || '';
20  const isJSON = contentType.includes('application/json');
21  const body = isJSON ? await response.json().catch(() => null) : await response.text();
22  if (!response.ok) {
23    throw new HTTPError(`HTTP $\{response.status\} $\{response.statusText\}`, {
24      status: response.status,
25      statusText: response.statusText,
26      url: response.url,
27      body,
28    });
29  }
30  return body;
31}

现在,当服务器返回 403 时:

1try {
2  const data = await fetchJSON('https://api.example.com/private');
3} catch (error) {
4  if (error instanceof HTTPError) {
5    console.error(`服务器返回了 $\{error.status\}:`, error.body);
6  } else {
7    console.error('网络或其他失败:', error);
8  }
9}

这个错误对象里带着状态码、响应体和 URL——做日志、告警或面向用户的提示时,你需要的信息都齐了。封装一次,处处可用。

AbortController 和超时:Node.js Fetch API 的生产环境模式

request-retry-pooling-flowchart.png

没有超时的话,一旦远端服务器没响应,fetch 请求就会一直挂着。你的 Express 路由会被卡住。你的 Lambda 会把执行预算耗光。你的脚本就这样……干坐着。

我看了搜索结果前几页:没有一篇面向 Node.js 的 fetch 教程会讲请求取消或超时。但超时恰恰是很多开发者坚持用 Axios 或 Got 的原因之一。Reddit 上甚至有一个帖子标题就叫 “Node fetch does not timeout”

使用 AbortSignal.timeout()(Node 18.11+)

最简单的方法——只需要多加一个选项:

1try {
2  const response = await fetch('https://api.example.com/data', {
3    signal: AbortSignal.timeout(5000), // 5 秒
4  });
5  if (!response.ok) throw new Error(`HTTP $\{response.status\}`);
6  const data = await response.json();
7  console.log(data);
8} catch (error) {
9  if (error.name === 'TimeoutError') {
10    console.error('请求在 5 秒后超时。');
11  } else {
12    throw error;
13  }
14}

注意:AbortSignal.timeout() 抛出的是 TimeoutError,不是 AbortError。这个细节连一些有经验的开发者都会弄错。

用 AbortController 手动超时

如果你想要更多控制,或者你需要根据用户操作来取消请求,而不只是计时器:

1const controller = new AbortController();
2const timeout = setTimeout(() => controller.abort(), 5000);
3try {
4  const response = await fetch('https://api.example.com/data', {
5    signal: controller.signal,
6  });
7  const data = await response.json();
8  console.log(data);
9} catch (error) {
10  if (error.name === 'AbortError') {
11    console.error('请求被手动中止了。');
12  } else {
13    throw error;
14  }
15} finally {
16  clearTimeout(timeout);
17}

区分 AbortError 和 TimeoutError

这个区别对日志记录和用户提示非常重要:

中止方式catch 块里的错误名
AbortSignal.timeout(ms)TimeoutError
controller.abort()AbortError
DNS/网络失败通常是 TypeError: fetch failed

下面是一个实用场景——一个调用外部 API 的 Express 路由,而且必须在 3 秒内响应:

1app.get('/dashboard', async (req, res, next) => {
2  try {
3    const data = await fetchJSON('https://api.example.com/report', {
4      signal: AbortSignal.timeout(3000),
5    });
6    res.json(data);
7  } catch (error) {
8    if (error.name === 'TimeoutError') {
9      res.status(504).json({ error: '上游 API 超时' });
10      return;
11    }
12    next(error);
13  }
14});

如果没有这个模式,一个缓慢的上游 API 就会把整个路由堵死,直到客户端放弃。

重试逻辑和连接复用:把 Node.js Fetch API 变成生产级

原生 fetch 没有内置重试。一次网络抖动或临时性的 503,都会直接失败。对于生产环境里的大多数读操作来说,这还不够。

一个可组合的重试封装,带指数退避

下面的代码刻意写得很短——实际逻辑大约 10 行:

1const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
2export async function fetchWithRetry(url, options = {}, retries = 2) {
3  for (let attempt = 0; ; attempt++) {
4    try {
5      const response = await fetch(url, options);
6      if (response.ok || ![408, 429, 500, 502, 503, 504].includes(response.status)) {
7        return response;
8      }
9      if (attempt >= retries) return response;
10    } catch (error) {
11      if (attempt >= retries) throw error;
12    }
13    await wait(250 * 2 ** attempt); // 250ms、500ms、1000ms……
14  }
15}

什么时候重试,什么时候不该重试

  • 应该重试: 幂等的 GET 和 HEAD 请求、临时性状态码(408、429、500、502、503、504)、网络抖动。
  • 不应该重试: 会创建记录、扣款或触发副作用的非幂等 POST 请求——除非你使用幂等键。
  • 尊重 Retry-After: 对于 429(限流)和 503(服务不可用),退避前先检查 Retry-After 请求头。

如果你不想自己造重试逻辑, 是一个轻量级的 fetch 封装,开箱即带重试、超时、hooks 和 HTTPError——而且零依赖。

使用 Undici 的 Agent 和 Pool 复用连接

对于高吞吐循环——例如抓取几百个页面、批量调用 API、轮询某个服务——复用 TCP 连接能省下很多时间。每建立一个新连接,都意味着新的 DNS 查询、TCP 握手,以及(对 HTTPS 来说)TLS 协商。

由于 Node 的 fetch 由 Undici 驱动,你可以传入自定义 dispatcher:

1import { Agent } from 'undici';
2const agent = new Agent({
3  keepAliveTimeout: 10_000,
4  keepAliveMaxTimeout: 60_000,
5});
6const response = await fetch('https://api.example.com/data', {
7  dispatcher: agent,
8});

如果你想对某个特定 origin 进行更精细的控制:

1import { Pool } from 'undici';
2const pool = new Pool('https://api.example.com', { connections: 10 });
3const response = await fetch('https://api.example.com/data', {
4  dispatcher: pool,
5});
6// 完成后:
7await pool.close();

显示,连接复用和连接池能显著提升吞吐量——他们本地基准里 undici - dispatch 约为 22,234 req/sec,而 undici - fetch 约为 5,904 req/sec。真实环境的数字会有所不同,但方向很明确:如果你在对同一个 origin 发起大量请求,连接池很重要。

还有一件事:一定要消费或取消响应体。未消费的响应体可能会导致 Node HTTP 内部资源泄漏。

使用 Node.js Fetch API 处理流式响应

大文件下载、分块 JSON 数据流、Server-Sent Events、LLM 输出——这些场景里,如果你非要等完整响应回来再处理,就会浪费时间和内存。流式处理可以让你边到边处理。

streaming-data-chunking-process.png

Node 18+ 已经包含与浏览器兼容的 ReadableStream。下面是如何流式读取一份以换行分隔的 JSON 响应,并在每一行到达时立刻处理:

1const response = await fetch('https://example.com/large-file.ndjson');
2if (!response.ok) throw new Error(`HTTP $\{response.status\}`);
3const reader = response.body.getReader();
4const decoder = new TextDecoder();
5let buffer = '';
6while (true) {
7  const { value, done } = await reader.read();
8  if (done) break;
9  buffer += decoder.decode(value, { stream: true });
10  let newlineIndex;
11  while ((newlineIndex = buffer.indexOf('\n')) >= 0) {
12    const line = buffer.slice(0, newlineIndex).trim();
13    buffer = buffer.slice(newlineIndex + 1);
14    if (line) {
15      const item = JSON.parse(line);
16      console.log('已处理:', item.id);
17    }
18  }
19}

如果只是更简单的文本流式处理(例如把 LLM 输出直接写到 stdout):

1const response = await fetch('https://example.com/stream');
2const reader = response.body.getReader();
3const decoder = new TextDecoder();
4for (;;) {
5  const { value, done } = await reader.read();
6  if (done) break;
7  process.stdout.write(decoder.decode(value, { stream: true }));
8}

流式处理是原生 fetch 和 Got 都很擅长的领域。Axios 的流式支持则相对有限。

当 fetch() 到达极限:用 API 做结构化网页抓取

总有那么一刻,fetch 不再是瓶颈。真正的问题变成了:“我已经拿到 HTML 了,然后呢?”

web-scraping-vs-ai-extraction-api-comparison.png

Fetch 只是一个 HTTP 客户端——它能拿到字节、文本、JSON 或 HTML。它并不知道什么是商品卡片、价格、评分或联系人表格。对于结构化网页抓取,典型的原始技术栈是这样:

  1. fetch() 下载 HTML
  2. 用 Cheerio(或类似工具)通过 CSS 选择器选取元素
  3. 自己写分页逻辑
  4. 页面由客户端渲染时还要执行 JavaScript
  5. 处理代理/反爬/CAPTCHA
  6. 网站布局一变,就得维护选择器

下面是一个典型的 fetch + Cheerio 示例——抓取商品标题大约要 15 行:

1import * as cheerio from 'cheerio';
2const response = await fetch('https://example-store.com/products');
3if (!response.ok) throw new Error(`HTTP $\{response.status\}`);
4const html = await response.text();
5const $ = cheerio.load(html);
6const products = $('.product-card')
7  .map((_, el) => ({
8    name: $(el).find('.product-title').text().trim(),
9    price: $(el).find('.price').text().trim(),
10    url: new URL($(el).find('a').attr('href'), response.url).href,
11  }))
12  .get();
13console.log(products);

这对 HTML 稳定、结构可预测的页面是可行的。但它很快就会变脆弱——JavaScript 渲染内容、变化中的 class 名、反爬机制和分页,都会让复杂度迅速上升。

Thunderbit 的开放 API:一次调用,把原始 HTML 变成结构化数据

这时候就需要另一类工具了。在 里,我们做了一层 API,帮你处理那些麻烦事——JavaScript 渲染、反爬保护、布局变化——让你专注于真正想要的数据。

Distill APIPOST /distill):把任意 URL 转成干净的 Markdown。适合喂给 LLM、构建知识库或做内容分析——不需要 HTML 解析器。

Extract APIPOST /extract):定义一个描述你想要的结构化数据的 JSON Schema(商品名、价格、评分),然后由 AI 来提取。不需要 CSS 选择器,也不会因为页面布局变化而坏掉。

下面是使用 Thunderbit Extract API 完成同样商品抓取任务的方式——直接用原生 fetch 调用:

1const response = await fetch('https://openapi.thunderbit.com/openapi/v1/extract', {
2  method: 'POST',
3  headers: {
4    Authorization: `Bearer $\{process.env.THUNDERBIT_API_KEY\}`,
5    'Content-Type': 'application/json',
6  },
7  body: JSON.stringify({
8    url: 'https://example-store.com/products',
9    renderMode: 'basic',
10    schema: {
11      type: 'object',
12      properties: {
13        products: {
14          type: 'array',
15          items: {
16            type: 'object',
17            properties: {
18              name: { type: 'string', description: '商品名称' },
19              price: { type: 'string', description: '页面展示的商品价格' },
20              rating: { type: 'number', description: '平均用户评分' },
21            },
22            required: ['name', 'price'],
23          },
24        },
25      },
26      required: ['products'],
27    },
28  }),
29});
30if (!response.ok) throw new Error(`Thunderbit API: $\{response.status\}`);
31const result = await response.json();
32console.log(result.data);

对比一下:大约 15 行的 fetch + Cheerio(外加脆弱的选择器),和一次就能返回干净 JSON 的 API 调用。对于批量任务,Thunderbit 支持单次批量 Extract 最多 50 个 URL,单次批量 Distill 最多 100 个 URL。

Thunderbit 不是 fetch 的替代品——fetch 是传输层。而当原始 HTML 解析本身成了问题时,Thunderbit 就是你该用的提取层。如果你关心价格, 提供 600 个 API 单位供你试用,付费方案从每月 6 美元起。你也可以查看 ,直接在浏览器里做无代码提取。

如果你想进一步了解结构化抓取方案,我们关于 的指南,覆盖了更具体的工作流。

快速参考:Node.js Fetch API 速查表

这一部分建议你收藏。需要复制粘贴某个模式时再回来查。

模式代码片段
基础 GETconst res = await fetch(url); const data = await res.json();
基础 POSTawait fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
HTTP 错误检查if (!res.ok) throw new Error(\HTTP ${res.status}`);`
超时(简单版)await fetch(url, { signal: AbortSignal.timeout(5000) });
手动中止const c = new AbortController(); setTimeout(() => c.abort(), 5000); await fetch(url, { signal: c.signal });
重试状态码重试 408、429、500、502、503、504。不要盲目重试 POST。
JSON 封装fetchJSON() 检查 ok、解析内容类型、抛出 HTTPError
连接池import { Pool } from 'undici'; const pool = new Pool(origin, { connections: 10 }); fetch(url, { dispatcher: pool });
流式分块const reader = res.body.getReader(); 循环 await reader.read()
结构化提取当目标是网页字段而不是原始 HTML 时,使用 Thunderbit Extract API。

结论与要点总结

到了 2026 年,Node.js 原生 fetch 已经可以用于生产环境——新项目不再需要 node-fetch,也不再默认依赖 Axios。可光靠原始 fetch() 本身,还不能算一套完整的生产级 HTTP 策略。

大多数教程会跳过的五件事,而这篇指南都讲到了:

  1. 错误陷阱: fetch() 不会在 4xx/5xx 时抛错。务必检查 response.ok,或者使用像 fetchJSON() 这样的封装。
  2. 超时: 简单场景用 AbortSignal.timeout()AbortSignal.timeout() 抛出的是 TimeoutError;手动 controller.abort() 抛出的是 AbortError
  3. 重试逻辑: 原生没有。对幂等请求和临时性失败加指数退避。或者直接用 Ky,开箱即带 fetch 风格重试。
  4. 连接复用: 在高吞吐循环里,通过 dispatcher 选项使用 Undici 的 AgentPool
  5. 结构化提取: 当你需要的是网页里的数据(而不只是原始 HTML)时,与其维护脆弱的 CSS 选择器,不如考虑像 Thunderbit 这样的提取 API。

一句话版的决策矩阵:大多数项目用原生 fetch,拦截器用 Axios,内置重试和 HTTP/2 用 Got,想要更好默认值的 fetch 用 Ky,而当你的基于 fetch 的抓取脚本复杂到难以维护时,就用 Thunderbit 的 API。

试试这篇指南里的这些模式。如果你想看看 Thunderbit 如何处理结构化提取, 是个不错的起点——或者去 看一段实操演示。

试用 Thunderbit 进行 AI 网页爬虫

常见问题

1. fetch 是内置在 Node.js 里的,还是需要安装?

Fetch 从 Node.js 18 开始就是内置的——不需要安装。它在 Node 21 中变成稳定版,并在 Node 22 LTS 和 Node 24 LTS 中得到完整支持。对于更老的 Node 版本,你可以使用 node-fetch 这个 npm 包,但新项目最好目标设定为受维护的 LTS 版本。

2. fetch 遇到 404 或 500 响应会抛错吗?

不会。Fetch 只会在网络层失败时拒绝 Promise(DNS 错误、无网络、请求被中止)。像 404、403、500 这类 HTTP 响应会正常 resolve,只是 response.ok === false。你必须显式检查 response.okresponse.status——或者像本指南里展示的那样,使用 fetchJSON() 这样的封装。

3. 我该如何给 Node.js 的 fetch 加超时?

最简单的方法是 AbortSignal.timeout(ms),Node 18.11+ 可用:await fetch(url, { signal: AbortSignal.timeout(5000) })。如果请求超过 5 秒,它会抛出 TimeoutError。如果想要更多控制,可以手动创建 AbortController,再通过 setTimeout 调用 controller.abort()。手动模式捕获 AbortErrorAbortSignal.timeout() 则捕获 TimeoutError

4. 我可以在 Node.js 里用 fetch 做网页爬虫吗?

可以,但 fetch 只会返回原始 HTML。你还需要像 Cheerio 这样的解析器来提取特定元素,并为分页、JavaScript 渲染页面和反爬措施写额外逻辑。如果你要做大规模的结构化数据提取——希望拿到商品名、价格或联系信息这类干净 JSON——可以考虑 ,它能用 AI 返回结构化数据,而不需要 CSS 选择器或依赖布局的代码。

5. 到了 2026 年,我应该从 Axios 换成原生 fetch 吗?

对于运行在 Node 22+ 上的新项目,原生 fetch 是很强的默认选择。它零依赖、基于 Promise,而且和浏览器的 fetch API 一样。只有当你依赖请求/响应拦截器、默认 HTTP 错误拒绝,或者需要兼容更老的 Node 版本时,才保留 Axios。两者都合理——关键取决于你的项目实际用了哪些功能。

了解更多

Fawad Khan
Fawad Khan
Fawad 靠写作谋生,而且说实话,他挺喜欢这份工作。他花了很多年琢磨,什么样的文案能真正打动人,什么样的内容又会让读者直接划过去。你要是问他营销,他能聊上几个小时;你要是问他卡邦尼意面,他能聊得更久。

试试 Thunderbit

只需 2 次点击即可抓取线索及其他数据。由 AI 驱动。

Get Thunderbit It’s free
使用 AI 提取数据
轻松将数据传输到 Google Sheets、Airtable 或 Notion
PRODUCT HUNT#1 Product of the Week