Skip to content
Axios 核心概念与请求流程
js
const axios = require('axios');
axios
.get('https://api.example.com/users', {
params: { page: 1 },
timeout: 5000,
})
.then((response) => {
console.log(response.data);
});这是一次最常见的 axios 请求。表面上看,它只是调用了 axios.get 并传入一个配置对象;实际上,从调用开始到 Promise 返回,请求会依次经过配置合并、请求拦截器、请求数据转换、网络适配器、响应数据转换、响应拦截器等阶段。
axios.get(url, config) 与 axios({ method: 'get', url, ...config }) 最终都进入同一个入口:Axios.prototype.request。后续章节会逐步拆开这条链路,说明每个阶段的行为。
Axios 实例与全局 axios 的差异
axios 模块的默认导出并不是一个普通实例对象,而是一个由 createInstance 工厂生成的函数。直接调用它等价于调用 request(options);同时这个函数上又挂载了 get、post、defaults、interceptors 等属性,所以在使用层面它表现得像一个实例。
axios.create() 则用于创建另一个独立实例:
js
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 6000,
});
instance.get('/users');
// 实际请求:GET https://api.example.com/users
axios.defaults.baseURL = 'https://api.another-example.com';
instance.get('/users');
// 仍然请求:GET https://api.example.com/users实例与全局 axios 的隔离体现在两点:
- 每个实例拥有自己独立的
defaults和interceptors。 - 实例创建时,会把当时的全局默认配置合并进实例默认配置;之后修改全局
axios.defaults,不会影响已经创建的实例。
因此,如果项目中有多个服务端地址或多种超时策略,可以创建多个实例分别管理,而不需要每次请求都写完整 URL。
请求配置的合并规则
一次请求最终使用的配置,由三层配置合并得到,合并优先级从高到低为:
- 调用请求方法时传入的配置(
axios.get(url, config)中的config) - 实例的
defaults(由axios.create(config)创建时生成) - 全局
axios.defaults
合并发生在 Axios.prototype.request 内部,由 lib/core/mergeConfig.js 完成[2]。例如:
js
axios.defaults.timeout = 3000;
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 6000,
});
instance.get('/users', {
timeout: 10000,
});这里最终生效的 timeout 是 10000。如果请求级配置不传 timeout,则使用实例的 6000;如果实例也没有,则使用全局的 3000。
headers 的合并不是简单覆盖,而是按 header 名称逐个合并。实例默认配置中如果设置了 Authorization,请求级配置中再设置 X-Client-Type,两个 header 都会出现在最终请求中。params 和 data 则不做深度合并,后者整体覆盖前者。
实例创建时已经完成了一次全局配置到实例配置的合并,所以:
js
const instance = axios.create();
axios.defaults.timeout = 5000;
// instance.defaults.timeout 仍为 undefined拦截器:请求前与响应后的钩子
拦截器允许在请求发送之前和响应返回之后插入自定义逻辑。
注册方式:
js
axios.interceptors.request.use(onFulfilled, onRejected);
axios.interceptors.response.use(onFulfilled, onRejected);请求拦截器收到的是合并后的配置对象:
js
axios.interceptors.request.use((config) => {
config.headers.set('X-Request-Id', crypto.randomUUID());
return config;
});响应拦截器收到的是 AxiosResponse 对象:
js
axios.interceptors.response.use((response) => {
console.log(response.status);
return response;
});请求拦截器必须返回 config;如果返回 Promise.reject(new Error(...)),请求会被中断,后续拦截器和 adapter 不会执行。
响应拦截器的返回值会成为 Promise 链中下一个环节的输入。因此常见的写法是:
js
axios.interceptors.response.use((response) => response.data);这样后续 then((data) => ...) 拿到的直接是响应体数据,而不是整个 AxiosResponse。
拦截器的执行顺序需要注意。请求拦截器按 LIFO(后注册先执行)顺序执行;响应拦截器按 FIFO(先注册先执行)顺序执行[3][4]。
js
axios.interceptors.request.use((config) => {
console.log('request 1');
return config;
});
axios.interceptors.request.use((config) => {
console.log('request 2');
return config;
});
// 输出:
// request 2
// request 1输出结果说明了后注册的请求拦截器先执行。响应拦截器的情况反过来:
js
axios.interceptors.response.use((response) => {
console.log('response 1');
return response;
});
axios.interceptors.response.use((response) => {
console.log('response 2');
return response;
});
// 输出:
// response 1
// response 2响应拦截器按注册顺序执行。这个顺序与源码中的实现一致:Axios.js 的 request 方法把 dispatchRequest 放入 Promise 链,请求拦截器通过 unshift 加入链前,响应拦截器通过 push 加入链后[4]。
适配器:一次请求在不同环境的执行方式
adapter 是 axios 中真正发起网络请求的函数。它接收最终配置对象,返回一个 Promise,成功时 resolve 一个 AxiosResponse,失败时 reject 一个错误[6]。
axios 的默认 adapter 会根据运行环境选择实现:
- 浏览器环境使用基于
XMLHttpRequest的 xhr adapter; - Node.js 环境使用基于
http/https模块的 adapter[6]。
adapter 的职责边界很明确:它只负责底层网络通信,不包含业务逻辑。配置合并、拦截器、数据转换都发生在 adapter 之外。因此,替换一个 adapter 不会影响上层的拦截器和转换器机制。
在 1.x 中,adapter 可以是一个自定义函数,也可以指定内置适配器的名称。
js
const instance = axios.create({
adapter: 'http', // 强制使用 Node.js HTTP adapter
});某些特殊宿主环境(例如 Electron 同时存在 XHR 和 Node 模块)中,默认环境判断可能选择不符合预期的 adapter,这时可以显式指定[7]。
自定义 adapter 的最小实现如下:
js
const instance = axios.create({
adapter: async (config) => {
return {
data: { mock: true },
status: 200,
statusText: 'OK',
headers: {},
config,
request: {},
};
},
});
const { data } = await instance.get('/any-path');
console.log(data); // { mock: true }这种自定义 adapter 常被用于单元测试或本地 mock。
转换器:数据在传输前后的自动变形
transformRequest 和 transformResponse 是 axios 中负责数据格式转换的配置项。
默认情况下,transformRequest 会在请求发送前把对象形式的 data 序列化为 JSON 字符串,并设置 Content-Type: application/json(如果尚未设置)。transformResponse 会在响应返回后把 JSON 字符串解析为 JavaScript 对象[5]。
看一个实际的转换过程:
js
const instance = axios.create();
// 发送时传入对象
instance.post('/users', { name: 'Alice' });
// 实际发送到服务器的 body 是:
// {"name":"Alice"}
// Content-Type: application/jsonjs
// 服务器返回:
// HTTP/1.1 200 OK
// Content-Type: application/json
// {"id":1,"name":"Alice"}
// axios 得到的 response.data 是:
// { id: 1, name: 'Alice' }transformRequest 是一个函数数组,在 dispatchRequest 内部、adapter 调用之前执行[5];transformResponse 同样是一个函数数组,在 adapter 拿到响应后、响应拦截器之前执行[5]。自定义数组会覆盖默认行为,因此如果需要在默认序列化之外增加逻辑,可以这样写:
js
const instance = axios.create({
transformRequest: [
(data, headers) => {
if (data instanceof FormData) {
return data;
}
return JSON.stringify(data);
},
],
transformResponse: [
(data) => {
if (typeof data === 'string') {
try {
return JSON.parse(data);
} catch {
return data;
}
}
return data;
},
],
});transformRequest 对 GET、HEAD 等没有请求体的方法没有实际意义;它主要影响 POST、PUT、PATCH、DELETE 等携带 data 的请求。
完整请求流程:从方法调用到 Promise 返回
把前面几节串联起来,一次成功请求的完整链路如下:
text
调用 axios.get('/users', config)
-> request(config)
-> mergeConfig 合并配置
-> 请求拦截器(后注册先执行)
-> dispatchRequest
-> transformRequest 转换请求数据
-> adapter 发起网络请求
-> validateStatus 校验状态码
-> transformResponse 转换响应数据
-> 响应拦截器(先注册先执行)
-> resolve(AxiosResponse)成功分支
当 adapter 成功拿到响应,并且 HTTP 状态码通过 validateStatus 校验(默认 status >= 200 && status < 300)时,流程进入成功分支:
js
const response = await axios.get('/users');
// AxiosResponse 的结构
{
data: [...], // 响应体,已经过 transformResponse 解析
status: 200, // HTTP 状态码
statusText: 'OK', // 状态文本
headers: {}, // 响应头
config: {}, // 本次请求的最终配置
request: {}, // 底层请求对象(XHR 或 Node ClientRequest)
}response.data 此时已经是解析后的 JavaScript 值,而不是 JSON 字符串。
失败分支
失败分支分为两类:
- HTTP 状态码不符合
validateStatus,例如 404、500。 - 网络层失败,例如 DNS 解析失败、连接被拒绝、超时、请求被取消。
第一类失败的特点是 error.response 存在,服务端返回的响应头、状态码、响应体都可以通过 error.response 读取:
js
try {
await axios.get('/users/not-exist');
} catch (error) {
if (error.response) {
console.log(error.response.status); // 404
console.log(error.response.data); // 服务端返回的错误信息
}
}第二类失败中,error.response 通常为 undefined:
js
try {
await axios.get('https://unresolvable-host.example.com');
} catch (error) {
console.log(error.response); // undefined
console.log(error.code); // ERR_NETWORK
}主动取消请求
axios 1.x 推荐使用 AbortController 的 signal 来取消请求:
js
const controller = new AbortController();
axios.get('/long-request', {
signal: controller.signal,
});
// 用户点击取消
controller.abort();取消后 Promise 进入 rejected 状态,抛出的错误是 CanceledError,其 code 为 ERR_CANCELED。
早期版本使用 CancelToken,axios 1.x 中仍然可用:
js
const source = axios.CancelToken.source();
axios.get('/long-request', {
cancelToken: source.token,
});
source.cancel('用户取消了请求');两套机制实现的效果相同,但 CancelToken 在 1.x 中已被官方标记为 deprecated。AbortController 是 Web 标准 API,新代码优先使用 signal。
取消请求只会中断客户端等待,不会阻止服务端继续处理已经到达的请求。
AxiosError:错误在流程中的位置
在 axios 自身发起的请求链路中,adapter 错误、状态码校验失败、请求取消等环节产生的错误都会以 AxiosError 形式抛出。CanceledError 也继承自 AxiosError,因此可以通过 axios.isAxiosError(error) 统一判断。拦截器或自定义 adapter 中手动抛出的普通 Error 不会被自动转换为 AxiosError。
AxiosError 的主要字段:
message:错误描述文本code:错误代码config:本次请求的最终配置request:底层请求对象(如果存在)response:AxiosResponse(如果 HTTP 响应已返回)cause:底层错误原因(在部分错误中可用,例如网络错误)
判断错误类型时,code 和 response 是最常用的两个字段。常见 code 包括:
ERR_NETWORK:网络层错误,没有收到响应ERR_CANCELED:请求被取消ECONNABORTED:请求超时(部分环境下可能表现为ETIMEDOUT)
超时是一个比较容易混淆的场景。浏览器端和 Node.js 端的 adapter 产生超时错误的方式不同,因此错误码不一定相同。建议在代码中不要依赖 message 文本判断超时,而是结合 error.code 或 config.timeout 处理。
validateStatus 决定哪些状态码被当作成功。默认函数是:
js
(status) => status >= 200 && status < 300;可以通过请求配置修改:
js
axios.get('/users', {
validateStatus: (status) => status < 500,
});这样即使接口返回 400,Promise 也会进入成功分支。这个配置适合需要自行处理 4xx 响应体的场景。
注意点与常见误区
axios.create()创建的实例和全局axios相互独立。修改全局defaults不会影响已创建的实例;反过来,修改实例defaults也不会污染全局。- 请求拦截器后注册先执行,响应拦截器先注册先执行。这与“注册顺序”这个直观印象相反,容易出错。
- 拦截器内部修改的是流程中合并后的配置对象,不是调用处传入的原始对象。不要在拦截器修改
config后,期待调用处的原始对象也发生同样变化。 transformRequest和transformResponse是数组,但自定义数组是整体替换,不会与默认合并。需要在默认行为基础上扩展时,要手动把默认函数一起传入。- HTTP 状态码错误同样会被
catch捕获,但不代表网络错误。判断依据是error.response是否存在。 - adapter 是环境层抽象,不是业务层钩子。需要统一添加 header 或统一处理业务状态码时,应该使用拦截器,而不是改动 adapter。
- 请求取消后抛出的
CanceledError也满足error instanceof AxiosError。如果全局错误处理需要区分“用户主动取消”和其他异常,需要用axios.isCancel(error)或error.code === 'ERR_CANCELED'判断。
参考链接
- [2] https://github.com/axios/axios/blob/v1.x/lib/core/mergeConfig.js
- [3] https://github.com/axios/axios/issues/7200
- [4] https://github.com/axios/axios/blob/v1.x/lib/core/Axios.js
- [5] https://github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js
- [6] https://github.com/axios/axios/issues/5474
- [7] https://github.com/axios/axios/issues/5263
