Skip to content
Axios 常见问题与限制
Axios 虽然提供了比原生 XMLHttpRequest 和 fetch 更一致的请求体验,但在浏览器兼容性、数据类型转换、进度事件和请求取消这些方面,仍然有明确的边界。理解这些边界,才能在设计请求层时做出合理的取舍。
浏览器兼容性:XHR 适配器与 fetch 的差距
浏览器中的 Axios 默认通过 XMLHttpRequest 发送请求。这个选择决定了它在兼容性上的表现:XHR 是一个很老的接口,所以 Axios 的请求能力在 IE11 等旧浏览器中仍然可用;而 fetch 是较新的标准,IE 完全没有原生支持。
这个“可用”有一个前提:Axios 内部依赖 Promise,IE11 本身不提供 Promise,需要引入 polyfill。同时,项目中如果使用了 async/await 等 ES2015+ 语法,还需要经过转译。XHR 适配器解决的是“浏览器有没有原生请求能力”的问题,不解决语法层面的兼容。
responseType 配置也与 fetch 有差异。Axios 默认的 responseType 是 'json',但它并不会把底层 xhr.responseType 设为 'json',而是保持 XHR 默认的文本读取方式,拿到响应文本后,再通过 transformResponse 中的 JSON 解析函数把字符串转成对象。这样做的原因是为了兼容老版本浏览器对 xhr.responseType = 'json' 支持不完整的问题。
js
const response = await axios.get('/api/user');
// response.data 已经是一个对象,例如 { name: '张三' }
console.log(response.data.name);如果换成 fetch,必须手动调用 res.json(),而且如果响应文本不是合法 JSON,会直接抛错。Axios 的默认行为则不同:JSON 解析失败时,response.data 会保留为字符串。
另一个差异是流式响应。在浏览器中,Axios 无法提供类似 response.body 的流,因为 XHR 不支持流式读取。responseType: 'stream' 只在 Node.js 的 http adapter 下生效,在浏览器中使用 Axios 拿不到 Node.js 的 Readable stream,也不会得到 Web ReadableStream。fetch 的 response.body 则是标准的 ReadableStream,可用于流式解析,这一点会在后文展开。
数据类型转换:序列化规则与真实行为
Axios 在发送请求时,会对 data 做一层默认转换。这个转换发生在 transformRequest 阶段,但它并不是对所有类型一视同仁。
对象、字符串与 FormData
当 data 是普通对象时,无论请求头是否已经设置了 Content-Type,Axios 的默认转换器都会执行 JSON.stringify,并自动把请求头设置为 application/json。因此:
js
axios.post('/api/users', { name: '张三' });
// 请求体: {"name":"张三"}
// 请求头: Content-Type: application/json如果请求头已经显式设置为 application/x-www-form-urlencoded,默认转换器会把对象编码为 URL 查询字符串;如果设置为 multipart/form-data,会尝试把对象转换为 FormData。多数场景下,使用默认的 JSON 行为即可。
当 data 是字符串时,行为取决于请求头。如果 Content-Type 是 application/json,默认转换器会对字符串执行 JSON.stringify,结果是字符串被加上引号:
js
axios.post('/api/search', 'keyword=axios', {
headers: { 'Content-Type': 'application/json' },
});
// 请求体: "\"keyword=axios\""因此在大多数场景下,手动拼 JSON 字符串再发送并不是一个好的选择。字符串通常在 Content-Type 为 text/plain 或 application/x-www-form-urlencoded 时使用,此时 Axios 会原样发送。Content-Type 需要自行设置,Axios 不会根据字符串内容推断它属于哪种类型:
js
axios.post('/api/search', 'keyword=axios', {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
// 请求体: keyword=axios如果 data 是 FormData,Axios 不会介入,浏览器会负责生成带 boundary 的 multipart/form-data 请求体。URLSearchParams 会被转换为字符串,并自动设置 Content-Type 为 application/x-www-form-urlencoded。
js
const form = new FormData();
form.append('file', file);
axios.post('/api/upload', form);Date 对象按普通对象处理。JSON.stringify(new Date()) 会得到 ISO 格式的字符串,因此直接发送 Date 实例时,请求体中的值是一个字符串:
js
axios.post('/api/events', { time: new Date() });
// 请求体: {"time":"2026-08-10T00:00:00.000Z"}如果希望把 Date 格式化为时间戳或自定义格式,需要在发送前自行转换。
null 和 undefined 需要留意:JSON.stringify({ a: null }) 得到 '{"a":null}',而 JSON.stringify({ a: undefined }) 得到 '{}'。如果构造请求体时没有处理空值,后端收到的字段可能会和预期不一致。
数组参数的编码
params 中的数组序列化方式也是常见差异点。Axios 1.x 默认把数组编码成带空方括号的键名:
js
axios.get('/api/search', {
params: { tags: ['axios', 'http'] },
});
// 默认 URL: /api/search?tags[]=axios&tags[]=http有些服务端不认 tags[] 这种格式。Axios 1.x 可以通过 paramsSerializer.indexes 调整:
js
axios.get('/api/search', {
params: { tags: ['axios', 'http'] },
paramsSerializer: {
indexes: null, // 输出 tags=axios&tags=http
},
});indexes 的取值与输出格式的对应关系如下:
| 取值 | 输出 |
|---|---|
false(默认) | tags[]=axios&tags[]=http |
null | tags=axios&tags=http |
true | tags[0]=axios&tags[1]=http |
'item' | tags[item]=axios&tags[item]=http |
服务端和前端需要约定同一种数组编码格式。在 Axios 0.x 中,paramsSerializer 只能传入自定义序列化函数,1.x 的对象配置写法更直接。
transformRequest / transformResponse 的作用边界
transformRequest 是 Axios 请求链上的一个环节。默认转换器的行为取决于 data 的类型和请求头:普通对象会被 JSON.stringify,URLSearchParams 会转为查询字符串,FormData、Blob、ArrayBuffer 等原生类型会原样保留。自定义 transformRequest 会覆盖默认行为,此时请求体的序列化完全由自己负责:
js
const instance = axios.create({
transformRequest: [
(data, headers) => {
// 如果这里返回 undefined,请求体就会为空
return data;
},
],
});transformResponse 默认在响应到达后尝试 JSON.parse。解析失败时,会原样返回响应文本。因此响应头为 Content-Type: application/json、但响应体不是合法 JSON 时,不会抛出异常,response.data 会是一个字符串。如果自定义 transformResponse 数组,默认的 JSON 解析函数会被替换,需要决定是否自己解析。
上传与下载进度:进度事件的信息边界
onUploadProgress 和 onDownloadProgress 是 Axios 在浏览器端提供的能力,底层来自 XHR 的 progress 事件。这两个回调可以拿到一个 progressEvent 对象,包含以下主要信息:
loaded:已传输的字节数total:总字节数,如果能够计算的话lengthComputable:布尔值,表示total是否有效timeStamp:事件发生时间
所以 Axios 可以告诉你“已经传了多少字节”,但不会直接给你网速。平均速度需要自己用两次事件之间的 loaded 差值和 timeStamp 差值计算。
total 为 0 的情况
当响应体使用 Transfer-Encoding: chunked,或者响应头没有 Content-Length 时,total 会是 0,lengthComputable 为 false。上传时,如果请求体是流且长度未知,也会出现同样的情况。计算百分比时如果不判断 lengthComputable,很容易得到 NaN 或无限值:
js
function getProgressPercent(progressEvent) {
if (!progressEvent.lengthComputable || progressEvent.total === 0) {
return 0;
}
return Math.round((progressEvent.loaded / progressEvent.total) * 100);
}fetch 的空白
fetch 没有提供上传进度事件。下载进度可以通过 response.body 这个 ReadableStream 手动读取并统计,但代码量比 Axios 多不少:
js
const res = await fetch('/api/download');
const reader = res.body.getReader();
let received = 0;
const total = Number(res.headers.get('Content-Length')) || 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.length;
const percent = total ? Math.round((received / total) * 100) : 0;
// 更新进度条
}上传进度在 fetch 中没有标准事件,这是 Axios 在浏览器端的一个实际优势。
Node.js 中的进度限制
Axios 在 Node.js 中默认使用 http/https adapter,而不是浏览器里的 XHR,因此进度事件的行为与浏览器不同。onDownloadProgress 在 Node.js 中可以工作,但它是通过监听响应流的 data 事件累计字节数实现的,触发频率和事件对象与浏览器 XHR 不完全一致。onUploadProgress 在 Node.js 中支持有限,请求体为普通对象时不会触发,通常只有使用流式请求体(如 form-data)时才有可能获得进度。
如果需要在 Node.js 中获取下载进度,更可控的方式是配合 responseType: 'stream' 自己监听流的 data 事件统计字节数。上传进度在 Node 端通常需要借助 form-data 这类库的流式接口实现。
取消请求:CancelToken 的弃用与 AbortController 的接入
Axios 从 v0.22.0 开始支持 AbortController,这是 fetch 使用的标准取消机制。从同一版本起,CancelToken 被标记为 deprecated,官方文档建议新项目不要再使用。虽然 1.x 中仍然保留 CancelToken,但它已经是一个历史遗留 API。
两种写法的对比
CancelToken 的旧写法:
js
const source = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: source.token,
});
// 需要取消时
source.cancel('用户取消了请求');AbortController 的新写法:
js
const controller = new AbortController();
axios.get('/api/data', {
signal: controller.signal,
});
// 需要取消时
controller.abort();同一个 signal 可以同时传给多个请求,调用一次 abort() 会取消所有关联请求。AbortController 是 Web 标准,在 Node.js 和浏览器中都可用。
取消后的错误识别
取消请求后,Axios 会抛出一个继承自 AxiosError 的 CanceledError。判断请求是否被取消,推荐使用 axios.isCancel(error),也可以检查 error.code === 'ERR_CANCELED'。
js
try {
await axios.get('/api/data', { signal });
} catch (error) {
if (axios.isCancel(error)) {
console.log('请求被取消');
} else {
console.error(error);
}
}当使用 AbortSignal.timeout(5000) 生成超时信号时,Axios 同样会把这个中止转换为取消错误,因此仍然可以用 axios.isCancel 判断。
超时与取消的竞争
timeout 配置只负责“响应超时”。当网络连接本身不可用时,请求可能一直挂起,直到系统层面的连接超时(服务端可能数分钟)。把 timeout 和 signal 结合使用,可以同时覆盖响应超时和连接建立阶段:
js
axios.get('/api/data', {
timeout: 5000,
signal: AbortSignal.timeout(5000),
});AbortSignal.timeout() 在 Node.js 17.3+ 和现代浏览器中支持。旧环境中需要手动实现:创建一个 AbortController,用 setTimeout 调用 abort(),并在请求结束时 clearTimeout。手动实现时要区分超时取消和用户主动取消,因为两者最终都是取消错误。
fetch 替代 Axios:能力对比与选型评估
fetch 是 Web 标准,Node.js 18+ 也内置了全局 fetch。对一个新项目来说,fetch 是否已经足够?这个问题取决于对请求层的要求。
fetch 所缺的能力
fetch 只负责发请求和接收响应,很多请求层需要的能力需要自己封装:
- 拦截器:fetch 没有内置的请求/响应拦截器,需要自己在包装函数中实现。
- 统一错误处理:fetch 只在网络错误时 reject,HTTP 4xx/5xx 不会 reject。必须手动检查
res.ok或res.status:
js
const res = await fetch('/api/data');
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();Axios 则会把 4xx/5xx 统一转为 AxiosError,并且错误对象中包含 config、request、response,方便统一处理。
- 上传/下载进度:上传进度 fetch 完全没有,下载进度需要自行从流中读取和统计。
- 响应结构:fetch 返回的
Response对象与 Axios 的AxiosResponse结构不同,后者把data、status、headers、config放在同一个对象上,对调用方更友好。
另外,Axios 在 Node.js 中可以直接设置 responseType: 'stream' 拿到 Node.js 风格的 Readable stream,而 fetch 的 response.body 是 Web ReadableStream。两者虽可互相转换,但直接使用上有些区别。
fetch 独有的流式优势
浏览器中的 Axios 基于 XHR,拿不到响应流,因此无法做真正的流式渲染。fetch 的 response.body 可以逐块读取,适合处理服务端推送、大文件下载或展示“边下载边解析”的场景。例如一个简单的流式文本解析:
js
const res = await fetch('/api/events');
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}Axios 在浏览器端没有对应的能力。如果项目的核心场景之一是流式响应,fetch 会更合适。
选型评估
选择 Axios 还是 fetch,本质是权衡项目需求:
- 需要支持 IE11:Axios 是更直接的选择,fetch 需要 polyfill 且行为不一定完全一致。
- 需要上传/下载进度条:浏览器端 Axios 开箱即用,fetch 需要自己实现。
- 需要拦截器和统一错误处理:Axios 内置,fetch 需要包装。
- 需要流式响应或 SSE:浏览器端 fetch 的优势明显,Axios 无法提供同等能力。
- 追求零依赖且只面向现代浏览器:fetch 可以满足大部分场景,但需要手动补齐请求层封装。
注意点:Axios 限制对请求层设计的影响
如果把 Axios 作为团队请求层的基础,以下几点在设计时需要考虑:
- 进度事件的行为因环境而异。
onUploadProgress在浏览器 XHR 环境中可用,在 Node.js 中支持有限;onDownloadProgress在 Node.js 中可用,但触发频率与浏览器不同。不要让业务代码假设这两个回调在所有环境下都有相同的表现。 - 取消请求优先使用
AbortController。CancelToken已经是弃用 API,新代码不应再依赖,同时要处理取消错误与普通错误。 timeout的覆盖范围有限。它主要解决响应超时,连接建立阶段的挂起需要用signal或AbortSignal.timeout()配合处理。- 数组参数序列化需与后端对齐。默认的
tags[]=a&tags[]=b格式不一定匹配所有后端,请求层应提供一个可配置的paramsSerializer。 - 流式响应有环境限制。浏览器端 Axios 没有流式响应能力;Node.js 中使用
responseType: 'stream'时,maxContentLength和maxBodyLength会被忽略。如果存在流式下载或 SSE 需求,应改用 fetch 或 EventSource。 - 跨域 Cookie 受浏览器策略影响。Axios 本身只是一层封装,
withCredentials开启后,Chrome 等浏览器对跨域Set-Cookie的处理策略仍可能使 Cookie 无法写入,这是浏览器层面的限制。
Axios 提供了很多便利,但它的能力边界由浏览器、Node.js 和 HTTP 本身共同决定。在边界之内使用,它仍然是可靠的请求工具;超出边界时,则需要根据具体场景选择 fetch 或其他方案。
