在Vue3项目中如何取消重复请求?

0 阅读7分钟

今天我们来聊聊一个实际开发中常见的问题:在Vue3项目中,如何取消重复的请求。

为什么需要取消重复请求?

这样做有几个明显的好处:

  1.  节省资源:释放浏览器连接,减轻服务器压力。
  2.  保证数据正确性:避免因请求响应顺序错乱导致页面显示旧数据。
  3.  提升用户体验:防止无效请求阻塞后续有效操作。

在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({
  timeout10000,
});

// 请求拦截器:在请求发出前,检查并取消重复请求
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({
  timeout10000,
});

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,我们可以用清晰的代码实现这一功能。希望本文介绍的方法和思路,能帮助你更好地处理项目中的网络请求问题。