第82集:请求拦截器与响应拦截器
概述
Axios拦截器是Axios库的核心特性之一,允许我们在发送请求之前和收到响应之后执行自定义逻辑。请求拦截器用于统一处理请求配置,如添加认证信息、设置请求头、处理加载状态等;响应拦截器用于统一处理响应数据、错误处理、刷新Token等。掌握拦截器的使用对于构建高效、可靠的API通信层至关重要。
核心知识点
1. 拦截器基础
1.1 拦截器概念
拦截器是Axios提供的一种机制,允许我们在请求发送之前和响应接收之后插入自定义逻辑。拦截器分为两类:
- 请求拦截器:在请求发送到服务器之前执行
- 响应拦截器:在服务器响应返回客户端之后执行
1.2 拦截器的执行流程
- 创建Axios实例
- 注册请求拦截器
- 发送请求
- 请求拦截器执行(修改请求配置)
- 服务器处理请求
- 返回响应
- 响应拦截器执行(处理响应数据)
- 客户端处理响应
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 请求拦截器的常见用途
- 添加认证信息
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)
}
)- 设置请求头
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)
}
)- 添加公共参数
http.interceptors.request.use(
(config) => {
// 添加公共查询参数
config.params = {
...config.params,
appVersion: '1.0.0',
timestamp: Date.now()
}
return config
},
(error) => {
return Promise.reject(error)
}
)- 处理加载状态
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 响应拦截器的常见用途
- 统一处理响应数据
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)
}
)- 统一处理错误
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)
}
)- 刷新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)
}
)- 处理加载状态
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 error4.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
进一步学习资源
课后练习
基础练习:
- 创建Axios实例并添加请求拦截器和响应拦截器
- 在请求拦截器中添加认证信息
- 在响应拦截器中统一处理错误
进阶练习:
- 实现多个拦截器的配置
- 测试拦截器的执行顺序
- 实现动态添加和移除拦截器
实战练习:
- 实现Token刷新机制
- 实现加载状态管理
- 实现跨域请求处理
性能优化练习:
- 优化拦截器中的异步操作
- 实现拦截器的延迟加载
- 测试拦截器的性能影响
通过本节课的学习,你应该能够掌握Axios拦截器的基本概念和使用方法,理解拦截器的执行顺序,掌握拦截器的高级用法,以及如何在实际项目中应用拦截器。这些知识将帮助你构建高效、可靠的API通信层,提高应用的性能和可维护性。