第82集:请求拦截器与响应拦截器

概述

Axios拦截器是Axios库的核心特性之一,允许我们在发送请求之前和收到响应之后执行自定义逻辑。请求拦截器用于统一处理请求配置,如添加认证信息、设置请求头、处理加载状态等;响应拦截器用于统一处理响应数据、错误处理、刷新Token等。掌握拦截器的使用对于构建高效、可靠的API通信层至关重要。

核心知识点

1. 拦截器基础

1.1 拦截器概念

拦截器是Axios提供的一种机制,允许我们在请求发送之前和响应接收之后插入自定义逻辑。拦截器分为两类:

  • 请求拦截器:在请求发送到服务器之前执行
  • 响应拦截器:在服务器响应返回客户端之后执行

1.2 拦截器的执行流程

  1. 创建Axios实例
  2. 注册请求拦截器
  3. 发送请求
  4. 请求拦截器执行(修改请求配置)
  5. 服务器处理请求
  6. 返回响应
  7. 响应拦截器执行(处理响应数据)
  8. 客户端处理响应

2. 请求拦截器

2.1 基本配置

请求拦截器用于在请求发送之前修改请求配置,如添加认证信息、设置请求头、处理加载状态等。

// src/utils/axios.ts
import axios from 'axios'

const http = axios.create({
  baseURL: '/api',
  timeout: 10000
})

// 添加请求拦截器
http.interceptors.request.use(
  (config) => {
    // 在发送请求之前做些什么
    console.log('Request interceptor called')
    return config
  },
  (error) => {
    // 对请求错误做些什么
    console.error('Request error interceptor called')
    return Promise.reject(error)
  }
)

2.2 请求拦截器的常见用途

  1. 添加认证信息
