JavaScript fetch 与 HTTP 请求实践
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
fetch 现在是浏览器和 Node 的通用 HTTP 客户端。它比 XMLHttpRequest 好用,但几个默认行为会让新手误判成功与否。
环境准备
浏览器直接可用;Node.js 22 内置全局 fetch,无需安装依赖。示例接口统一写成 /api/…,换成真实地址即可。
最小请求
const res = await fetch("https://api.example.com/items");
const data = await res.json();
fetch 只在网络层失败(DNS、断网、连接被拒)时 reject。服务器返回 404 或 500,Promise 依然 fulfilled——必须自己检查 res.ok:
async function getJson(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`请求失败:${res.status} ${res.statusText}`);
}
return res.json();
}
Response 常用成员
res.status; // 200
res.ok; // 200–299
res.statusText; // "OK"
res.headers.get("content-type"); // "application/json; charset=utf-8"
res.url; // 最终地址(跟随重定向后)
读取响应体只能选一种方式,且只能读一次:
await res.json(); // 解析 JSON
await res.text(); // 原始文本
await res.blob(); // 二进制
await res.arrayBuffer();
需要读两次就先克隆:const copy = res.clone()。调试时直接 console.log(res) 看不到内容,得 await res.text()。
POST 与请求头
const res = await fetch("/api/items", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title: "写一篇稿子" }),
});
注意:
body必须是字符串或二进制;给对象要自己JSON.stringify。- 手写
content-type后,服务端才会按 JSON 解析;漏了会当成表单文本。 - 需要带 cookie 时,浏览器端写
credentials: "include",同时服务端要允许该来源凭证。
超时与取消
fetch 默认没有超时,一个卡住的请求会一直挂着。用 AbortSignal:
// 固定超时
const res = await fetch("/api/slow", { signal: AbortSignal.timeout(5000) });
// 手动取消(比如用户点了「取消」)
const controller = new AbortController();
const promise = fetch("/api/search?q=go", { signal: controller.signal });
cancelButton.onclick = () => controller.abort();
try {
const res = await promise;
} catch (err) {
if (err.name === "AbortError") return; // 主动取消,不算错误
throw err;
}
超时触发的是 TimeoutError(AbortSignal.timeout),用户取消是 AbortError,两者要分开处理。
失败重试与退避
网络抖动、限流(429)、网关错误(502/503/504)都值得重试;400/401/404 重试没有意义。
async function fetchWithRetry(url, { retries = 3, baseDelay = 300 } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
const retryable = res.status === 429 || res.status >= 500;
if (res.ok || !retryable || attempt >= retries) return res;
const retryAfter = Number(res.headers.get("retry-after"));
const delay = Number.isFinite(retryAfter)
? retryAfter * 1000
: baseDelay * 2 ** attempt + Math.random() * 100; // 指数退避 + 抖动
await new Promise((r) => setTimeout(r, delay));
}
}
指数退避加随机抖动,避免所有客户端在同一秒重试。
并发请求
const urls = ["/api/a", "/api/b", "/api/c"];
// 全部成功才继续
const [a, b, c] = await Promise.all(urls.map(getJson));
// 允许部分失败
const results = await Promise.allSettled(urls.map(getJson));
for (const r of results) {
if (r.status === "fulfilled") console.log(r.value);
else console.warn("跳过失败项", r.reason.message);
}
并发数要控制(见上一篇的 mapLimit),几十个以上就可能触发浏览器同域连接上限或服务端限流。
流式读取
响应体是 ReadableStream,可以边下边处理,适合大文件或 SSE:
const res = await fetch("/api/export.ndjson");
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) if (line) console.log(JSON.parse(line));
}
真要逐条解析 SSE 事件,优先用 EventSource 或现成库,别自己切分。
CORS 与跨域
浏览器端跨域请求受同源策略限制,报错信息通常是 blocked by CORS policy。这是浏览器在拦截,不是服务端没返回数据:
- 服务端需返回
Access-Control-Allow-Origin(不能同时用*和凭证)。 - 带自定义头或
PUT/DELETE会先发OPTIONS预检。 curl能通、页面报 CORS,说明是响应头问题。- 开发阶段用 Vite 的
server.proxy转发,比让后端放开*干净。
Node 里没有同源策略,fetch 不受 CORS 限制;相反,浏览器里的 file:// 页面请求本地文件会被拦。
Node 与浏览器的差异
| 项 | 浏览器 | Node.js |
|---|---|---|
| 相对 URL | 相对当前页面 | 必须绝对 URL(/api 会报错) |
| Cookie | 同源自动带 | 需手动传 headers.cookie |
| CORS | 受限 | 不适用 |
| 代理 | 受浏览器/服务端配置 | 需自己处理或设置全局 undici dispatcher |
在 Node 里请求本地服务,写成 http://localhost:3000/api/x 才有效。
常见错误与注意事项
1. 把 404 当成异常
fetch 不 reject,只有自己检查 res.ok 才不会把错误数据当成功处理。
2. 忘记 await 就直接用 res.json()
res.json() 返回 Promise,忘了 await 会拿到 Promise { <pending> }。
3. 读取两次响应体
res.json() 之后再 res.text() 会抛 Body is unusable,需要 res.clone()。
4. 用 JSON.stringify 处理 FormData
上传文件用 FormData,此时不要设 content-type,浏览器会自动带上 boundary。
5. 重试所有失败
POST 请求盲目重试可能造成重复下单。只重试幂等请求,或给请求加幂等键。
6. 错误信息只有 Failed to fetch
浏览器出于安全不暴露细节。用 try/catch 打印 err.name、err.cause,并在 Network 面板确认请求是否真的发出。
7. 忘了 AbortSignal
页面切走了请求还在跑,回来时用过期数据覆盖新状态。组件卸载/路由切换时记得 abort()。
小结
fetch只在网络层失败时 reject,业务错误要查res.ok。- 响应体只能读一次,需要复用就
clone()。 - 超时用
AbortSignal.timeout,可取消用AbortController。 - 重试只针对 429/5xx,配合指数退避与抖动。
- 无依赖请求并行,批量任务限并发;浏览器端注意 CORS,Node 端注意绝对 URL。