Axios 请求取消

以下是为项目准备的完整文件代码,实现了自动取消重复请求组件销毁时取消请求 ,并通过 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,组件销毁取消请求的样板代码被彻底消除,开发者只需关注业务参数。

相关推荐
李白你好1 小时前
DesJsFinder被动JS分析 + 框架识别 + 主动Fuzz + 响应指纹 — 红队API挖掘利器
javascript
IT_陈寒2 小时前
Redis客户端连接池不关闭的后果,程序直接崩给我看
前端·人工智能·后端
怕浪猫2 小时前
Electron 开发实战(九):调试技巧与开发者工具|测试、性能分析、日志追踪全解
前端·javascript·electron
喜欢踢足球的老罗2 小时前
产品方案:从已有 CRM AI 系统切入 WhatsApp Chrome 插件赛道
前端·人工智能·chrome
无心使然2 小时前
OpenLayers 10.9.0 渲染架构分析
前端·gis·数据可视化
智能制造产品经理代码提升2 小时前
ES6+ 标准使用手册
前端·javascript·es6
xiaofeichaichai2 小时前
ES6+ 模块
前端·ecmascript·es6
xuankuxiaoyao2 小时前
阶段案例——后台管理系统
java·linux·前端
恋猫de小郭2 小时前
Android 17 内存管理将严格管控,App 要注意适配
android·前端·flutter