← [ BACK_TO_NOTES ]

Fetch API

浏览器原生的网络请求接口,以 Request、Response、Headers 和 Stream 抽象请求与响应。

#concept #growing #network #browser #http #javascript

[!info] related notes

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,也可以是 Request
  • init 是 RequestInit

常见选项有:

  • method
  • headers
  • body
  • signal
  • mode
  • credentials
  • cache
  • redirect
  • referrerPolicy
  • keepalive

如果同一个选项同时出现在 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 或其他凭证:

  • omit
  • same-origin
  • include

跨源请求即使写了 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() 不等于拿到业务数据,只是拿到 Response
  • 404、500 默认不会进 catch
  • mode: "no-cors" 不是“绕过跨域”开关
  • 一个 response 不能先 json() 再 text() 随便读两次
  • credentials: "include" 不代表跨域 cookie 一定能成功带上

和 Axios 的边界

fetch 是浏览器原生 API;Axios 是额外封装过的库。两者的选型、错误语义差异和工程实践建议,放在关系笔记里统一看更清楚:

参考

创建于 2025/12/8 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本