问题总结:关于封装axios问题,导致外部使用接口报错,无法进入error回调

12 阅读9分钟

从一段有问题的axios,学习axios封装以及axios知识点,大家一起来找问题

1.问题代码

1.1问题描述

 使用封装Ajax调用接口报错之后,并未进入error,去执行清空定时器的操作,导致接口报错之后,定时器未关闭,一直调用报错接口

1.2使用Ajax组件

import Axios from 'axios';
//响应拦截器
Axios.interceptors.response.use(
  response => { return response; },
  error => {
    console.error('拦截器:',error);
    try {
      console.error('拦截器Try:',error);
      if (error.response.status === 401) {
        window.location.href = '/login';
      }
    } catch(e) {
      console.error('拦截器catch:',e);
    }
    console.log('拦截器执行了Promise.reject(error)')
    return Promise.reject(error);
   
  }
)
const errorStatusHandle = function (data, headers,statusTemp) {
  console.log('errorStatusHandle参数打印:', data, headers,statusTemp)
   if (statusTemp === 500) {
    return null
   }
   if(statusTemp === 401) {
    return null
   }
   return data
}
const AjaxMain = (params1, error = ()=> {}, success = ()=> {}) => {
  let {
    url,
    params,
    methods
  } = params1
  Axios({
    url,
    params,
    methods,
    validateStatus: (status)=> {
      return status < 500
    },
    transformResponse:[
      function(data,headers,status) {
        if (!errorStatusHandle(data, headers,status)) {
          console.log('transformResponse参数打印:',data, headers,status)
          throw new Error(data)
        }
      }
    ]
  }).then(res => {
    console.log('AjaxMain.then打印:', res)
     if (res.status === 200) {
        let successStatus= true
        if (successStatus) {
          success(res)
        } else {
          error(data)
        }
     }
  })
  .catch(err => {
    console.log('AjaxMain.catch打印:', err)
    if (!errorStatusHandle(err, url)) {
      return null
    }
  })
}
export default AjaxMain

1.3封装Ajax代码

import React, { Component } from 'react'
import Ajax from './ajax'
import axios from 'axios'
export default class TopSearch extends Component {
  serchBtn = () => {
    console.log('进入搜索', this.keydownVal.value)
    let searchVal = this.keydownVal.value
   
    // 使用代理请求数据--5000代理器
    Ajax({
      url: 'https://api.github.com/search/users',
      params: {
        q:searchVal
      },
      methods: 'get'
     },
    res => {console.log(res, '成功')
    },
    error => {
      console.log(error, '报错信息')
    }
      
    )
  }
  render() {
    return (
      <section className="jumbotron">
        <h3 className="jumbotron-heading">Search Github Users</h3>
        <div>
          <input ref={c => this.keydownVal= c} type="text" placeholder="enter the name you search"/>&nbsp;
          <button onClick={this.serchBtn}>搜索</button>
        </div>
      </section>
    )
  }
}

2. 解决思路

2.1 解决问题思路

1.首先封装使用的是回调暴露错误的-检查代码封装是否在所有报错会进入的地方都执行了error()-检查发现AjaxMain.catch中没有执行error() 2.检查是否有抛出错误被吞掉的问题,以及错误抛出错误会导致走向哪里 3.对于不同的报错码,是否会进入catch还是then

3. Axios知识点总结

