Fetch API
浏览器原生的网络请求接口,以 Request、Response、Headers 和 Stream 抽象请求与响应。
[!info] related notes
- 所属 MOC: HTTP 与前端网络请求 MOC
- 前置概念: 浏览器中的 JavaScript, same-origin, cross-origin
- 并列概念: xhr, axios
- 易混淆概念: frontend-http-requests-xhr-fetch-axios
- 取消协议: AbortController 与异步取消
- 相关笔记: frontend-http-status-code-handling, frontend-streaming-data-handling, stream, 前端埋点 SDK
Fetch API
一句话定义
fetch() 是浏览器和 Worker 环境中的原生网络请求接口,Node.js 等服务端运行时也提供实现;本文以浏览器行为为主。它返回的不是“数据”,而是一个会在响应可用时兑现为 Response 对象的 Promise。
正确心智模型
很多人把 fetch 理解成“发请求然后拿 JSON”,这不够准确。fetch 更像一套请求/响应抽象:
fetch(input, init):发起请求Request:描述我要发什么请求Response:描述服务器回了什么Headers:请求头和响应头容器body:请求体或响应体,本质上是流
因此:
const response = await fetch("/api/user");
console.log(response); // Response
上面拿到的是 Response 外壳,不是最终业务数据。真正的数据还要继续读取:
const response = await fetch("/api/user");
const data = await response.json();
最关键的语义
await fetch() 不等于拿到最终数据
fetch() 的 Promise 兑现后,你只是拿到了 Response。读取 body 仍然要额外调用:
response.json()response.text()response.blob()response.arrayBuffer()
HTTP 404/500 默认不会抛错
fetch() 只会在请求本身失败时 reject,例如:
- 网络错误
- URL 非法
- 请求被浏览器拦截
- 被
AbortController取消
如果服务器正常返回了一个 HTTP 响应,即使状态码是 404、500、504,Promise 仍然会 resolve,所以要自己检查:
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
这也是 fetch 和很多人直觉不一致的地方:
“拿到了 HTTP 响应”
不等于
“业务上成功了”
基础用法
GET
async function getUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
POST
async function createUser(user) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
fetch(input, init) 在做什么
常见签名是:
fetch(input, init);
input可以是 URL,也可以是Requestinit是RequestInit
常见选项有:
methodheadersbodysignalmodecredentialscacheredirectreferrerPolicykeepalive
如果同一个选项同时出现在 Request 和 init 中,fetch() 里直接传入的值优先。
headers 不是普通对象
可以直接传对象:
headers: {
"Content-Type": "application/json"
}
也可以显式使用 Headers:
const headers = new Headers();
headers.append("Content-Type", "application/json");
Headers 不只是 key-value 容器,它还带有浏览器侧的规范化和限制:
- header 名会被规范化
- 值会做基础清洗
- 某些受限制的请求头不能由脚本自由设置
所以浏览器环境里不是“想设什么 header 都能设什么”。
body 的本质是流,而且通常只能消费一次
fetch 的请求体和响应体都带有流语义。无论是发送请求还是读取响应,都会消费 body。
例如:
const request = new Request("/api/post", {
method: "POST",
body: JSON.stringify({ hello: "world" })
});
await fetch(request);
await fetch(request); // Body has already been consumed
如果确实要复用,需要先克隆:
const request = new Request("/api/post", {
method: "POST",
body: JSON.stringify({ hello: "world" })
});
const copy = request.clone();
await fetch(request);
await fetch(copy);
响应体同样如此:
const response = await fetch("/api/user");
const data = await response.json();
// await response.text(); // 通常会失败
如何读取 Response
常见读取方式:
response.json():读完整个 body,再按 JSON 解析response.text():读成字符串response.blob():适合图片、文件等二进制对象response.arrayBuffer():适合更底层的二进制处理
json() 返回的是解析后的 JavaScript 值,不是 JSON 字符串。
const response = await fetch("/api/user");
const user = await response.json();
response.json() 会因非法 JSON 或空响应体(如 204)失败,也可能因读取中断失败。下面按媒体类型选择解析器;这不能替代 HTTP 状态检查或业务数据校验:
const response = await fetch("/api/data");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const mediaType = (response.headers.get("content-type") || "")
.split(";", 1)[0].trim().toLowerCase();
const isJson = mediaType === "application/json" || mediaType.endsWith("+json");
const payload = response.status === 204
? undefined
: isJson ? await response.json() : await response.text();
bodyUsed、clone() 与“一次性读取”
Response.bodyUsed 表示 body 流是否已被扰动(disturbed),开始读取后就可能为真,不表示已完整读完。流被 reader 锁定时,即使 bodyUsed 为假,也不能直接换另一种读取方式。
const response = await fetch("/api/user");
console.log(response.bodyUsed); // false
await response.json();
console.log(response.bodyUsed); // true
如果确实需要读取两次,要在第一次读取前 clone():
const response = await fetch("/api/user");
const copy = response.clone();
const data = await response.json();
const raw = await copy.text();
但 clone() 并不是免费操作。对于大响应体,如果两个副本消费速度差异很大,慢的一侧可能积压更多内存。
跨域、mode 与 credentials
fetch 的“跨域问题”本质上不是语法问题,而是浏览器的同源策略和 CORS 机制在起作用。见:
mode
常见模式:
cors:常规跨源请求模式,服务器允许时前端可读取响应same-origin:只允许同源no-cors:不是绕过跨域,而是发出受限请求,前端通常只能拿到opaque响应
所以:
fetch(url, { mode: "no-cors" })
通常不是解决跨域的正确方式。
credentials
credentials 决定是否携带 cookie 或其他凭证:
omitsame-origininclude
跨源请求即使写了 credentials: "include",也还需要服务端同时正确返回 CORS 凭证相关头部;否则浏览器不会把响应正常暴露给前端代码。
取消请求与超时
fetch 没有 timeout 配置项。支持 AbortSignal.timeout() 和 AbortSignal.any() 的运行环境可组合超时与调用方取消信号:
async function fetchJsonWithTimeout(url, init = {}, timeoutMs = 8000) {
const deadline = AbortSignal.timeout(timeoutMs);
const signal = init.signal
? AbortSignal.any([init.signal, deadline])
: deadline;
const response = await fetch(url, { ...init, signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (response.status === 204) return undefined;
return await response.json();
}
此例针对约定返回 JSON 的接口,信号覆盖等待响应与读取 body;同步 JSON 解析并不是可抢占任务。旧环境需兼容实现,并在 body 读取完成后清理计时器和监听器。
只在 await fetch() 后清理计时器的包装,通常只限制响应头到达前的等待,无法限制后续 body 下载。不要覆盖调用方已有的 signal。
默认主动取消通常是 AbortError,超时信号的 reason 是 TimeoutError;响应体中断可能表现为 AbortError,识别原因时结合 signal 状态。传入自定义取消原因时错误值也可能不同。取消的通用协议见 abortcontroller-and-async-cancellation,它不会回滚服务端已执行的操作。
fetch 很强的一面:响应体可以流式读取
非空的 Response.body 是 ReadableStream;HEAD、204 等无响应体场景需单独处理。这意味着你不一定要等整个响应体下载完成后再处理,适合:
- AI 输出流
- 大文件下载
- 日志流
- 渐进式文本展示
const response = await fetch("/api/stream");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (response.body) {
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value);
}
} finally {
reader.releaseLock();
}
}
文本块边界不是 SSE 事件或 JSON 对象边界,需要按协议累积解析。若业务提前停止读取,还需取消流或请求;释放 reader 锁本身不等于取消。
这部分属于更广义的流式处理,展开看:
进阶选项
keepalive
允许少量请求在页面离开后继续,但不是送达保证;请求体受共享的 64 KiB 在途配额约束。退出事件未必触发,不能把它当可靠消息队列。
fetch("/analytics", {
method: "POST",
body: JSON.stringify({ action: "leave" }),
headers: {
"Content-Type": "application/json"
},
keepalive: true
});
duplex
它和流式请求体上传有关,属于更前沿、兼容性也更挑环境的能力。理解它即可,生产里要谨慎看兼容性。
常见误区
await fetch()不等于拿到业务数据,只是拿到Response404、500默认不会进catchmode: "no-cors"不是“绕过跨域”开关- 一个
response不能先json()再text()随便读两次 credentials: "include"不代表跨域 cookie 一定能成功带上
和 Axios 的边界
fetch 是浏览器原生 API;Axios 是额外封装过的库。两者的选型、错误语义差异和工程实践建议,放在关系笔记里统一看更清楚: