今天我们来聊聊一个实际开发中常见的问题:在Vue3项目中,如何取消重复的请求。
为什么需要取消重复请求?
这样做有几个明显的好处:
- 节省资源:释放浏览器连接,减轻服务器压力。
- 保证数据正确性:避免因请求响应顺序错乱导致页面显示旧数据。
- 提升用户体验:防止无效请求阻塞后续有效操作。
在Vue3的生态中,我们最常用的HTTP客户端是 axios。因此,本文的核心将围绕如何使用 axios 的取消令牌(CancelToken)及其更现代的替代品——AbortController——来实现请求取消。
方案一:使用 Axios 的 CancelToken
axios 从很早就支持通过 CancelToken 来取消请求。虽然它在 axios v0.22.0 之后被标记为已弃用,转而推荐使用标准的 AbortController,但很多现有项目仍在使用这个API,所以我们有必要了解一下。
它的工作原理是:创建一个“取消令牌”的源(source),将这个令牌配置到请求中。当我们需要取消请求时,调用这个源的 cancel 方法。
基本使用示例
import axios from 'axios';
// 1. 创建一个 CancelToken Source
const source = axios.CancelToken.source();
// 2. 发起请求,并配置 cancelToken
axios.get('/api/user/123', {
cancelToken: source.token
}).then(response => {
console.log(response.data);
}).catch(function (thrown) {
// 判断错误是否是因为请求被取消
if (axios.isCancel(thrown)) {
console.log('请求被取消:', thrown.message);
} else {
// 处理其他错误
}
});
// 3. 在需要的时候取消请求
source.cancel('操作被用户取消');
在Vue3组件中管理重复请求
在实际项目中,我们通常需要一种机制来管理“同一个”重复请求。一个常见的思路是:将每个请求的唯一标识(例如:请求方法+URL)和一个取消函数关联起来,在发起新请求前,检查并取消旧的、未完成的相同请求。
我们可以利用Vue3的响应式系统和组合式API,封装一个可复用的逻辑。
首先,我们创建一个用于存储所有进行中请求的Map,以及相关的操作函数。
// utils/request.js
import axios from 'axios';
// 存储所有进行中请求的Map
const pendingRequestMap = new Map();
/**
* 生成请求的唯一标识键
* @param {*} config axios的请求配置对象
* @returns {string} 唯一键
*/
function generateReqKey(config) {
const { method, url, params, data } = config;
// 简单拼接,可根据业务复杂化(如对data排序)
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
/**
* 添加请求到等待队列
* @param {*} config axios的请求配置对象
*/
function addPendingRequest(config) {
const requestKey = generateReqKey(config);
// 为这个请求创建一个取消令牌源
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
// 如果Map中还没有这个key,则添加进去
if (!pendingRequestMap.has(requestKey)) {
pendingRequestMap.set(requestKey, cancel);
}
});
}
/**
* 移除等待队列中的请求
* @param {*} config axios的请求配置对象
*/
function removePendingRequest(config) {
const requestKey = generateReqKey(config);
if (pendingRequestMap.has(requestKey)) {
// 如果在Map中存在这个请求,说明它还未完成,将其取消并从Map中移除
const cancel = pendingRequestMap.get(requestKey);
cancel(requestKey); // 取消请求,可以传递消息
pendingRequestMap.delete(requestKey);
}
}
// 创建axios实例
const service = axios.create({
timeout: 10000,
});
// 请求拦截器:在请求发出前,检查并取消重复请求
service.interceptors.request.use(
(config) => {
// 检查并取消之前的相同请求
removePendingRequest(config);
// 将当前请求添加到等待队列
addPendingRequest(config);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器:请求完成后(无论成功失败),将其从等待队列中移除
service.interceptors.response.use(
(response) => {
removePendingRequest(response.config);
return response;
},
(error) => {
// 如果错误是因为取消请求造成的,我们选择忽略这个错误,不抛出到业务层
if (axios.isCancel(error)) {
console.log('已取消的重复请求:', error.message);
return new Promise(() => {}); // 返回一个“永远pending”的Promise,中断Promise链
}
// 对于其他错误,移除请求并正常抛出
removePendingRequest(error.config || {});
return Promise.reject(error);
}
);
export default service;
然后,在Vue组件中,你可以直接使用这个封装好的 service 来发起请求。它会自动处理重复请求的取消。
<script setup>
import { ref } from 'vue';
import request from '@/utils/request';
const searchResults = ref([]);
const loading = ref(false);
const handleSearch = async (keyword) => {
loading.value = true;
try {
const response = await request.get('/api/search', {
params: { keyword }
});
searchResults.value = response.data;
} catch (error) {
// 这里不会捕获到因重复请求被取消而抛出的错误,因为我们在拦截器中已经处理了
console.error('搜索失败:', error);
} finally {
loading.value = false;
}
};
</script>
注意:
CancelToken方式在axios新版本中已被标记为弃用。对于新项目,建议直接使用下面介绍的更现代的AbortController方案。
方案二: AbortController
AbortController 是一个现代的Web API,它提供了一种更通用、更标准的方式来中止一个或多个Web请求。fetch API 和新的 axios 版本都原生支持它。使用 AbortController 是当前推荐的做法。
基本概念
- •
AbortController:控制器对象,用于触发中止信号。 - •
AbortSignal:信号对象,关联到具体的请求上。控制器可以通过它来通知请求“需要中止”。
基本使用示例
// 1. 创建一个 AbortController 实例
const controller = new AbortController();
const signal = controller.signal; // 获取它的 signal
// 2. 发起 fetch 请求,并将 signal 关联上去
fetch('/api/some-data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
// 如果错误是因为请求被中止
if (err.name === 'AbortError') {
console.log('Fetch 请求被中止');
} else {
console.error('其他错误:', err);
}
});
// 3. 在需要的时候中止请求
controller.abort(); // 这会触发 signal 的中止事件,从而取消请求
在 Axios 中使用 AbortController
从 axios v0.22.0 开始,你可以使用 signal 属性来配置 AbortSignal。
import axios from 'axios';
const controller = new AbortController();
axios.get('/api/user/123', {
signal: controller.signal // 将 signal 传递给请求配置
}).then(response => {
console.log(response.data);
}).catch(function (error) {
// 判断错误是否是因为请求被取消
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
} else {
// 处理其他错误
}
});
// 取消请求
controller.abort();
封装基于 AbortController 的重复请求取消
我们可以用类似的思路,用 AbortController 重构之前的工具函数。主要变化是将存储的 cancel 函数替换为 AbortController 实例。
// utils/request-abort.js
import axios from 'axios';
const pendingRequestMap = new Map();
function generateReqKey(config) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
function addPendingRequest(config) {
const requestKey = generateReqKey(config);
// 如果已有相同请求在进行,则中止它
if (pendingRequestMap.has(requestKey)) {
const oldController = pendingRequestMap.get(requestKey);
oldController.abort(); // 中止旧的请求
pendingRequestMap.delete(requestKey);
}
// 为当前请求创建新的控制器并存储
const controller = new AbortController();
config.signal = controller.signal; // 关键:将 signal 赋给请求配置
pendingRequestMap.set(requestKey, controller);
}
function removePendingRequest(config) {
const requestKey = generateReqKey(config);
if (pendingRequestMap.has(requestKey)) {
// 请求完成,直接从Map中移除即可,不需要手动abort
pendingRequestMap.delete(requestKey);
}
}
const service = axios.create({
timeout: 10000,
});
service.interceptors.request.use(
(config) => {
removePendingRequest(config); // 先移除可能存在的旧记录(清理作用)
addPendingRequest(config); // 添加新请求
return config;
},
(error) => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
(response) => {
removePendingRequest(response.config);
return response;
},
(error) => {
// 判断错误是否由取消请求导致
if (axios.isCancel(error)) {
console.log('已取消的重复请求:', error.message);
return new Promise(() => {}); // 中断Promise链
}
removePendingRequest(error.config || {});
return Promise.reject(error);
}
);
export default service;
这个版本的逻辑更清晰:在添加新请求时,直接中止旧的相同请求。响应拦截器里只需要做清理工作。
进阶与优化
上面的方案解决了核心问题,但在实际项目中,我们可能还需要考虑更多细节。
1. 白名单控制
有些请求我们可能不希望被自动取消。例如,一个轮询定时请求,或者多个并行的不同数据请求。我们可以通过给请求配置添加一个自定义标记(如 allowRepeat: true)来实现白名单。
// 在拦截器中
service.interceptors.request.use(
(config) => {
// 如果配置了允许重复,则跳过取消逻辑
if (config.allowRepeat) {
return config;
}
removePendingRequest(config);
addPendingRequest(config);
return config;
}
);
// 在组件中使用
request.get('/api/polling', { allowRepeat: true });
2. 与 Vue Router 导航守卫结合
当用户切换页面时,我们通常希望取消所有未完成的请求,以免它们在后台继续运行并可能更新一个已经销毁的组件状态。这可以在Vue Router的全局前置守卫中实现。
// router/index.js
import router from './router';
import { pendingRequestMap } from '@/utils/request-abort'; // 需要将map导出
router.beforeEach((to, from, next) => {
// 遍历并中止所有进行中的请求
pendingRequestMap.forEach((controller, key) => {
controller.abort(`路由跳转至 ${to.path}`);
});
// 清空Map
pendingRequestMap.clear();
next();
});
3. 使用 Composition API 封装
在Vue3中,我们可以利用组合式API,将取消逻辑封装成一个更优雅、可复用的 useRequest Hook。
// composables/useRequest.js
import { ref, onUnmounted } from 'vue';
import request from '@/utils/request-abort';
export function useRequest() {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
let abortController = null;
const execute = async (config) => {
loading.value = true;
error.value = null;
// 为这次执行创建一个独立的控制器(可选,如果全局已管理则可省略)
abortController = new AbortController();
try {
const response = await request({
...config,
signal: abortController.signal
});
data.value = response.data;
return response;
} catch (err) {
if (!axios.isCancel(err)) {
error.value = err;
}
throw err;
} finally {
loading.value = false;
}
};
// 组件卸载时,取消由这个Hook发起的请求
onUnmounted(() => {
if (abortController) {
abortController.abort();
}
});
// 提供一个手动取消的方法
const cancel = () => {
if (abortController) {
abortController.abort();
}
};
return {
data,
error,
loading,
execute,
cancel
};
}
在组件中使用:
<script setup>
import { useRequest } from '@/composables/useRequest';
const { data, loading, execute } = useRequest();
const handleSubmit = () => {
execute({
method: 'post',
url: '/api/submit',
data: { /* ... */ }
});
};
</script>
取消重复请求是一个看似简单,但能显著提升应用健壮性的优化点。在Vue3项目中,结合 axios 和 AbortController,我们可以用清晰的代码实现这一功能。希望本文介绍的方法和思路,能帮助你更好地处理项目中的网络请求问题。