3.1 Axios封装的俩种主流方式

  1. 回调函数方式(callback)
    在请求方法中传直接传入success和error俩个回调函数,内部根据请求结果调用对应的回调
  2. 返回promise的方式
    封装方法直接返回Axios调用产生的promise对象,调用是使用.then,.catch或async/await获取结果

    3.1.1 回调函数方式(callback)

    1)特点:
    • 显示控制成功和失败的路径:调用方需要提供一个成功函数和一个失败函数
    • 适合简单的场景:如早起的jQuery的$.ajax风格。
    • 代码嵌套有风险:如果多个请求有依赖关系,容易形成"回调地狱" 2)代码案例:
      import axios from 'axios';
    
      /**
      * 通用请求封装(回调版)
      * @param {Object} config - axios 配置对象
      * @param {Function} success - 请求成功时的回调,接收响应数据
      * @param {Function} error - 请求失败时的回调,接收错误对象
      */
      function request(config, success, error) {
        axios(config)
          .then(response => {
            // 成功:调用 success 回调,通常传入 response.data
            success(response.data);
          })
          .catch(err => {
            // 失败:调用 error 回调,可以传入错误对象或自定义信息
            error(err);
          });
      }
    
      // 使用示例:获取用户信息
      request(
        { url: '/api/user', method: 'get' },
        (data) => {
          console.log('用户数据:', data);
          // 更新 UI 等操作
        },
        (err) => {
          console.error('获取用户失败:', err.message);
          // 显示错误提示
        }
      );
    

    3.1.2 返回promise的方式

    1)特点:
    • 返回 Promise 对象:封装函数直接返回 axios(config) 的结果,该结果本身就是一个 Promise。
    • 调用灵活:可以使用 .then().catch() 或 async/await,支持链式调用。
    • 易于组合:Promise.all、Promise.race 等并发控制非常方便。
    • 主流实践:目前大多数现代前端项目都采用这种方式。 2)代码案例:
      import axios from 'axios';
      /**
      * 通用请求封装(Promise 版)
      * @param {Object} config - axios 配置对象
      * @returns {Promise} 返回 axios 请求的 Promise
      */
      function request(config) {
        return axios(config);
      }
    
      // 使用示例:获取用户信息
      request({ url: '/api/user', method: 'get' })
        .then(response => {
          console.log('用户数据:', response.data);
        })
        .catch(error => {
          console.error('请求失败:', error.message);
        });
    
      // 或者使用 async/await
      async function fetchUser() {
        try {
          const response = await request({ url: '/api/user' });
          console.log('用户数据:', response.data);
        } catch (error) {
          console.error('请求失败:', error.message);
        }
      }
    

3.2 关于响应拦截器和transformResponse使用区别

3.2.1 transformResponse:纯粹的数据转换器

  • 核心功能: transformResponse 是一个纯粹的数据转换函数,专用于同步地修改响应体(data)和响应头(headers)。例如,其常见用途是解析后端返回的 JSON 字符串,或处理一些特殊数据格式,比如使用 json-bigint 库来解析可能超出 JavaScript 安全整数范围的大数字

  • 主要特点:
    同步操作: 设计上必须是同步的,以保证数据处理的顺序性。
    只关注数据: 其函数签名通常为 (data, headers) => transformedData,只能访问 data 和 headers,无法操作完整的响应对象或请求配置。但是也可以获取status请求状态
    内置于请求链: 它是 Axios 请求分发(dispatchRequest)流程中的一环,位于请求拦截器之后、响应拦截器之前,是所有请求都必经的一个“内置关卡”。

  • 使用场景
    全局数据格式统一: 例如,所有接口返回的日期字符串都需要转换成 Date 对象。
    数据反序列化: 后端返回的是特殊格式的字符串(如 XML、自定义格式),需要在业务逻辑处理前统一转换。
    轻量级同步处理: 只是需要简单、快速地修改一下响应数据,不需要访问复杂的请求配置或状态码。

    3.2.2 响应拦截器:强大的流程控制器

  • 核心功能: 响应拦截器是一个更通用的流程控制工具。它不仅能处理成功响应,还能捕获错误(即 Promise 链中的 reject 状态),允许进行同步或异步的操作,并执行带有“副作用”的任务。

  • 主要特点:
    能力全面: 拦截器的函数签名通常为 (response) => response,能访问完整的 response 对象,包括 config(请求配置)、status(HTTP状态码)、data(响应体)和 headers(响应头)等所有信息。
    全局与局部: 拦截器通常是全局的,一旦在应用启动时设置,就会作用于通过该 Axios 实例发出的每一个请求。
    流程枢纽: 它处于 Promise 链中,位于 transformResponse 处理完成之后,业务代码的 .then 或 .catch 之前,是一个处理HTTP请求生命周期的“中间枢纽”。

  • 使用场景 集中错误处理: 根据不同的 HTTP 状态码(如 401 未授权、403 无权限、500 服务端错误)执行统一的业务逻辑,如跳转到登录页或弹出错误提示。
    执行异步任务: 需要在拿到响应后,先执行一个异步操作(如刷新 Token),再继续传递数据。
    执行有副作用的操作: 例如,在请求完成后,不论成功或失败,都进行统一的埋点日志记录。
    需要对完整响应做决策: 当你的处理逻辑依赖于状态码、响应头等信息时(如根据 Content-Disposition 判断文件流),就必须使用拦截器

