以下是为项目准备的完整文件代码,实现了自动取消重复请求 与组件销毁时取消请求 ,并通过 useRequest Hook 彻底简化组件中的使用。所有文件可直接复制使用。
文件结构
src
├── utils
│ └── request.js # Axios 封装(核心)
├── api
│ └── user.js # 业务 API 示例
├── composables
│ └── useRequest.js # Vue 3 组合式 Hook
├── hooks
│ └── useRequest.js # React Hook(可选)
└── router
└── index.js # 路由全局清理示例
1. Axios 封装 utils/request.js
javascript
// utils/request.js
import axios from 'axios';
// 创建 Axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 15000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
});
// ---------- 存储等待中的请求 ----------
// key: 请求唯一标识 (method + url + params + data)
// value: AbortController
const pendingMap = new Map();
/**
* 生成请求唯一标识
* 可根据业务自行调整,例如加入时间戳忽略、使用稳定序列化库
*/
function getRequestKey(config) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
/**
* 添加待处理请求,同时处理重复请求自动取消
*/
function addPending(config) {
// 若显式指定 cancelFlag: false,则跳过自动取消逻辑
if (config.cancelFlag === false) return;
const key = getRequestKey(config);
// 如果已有相同请求未完成,取消前一个
if (pendingMap.has(key)) {
pendingMap.get(key).abort();
}
// 创建内部 AbortController
const controller = new AbortController();
// 如果外部传入了 signal(例如组件级的 AbortController)
// 则监听外部取消,同步取消内部控制器
if (config.signal) {
config.signal.addEventListener(
'abort',
() => controller.abort(),
{ once: true } // 只监听一次
);
}
// 将请求的 signal 设置为内部控制器,实现双重控制
config.signal = controller.signal;
pendingMap.set(key, controller);
}
/**
* 移除已完成的请求
*/
function removePending(config) {
const key = getRequestKey(config);
if (pendingMap.has(key)) {
pendingMap.delete(key);
}
}
/**
* 清空所有等待中的请求(例如路由跳转时调用)
*/
export function clearAllPending() {
pendingMap.forEach(controller => {
controller.abort();
});
pendingMap.clear();
}
// ---------- 请求拦截器 ----------
service.interceptors.request.use(
config => {
// 示例:添加 token
// const token = getToken();
// if (token) config.headers.Authorization = `Bearer ${token}`;
// 处理重复请求取消
addPending(config);
return config;
},
error => Promise.reject(error)
);
// ---------- 响应拦截器 ----------
service.interceptors.response.use(
response => {
// 请求完成,移除 pending 记录
removePending(response.config);
// 可在此统一处理业务状态码
const res = response.data;
if (res.code !== 0 && res.code !== 200) {
return Promise.reject(new Error(res.message || 'Error'));
}
return res;
},
error => {
// 取消请求的错误不进行常规提示
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
return Promise.reject(error);
}
// 其他错误移除记录并统一处理
if (error.config) {
removePending(error.config);
}
// 这里可添加全局错误提示
return Promise.reject(error);
}
);
export default service;
2. 业务 API 模块 api/user.js
javascript
// api/user.js
import request from '@/utils/request';
/**
* 获取用户列表
* @param {Object} params 业务参数
* @param {Object} config 额外的 axios 配置(可传入 signal、cancelFlag 等)
* @returns Promise
*/
export function getList(params = {}, config = {}) {
return request.get('/user/list', { params, ...config });
}
/**
* 获取用户详情
* @param {Number} id 用户ID
* @param {Object} config 额外配置
* @returns Promise
*/
export function getUserDetail(id, config = {}) {
return request.get(`/user/${id}`, config);
}
/**
* 创建用户
*/
export function createUser(data, config = {}) {
return request.post('/user', data, config);
}
// 其他 API...
调用示例(组件中):
javascript
import { getList } from '@/api/user';
// 普通调用,不关心取消
const data = await getList({ page: 1 });
// 需要取消时,传入 signal
const controller = new AbortController();
const data = await getList({ page: 1 }, { signal: controller.signal });
3. Vue 3 组合式 Hook composables/useRequest.js
这个 Hook 自动管理 AbortController、loading 状态和组件卸载时取消,组件中无需再手动创建控制器。
javascript
// composables/useRequest.js
import { ref, onUnmounted } from 'vue';
import axios from 'axios';
/**
* 使用请求 Hook,自动处理取消、loading、错误
* @param {Function} apiFn API 函数(如 getList)
* @returns { run, loading, error }
*/
export function useRequest(apiFn) {
const loading = ref(false);
const error = ref(null);
let controller = null;
/**
* 执行请求,自动取消上一次未完成的请求
* @param {...any} args 业务参数(不包含 config),signal 会自动注入
* @returns {Promise} 响应数据
*/
const run = async (...args) => {
// 取消上一次请求(避免快速连续调用导致状态错乱)
if (controller) {
controller.abort();
}
controller = new AbortController();
loading.value = true;
error.value = null;
try {
// 将 signal 作为 config 对象的最后一个参数注入
// 要求 API 函数最后接受一个 config 对象
const res = await apiFn(...args, { signal: controller.signal });
return res;
} catch (err) {
if (!axios.isCancel(err)) {
error.value = err;
}
throw err;
} finally {
loading.value = false;
}
};
// 组件卸载时取消正在进行的请求
onUnmounted(() => {
if (controller) {
controller.abort();
}
});
return { run, loading, error };
}
使用示例(Vue 3 组件):
vue
<template>
<div v-if="loading">加载中...</div>
<div v-else>
<div v-for="item in list" :key="item.id">{{ item.name }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRequest } from '@/composables/useRequest';
import { getList } from '@/api/user';
const list = ref([]);
// 只需这一行,自动获得取消、loading、错误处理能力
const { run: fetchList, loading } = useRequest(getList);
// 业务调用,参数与 API 函数完全一致
const handleSearch = async (name) => {
try {
const res = await fetchList({ name });
list.value = res.data;
} catch (err) {
// 错误已在 hook 中处理,这里可按需添加业务逻辑
}
};
</script>
React 版本
hooks/useRequest.js可实现同样效果,见下方。
4. React Hook hooks/useRequest.js(可选)
javascript
// hooks/useRequest.js
import { useCallback, useRef, useEffect, useState } from 'react';
import axios from 'axios';
export function useRequest(apiFn) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const controllerRef = useRef(null);
const run = useCallback(async (...args) => {
// 取消上一次请求
if (controllerRef.current) {
controllerRef.current.abort();
}
const controller = new AbortController();
controllerRef.current = controller;
setLoading(true);
setError(null);
try {
const res = await apiFn(...args, { signal: controller.signal });
return res;
} catch (err) {
if (!axios.isCancel(err)) {
setError(err);
}
throw err;
} finally {
setLoading(false);
}
}, [apiFn]);
// 组件卸载时取消请求
useEffect(() => {
return () => {
if (controllerRef.current) {
controllerRef.current.abort();
}
};
}, []);
return { run, loading, error };
}
React 使用示例:
jsx
import React, { useState } from 'react';
import { useRequest } from '@/hooks/useRequest';
import { getList } from '@/api/user';
function UserList() {
const [list, setList] = useState([]);
const { run: fetchList, loading } = useRequest(getList);
const handleClick = async () => {
const res = await fetchList({ page: 1 });
setList(res.data);
};
return (
<div>
{loading ? <p>加载中...</p> : <button onClick={handleClick}>获取列表</button>}
</div>
);
}
5. 路由全局清理(可选,简化版)
如果不使用 useRequest,或希望页面跳转时彻底清理所有挂起请求,可在路由守卫中调用 clearAllPending。
javascript
// router/index.js
import { createRouter } from 'vue-router';
import { clearAllPending } from '@/utils/request';
const router = createRouter({ ... });
router.beforeEach((to, from, next) => {
// 每次路由切换前取消所有未完成的请求
clearAllPending();
next();
});
export default router;
关键设计说明
| 特性 | 实现方式 |
|---|---|
| 自动取消重复请求 | 请求拦截器根据 method+url+params+data 生成 key,若已存在则中止旧请求。 |
| 组件销毁取消请求 | 外部传入 signal(通过 useRequest 或手动创建),拦截器监听外部取消并同步中止内部控制器。 |
| Hook 一键集成 | useRequest 自动管理 AbortController 的生命周期,组件中只需调用 run(params),无需手动 new AbortController。 |
| 绕过自动取消 | 需要同时发出多个相同请求时,传入 cancelFlag: false。 |
| 全局清理 | 提供 clearAllPending(),可在路由守卫中使用。 |
以上所有文件代码均已补全,可直接整合到项目中使用。通过 useRequest Hook,组件销毁取消请求的样板代码被彻底消除,开发者只需关注业务参数。