Vue项目中判断相同请求的实现方案:从原理到实战

在前端开发中,判断两个请求是否为"相同请求"是高频需求,尤其在 防止重复提交取消重复请求请求防抖/节流 等场景中不可或缺。本文将结合 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 会因属性顺序不同导致误判,因此需要先 序列化(将对象转为有序字符串)。核心逻辑:

  1. 对对象参数按属性名排序;

  2. 将排序后的对象转为 JSON 字符串;

  3. 非对象类型(如字符串、数字)直接转为字符串。

二、完整实现:通用工具封装

首先封装核心工具函数,用于生成请求唯一标识和判断请求是否相同。推荐放在项目的 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 序列化的边界情况

如果参数中包含 undefinedfunction,JSON.stringify 会忽略这些属性,需提前处理(如将 undefined 转为 null)。

5.3 取消请求的业务场景

取消重复请求仅适用于「同一请求重复触发」的场景,如:

  • 用户快速点击按钮;

  • 页面切换时,前一页的请求未完成。

对于需要并行执行的相同请求(如批量操作),需关闭此拦截逻辑。

六、总结

Vue 项目中判断相同请求的核心流程的是:

  1. 提取请求的 method、url、params、data 核心属性;

  2. 对参数进行有序序列化,避免属性顺序导致的误判;

  3. 拼接生成唯一请求 Key,通过 Key 比对判断是否为相同请求;

  4. 结合 Axios 拦截器实现全局统一拦截,简化组件代码。

本文提供的方案兼容 Vue2 和 Vue3,可直接集成到实际项目中,有效解决重复请求问题,提升用户体验和接口稳定性。

相关推荐
why技术2 小时前
如果让我站在科技从业者的角度去回看 2025 年,让我选一个词出来形容它,我会选择“vibe coding”这个词。
前端·后端·程序员
worxfr2 小时前
CSS Flexbox 布局完全指南
前端·css
0思必得02 小时前
[Web自动化] JS基础语法与数据类型
前端·javascript·自动化·html·web自动化
xiaohe06012 小时前
📦 Uni ECharts 是如何使用定制 echarts 的?一篇文章轻松掌握!
vue.js·uni-app·echarts
Hy行者勇哥2 小时前
JavaScript性能优化实战:从入门到精通
开发语言·javascript·性能优化
Dreamcatcher_AC2 小时前
前端面试高频问题解析
前端·css·html
Irene19912 小时前
JavaScript 常见算法复杂度总结(大O表示法)
javascript·算法
damo王2 小时前
how to install npm in ubuntu24.04?
前端·npm·node.js
光影少年2 小时前
Vue 2 / Vue 3 diff算法
前端·javascript·vue.js