执行时机与协同工作 两者并非二选一,而是按严格的顺序协同工作: 请求拦截器 -> 2. transformRequest -> 3. 发送请求 -> 4. transformResponse -> 5. 响应拦截器 -> 6. 业务代码 因此,transformResponse 会先于响应拦截器执行。这意味着在响应拦截器中拿到的 response.data,已经是经过 transformResponse 处理后的结果了,如果服务器返回的响应格式异常(例如本应是 JSON 却返回了空字符串、HTML 等),默认的 JSON.parse 操作就会失败并抛出异常或者代码体有异常抛出,在响应拦截器的错误回调中也可以捕获

3.3 关于validateStatus方法

validateStatus 是 Axios 的一个配置项,用于自定义判断 HTTP 响应状态码是否被视为成功(即 Promise 进入 resolve 还是 reject)。
执行时机与影响 执行顺序:响应到达 → transformResponse → validateStatus → 决定 Promise 状态。 如果 validateStatus 返回 false: Promise 进入 catch 分支。 错误对象 error.response 依然存在,包含完整的响应数据(已经过 transformResponse 转换)。 error.request 和 error.config 也都能访问。 如果请求根本没有到达服务器(如网络故障、DNS 解析失败),不会调用 validateStatus,直接进入 catch,且 error.response 为 undefined。
Axios 的处理流程大致如下: 接收到 HTTP 响应(包括 502)。 调用 transformResponse 对原始响应数据进行转换(无论状态码是否成功)。 根据 validateStatus 判断是否将 Promise 标记为 resolve 或 reject。 如果 validateStatus 返回 false,则进入 catch 分支,但 error.response.data 已经是经过 transformResponse 处理后的数据。

3.4 关于哪些操作会吞掉报错信息,导致无法进入错误回调

具体操作包括以下几种:

  1. 在拦截器的错误处理函数中返回一个普通值(或 Promise.resolve)

      axios.interceptors.response.use(
       null,
       error => {
         if (error.response?.status === 401) {
           // 返回一个普通对象,表示“我已处理这个错误,请当作成功”
           return { code: 'AUTH_FAIL', message: '需要登录' };
           // 或者 return Promise.resolve({ ... });
         }
         return Promise.reject(error);
       }
     );
    

此时,原本的 401 错误被转换成一个 fulfilled 的 Promise,后续的 .then 会收到这个对象,而 .catch 或回调中的 error 函数不会触发。

  1. 没有显式返回值(即 return undefined)
axios.interceptors.response.use(
 null,
 error => {
   if (error.response?.status === 401) {
     // 做了一些事,但没有 return
     console.log('401 发生');
     // 相当于 return undefined;
   }
   return Promise.reject(error);
 }
);

当进入 if 分支后,函数执行完毕,隐式返回 undefined。undefined 不是错误,Axios 会将其包装为 Promise.resolve(undefined),从而吞掉错误。

  1. 在拦截器中抛出错误但没有用 Promise.reject 包装? 实际上,在 async 函数中 throw error 等价于返回 Promise.reject(error),这不会吞掉错误。但如果在非 async 函数中直接 throw error,Axios 会捕获并转为 reject,同样不会吞掉。真正的吞掉是返回非 reject 的值,所以这一点要区分清楚。

  2. 返回一个最终 resolve 的 Promise(例如刷新 token 后重试成功)

axios.interceptors.response.use(
  null,
  async error => {
    if (error.response?.status === 401) {
      await refreshToken();
      // 重新发起原请求,假设成功
      const newResponse = await axios.request(error.config);
      return newResponse;  // 返回成功结果 → 吞掉原始 401
    }
    return Promise.reject(error);
  }
);

原始 401 错误被“消化”,最终返回了一个成功的响应数据,错误链不会触发。

  1. 在拦截器中使用了 try/catch 但没有重新 throw
axios.interceptors.response.use(
  null,
  error => {
    try {
      if (error.response?.status === 401) {
        // 可能调用某个会抛异常的函数
        handle401();
      }
      return Promise.reject(error);
    } catch (e) {
      // 捕获了异常,但没有 throw 或 return Promise.reject(e)
      console.log('内部错误被吞了', e);
      // 隐式返回 undefined → 吞掉
    }
  }
);`
  1. 在成功拦截器中(第一个参数)意外返回了非预期值

虽然通常错误由第二个参数处理,但如果在成功拦截器中抛错或返回 Promise.reject,也会进入错误链。反过来,如果在成功拦截器里吞掉错误(比如返回一个普通值),那只是修改了成功数据,不影响错误传递。但要注意:如果成功拦截器里发生了同步错误且没有处理,Axios 会将其转为 reject,这不会吞掉错误。