XHR/fetch/axios 对比与差异
XHR / fetch / axios 对比与差异 · 学习资料
一、先理清关系(很多人搞混)
浏览器原生层
├─ XMLHttpRequest (1999) ── 老牌 API,回调式
└─ fetch (ES2015) ── 新标准,Promise 式,底层不再走 XHR
第三方库层
└─ axios ── 封装库
├─ 浏览器端:adapter = XHR ← 所以有「上传进度」
└─ Node 端 :adapter = http 模块
关键认知:
- axios 不是 fetch 的替代品,也不是 XHR 的替代品 —— 它是基于 XHR 的封装(浏览器端)。
- fetch 与 XHR 是并列的两套原生能力;axios 站在 XHR 之上。
- 所以「axios 有上传进度、fetch 没有」这句话的根源就在这里。
二、同一需求的三种写法
需求:POST 一个 JSON,拿回数据。
① XHR
function request(url, data) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', url)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.responseType = 'json'
xhr.timeout = 10000
xhr.onload = () => {
// ⚠️ 404/500 也会进 onload,必须自己判 status
if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response)
else reject(new Error(`HTTP ${xhr.status}`))
}
xhr.onerror = () => reject(new Error('network error'))
xhr.ontimeout = () => reject(new Error('timeout'))
xhr.send(JSON.stringify(data))
})
}
② fetch
async function request(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal: AbortSignal.timeout(10000),
})
// ⚠️ 404/500 不会 reject,必须自己判 res.ok
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json() // ⚠️ 还要再 await 一次
}
③ axios
const { data } = await axios.post(url, data, { timeout: 10000 })
// ✅ 2xx 之外的状态码自动 reject(validateStatus 可改)
// ✅ 自动 JSON 序列化/反序列化
三、核心差异总表
| 维度 | XHR | fetch | axios |
|---|---|---|---|
| 出身 | 浏览器原生(老) | 浏览器原生(新标准) | 第三方库 |
| 底层 | — | 浏览器网络栈 | XHR(浏览器)/ http(Node) |
| API 形态 | 事件回调 | Promise | Promise + 拦截器 |
| Promise | ❌ 需手写包装 | ✅ | ✅ |
| 拦截器 | ❌ | ❌(要自己包) | ✅ 开箱即用 |
| 自动 JSON | ❌ | ❌(两次 await) | ✅ |
| HTTP 错误自动 reject | ❌ 手动判 status |
❌ 手动判 res.ok |
✅(可配 validateStatus) |
| 网络错误 | onerror |
reject | reject |
| 超时 | xhr.timeout |
AbortSignal.timeout() |
timeout 配置 |
| 取消 | xhr.abort() |
AbortController |
signal(v0.22+,CancelToken 已废弃) |
| 上传进度 | ✅ upload.onprogress |
❌ | ✅ onUploadProgress |
| 下载进度 | ✅ onprogress |
⚠️ 需读流自算 | ✅ onDownloadProgress |
| 流式响应 | ❌ | ✅ res.body(ReadableStream) |
⚠️ responseType: 'stream'(Node) |
| 携带 cookie | 同域默认带 | credentials: 'same-origin'(默认)/ 'include' |
withCredentials: true |
| 请求体类型 | string / FormData / Blob | + ReadableStream、URLSearchParams |
对象自动 JSON、FormData 自动识别 |
| 响应类型 | responseType |
.json()/.text()/.blob()/.arrayBuffer() |
responseType: 'json'/'blob'/'arraybuffer'/'text'/'stream' |
| 并发/重试 | 自己写 | 自己写 | 自己写(或插件) |
| Node 支持 | ❌ | Node 18+ 原生 | ✅ |
| 浏览器兼容 | 全(含 IE) | 现代浏览器(IE 需 polyfill) | 同 XHR,兼容性好 |
| 体积 | 0 | 0 | ~13KB(gzip 后更小) |
| TypeScript | 一般 | 一般 | ✅ 泛型友好 |
四、逐项深挖(易错点)
1. HTTP 错误语义(最容易踩)
| 404 / 500 时 | |
|---|---|
| XHR | 进 onload/onreadystatechange,status=500,不触发 onerror |
| fetch | 正常 resolve,需 if (!res.ok) |
| axios | 自动 reject(默认 validateStatus: s => s >= 200 && s < 300) |
fetch 只有网络层失败(断网、DNS、CORS 被拦)才 reject。
2. JSON 处理
// fetch:两步,且 .json() 本身也可能失败(返回了 HTML 错误页)
const res = await fetch(url)
const data = await res.json() // 可能抛 SyntaxError
// axios:一步,且自带 Content-Type 识别
const { data } = await axios.get(url)
3. Cookie / 凭证
// xhr
xhr.withCredentials = true // 跨域带 cookie
// fetch
fetch(url, { credentials: 'include' }) // 跨域带
fetch(url, { credentials: 'same-origin' }) // 默认值,同域带
// axios
axios.get(url, { withCredentials: true })
三者都要求服务端返回 Access-Control-Allow-Credentials: true 且 Access-Control-Allow-Origin 不能为 *。
4. 超时与取消
// xhr
xhr.timeout = 5000; xhr.ontimeout = fn; xhr.abort()
// fetch(现代写法)
fetch(url, { signal: AbortSignal.timeout(5000) })
const ac = new AbortController(); fetch(url, { signal: ac.signal }); ac.abort()
// axios(推荐 signal,CancelToken 已废弃)
axios.get(url, { timeout: 5000, signal: ac.signal })
5. 取消后如何识别
// fetch → err.name === 'AbortError'
// axios → err.code === 'ERR_CANCELED'(或 axios.isCancel(err))
// xhr → 触发 onabort,status === 0
6. 流式能力(fetch 独有优势)
const res = await fetch(url)
const reader = res.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
console.log(new TextDecoder().decode(value)) // AI 打字机 / 大文件分片读
}
XHR / axios 做不到(axios 只能 responseType: 'stream' 在 Node 端读流)。
7. 上传进度(XHR / axios 独有)
// xhr
xhr.upload.onprogress = e => console.log(e.loaded / e.total)
// axios
axios.post(url, form, {
onUploadProgress: e => console.log(e.loaded / e.total),
})
// fetch:❌ 无(实验性 ReadableStream + duplex:'half' 不可用于生产)
五、选型决策树
需要上传/下载进度?
├─ 是 → axios(或原生 XHR)
└─ 否
├─ 需要拦截器、统一错误处理、自动 JSON、Node 同构?
│ └─ axios(业务项目首选)
├─ 需要流式读取(SSE 替代、AI 流式、大文件)?
│ └─ fetch
├─ 追求零依赖 / 只做几个简单请求?
│ └─ fetch
└─ 需要兼容 IE / 老内核 WebView?
└─ XHR 或 axios(axios 底层就是 XHR)
一句话:
- 业务系统 / 中后台:axios(工程化能力最全)
- 轻量 / 边缘场景 / 需要 Stream:fetch
- 维护老代码 / 需要底层控制:XHR
六、代码迁移对照表
| 需求 | XHR | fetch | axios |
|---|---|---|---|
| 发 GET | xhr.open('GET', url); xhr.send() |
fetch(url) |
axios.get(url) |
| 发 POST JSON | setRequestHeader + send(JSON.stringify(d)) |
fetch(url,{method:'POST',headers,body:JSON.stringify(d)}) |
axios.post(url, d) |
| 取响应 | xhr.response / responseText |
await res.json() |
res.data |
| 判成功 | status >= 200 && < 300 |
res.ok |
自动(失败走 catch) |
| 设 header | xhr.setRequestHeader(k,v) |
headers: {k:v} |
headers: {k:v} |
| 超时 | xhr.timeout = ms |
AbortSignal.timeout(ms) |
timeout: ms |
| 取消 | xhr.abort() |
ac.abort() |
ac.abort()(传 signal) |
| 上传进度 | xhr.upload.onprogress |
无 | onUploadProgress |
| 二进制 | responseType='blob' |
await res.blob() |
responseType:'blob' |
| 带 cookie | withCredentials=true |
credentials:'include' |
withCredentials:true |
七、三层"错误"速查
| 错误类型 | XHR | fetch | axios |
|---|---|---|---|
| 断网 / DNS 失败 | onerror |
reject(TypeError) |
reject(ERR_NETWORK) |
| HTTP 4xx/5xx | onload + status |
resolve + res.ok=false |
reject |
| 超时 | ontimeout |
reject(TimeoutError) |
reject(ECONNABORTED) |
| 取消 | onabort |
reject(AbortError) |
reject(ERR_CANCELED) |
| JSON 解析失败 | 自己 try | .json() 抛错 |
自己 try(自动转换失败会抛) |
八、自检题
Q1:axios 的上传进度是从哪来的?
浏览器端 adapter 用的是 XHR,
upload.onprogress是 XHR 的能力。
Q2:fetch 请求返回 500,会进 catch 吗?
不会。只有网络层失败才 reject,500 需要
if (!res.ok) throw。
Q3:axios 请求返回 500,会进 catch 吗?
会(默认
validateStatus只认 2xx),除非自定义validateStatus。
Q4:现代取消请求的标准方案是什么?三者分别怎么用?
AbortController+signal;xhr 用xhr.abort(),fetch 与 axios 都传signal。
Q5:需要读取流式响应(如 AI 逐字输出)选哪个?
fetch(
res.body.getReader())。
Q6:fetch 默认会带 cookie 吗?跨域呢?
默认
credentials: 'same-origin'(同域带);跨域需显式'include'+ 服务端Allow-Credentials。
九、一句话记忆
XHR 是地基(有进度、回调式);fetch 是新标准(Promise、有流、没进度、错误要自己判);axios 是盖在 XHR 上的房子(拦截器、自动 JSON、自动 reject、有进度)。

浙公网安备 33010602011771号