在前端开发中,判断两个请求是否为"相同请求"是高频需求,尤其在 防止重复提交 、取消重复请求 、请求防抖/节流 等场景中不可或缺。本文将结合 Vue2/Vue3 实际项目,从核心原理出发,提供完整的实现方案和可直接复用的代码,帮你彻底解决这一问题。
一、核心原理:如何定义"相同请求"?
判断请求是否相同的核心是 提取请求的"唯一性标识"。一个请求的本质由多个核心属性决定,只有这些属性完全一致时,才能视为相同请求。
1.1 核心判断维度(4要素)
无论哪种场景,以下4个核心属性是判断的基础(需全部一致):
-
请求方法(Method):如 GET/POST/PUT/DELETE,大小写需兼容(GET 和 get 应视为相同);
-
请求URL:包含完整路径和查询参数(如 /api/user 和 /api/user?id=1 是不同请求);
-
查询参数(Params):URL 后的参数,需考虑对象属性顺序(如 {a:1,b:2} 和 {b:2,a:1} 应视为相同);
-
请求体(Data):仅针对 POST/PUT 等带请求体的方法,核心内容需一致。
1.2 关键优化:参数序列化
直接对比对象类型的 params/data 会因属性顺序不同导致误判,因此需要先 序列化(将对象转为有序字符串)。核心逻辑:
-
对对象参数按属性名排序;
-
将排序后的对象转为 JSON 字符串;
-
非对象类型(如字符串、数字)直接转为字符串。
二、完整实现:通用工具封装
首先封装核心工具函数,用于生成请求唯一标识和判断请求是否相同。推荐放在项目的 utils/requestKey.js 文件中,全局复用。
2.1 工具函数代码
javascript
/**
* 序列化参数:解决对象属性顺序不同导致的 Key 不一致问题
* @param {Object|Array} obj - 需要序列化的参数(params 或 data)
* @returns {String} 序列化后的字符串
*/
export function serializeParams(obj) {
// 非对象/数组直接返回空字符串或原值
if (!obj || typeof obj !== 'object') {
return obj === undefined || obj === null ? '' : String(obj);
}
// 数组直接序列化
if (Array.isArray(obj)) {
return JSON.stringify(obj);
}
// 对象:先按属性名排序,再序列化(核心:避免 {a:1,b:2} 和 {b:2,a:1} 判为不同参数)
return JSON.stringify(
Object.keys(obj)
.sort() // 按属性名升序排序
.reduce((acc, key) => {
acc[key] = obj[key];
return acc;
}, {})
);
}
/**
* 生成请求唯一 Key,用于判断两个请求是否相同
* @param {Object} requestConfig - 请求配置对象(包含 method/url/params/data)
* @returns {String} 请求唯一标识
*/
export function generateRequestKey(requestConfig) {
// 默认值处理(兼容 GET/POST 等不同请求方式)
const {
method = 'get', // 默认 GET 请求
url = '',
params = {}, // URL查询参数
data = {} // 请求体参数
} = requestConfig;
// 序列化参数
const paramsStr = serializeParams(params);
const dataStr = serializeParams(data);
// 拼接唯一 Key(转小写:避免 GET 和 get 被误判为不同请求)
return `${method.toLowerCase()}-${url}-${paramsStr}-${dataStr}`;
}
/**
* 判断两个请求是否为相同请求
* @param {Object} config1 - 第一个请求配置
* @param {Object} config2 - 第二个请求配置
* @returns {Boolean} true=相同请求,false=不同请求
*/
export function isSameRequest(config1, config2) {
const key1 = generateRequestKey(config1);
const key2 = generateRequestKey(config2);
return key1 === key2;
}
三、实战场景:Vue 组件中使用
以下分别提供 Vue3(组合式 API)和 Vue2(选项式 API)的组件内使用示例,核心场景为「防止重复点击触发重复请求」。
3.1 Vue3 示例(<script setup>)
javascript
<template>
<div class="request-demo">
<el-button @click="fetchUser(1)" type="primary">查询用户1(重复点击测试)</el-button>
<el-button @click="fetchUser(2)" type="success" style="margin-left: 10px;">查询用户2(不同请求)</el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import axios from 'axios';
import { isSameRequest, generateRequestKey } from '@/utils/requestKey';
// 存储当前正在进行的请求:Key=请求唯一标识,Value=取消请求的方法
const pendingRequests = ref(new Map());
/**
* 模拟用户查询请求
* @param {Number} userId - 用户ID
*/
const fetchUser = async (userId) => {
// 1. 构造请求配置(和 Axios 配置格式一致)
const requestConfig = {
method: 'get',
url: '/api/user',
params: { id: userId }, // 查询参数
};
// 2. 生成请求唯一 Key,判断是否存在相同的 pending 请求
const requestKey = generateRequestKey(requestConfig);
const hasSamePending = pendingRequests.value.has(requestKey);
if (hasSamePending) {
console.log(`[拦截重复请求] ${requestKey} 已在请求中,取消当前重复请求`);
// 取消前一个重复请求(可选,根据业务需求)
pendingRequests.value.get(requestKey)('取消重复请求');
pendingRequests.value.delete(requestKey);
return;
}
// 3. 创建 Axios CancelToken(用于取消请求)
const source = axios.CancelToken.source();
pendingRequests.value.set(requestKey, source.cancel);
try {
// 发起请求
const res = await axios({
...requestConfig,
cancelToken: source.token, // 绑定取消令牌
});
console.log('请求成功:', res.data);
return res.data;
} catch (err) {
if (axios.isCancel(err)) {
console.log('请求被取消:', err.message);
} else {
console.error('请求失败:', err);
}
} finally {
// 4. 请求完成(成功/失败)后,移除 pending 状态
pendingRequests.value.delete(requestKey);
}
// 测试:手动判断两个请求是否相同
const config1 = { method: 'get', url: '/api/user', params: { id: 1 } };
const config2 = { method: 'GET', url: '/api/user', params: { id: 1 } };
const config3 = { method: 'get', url: '/api/user', params: { id: 2 } };
console.log('config1 和 config2 是否相同:', isSameRequest(config1, config2)); // true
console.log('config1 和 config3 是否相同:', isSameRequest(config1, config3)); // false
};
</script>
<style scoped>
.request-demo {
padding: 20px;
}
</style>
3.2 Vue2 示例(选项式 API)
javascript
<template>
<div class="request-demo">
<el-button @click="fetchUser(1)" type="primary">查询用户1(重复点击测试)</el-button>
</div>
</template>
<script>
import axios from 'axios';
import { isSameRequest, generateRequestKey } from '@/utils/requestKey';
export default {
name: 'RequestDemo',
data() {
return {
// 存储 pending 请求的 Key 和取消方法
pendingRequests: new Map()
};
},
methods: {
async fetchUser(userId) {
// 1. 构造请求配置
const requestConfig = {
method: 'get',
url: '/api/user',
params: { id: userId }
};
// 2. 判断是否存在相同请求
const requestKey = generateRequestKey(requestConfig);
if (this.pendingRequests.has(requestKey)) {
console.log('拦截重复请求:', requestKey);
this.pendingRequests.get(requestKey)('取消重复请求');
this.pendingRequests.delete(requestKey);
return;
}
// 3. 创建 CancelToken
const source = axios.CancelToken.source();
this.pendingRequests.set(requestKey, source.cancel);
try {
const res = await axios({
...requestConfig,
cancelToken: source.token
});
console.log('请求成功:', res.data);
} catch (err) {
if (axios.isCancel(err)) {
console.log('请求被取消:', err.message);
} else {
console.error('请求失败:', err);
}
} finally {
this.pendingRequests.delete(requestKey);
}
// 测试判断逻辑
const config1 = { method: 'post', url: '/api/user', data: { name: '张三' } };
const config2 = { method: 'POST', url: '/api/user', data: { name: '张三' } };
console.log('config1 和 config2 是否相同:', isSameRequest(config1, config2)); // true
}
}
};
</script>
四、进阶:全局统一拦截(Axios 拦截器)
在实际项目中,推荐将判断逻辑集成到 Axios 拦截器中,实现全局统一拦截重复请求,无需在每个组件中单独处理。
4.1 全局 Axios 封装(utils/request.js)
javascript
import axios from 'axios';
import { generateRequestKey } from './requestKey';
// 创建 Axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // Vue3 Vite 环境变量
// baseURL: process.env.VUE_APP_API_BASE_URL, // Vue2 环境变量(webpack 构建)
timeout: 5000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
// 存储当前 pending 的请求:Key=请求唯一标识,Value=取消方法
const pendingRequests = new Map();
// 请求拦截器:判断并取消重复请求
service.interceptors.request.use(
(config) => {
// 生成请求唯一 Key
const requestKey = generateRequestKey(config);
// 存在相同请求则取消前一个
if (pendingRequests.has(requestKey)) {
pendingRequests.get(requestKey)('取消重复请求');
pendingRequests.delete(requestKey);
}
// 存储当前请求的取消方法
const source = axios.CancelToken.source();
config.cancelToken = source.token;
pendingRequests.set(requestKey, source.cancel);
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器:请求完成后移除 pending 状态
service.interceptors.response.use(
(response) => {
// 移除 pending 状态
const requestKey = generateRequestKey(response.config);
pendingRequests.delete(requestKey);
// 统一处理响应数据(根据业务需求调整)
return response.data;
},
(error) => {
// 处理取消请求的错误
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message);
return Promise.reject(new Error('请求被取消'));
}
// 非取消错误,移除 pending 状态
const requestKey = generateRequestKey(error.config || {});
pendingRequests.delete(requestKey);
// 统一处理其他错误(如网络错误、接口错误)
console.error('请求错误:', error);
return Promise.reject(error);
}
);
export default service;
4.2 组件中直接使用封装后的 Axios
javascript
<script setup>
import request from '@/utils/request';
// 直接调用,无需手动判断重复请求
const fetchUser = async (userId) => {
try {
const res = await request({
method: 'get',
url: '/api/user',
params: { id: userId }
});
console.log('请求成功:', res);
} catch (err) {
console.error('请求失败:', err);
}
};
</script>
五、关键注意事项
5.1 兼容不同请求类型
-
GET 请求:核心比对
method + url + params; -
POST/PUT 请求:核心比对
method + url + data; -
如果有特殊需求(如忽略某些参数),可修改
generateRequestKey函数,过滤无需比对的参数(如 timestamp 防缓存参数)。
5.2 序列化的边界情况
如果参数中包含 undefined 或 function,JSON.stringify 会忽略这些属性,需提前处理(如将 undefined 转为 null)。
5.3 取消请求的业务场景
取消重复请求仅适用于「同一请求重复触发」的场景,如:
-
用户快速点击按钮;
-
页面切换时,前一页的请求未完成。
对于需要并行执行的相同请求(如批量操作),需关闭此拦截逻辑。
六、总结
Vue 项目中判断相同请求的核心流程的是:
-
提取请求的 method、url、params、data 核心属性;
-
对参数进行有序序列化,避免属性顺序导致的误判;
-
拼接生成唯一请求 Key,通过 Key 比对判断是否为相同请求;
-
结合 Axios 拦截器实现全局统一拦截,简化组件代码。
本文提供的方案兼容 Vue2 和 Vue3,可直接集成到实际项目中,有效解决重复请求问题,提升用户体验和接口稳定性。