axios取消错误:CanceledError取消异常

axios取消错误:CanceledError取消异常

【免费下载链接】axios axios/axios: Axios 是一个基于Promise的HTTP客户端库,适用于浏览器和Node.js环境,用于在JavaScript应用中执行异步HTTP请求。相较于原生的XMLHttpRequest或Fetch API,Axios提供了更简洁的API和更强大的功能。 【免费下载链接】axios 项目地址: https://gitcode.com/GitHub_Trending/ax/axios

在使用axios进行异步HTTP请求时,取消请求是一个常见需求,比如用户导航离开当前页面或发起新的请求时需要取消之前的请求。此时,axios会抛出CanceledError取消异常。本文将深入探讨CanceledError的产生机制、处理方法及最佳实践,帮助开发者更好地理解和应对这一常见错误。

CanceledError的定义与源码解析

CanceledError是axios中用于表示请求被取消的错误类型,它继承自AxiosError,并添加了特定的标识属性。

源码定义

CanceledError的源码位于lib/cancel/CanceledError.js

import AxiosError from '../core/AxiosError.js';
import utils from '../utils.js';

/**
 * A `CanceledError` is an object that is thrown when an operation is canceled.
 *
 * @param {string=} message The message.
 * @param {Object=} config The config.
 * @param {Object=} request The request.
 *
 * @returns {CanceledError} The created error.
 */
function CanceledError(message, config, request) {
  // eslint-disable-next-line no-eq-null,eqeqeq
  AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request);
  this.name = 'CanceledError';
}

utils.inherits(CanceledError, AxiosError, {
  __CANCEL__: true
});

export default CanceledError;

从源码可以看出,CanceledError具有以下特点:

  • 继承自AxiosError类
  • 错误代码为AxiosError.ERR_CANCELED
  • 名称为'CanceledError'
  • 具有__CANCEL__: true的标识属性,用于判断错误类型

错误判断工具

axios提供了一个工具函数isCancel来判断一个错误是否为取消错误,源码位于lib/cancel/isCancel.js

export default function isCancel(value) {
  return !!(value && value.__CANCEL__);
}

这个简单的函数通过检查对象是否具有__CANCEL__属性来判断是否为取消错误。

取消机制详解

axios提供了两种主要方式来取消请求并触发CanceledError:CancelToken和AbortController。

CancelToken方式

CancelToken是axios早期版本引入的取消机制,通过创建取消令牌来实现请求取消。其源码位于lib/cancel/CancelToken.js

基本用法
// 创建取消令牌
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 发起请求时关联取消令牌
axios.get('/api/data', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('请求已取消:', thrown.message);
  } else {
    // 处理其他错误
  }
});

// 取消请求(参数为可选的取消消息)
source.cancel('用户取消了请求');
源码解析

CancelToken的核心实现是基于Promise:

class CancelToken {
  constructor(executor) {
    // ...省略部分代码...
    
    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });
    
    // ...省略部分代码...
    
    executor(function cancel(message, config, request) {
      if (token.reason) {
        // 已经取消过了
        return;
      }
      
      token.reason = new CanceledError(message, config, request);
      resolvePromise(token.reason);
    });
  }
  
  // ...省略其他方法...
  
  /**
   * 静态方法,返回一个包含token和cancel函数的对象
   */
  static source() {
    let cancel;
    const token = new CancelToken(function executor(c) {
      cancel = c;
    });
    return {
      token,
      cancel
    };
  }
}

当调用cancel方法时,会创建一个CanceledError实例并通过resolvePromise触发promise的解决,从而取消请求并抛出CanceledError。

AbortController方式

AbortController是现代浏览器提供的标准API,用于取消一个或多个DOM请求。axios从0.22.0版本开始支持AbortController。

基本用法
// 创建AbortController实例
const controller = new AbortController();

// 发起请求时关联signal
axios.get('/api/data', {
  signal: controller.signal
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('请求已取消:', thrown.message);
  } else {
    // 处理其他错误
  }
});

// 取消请求
controller.abort();

