Fetch API
浏览器原生的网络请求接口,以 Request、Response、Headers 和 Stream 抽象请求与响应。
[!info] related notes
Fetch API
一句话定义
fetch() 是浏览器和 Worker 环境中的原生网络请求接口。它返回的不是“数据”,而是一个会在响应可用时兑现为 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();
一个常见坑是:服务端如果返回的是 HTML 报错页、半截字符串、或非法 JSON,response.json() 会抛解析错误。工程上更稳的做法是先判断 Content-Type:
const response = await fetch("/api/data");
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json")
? await response.json()
: await response.text();
bodyUsed、clone() 与“一次性读取”
Response.bodyUsed 表示 body 是否已经被消费过。
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 参数。标准做法是 AbortController:
async function fetchWithTimeout(url, init = {}, timeout = 8000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, {
...init,
signal: controller.signal
});
} finally {
clearTimeout(timer);
}
}
如果被取消,通常会抛出 AbortError:
try {
await fetchWithTimeout("/api/slow");
} catch (error) {
if (error.name === "AbortError") {
console.log("request aborted");
}
}
fetch 很强的一面:响应体可以流式读取
Response.body 是 ReadableStream。这意味着你不一定要等整个响应体下载完成后再处理,适合:
- AI 输出流
- 大文件下载
- 日志流
- 渐进式文本展示
const response = await fetch("/api/stream");
const textStream = response.body.pipeThrough(new TextDecoderStream());
const reader = textStream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value);
}
这部分属于更广义的流式处理,展开看:
进阶选项
keepalive
适合页面卸载前上报少量数据,例如埋点或退出事件。
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 是额外封装过的库。两者的选型、错误语义差异和工程实践建议,放在关系笔记里统一看更清楚: