与 HTTP 端点通信过去意味着必须引入第三方包。在服务端,这已经不再成立。从 Node.js 18 开始,全局 fetch 函数随运行时一同提供,因此你可以使用在浏览器中早已熟悉的基于 Promise 的接口在 Node.js 中发起 HTTP 请求,无需安装 node-fetch,也无需开启任何标志。
本指南是在服务端使用 fetch 的实操概览:GET 和 POST、查询字符串、发送和读取 JSON、几乎人人都会踩的错误处理规则、用 AbortController 实现超时、并发请求,以及 Axios 仍有用武之地的场景。最后介绍裸请求不够用的那个场景, 抓取真实网站,以及如何通过 API 获取渲染后的 HTML。
为什么选择 fetch,以及 Node 有何变化
Fetch API 基于 Promise,因此与 async/await 组合流畅,避免了旧版 http 模块的回调嵌套。它支持你所需的每种 HTTP 方法(GET、POST、PUT、PATCH、DELETE),提供真正的 Headers 对象,并通过一次方法调用即可处理 JSON。接口在浏览器和服务端完全一致,这意味着代码和心智模型可以双向复用。
实际关键在于版本分界线。在 Node.js 18 及更高版本中,fetch 是全局变量,就像 setTimeout 一样。你不需要导入它,也不需要安装任何东西。在编写代码之前,先确认你的运行时版本:
node -v # v18.x or higher means global fetch is available
只有在 Node 16 或更早版本上才需要,届时你需要安装 node-fetch 并导入它。在 Node 18+ 上,该包是多余的,添加它只会遮蔽更快的内置实现。如果你使用的是当前 LTS 版本,删除该依赖并使用全局变量即可。
使用 Fetch API 发起 GET 请求
GET 请求只需一次调用。fetch() 返回一个 Promise,一旦头部到达就解析为 Response 对象。响应体需要单独读取:JSON 用 response.json(),纯文本用 response.text(),这两者各自返回自己的 Promise。
最重要的细节:在读取响应体之前始终检查 response.ok。就 fetch 而言,404 或 500 仍然是成功的 HTTP 交换,因此 Promise 会正常解析。response.ok 仅在状态码为 200 到 299 范围内时为 true。
async function getPost(id) { const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`); if (!response.ok) { throw new Error(`HTTP ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } getPost(1).then(console.log).catch(console.error);
如果端点返回的是文本或 HTML 而非 JSON,替换响应体读取器即可,其余流程不变。
const response = await fetch("https://example.com/"); const html = await response.text(); console.log(html.slice(0, 200));
响应体只能读取一次。在同一个响应上先调用 response.text() 再调用 response.json() 会抛出异常,因为流已经被消费。每个响应只选择一个读取器。
读取和设置响应头
Response 对象暴露了一个行为类似 Map 的 headers 属性。用 get() 读取单个头部,或遍历整个集合。头部名称不区分大小写,因此 "content-type" 和 "Content-Type" 解析为相同的值。
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1"); console.log(response.headers.get("content-type")); for (const [key, value] of response.headers) { console.log(`${key}: ${value}`); }
在服务端运行的一个优势是:浏览器中隐藏大多数头部的 CORS 限制在 Node 中不适用。你可以完整访问服务端发送的每一个响应头。
用 URLSearchParams 添加查询字符串
你可以手动拼接查询参数,但这意味着你需要自己转义空格、连接符和其他保留字符,很容易出错。URLSearchParams 能正确编码值,代码也更清晰易读。
const params = new URLSearchParams({ userId: 1, _limit: 5, }); const url = `https://jsonplaceholder.typicode.com/posts?${params}`; const response = await fetch(url); const posts = await response.json(); console.log(`Got ${posts.length} posts`);
将 params 插值进模板字面量会调用其 toString(),生成经过正确编码的查询字符串。之后要添加或删除过滤条件只需对对象做一行修改,而无需进行字符串拼接。
发送 POST 请求和 JSON 请求体
要发送数据,需要传递第二个参数:一个包含 method、headers 和 body 的选项对象。请求体必须是字符串,因此 JSON 负载需要通过 JSON.stringify() 处理,并设置 Content-Type: application/json,以便服务端知道如何解析接收到的内容。
async function createPost(payload) { const response = await fetch("https://jsonplaceholder.typicode.com/posts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`POST failed: HTTP ${response.status}`); } return response.json(); } const created = await createPost({ title: "Fetch on the server", body: "Built into Node 18+", userId: 1, }); console.log(created);
相同的结构适用于 PUT 和 PATCH(更新)以及 DELETE(删除):修改 method,并在需要时修改 body。对于表单提交而非 JSON,可将 URLSearchParams 或 FormData 实例作为请求体传入,省略显式的 Content-Type;fetch 会自动为这些格式设置正确的头部。
错误处理:人人必知的规则
关于 fetch 最重要的一点就是这个。它只在网络层面失败时才拒绝:DNS 解析失败、连接断开、请求被中止。来自服务端的任何 HTTP 响应(包括 404 和 500)都算作已解析的 Promise。对 fetch 套一个裸 try/catch 会开心地吞掉 500,并继续执行,就好像一切正常。
健壮的模式结合了两种检查:用 try/catch 处理网络层,并用显式的 response.ok 检查处理 HTTP 层。
async function getJson(url) { try { const response = await fetch(url); if (!response.ok) { // HTTP error: server replied, but with a 4xx or 5xx throw new Error(`HTTP ${response.status} for ${url}`); } return await response.json(); } catch (error) { // Network failure, abort, or the thrown HTTP error above console.error("Request failed:", error.message); throw error; } }
在 !response.ok 时抛出异常,可将 HTTP 错误路由到与网络错误相同的 catch 块中,从而用一个块处理两类失败。没有这个显式检查,500 就会直接传递到 response.json(),然后在 HTML 错误页面上抛出令人困惑的解析错误,而不是真正的状态码。
用 AbortController 实现超时和取消
fetch 没有内置超时。不加干预的话,请求可以在连接保持打开的情况下无限挂起,这对任何面向用户的场景都是不可接受的。标准解法是 AbortController:创建一个,将其 signal 传给 fetch,并在计时器触发时调用 abort()。
async function fetchWithTimeout(url, ms = 5000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), ms); try { const response = await fetch(url, { signal: controller.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { if (error.name === "AbortError") { throw new Error(`Request to ${url} timed out after ${ms}ms`); } throw error; } finally { clearTimeout(timer); } }
被中止的请求会以 AbortError 拒绝,这就是为什么 catch 要检查 error.name 以给出清晰的超时消息。在 finally 中清除计时器,可防止提前成功的响应在之后触发无关的中止。同一个 signal 也可以将 fetch 连接到用户触发的取消按钮,而不仅仅是计时器。在最近的 Node 版本中,AbortSignal.timeout(ms) 是这种计时器模式的简写。
用 Promise.all 并发运行请求
当你有多个相互独立的请求时,逐一等待它们会浪费时间:每个请求都要等待前一个完成。Promise.all 同时启动所有请求,并在最后一个完成时解析,因此总时间大约等于最慢那个请求的时间,而非所有请求时间之和。
const ids = [1, 2, 3, 4, 5]; const posts = await Promise.all( ids.map((id) => getPost(id).catch((error) => ({ id, error: error.message })), ), ); console.log(posts);
有一点需要注意:Promise.all 一旦任何单个 Promise 拒绝就立即拒绝,其余的会被丢弃。上面的内联 .catch() 将每个请求的失败转换为一个值,这样一个失败的请求不会拖垮整批。如果你希望显式检查每个结果,Promise.allSettled 会为每个 Promise 返回一个状态和值的记录。此外,不要一次发起成千上万个请求:那样会触发速率限制或耗尽套接字。对于大批量任务,以固定大小的块处理列表来限制并发。
Fetch API vs Axios:如何选择
Axios 早于服务端 fetch 出现,至今仍有忠实拥趸。客观比较很简短:
-
选择
fetch,当你需要零依赖和标准 API 时。它是内置的,在浏览器中也一样,对大多数 GET 和 POST 工作已经足够。 - 选择 Axios,当你需要它捆绑的便利功能时:双向自动 JSON 解析、请求和响应拦截器、内置超时配置、对非 2xx 状态的自动拒绝,以及上传或下载进度。在大型代码库中,这些功能能节省大量样板代码。
差距比以前小多了。大多数 Axios 的便利功能(超时、状态检查、JSON 处理)只需要在 fetch 之上写几行辅助代码,就像本文中的封装函数一样。如果你在 Node 18+ 上从头开始,且不需要拦截器,内置实现是更精简的选择。如果项目已经统一使用 Axios,没有紧迫理由替换它。
裸 fetch 的局限:抓取真实网站
上面所有内容对 API 和协作服务器都很好用。但将 fetch 指向一个现代商业网站来抓取其 HTML,你很快就会遇到两个硬性限制。
首先,fetch 只能检索服务端发送的初始 HTML。它不运行浏览器,因此永远不会执行页面的 JavaScript。在客户端渲染内容的网站上,你得到的标记几乎是空框架,你想要的数据由脚本在之后填充,而这些脚本永远不会运行。其次,在意爬虫的网站会对入站流量进行指纹识别。来自数据中心 IP、带有默认 Node 用户代理且缺乏浏览器特征头部的请求,在返回任何有用内容之前就会被挑战或封锁。
你可以自己应对这两个问题,用无头浏览器渲染页面,加上轮换住宅代理来模拟真实访客,但维护这套系统才是大部分工作。更简洁的路径是将你的 fetch 调用发送到一个在服务端处理渲染和 IP 轮换、并返回完整 HTML 的 API。
抓取真实网站需要在受信任 IP 后面渲染出完整页面,一次请求搞定。Crawling API 接受你的 token 和目标 URL,在真实浏览器中运行页面,为你轮换住宅 IP,并返回完整 HTML,让你继续使用普通的 fetch,省去运营无头浏览器集群和代理池的麻烦。从免费套餐开始。
调用方式与你已知的 fetch 相同。用 token 和 URL 编码的目标构建端点 URL,发送 GET 请求,从响应中读取渲染后的 HTML。之后用 cheerio 这样的解析器将 HTML 转换为结构化数据。
import * as cheerio from "cheerio"; const TOKEN = "YOUR_CRAWLBASE_TOKEN"; async function scrape(targetUrl) { const params = new URLSearchParams({ token: TOKEN, url: targetUrl, }); const response = await fetch(`https://api.crawlbase.com/?${params}`); if (!response.ok) { throw new Error(`Crawl failed: HTTP ${response.status}`); } const html = await response.text(); const $ = cheerio.load(html); return { title: $("title").text().trim(), headings: $("h2").map((_, el) => $(el).text().trim()).get(), }; } scrape("https://www.example.com/").then(console.log);
对于通过 JavaScript 渲染内容的页面,通过传入 JS 渲染 token 添加 "&javascript=true" 等效参数,该 token 会在返回 HTML 之前在真实浏览器中运行目标页面。因为请求仍然是 fetch,本文中的所有技巧都适用:将其包裹在超时辅助函数中,通过 Promise.all 以合理的并发限制处理 URL 列表,并在每次调用时检查 response.ok。更完整的 Node 爬虫教程请参见如何用 Node.js 构建网页爬虫,关于渲染问题的专项内容请参见如何抓取 JavaScript 网站。
核心要点
-
fetch 内置于 Node 18+。 无需
node-fetch,无需标志,无需导入。它是全局变量,与浏览器中一样。 -
始终检查
response.ok。fetch只在网络错误时拒绝,因此 404 或 500 会正常解析,绕过裸try/catch。 -
POST 通过第二个参数发送。 设置
method、Content-Type头部和经JSON.stringify处理的body;用URLSearchParams构建查询字符串。 -
用
AbortController添加超时。 没有内置超时;传入signal并在计时器或取消操作时中止。 -
用
Promise.all并发处理。 同时发起独立请求,用.catch()保护每个请求,并对大批量任务限制并发。 -
裸 fetch 无法抓取现代网站。 它不会渲染 JavaScript 且会被封锁;通过 Crawling API 路由同样的
fetch获取渲染后的 HTML,再用 cheerio 解析。
常见问题
在 Node.js 中还需要 node-fetch 吗?
在 Node.js 18 或更高版本上不需要。fetch 在那里是全局变量,可以直接调用,无需安装和导入。只有在 Node 16 或更早版本上才需要 node-fetch。如果你使用的是当前 LTS 版本,删除该依赖是安全的,还能使用更快的内置实现。
为什么 fetch 在 404 或 500 时不抛出异常?
因为 fetch 将任何完成的 HTTP 交换都视为成功,即使状态码是错误也不例外。Promise 只在网络层面失败(如连接断开、DNS 失败或中止)时拒绝。要捕获 HTTP 错误,你必须自己检查 response.ok(仅在 200 到 299 范围内为 true),并在为 false 时抛出异常,然后让你的 catch 处理它。
如何用 fetch 在 POST 请求中发送 JSON?
将选项对象作为第二个参数传入,设置 method: "POST",一个设置了 "Content-Type": "application/json" 的 headers 对象,以及由 JSON.stringify() 生成的 body。请求体必须是字符串,这就是为什么要先将对象序列化。确认 response.ok 后,用 await response.json() 读回服务端的回复。
如何为 fetch 请求添加超时?
使用 AbortController。创建一个,将其 signal 传入 fetch 选项,并在 setTimeout 中调用 controller.abort()。被中止的请求会以 AbortError 拒绝,因此在 catch 中检查 error.name 以清晰地报告超时。在最近的 Node 版本中,AbortSignal.timeout(ms) 将此模式封装为一次调用。
在 Node 中应该用 fetch 还是 Axios?
对于基于标准、零依赖的请求,使用 fetch;它在 Node 18+ 上开箱即用,覆盖大多数 GET 和 POST 工作。当你需要 Axios 的内置便利功能(如拦截器、自动 JSON 处理、配置超时和对非 2xx 响应的自动拒绝)时选择 Axios。对于不需要这些功能的新项目,内置实现是更精简的选择;现有 Axios 代码库没有紧迫理由切换。
可以用 fetch 抓取网站吗?
可以获取页面的原始 HTML,但裸 fetch 在真实网站上有两个局限:它不运行 JavaScript,所以客户端渲染的内容会返回空;来自数据中心 IP 的请求会被挑战或封锁。解决方案是将你的 fetch 调用发送到 Crawling API,后者在轮换住宅 IP 后面的真实浏览器中渲染页面,并返回供 cheerio 解析的完整 HTML。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