两种取消方式对比

特性CancelTokenAbortController
标准性axios特有,非标准浏览器标准API
创建方式CancelToken.source()new AbortController()
取消触发source.cancel()controller.abort()
信号传递通过token属性通过signal属性
取消消息支持自定义消息不直接支持,需额外实现
多请求取消需多个token可同一signal关联多个请求
兼容性所有axios版本现代浏览器和Node.js 15+
推荐程度不推荐使用(已过时)推荐使用(标准方案)

CancelToken转AbortSignal

为了平滑过渡到标准API,axios在CancelToken上提供了toAbortSignal()方法:

const source = CancelToken.source();
const signal = source.token.toAbortSignal();

// 可以将signal用于其他支持AbortSignal的API
fetch('/api/data', { signal })
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求已取消');
    }
  });

// 通过CancelToken的cancel方法取消请求
source.cancel();

CanceledError触发流程

当请求被取消时,CanceledError的触发流程如下:

mermaid

从源码角度看,在lib/core/dispatchRequest.js中定义了请求调度的核心逻辑:

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }

  if (config.signal && config.signal.aborted) {
    throw new CanceledError(null, config);
  }
}

export default function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  
  // ...省略其他代码...
  
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // ...处理响应...
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      // ...处理错误...
    }
    return Promise.reject(reason);
  });
}

throwIfCancellationRequested函数会在请求发起前和响应处理前检查取消状态,如果已取消则抛出CanceledError。

错误捕获与处理

正确捕获和处理CanceledError是确保应用健壮性的重要部分。

基本错误捕获

// 使用try/catch捕获取消错误
async function fetchData() {
  const controller = new AbortController();
  
  try {
    const response = await axios.get('/api/data', {
      signal: controller.signal
    });
    return response.data;
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('请求已取消:', error.message);
      // 可以在这里进行取消后的清理工作
      return null; // 或其他合适的默认值
    }
    // 处理其他类型的错误
    console.error('请求失败:', error.message);
    throw error; // 重新抛出非取消错误
  }
}

在拦截器中处理取消错误

可以在axios拦截器中统一处理CanceledError:

// 请求拦截器
axios.interceptors.request.use(
  config => {
    // 在请求发送前的处理
    return config;
  },
  error => {
    if (axios.isCancel(error)) {
      console.log('请求拦截器捕获到取消错误:', error.message);
    }
    return Promise.reject(error);
  }
);

// 响应拦截器
axios.interceptors.response.use(
  response => response,
  error => {
    if (axios.isCancel(error)) {
      console.log('响应拦截器捕获到取消错误:', error.message);
      // 可以在这里统一处理取消后的逻辑
    }
    return Promise.reject(error);
  }
);

在React组件中处理取消错误

在React组件中使用axios时,通常需要在组件卸载时取消未完成的请求:

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function DataFetchingComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    const controller = new AbortController();
    
    const fetchData = async () => {
      try {
        const response = await axios.get('/api/data', {
          signal: controller.signal
        });
        setData(response.data);
      } catch (error) {
        if (!axios.isCancel(error)) {
          console.error('请求失败:', error);
        }
      }
    };
    
    fetchData();
    
    // 组件卸载时取消请求
    return () => {
      controller.abort();
    };
  }, []);
  
  return (
    <div>
      {data ? <div>{JSON.stringify(data)}</div> : <div>加载中...</div>}
    </div>
  );
}

实际应用场景与示例

1. 用户操作取消

实现一个搜索框,当用户输入新内容时取消之前的搜索请求:

// HTML: <input type="text" id="search-input" />

const input = document.getElementById('search-input');
let controller = null;

input.addEventListener('input', debounce(async (e) => {
  // 如果有正在进行的请求,取消它
  if (controller) {
    controller.abort();
  }
  
  // 创建新的AbortController
  controller = new AbortController();
  
  try {
    const response = await axios.get(`/api/search?q=${e.target.value}`, {
      signal: controller.signal
    });
    
    // 处理搜索结果
    renderResults(response.data);
  } catch (error) {
    if (!axios.isCancel(error)) {
      console.error('搜索失败:', error);
      showError('搜索失败,请重试');
    }
  } finally {
    // 如果请求完成或取消,重置controller
    if (controller.signal.aborted) {
      controller = null;
    }
  }
}, 300));

