Skip to content
Axios 拦截器与取消机制
概述
在项目中使用 Axios 时,请求的认证信息注入、统一错误处理和请求取消是三个绕不开的问题。
先看一个没有拦截器的请求函数:
js
function getUsers() {
const token = getToken();
return axios.get('/users', {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
}单个请求的写法尚可接受。当项目里有几十个接口,每个接口都要重复写 token 注入、错误提示、登录跳转时,代码会迅速变得混乱。更麻烦的是,如果后端要求把 token 从 header 换成 cookie,或者统一调整错误提示文案,改动会散落到所有调用点。
拦截器可以在请求发出之前统一修改请求配置,在响应返回之后统一处理响应数据或错误。取消机制则解决另一类问题:用户快速切换搜索关键词时,前一个请求的响应可能比后一个晚到,导致页面被旧数据覆盖;用户已经离开当前页面,请求完成后仍然去更新 UI;用户连续点击提交按钮,同一个请求被发送多次。这些场景都需要主动取消正在进行的请求,或者在请求结束后正确地忽略掉过期结果。
拦截器的注册与基本用法
在 Axios 实例上注册拦截器,使用的是 interceptors 对象上的 use 方法:
js
import axios from 'axios';
const http = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
});
http.interceptors.request.use((config) => {
// 请求发出前的最后一个环节,可以修改 config
return config;
});
http.interceptors.response.use(
(response) => {
// 状态码为 2xx 时进入这里
return response;
},
(error) => {
// 状态码非 2xx,或网络错误、取消请求时进入这里
return Promise.reject(error);
}
);use 方法接收两个参数:onFulfilled 和可选的 onRejected。请求拦截器的 onFulfilled 接收一个 config 对象,必须返回 config,或者返回一个解析到 config 的 Promise,请求流程才能继续。响应拦截器的 onFulfilled 接收一个 response 对象,它的返回值会成为下一个拦截器的入参,也会最终成为调用方拿到的 Promise 结果。
use 返回一个数字 id,可以用 eject(id) 移除拦截器:
js
const requestId = http.interceptors.request.use((config) => {
console.log('仅在某个阶段生效');
return config;
});
http.interceptors.request.eject(requestId);Axios 内部通过 InterceptorManager 管理这些回调,它维护一个拦截器数组,提供 use、eject 和内部遍历方法 [1]。
请求拦截器:认证信息注入与登录态缺失处理
最常用的请求拦截器逻辑是认证信息注入。假设项目用一个 getToken() 从 localStorage 或状态容器中读取登录凭证:
js
http.interceptors.request.use((config) => {
const token = getToken();
if (!token) {
window.location.href = '/login';
return Promise.reject(new Error('缺少登录凭证,请先登录'));
}
config.headers.Authorization = `Bearer ${token}`;
return config;
});这里有两件事需要注意。
第一,如果 token 不存在,请求拦截器返回了一个 rejected Promise。此时 Axios 不会产生任何网络请求,调用方拿到的 Promise 会以这个 Error 进入 rejected 状态。这个错误也会沿着 Promise 链向后传递;如果注册了全局的响应错误处理,它会收到这个错误,只不过 error.response 和 error.request 都不存在。
第二,在 Axios 1.x 中,进入请求拦截器时 config.headers 已经是一个 AxiosHeaders 实例,可以直接写 config.headers.Authorization = ...。如果是在更早的配置阶段或其他封装里操作 headers,建议先做一次初始化:
js
config.headers = config.headers ?? {};登录态缺失的处理方式取决于应用形态。浏览器应用通常跳转登录页;如果希望由调用方决定如何响应,可以只 reject 而不做跳转,把决定权留给业务代码。
响应拦截器:统一解包与错误处理
响应拦截器适合做两件事:解包响应数据,和统一错误处理。
先看解包。默认情况下 http.get('/users') 的 Promise 结果是 AxiosResponse,包含 data、status、headers 等字段。如果后端响应体结构是 { code, data, message },可以在响应拦截器中把业务数据解出来:
js
http.interceptors.response.use(
(response) => {
const { code, data, message } = response.data;
if (code !== 0) {
return Promise.reject(new Error(message));
}
return data;
},
(error) => {
return Promise.reject(error);
}
);这样调用方的代码就从:
js
const resp = await http.get('/users');
console.log(resp.data);变成:
js
const users = await http.get('/users');
console.log(users);再叠加 HTTP 层错误处理。AxiosError 有三个有用分支:error.response 存在表示服务端返回了非 2xx 状态码;error.request 存在但没有 error.response 表示请求已发出但没有收到响应;两者都不存在说明请求在构造阶段就失败了,例如请求拦截器中主动 reject。
错误处理分支的前面需要先判断请求是否被主动取消,避免取消操作被误报为网络异常:
js
http.interceptors.response.use(
(response) => {
const { code, data, message } = response.data;
if (code !== 0) {
return Promise.reject(new Error(message));
}
return data;
},
(error) => {
if (axios.isCancel(error)) {
return Promise.reject(error);
}
if (error.response) {
const { status } = error.response;
if (status === 401) {
redirectToLogin();
} else if (status === 403) {
showMessage('没有权限执行此操作');
} else if (status >= 500) {
showMessage('服务暂时不可用');
}
} else if (error.request) {
showMessage('网络异常,请检查连接');
} else {
console.error('请求构造失败', error.message);
}
return Promise.reject(error);
}
);取消后的错误对象是 CanceledError,它没有 response 和 request 属性。如果不先做 isCancel 判断,取消请求会落入 error.request 存在的分支,被误判为网络异常。
401 的处理可以简单跳登录页,也可以做刷新 token 后重放原始请求。后者涉及并发请求协调(多个请求同时 401 时只需要刷新一次 token),这块内容放在下一篇的模块化封装中展开;这里先记住入口在响应拦截器的 error.response.status === 401 判断中即可。
一个容易被忽略的行为:如果响应拦截器的 onRejected 内部 return 了一个普通值而不是 Promise.reject(error),Promise 会变成 resolved 状态,调用方的 try/catch 将捕获不到任何错误。所以不要省略 return Promise.reject(error)。
拦截器的执行顺序与异常传递
拦截器的执行顺序用一段带日志的代码验证最直接:
js
http.interceptors.request.use((config) => {
console.log('request interceptor 1');
return config;
});
http.interceptors.request.use((config) => {
console.log('request interceptor 2');
return config;
});
http.interceptors.response.use((response) => {
console.log('response interceptor 1');
return response;
});
http.interceptors.response.use((response) => {
console.log('response interceptor 2');
return response;
});
http.get('/users');输出依次是:
request interceptor 2
request interceptor 1
response interceptor 1
response interceptor 2原因是 Axios 在 request 方法内部把拦截器组织成了一条 Promise 链。初始链只有 [dispatchRequest, undefined] 两项,dispatchRequest 是真正调用 adapter 发起网络请求的模块 [2]。每注册一个请求拦截器,它就被 unshift 到链头;每注册一个响应拦截器,它就被 push 到链尾。所以请求拦截器是「后注册的先执行」,响应拦截器是「先注册的先执行」。
执行链大致如下:
request interceptor 2
→ request interceptor 1
→ dispatchRequest(发送 HTTP 请求)
→ response interceptor 1
→ response interceptor 2这个顺序带来一个实际的注意点:如果注册了多个响应拦截器,前一个拦截器 return 的值会成为后一个拦截器的入参。假设第一个响应拦截器返回了 response.data,第二个响应拦截器接收到的就不是 AxiosResponse,而是普通对象。所以多个响应拦截器的职责要划分清楚,解包操作应放在最后一个响应拦截器,或者只保留一个响应拦截器。
异常传播的规则与 Promise 链完全一致。请求拦截器中抛出的错误会向后传递,一路绕过 dispatchRequest,最终可以被响应拦截器的 onRejected 捕获。因此,如果在请求拦截器里因为缺少 token 主动 reject,全局响应错误处理也会看到它,只是此时 error.response 和 error.request 都不存在。这也是上面那段错误分支判断里保留最后一个 else 的原因。
另外,同一个 use 的 onFulfilled 中 reject 的错误,不会被同一个 use 的 onRejected 捕获,因为这对回调是 Promise.prototype.then 的两个参数:第一个回调返回的 rejected Promise 只会进入后续的 then。如果存在多个响应拦截器,它可能被下一个拦截器的 onRejected 或调用方的 catch 捕获。
取消请求的两种方式
Axios 1.x 中同时存在两套取消机制:
| 机制 | 配置字段 | 触发方式 | 状态 |
|---|---|---|---|
| CancelToken | cancelToken | source.cancel() 或构造函数拿到的 cancel() | 官方文档已标记为 deprecated [3] |
| AbortController | signal | controller.abort() | Web 标准 API [4] |
两套机制都会让请求对应的 Promise 以 rejected 状态结束,错误是一个 CanceledError。CancelToken 是 Axios 早期引入的 API,为了兼容历史代码保留;新项目推荐使用 AbortController。
CancelToken:经典取消方式
用 CancelToken.source() 创建 token 和取消函数:
js
const source = axios.CancelToken.source();
http
.get('/users', { cancelToken: source.token })
.then((users) => console.log(users))
.catch((error) => {
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
}
});
// 某一时刻触发取消
source.cancel('用户取消了请求');也可以使用 CancelToken 构造函数形式,把 cancel 函数保存到外部变量:
js
let cancelRequest;
http.get('/users', {
cancelToken: new axios.CancelToken((cancel) => {
cancelRequest = cancel;
}),
});
// 稍后在任何位置调用
cancelRequest('由外部逻辑取消');CancelToken 构造函数接收一个 executor,executor 会拿到一个 cancel 函数。调用 cancel 时,Axios 内部触发取消信号,并把请求失败原因设置成 CanceledError [5]。被取消请求的 Promise 进入 rejected 状态,所以调用方必须有 catch 处理,否则会产生 unhandled rejection。
CancelToken 的 source() 返回值里同时持有 token 和 cancel 函数,其中 token 负责挂在请求配置上,cancel 函数负责触发取消。请求已经完成之后再调用 cancel 不会产生额外副作用。
AbortController:标准化取消方式
AbortController 是 Web 标准 API,不依赖 Axios。它有两个成员:signal 属性,以及 abort() 方法 [4]。
js
const controller = new AbortController();
http
.get('/users', { signal: controller.signal })
.then((users) => console.log(users))
.catch((error) => {
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
}
});
controller.abort();调用 abort() 后,controller.signal 的状态变为已中止,Axios 在 adapter 层监听这个 signal。请求被中止后,Promise 同样以 CanceledError 结束。
如果多个请求需要一起取消,可以复用同一个 controller:
js
const controller = new AbortController();
http.get('/users', { signal: controller.signal });
http.get('/menus', { signal: controller.signal });只需要调用一次 controller.abort(),两个请求都会被取消。同一个 signal 也可以传给 fetch:
js
fetch('/api/menus', { signal: controller.signal });Axios 的请求配置中同时定义了 cancelToken 和 signal 两个字段,两者可以同时存在 [6]。新代码没有必要同时使用两套机制,选择 signal 即可。
AbortSignal.timeout() 在较新的运行环境中可以直接生成一个到时自动中止的 signal:
js
http.get('/users', {
signal: AbortSignal.timeout(5000),
});它与 timeout 配置的触发机制不同:timeout 由 Axios 内部的超时逻辑触发,AbortSignal.timeout() 由 signal 驱动。同一个请求中两者可以同时配置,先到先触发。是否使用 AbortSignal.timeout(),以目标环境的支持情况为准。
取消后的错误识别
无论是 CancelToken 还是 AbortController,取消后 Promise 都以 rejected 结束。要区分「主动取消」和「真实错误」,依赖 axios.isCancel:
js
try {
const users = await http.get('/users', { signal: controller.signal });
console.log(users);
} catch (error) {
if (axios.isCancel(error)) {
console.log('取消原因:', error.message);
return;
}
// 真实错误,继续处理
throw error;
}axios.isCancel(payload) 的实现是检查 payload instanceof CanceledError [7]。CanceledError 继承自 AxiosError,默认 code 为 ERR_CANCELED,默认 message 为 'canceled'。调用 source.cancel('reason') 时,这个字符串会成为 CanceledError 的 message;controller.abort(reason) 传入的 reason 会作为取消原因保留在错误对象中。因此也可以写成:
js
if (error.code === 'ERR_CANCELED') {
// 主动取消
}关键在于把取消判断放在所有错误处理的最前面。如果先弹错误提示再判断取消,每次取消操作都会被误报成接口异常。
竞态控制:只保留最后一次请求
搜索场景是竞态控制最典型的例子:用户输入关键词触发请求,输入速度快于接口返回速度时,前一个请求的结果可能覆盖后一个请求的结果。用 AbortController 可以做到每次输入都取消上一次未完成的请求:
js
let searchController;
async function search(keyword) {
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
try {
const list = await http.get('/search', {
params: { q: keyword },
signal: searchController.signal,
});
renderList(list);
} catch (error) {
if (!axios.isCancel(error)) {
renderError(error);
}
}
}行为分析:旧请求的底层网络通信被中止,它的 Promise 进入 rejected 状态;catch 中 isCancel(error) 为 true,因此不会调用 renderList 或 renderError。只有最后一次请求的 signal 没有被 abort,它的成功回调才能更新页面。
如果使用 CancelToken,等效写法是每次搜索前调用上一次的 source.cancel():
js
let source;
async function search(keyword) {
if (source) {
source.cancel('丢弃旧请求');
}
source = axios.CancelToken.source();
try {
const list = await http.get('/search', {
params: { q: keyword },
cancelToken: source.token,
});
renderList(list);
} catch (error) {
if (!axios.isCancel(error)) {
renderError(error);
}
}
}两段代码的核心逻辑一样:进入新请求时先让旧请求以取消的方式结束,然后用 isCancel 屏蔽掉旧请求的错误。
组件卸载后的异步清理
在 React 组件中,请求结果返回之前组件可能已被卸载,继续 setState 会造成状态更新泄漏。常见做法是在卸载时取消请求:
js
import { useEffect, useState } from 'react';
function UserDetail({ userId }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
http
.get(`/users/${userId}`, { signal: controller.signal })
.then((user) => setUser(user))
.catch((error) => {
if (!axios.isCancel(error)) {
setError(error);
}
});
return () => controller.abort();
}, [userId]);
// 渲染逻辑略
}组件卸载时 controller.abort() 触发,请求的 Promise 以 CanceledError 结束,.catch 中 isCancel(error) 为 true,setError 不会执行。由于 Promise 链已经结束,链上游的 then 回调也不会执行,自然避免了卸载后的 setUser。
AbortController 本身没有需要手动清理的全局监听。controller 被引用它的作用域回收后即可被垃圾回收;Axios 在请求结束时也会从 signal 上移除自己的监听器。CancelToken 同理,token 上注册的回调在取消或请求完成后会被内部清理 [5]。
从 CancelToken 迁移到 AbortController
存量项目迁移时,改动点很集中:
js
// 迁移前
const source = axios.CancelToken.source();
http.get('/users', { cancelToken: source.token });
source.cancel('取消');
// 迁移后
const controller = new AbortController();
http.get('/users', { signal: controller.signal });
controller.abort('取消');在 Axios 1.x 中,两种方式在调用方用 axios.isCancel(error) 判断时表现一致。迁移过程中不需要改动调用方的 isCancel 分支逻辑,只需要替换创建和触发取消的代码。
需要留意的差异是:source.cancel('取消') 中的字符串会成为 CanceledError 的 message;而 AbortController.abort() 的入参在浏览器标准中会成为 signal 的 abort reason,Axios 会把它保留在错误对象中 [4]。如果业务代码依赖 error.message 做精确判断,迁移时可能需要同步调整断言逻辑。
另外,TypeScript 项目中 axios.isCancel 是一个类型守卫,它的类型收窄行为在 Axios 1.x 中曾有过修正 [8];在类型声明上不要把收窄后的类型与普通 AxiosError 混用。
小结与下一篇预告
这一篇围绕两条主线展开。拦截器部分覆盖了请求拦截器与响应拦截器的注册方式、认证信息注入、统一解包、错误处理,以及 Promise 链中的执行顺序和异常传播规则。取消机制部分对比了 CancelToken 与 AbortController 两种方式,并说明了取消后的错误识别、竞态控制和组件清理。
实际项目中,拦截器和取消机制通常被组合进一个统一的请求层:拦截器负责注入 token、处理业务码和登录态,取消机制负责处理搜索竞态、重复提交和组件卸载场景。这样的请求层如何设计,是下一篇的主题:Axios 项目实践:模块化封装与统一处理。
参考链接
- [1] https://github.com/reduxjs/redux-toolkit/issues/2147
- [2] https://www.npmjs.com/package/axios
- [3] https://github.com/axios/axios/blob/v1.x/lib/core/InterceptorManager.js
- [4] https://github.com/axios/axios/blob/v1.x/lib/cancel/CancelToken.js
- [5] https://github.com/axios/axios/blob/v1.x/lib/cancel/CanceledError.js
- [6] https://github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js
- [7] https://developer.mozilla.org/en-US/docs/Web/API/AbortController
- [8] https://github.com/axios/axios/issues/5153