http.interceptors.request.use(
  (config) => {
    // 从本地存储获取token
    const token = localStorage.getItem('token')
    if (token) {
      // 添加Authorization请求头
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)
  1. 设置请求头
http.interceptors.request.use(
  (config) => {
    // 设置Content-Type
    if (!config.headers['Content-Type']) {
      config.headers['Content-Type'] = 'application/json'
    }
    // 设置语言
    config.headers['Accept-Language'] = navigator.language || 'zh-CN'
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)
  1. 添加公共参数
http.interceptors.request.use(
  (config) => {
    // 添加公共查询参数
    config.params = {
      ...config.params,
      appVersion: '1.0.0',
      timestamp: Date.now()
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)
  1. 处理加载状态
http.interceptors.request.use(
  (config) => {
    // 触发加载状态
    // eventBus.emit('loading', true)
    return config
  },
  (error) => {
    // 关闭加载状态
    // eventBus.emit('loading', false)
    return Promise.reject(error)
  }
)

3. 响应拦截器

3.1 基本配置

响应拦截器用于在接收响应之后处理响应数据,如统一处理错误、转换响应格式、刷新Token等。

// src/utils/axios.ts

// 添加响应拦截器
http.interceptors.response.use(
  (response) => {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    console.log('Response interceptor called')
    return response
  },
  (error) => {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    console.error('Response error interceptor called')
    return Promise.reject(error)
  }
)

3.2 响应拦截器的常见用途

  1. 统一处理响应数据
http.interceptors.response.use(
  (response) => {
    // 统一处理响应数据格式
    const { data, status } = response
    if (status === 200) {
      // 假设API返回格式为 { code: number, message: string, data: any }
      if (data.code === 200) {
        return data.data // 只返回业务数据
      } else {
        // 业务错误,返回错误信息
        return Promise.reject(new Error(data.message || 'Request failed'))
      }
    } else {
      return Promise.reject(new Error('Request failed'))
    }
  },
  (error) => {
    return Promise.reject(error)
  }
)
  1. 统一处理错误
http.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    const { response } = error
    if (response) {
      // HTTP状态码错误处理
      switch (response.status) {
        case 400:
          console.error('请求参数错误')
          break
        case 401:
          console.error('未授权,请重新登录')
          // 跳转到登录页面
          // router.push('/login')
          break
        case 403:
          console.error('拒绝访问')
          break
        case 404:
          console.error('请求地址不存在')
          break
        case 500:
          console.error('服务器内部错误')
          break
        default:
          console.error('请求失败')
      }
    } else if (error.request) {
      // 请求已发出,但没有收到响应
      console.error('网络错误,请检查网络连接')
    } else {
      // 请求配置错误
      console.error('请求配置错误')
    }
    return Promise.reject(error)
  }
)
  1. 刷新Token
http.interceptors.response.use(
  (response) => {
    return response
  },
  async (error) => {
    const originalRequest = error.config
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true
      
      try {
        // 刷新Token
        const refreshToken = localStorage.getItem('refreshToken')
        const { data } = await http.post('/auth/refresh', { refreshToken })
        
        // 更新Token
        localStorage.setItem('token', data.token)
        localStorage.setItem('refreshToken', data.refreshToken)
        
        // 重试原始请求
        originalRequest.headers.Authorization = `Bearer ${data.token}`
        return http(originalRequest)
      } catch (refreshError) {
        // 刷新Token失败,跳转到登录页面
        // router.push('/login')
        return Promise.reject(refreshError)
      }
    }
    return Promise.reject(error)
  }
)
  1. 处理加载状态
http.interceptors.response.use(
  (response) => {
    // 关闭加载状态
    // eventBus.emit('loading', false)
    return response
  },
  (error) => {
    // 关闭加载状态
    // eventBus.emit('loading', false)
    return Promise.reject(error)
  }
)

4. 拦截器的高级用法

4.1 多个拦截器的配置

Axios允许配置多个拦截器,它们会按照添加的顺序执行:

// 第一个请求拦截器
http.interceptors.request.use(
  (config) => {
    console.log('First request interceptor')
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 第二个请求拦截器
http.interceptors.request.use(
  (config) => {
    console.log('Second request interceptor')
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 第一个响应拦截器
http.interceptors.response.use(
  (response) => {
    console.log('First response interceptor')
    return response
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 第二个响应拦截器
http.interceptors.response.use(
  (response) => {
    console.log('Second response interceptor')
    return response
  },
  (error) => {
    return Promise.reject(error)
  }
)

执行顺序

  • 请求:First request interceptor → Second request interceptor → 发送请求
  • 响应:接收到响应 → First response interceptor → Second response interceptor → 返回给调用者

4.2 拦截器的错误处理

拦截器中的错误会被传递到下一个拦截器或最终的Promise catch:

http.interceptors.request.use(
  (config) => {
    // 抛出错误
    throw new Error('Request interceptor error')
  },
  (error) => {
    console.error('Request error interceptor:', error.message)
    return Promise.reject(error)
  }
)

// 使用示例
try {
  await http.get('/users')
} catch (error) {
  console.error('Final error:', error.message)
}

输出顺序

Request error interceptor: Request interceptor error
Final error: Request interceptor error

4.3 动态添加和移除拦截器

可以动态添加和移除拦截器,这在某些场景下非常有用,如根据条件启用或禁用某些拦截器:

// 添加拦截器并保存引用
const requestInterceptor = http.interceptors.request.use(
  (config) => {
    // 拦截器逻辑
    return config
  }
)

// 移除拦截器
http.interceptors.request.eject(requestInterceptor)

// 添加响应拦截器并保存引用
const responseInterceptor = http.interceptors.response.use(
  (response) => {
    // 拦截器逻辑
    return response
  }
)

// 移除响应拦截器
http.interceptors.response.eject(responseInterceptor)

4.4 拦截器中的异步操作

拦截器中可以执行异步操作,如从服务器获取数据或验证Token:

http.interceptors.request.use(
  async (config) => {
    // 异步获取Token
    const token = await getTokenFromServer()
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

最佳实践

1. 拦截器的职责单一

每个拦截器应该只负责一个特定的功能,避免将多个不相关的逻辑放在同一个拦截器中:

// ✅ 正确:职责单一的拦截器
// 认证拦截器
http.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  }
)

// 请求头拦截器
http.interceptors.request.use(
  (config) => {
    config.headers['Content-Type'] = 'application/json'
    return config
  }
)

// ❌ 错误:职责不单一的拦截器
http.interceptors.request.use(
  (config) => {
    // 认证逻辑
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    // 请求头逻辑
    config.headers['Content-Type'] = 'application/json'
    
    // 公共参数逻辑
    config.params = {
      ...config.params,
      appVersion: '1.0.0'
    }
    
    return config
  }
)

2. 错误处理的统一性

在拦截器中统一处理错误,避免在每个API调用中重复处理相同的错误:

http.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    // 统一错误处理
    showErrorNotification(error.message)
    return Promise.reject(error)
  }
)

3. 拦截器的条件执行

根据请求配置条件执行拦截器逻辑,避免不必要的处理:

http.interceptors.request.use(
  (config) => {
    // 只对API请求添加认证信息
    if (config.url?.startsWith('/api')) {
      const token = localStorage.getItem('token')
      if (token) {
        config.headers.Authorization = `Bearer ${token}`
      }
    }
    return config
  }
)

4. 拦截器的日志记录

在开发环境中添加日志记录,便于调试:

if (process.env.NODE_ENV === 'development') {
  http.interceptors.request.use(
    (config) => {
      console.log('Request:', config.method?.toUpperCase(), config.url)
      return config
    },
    (error) => {
      console.error('Request Error:', error)
      return Promise.reject(error)
    }
  )
  
  http.interceptors.response.use(
    (response) => {
      console.log('Response:', response.config?.method?.toUpperCase(), response.config?.url, response.status)
      return response
    },
    (error) => {
      console.error('Response Error:', error)
      return Promise.reject(error)
    }
  )
}

5. 拦截器的测试

拦截器应该被测试,确保它们按预期工作:

// 使用Vitest测试拦截器
describe('Axios Interceptors', () => {
  it('should add token to request header', async () => {
    // 模拟localStorage
    localStorage.setItem('token', 'test-token')
    
    // 发送请求
    await http.get('/test')
    
    // 验证请求头是否包含token
    expect(fetch).toHaveBeenCalledWith(expect.objectContaining({
      headers: expect.objectContaining({
        Authorization: 'Bearer test-token'
      })
    }))
  })
})

常见问题与解决方案

1. 拦截器执行顺序问题

问题:多个拦截器的执行顺序不符合预期。

解决方案

  • 了解拦截器的执行顺序:请求拦截器按添加顺序执行,响应拦截器按相反顺序执行
  • 合理安排拦截器的添加顺序

2. 拦截器中的无限循环

问题:拦截器中的错误处理导致无限循环。

解决方案

  • 避免在拦截器中重新发送相同的请求
  • 使用标记避免重复处理
http.interceptors.response.use(
  (response) => {
    return response
  },
  async (error) => {
    const originalRequest = error.config
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true
      // 刷新Token逻辑
    }
    return Promise.reject(error)
  }
)

3. 拦截器中的上下文丢失

问题:在拦截器中无法访问组件上下文。

解决方案

  • 使用事件总线或状态管理库传递信息
  • 将上下文信息作为参数传递给拦截器

4. 拦截器的兼容性问题

问题:某些浏览器或环境不支持拦截器。

解决方案

  • 检查浏览器兼容性
  • 提供降级方案
  • 使用Polyfill

进一步学习资源

  1. Axios官方文档 - 拦截器
  2. MDN - HTTP请求
  3. MDN - HTTP响应状态码
  4. RESTful API设计指南
  5. OAuth 2.0官方文档

课后练习

  1. 基础练习

    • 创建Axios实例并添加请求拦截器和响应拦截器
    • 在请求拦截器中添加认证信息
    • 在响应拦截器中统一处理错误
  2. 进阶练习

    • 实现多个拦截器的配置
    • 测试拦截器的执行顺序
    • 实现动态添加和移除拦截器
  3. 实战练习

    • 实现Token刷新机制
    • 实现加载状态管理
    • 实现跨域请求处理
  4. 性能优化练习

    • 优化拦截器中的异步操作
    • 实现拦截器的延迟加载
    • 测试拦截器的性能影响

通过本节课的学习,你应该能够掌握Axios拦截器的基本概念和使用方法,理解拦截器的执行顺序,掌握拦截器的高级用法,以及如何在实际项目中应用拦截器。这些知识将帮助你构建高效、可靠的API通信层,提高应用的性能和可维护性。

« 上一篇 Axios封装与配置 - Vue HTTP客户端优化 下一篇 » 统一错误处理机制 - Vue 3全栈应用稳定性保障