// 简单的防抖函数实现
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

2. 超时自动取消

结合axios的timeout配置实现超时自动取消:

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const { signal } = controller;
  
  // 设置超时取消
  const timeoutId = setTimeout(() => {
    controller.abort();
  }, timeout);
  
  try {
    const response = await axios({
      url,
      ...options,
      signal
    });
    
    clearTimeout(timeoutId); // 请求成功,清除超时
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (axios.isCancel(error)) {
      // 判断是用户取消还是超时取消
      const errorMessage = error.message || '请求超时';
      throw new Error(`请求已取消: ${errorMessage}`);
    }
    
    throw error;
  }
}

// 使用示例
fetchWithTimeout('/api/data', { method: 'GET' }, 3000)
  .then(response => console.log(response.data))
  .catch(error => console.error(error.message));

3. 取消多个并发请求

同时取消多个并发请求:

// 使用AbortController取消多个请求
async function fetchMultipleResources() {
  const controller = new AbortController();
  const { signal } = controller;
  
  try {
    // 多个请求共享同一个signal
    const requests = [
      axios.get('/api/data1', { signal }),
      axios.get('/api/data2', { signal }),
      axios.get('/api/data3', { signal })
    ];
    
    // 同时取消的按钮点击处理
    document.getElementById('cancel-btn').addEventListener('click', () => {
      controller.abort();
    });
    
    // 等待所有请求完成
    const results = await Promise.all(requests);
    return results.map(res => res.data);
  } catch (error) {
    if (axios.isCancel(error)) {
      console.log('多个请求已被取消');
      // 可以在这里处理取消后的逻辑
      return [];
    }
    console.error('请求出错:', error);
    throw error;
  }
}

4. React Hooks封装

封装一个带取消功能的自定义Hook:

import { useRef, useState, useCallback } from 'react';
import axios from 'axios';

function useAxiosWithCancel() {
  const cancelControllers = useRef(new Map());
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const axiosWithCancel = useCallback(async (config, requestId = 'default') => {
    // 取消同一ID的现有请求
    if (cancelControllers.current.has(requestId)) {
      const controller = cancelControllers.current.get(requestId);
      controller.abort();
    }
    
    const controller = new AbortController();
    cancelControllers.current.set(requestId, controller);
    
    setLoading(true);
    setError(null);
    
    try {
      const response = await axios({
        ...config,
        signal: controller.signal
      });
      
      return response.data;
    } catch (err) {
      if (!axios.isCancel(err)) {
        setError(err);
        throw err;
      }
      // 取消错误不向上抛出,由组件决定是否处理
      return null;
    } finally {
      setLoading(false);
      cancelControllers.current.delete(requestId);
    }
  }, []);
  
  // 取消所有请求的方法
  const cancelAllRequests = useCallback(() => {
    cancelControllers.current.forEach(controller => {
      controller.abort();
    });
    cancelControllers.current.clear();
  }, []);
  
  return { axiosWithCancel, loading, error, cancelAllRequests };
}

// 组件中使用
function DataComponent() {
  const { axiosWithCancel, loading, error, cancelAllRequests } = useAxiosWithCancel();
  
  const fetchData = async () => {
    try {
      const data = await axiosWithCancel({
        url: '/api/data',
        method: 'GET'
      }, 'data-request');
      
      if (data) {
        // 处理数据
        console.log(data);
      }
    } catch (err) {
      console.error('请求失败:', err);
    }
  };
  
  useEffect(() => {
    return () => {
      cancelAllRequests();
    };
  }, [cancelAllRequests]);
  
  return (
    <div>
      <button onClick={fetchData} disabled={loading}>
        {loading ? '加载中...' : '获取数据'}
      </button>
      {error && <div className="error">错误: {error.message}</div>}
    </div>
  );
}

最佳实践与常见问题

避免内存泄漏

取消请求不仅是为了避免无用的网络请求,也是为了防止内存泄漏:

// 不好的实践:未清理的订阅可能导致内存泄漏
function badPractice() {
  const source = axios.CancelToken.source();
  
  axios.get('/api/data', { cancelToken: source.token })
    .then(response => {
      // 处理响应
      updateUI(response.data);
    });
  
  // 没有在组件卸载或适当时候调用source.cancel()
}

// 好的实践:组件卸载时取消请求
function goodPractice() {
  const controller = new AbortController();
  
  axios.get('/api/data', { signal: controller.signal })
    .then(response => {
      updateUI(response.data);
    })
    .catch(error => {
      if (!axios.isCancel(error)) {
        handleError(error);
      }
    });
  
  // 在组件卸载或不需要结果时调用
  return () => {
    controller.abort();
  };
}

区分不同类型的取消错误

在复杂应用中,可能需要区分不同原因的取消:

// 自定义取消原因枚举
const CancelReason = {
  USER_ACTION: 'user_action',
  NAVIGATION: 'navigation',
  TIMEOUT: 'timeout',
  DUPLICATE: 'duplicate'
};

// 扩展取消方法,包含原因
function cancelWithReason(controller, reason, message) {
  // 使用AbortSignal的reason属性传递额外信息
  controller.abort({
    type: 'cancel',
    reason,
    message: message || '请求已取消'
  });
}

// 使用示例
const controller = new AbortController();

axios.get('/api/data', { 
  signal: controller.signal,
  cancelReason: CancelReason.DUPLICATE // 自定义属性传递原因
})
.catch(error => {
  if (axios.isCancel(error)) {
    // 从signal的reason中获取详细信息
    const cancelDetails = controller.signal.reason;
    
    switch (cancelDetails.reason) {
      case CancelReason.USER_ACTION:
        showNotification('操作已取消');
        break;
      case CancelReason.TIMEOUT:
        showError('请求超时,请重试');
        break;
      case CancelReason.DUPLICATE:
        console.log('重复请求已取消');
        break;
      default:
        showError('请求已取消');
    }
  }
});

// 取消请求并指定原因
cancelWithReason(controller, CancelReason.DUPLICATE, '检测到重复请求');

常见问题与解决方案

问题解决方案
取消后仍收到响应确保在then/catch中检查请求状态
重复取消导致错误使用标志位或try/finally确保只取消一次
内存泄漏在组件卸载或不需要结果时取消请求
无法区分取消类型自定义取消原因并在错误中传递
旧代码兼容性使用CancelToken.toAbortSignal()桥接

总结

CanceledError是axios中处理请求取消的核心机制,通过本文的深入解析,我们了解了:

  1. CanceledError的源码实现与基本特性
  2. 两种取消请求的方式:CancelToken(已过时)和AbortController(推荐)
  3. CanceledError的触发流程与捕获处理方法
  4. 多种实际应用场景与代码示例
  5. 最佳实践与常见问题解决方案

在实际开发中,推荐使用标准的AbortController API来处理请求取消,它不仅是浏览器标准,还能与其他支持AbortSignal的API(如fetch、Streams等)兼容。同时,良好的取消错误处理机制能够提升应用的健壮性和用户体验,特别是在用户频繁操作或网络不稳定的情况下。

正确使用和处理CanceledError,能够让我们的应用更加健壮、响应更快,同时避免不必要的资源浪费和潜在的内存泄漏问题。

参考资料

【免费下载链接】axios axios/axios: Axios 是一个基于Promise的HTTP客户端库,适用于浏览器和Node.js环境,用于在JavaScript应用中执行异步HTTP请求。相较于原生的XMLHttpRequest或Fetch API,Axios提供了更简洁的API和更强大的功能。 【免费下载链接】axios 项目地址: https://gitcode.com/GitHub_Trending/ax/axios

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值