1. 为什么我们需要封装Axios拦截器?
在前后端分离的现代Web开发中,HTTP请求库就像项目的血管系统。Axios作为目前最流行的Promise-based HTTP客户端,其拦截器(interceptors)功能让我们能够在请求和响应被处理前进行统一操作。但原生的使用方式存在几个明显痛点:
- 代码重复:每个请求都需要手动添加错误处理逻辑
- 维护困难:拦截器逻辑分散在各个业务文件中
- 可读性差:业务代码与基础配置混杂在一起
- 扩展性弱:新增全局功能(如重试机制)需要修改多处
我曾在维护一个中型电商项目时,发现17个不同文件中都存在几乎相同的错误处理代码。当需要调整超时时间时,不得不进行全局搜索替换——这正是缺乏统一封装的典型后果。
2. 拦截器基础架构设计
2.1 核心拦截点分析
一个健壮的Axios封装应该处理以下关键节点:
javascript复制// 典型拦截流程示意图
请求 --> 请求拦截器 --> 服务器 --> 响应拦截器 --> 业务代码
│ │
└─ 添加鉴权头 └─ 统一错误处理
2.2 基础封装方案
我们先实现一个最小可行版本:
javascript复制// http.js
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 此处添加token等逻辑
if (store.getters.token) {
config.headers['Authorization'] = `Bearer ${store.getters.token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const res = response.data
// 假设业务代码中code=20000表示成功
if (res.code !== 20000) {
// 处理业务错误
return Promise.reject(new Error(res.message || 'Error'))
}
return res
},
error => {
// HTTP状态码错误处理
if (error.response.status === 401) {
// 跳转登录
}
return Promise.reject(error)
}
)
export default service
关键点:拦截器执行顺序是先进后出(栈结构),第一个添加的请求拦截器最后执行,而第一个响应拦截器最先执行。
3. 高级封装技巧与实践
3.1 多实例管理策略
当项目需要对接多个后端服务时,推荐采用多实例方案:
javascript复制// http.js
export const mainService = createService({
baseURL: 'https://api.main.com'
})
export const paymentService = createService({
baseURL: 'https://api.payment.com'
})
function createService(config) {
const instance = axios.create(config)
// 公共拦截逻辑
instance.interceptors.request.use(/*...*/)
return instance
}
3.2 智能重试机制
对于网络波动导致的失败请求,可以自动重试:
javascript复制service.interceptors.response.use(null, async error => {
const config = error.config
// 设置重试次数和间隔
if (!config._retryCount) {
config._retryCount = 0
config._retryDelay = 1000
}
if (config._retryCount < 3 && error.code === 'ECONNABORTED') {
config._retryCount++
await new Promise(resolve =>
setTimeout(resolve, config._retryDelay)
)
return service(config)
}
return Promise.reject(error)
})
3.3 请求取消与竞态处理
使用CancelToken避免重复请求:
javascript复制const pendingRequests = new Map()
service.interceptors.request.use(config => {
const requestKey = `${config.method}-${config.url}`
if (pendingRequests.has(requestKey)) {
pendingRequests.get(requestKey).cancel()
}
config.cancelToken = new axios.CancelToken(cancel => {
pendingRequests.set(requestKey, { cancel })
})
return config
})
service.interceptors.response.use(response => {
const requestKey = `${response.config.method}-${response.config.url}`
pendingRequests.delete(requestKey)
return response
})
4. 企业级解决方案设计
4.1 类型安全增强
对于TypeScript项目,我们可以增强类型提示:
typescript复制declare module 'axios' {
interface AxiosRequestConfig {
_retryCount?: number
_retryDelay?: number
_showLoading?: boolean
}
}
interface ResponseData<T = any> {
code: number
data: T
message: string
}
export function request<T>(config: AxiosRequestConfig): Promise<T> {
return service(config).then(res => res.data)
}
4.2 性能监控集成
在拦截器中添加性能埋点:
javascript复制service.interceptors.request.use(config => {
config.metadata = { startTime: Date.now() }
return config
})
service.interceptors.response.use(response => {
const duration = Date.now() - response.config.metadata.startTime
trackApiPerformance(response.config.url, duration)
return response
})
4.3 动态配置管理
通过拦截器实现环境感知:
javascript复制service.interceptors.request.use(config => {
if (process.env.NODE_ENV === 'mock') {
config.adapter = require('./mockAdapter')
}
return config
})
5. 实战中的坑与解决方案
5.1 拦截器执行顺序陷阱
我曾遇到一个诡异的问题:后添加的拦截器竟然先执行了。这是因为:
javascript复制// 错误示例:多次调用use会创建独立链
axios.interceptors.request.use(a)
axios.interceptors.request.use(b)
// 实际执行顺序:b -> a
解决方案是始终在同一个实例上操作:
javascript复制// 正确做法
const instance = axios.create()
instance.interceptors.request.use(a)
instance.interceptors.request.use(b)
5.2 响应数据格式不一致
当第三方API返回格式不一致时:
javascript复制service.interceptors.response.use(response => {
// 处理多种响应格式
let data = response.data
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch {}
}
// 统一为 { code, data, message } 格式
return {
code: data.code || 200,
data: data.data || data,
message: data.message || ''
}
})
5.3 内存泄漏预防
全局拦截器可能导致内存泄漏:
javascript复制// 错误示例:组件内直接添加全局拦截器
created() {
axios.interceptors.request.use(/*...*/)
}
// 正确做法:使用实例或清理钩子
const reqInterceptor = service.interceptors.request.use(/*...*/)
beforeDestroy() {
service.interceptors.request.eject(reqInterceptor)
}
6. 现代前端框架集成方案
6.1 Vue插件封装
javascript复制// axiosPlugin.js
export default {
install: (app, options) => {
app.config.globalProperties.$http = createService(options)
}
}
// main.js
import axiosPlugin from './axiosPlugin'
app.use(axiosPlugin, {
baseURL: 'https://api.example.com'
})
6.2 React Context集成
jsx复制// ApiContext.js
const ApiContext = createContext()
export function ApiProvider({ children }) {
const api = useMemo(() => createService(), [])
return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>
}
export function useApi() {
return useContext(ApiContext)
}
6.3 Nuxt模块方案
javascript复制// nuxt.config.js
export default {
modules: [
['@nuxtjs/axios', {
baseURL: process.env.BASE_URL,
interceptors: {
request: [
function ({ config }) {
// 添加认证头
}
],
response: [
function (response) {
// 处理响应
}
]
}
}]
]
}
7. 测试策略与Mock方案
7.1 单元测试要点
javascript复制describe('请求拦截器', () => {
let instance
beforeEach(() => {
instance = axios.create()
setupRequestInterceptor(instance)
})
it('应添加认证头', async () => {
const token = 'test-token'
store.getters.token = token
const config = await instance.interceptors.request.handlers[0].fulfilled({})
expect(config.headers.Authorization).toBe(`Bearer ${token}`)
})
})
7.2 Mock Service Worker集成
javascript复制// src/mocks/server.js
import { setupServer } from 'msw/node'
import { rest } from 'msw'
const server = setupServer(
rest.get('/api/user', (req, res, ctx) => {
return res(
ctx.delay(150),
ctx.json({ code: 20000, data: { name: 'John' } })
)
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
7.3 异常场景测试
javascript复制it('应处理504超时错误', async () => {
server.use(
rest.get('/api/timeout', (req, res, ctx) => {
return res(ctx.status(504), ctx.delay(200))
})
)
await expect(service.get('/api/timeout')).rejects.toThrow('请求超时')
})
8. 性能优化实践
8.1 请求合并策略
对于高频触发但实际可以合并的请求:
javascript复制const requestQueue = new Map()
function debounceRequest(key, config) {
if (requestQueue.has(key)) {
return requestQueue.get(key)
}
const promise = service(config).finally(() => {
requestQueue.delete(key)
})
requestQueue.set(key, promise)
return promise
}
8.2 缓存控制实现
javascript复制const cache = new Map()
service.interceptors.request.use(config => {
if (config.cache) {
const cacheKey = JSON.stringify(config)
if (cache.has(cacheKey)) {
return Promise.resolve(cache.get(cacheKey))
}
}
return config
})
service.interceptors.response.use(response => {
if (response.config.cache) {
const cacheKey = JSON.stringify(response.config)
cache.set(cacheKey, response)
}
return response
})
8.3 压缩请求优化
对于大数据量请求:
javascript复制service.interceptors.request.use(async config => {
if (config.data && config.data.length > 1024) {
const compressed = await compressData(config.data)
config.data = compressed
config.headers['Content-Encoding'] = 'gzip'
}
return config
})
9. 安全加固方案
9.1 CSRF防护
javascript复制service.interceptors.request.use(config => {
if (['post', 'put', 'delete'].includes(config.method.toLowerCase())) {
config.headers['X-CSRF-TOKEN'] = getCSRFToken()
}
return config
})
9.2 请求签名验证
javascript复制service.interceptors.request.use(config => {
const timestamp = Date.now()
const nonce = generateNonce()
const signature = createSignature({
url: config.url,
method: config.method,
timestamp,
nonce,
body: config.data
})
config.headers['X-Timestamp'] = timestamp
config.headers['X-Nonce'] = nonce
config.headers['X-Signature'] = signature
return config
})
9.3 敏感数据过滤
javascript复制service.interceptors.response.use(response => {
if (response.data?.token) {
delete response.data.token
}
return response
})
10. 微前端场景适配
10.1 主子应用隔离
javascript复制// 主应用
const mainService = createService({
baseURL: '/main-api'
})
// 子应用
const subService = createService({
baseURL: '/sub-api'
})
// 避免全局污染
delete window.axios
10.2 共享实例方案
javascript复制// shared-axios.js
export const sharedService = createSharedService()
function createSharedService() {
const instance = axios.create()
// 基础配置...
return instance
}
10.3 跨应用通信
javascript复制// 通过自定义事件传递token变更
window.addEventListener('token-changed', (e) => {
sharedService.defaults.headers.common['Authorization'] = e.detail.token
})
11. 监控与日志系统
11.1 全链路追踪
javascript复制service.interceptors.request.use(config => {
config.headers['X-Trace-ID'] = generateTraceId()
return config
})
service.interceptors.response.use(response => {
logRequest({
url: response.config.url,
duration: Date.now() - response.config.metadata.startTime,
traceId: response.config.headers['X-Trace-ID']
})
return response
})
11.2 错误日志上报
javascript复制service.interceptors.response.use(null, error => {
captureError({
url: error.config?.url,
method: error.config?.method,
status: error.response?.status,
message: error.message
})
return Promise.reject(error)
})
11.3 性能分析
javascript复制const stats = {
total: 0,
success: 0,
failed: 0,
durations: []
}
service.interceptors.response.use(response => {
stats.total++
stats.success++
stats.durations.push(Date.now() - response.config.metadata.startTime)
return response
}, error => {
stats.total++
stats.failed++
return Promise.reject(error)
})
// 定期上报统计数据
setInterval(() => {
if (stats.total > 0) {
reportPerformanceStats(stats)
// 重置统计
stats.total = stats.success = stats.failed = 0
stats.durations = []
}
}, 60000)
12. 移动端特殊处理
12.1 弱网优化
javascript复制service.defaults.adapter = config => {
return new Promise((resolve, reject) => {
const connection = navigator.connection || navigator.mozConnection
if (connection?.effectiveType === 'slow-2g') {
config.timeout = 30000
config.data = simplifyPayload(config.data)
}
axios.defaults.adapter(config).then(resolve).catch(reject)
})
}
12.2 请求优先级管理
javascript复制const priorityQueue = {
high: [],
normal: [],
low: []
}
function executeWithPriority(config) {
return new Promise((resolve) => {
const queue = priorityQueue[config.priority || 'normal']
queue.push({ config, resolve })
if (!isProcessing) {
processQueue()
}
})
}
async function processQueue() {
isProcessing = true
while (priorityQueue.high.length > 0) {
await processItem(priorityQueue.high.shift())
}
while (priorityQueue.normal.length > 0) {
await processItem(priorityQueue.normal.shift())
}
while (priorityQueue.low.length > 0) {
await processItem(priorityQueue.low.shift())
}
isProcessing = false
}
12.3 离线队列处理
javascript复制// 使用IndexedDB存储离线请求
const offlineQueue = new IDBWrapper('offline-requests')
service.interceptors.request.use(async config => {
if (!navigator.onLine && config.method !== 'get') {
await offlineQueue.add(config)
throw new axios.Cancel('离线请求已保存')
}
return config
})
// 网络恢复时处理队列
window.addEventListener('online', async () => {
const requests = await offlineQueue.getAll()
for (const config of requests) {
try {
await service(config)
await offlineQueue.delete(config.id)
} catch (error) {
console.error('离线请求重试失败:', error)
}
}
})
13. 服务端渲染适配
13.1 Cookie传递处理
javascript复制// 创建基于请求的实例
export function createSSRService(req) {
const instance = axios.create({
baseURL: 'https://api.example.com'
})
instance.interceptors.request.use(config => {
if (req && req.headers.cookie) {
config.headers.Cookie = req.headers.cookie
}
return config
})
return instance
}
13.2 避免内存泄漏
javascript复制// nuxt.js插件示例
export default function ({ $axios, req, res }) {
$axios.onRequest(config => {
if (process.server) {
config.headers.common['X-Forwarded-For'] =
req.headers['x-forwarded-for'] || req.connection.remoteAddress
}
})
$axios.onResponse(response => {
if (process.server && response.headers['set-cookie']) {
res.setHeader('set-cookie', response.headers['set-cookie'])
}
})
}
13.3 数据预取优化
javascript复制// Vue SSR数据预取
export async function prefetchData(route) {
const requests = route.matched.flatMap(record => {
return Object.values(record.components).map(component => {
const options = component.options || component
return options.asyncData && options.asyncData({
$axios: createSSRService()
})
})
})
return Promise.all(requests)
}
14. 可视化监控面板
14.1 实时请求监控
javascript复制const monitor = {
requests: [],
add(config) {
this.requests.push({
id: config.metadata.id,
url: config.url,
method: config.method,
startTime: config.metadata.startTime,
status: 'pending'
})
this.updateUI()
},
update(id, status) {
const req = this.requests.find(r => r.id === id)
if (req) {
req.status = status
req.endTime = Date.now()
this.updateUI()
}
},
updateUI() {
// 发送数据到监控面板
postMessageToMonitor({
type: 'requests-update',
data: this.requests
})
}
}
service.interceptors.request.use(config => {
config.metadata = {
id: generateId(),
startTime: Date.now()
}
monitor.add(config)
return config
})
service.interceptors.response.use(response => {
monitor.update(response.config.metadata.id, 'success')
return response
}, error => {
if (error.config) {
monitor.update(error.config.metadata.id, 'failed')
}
return Promise.reject(error)
})
14.2 性能热力图
javascript复制function collectPerformanceData() {
const data = {
endpoints: {},
slowThreshold: 1000
}
return {
record(config, duration) {
const endpoint = config.url.split('?')[0]
if (!data.endpoints[endpoint]) {
data.endpoints[endpoint] = {
count: 0,
totalDuration: 0,
slowCount: 0
}
}
const ep = data.endpoints[endpoint]
ep.count++
ep.totalDuration += duration
if (duration > data.slowThreshold) {
ep.slowCount++
}
},
getHeatmapData() {
return Object.entries(data.endpoints).map(([url, stats]) => ({
url,
avgDuration: stats.totalDuration / stats.count,
slowRate: stats.slowCount / stats.count
}))
}
}
}
14.3 异常告警系统
javascript复制const alertRules = {
errorRateThreshold: 0.1,
slowRateThreshold: 0.2
}
function checkForAlerts(stats) {
const errorRate = stats.failed / stats.total
const slowRate = stats.durations.filter(d => d > 1000).length / stats.durations.length
if (errorRate > alertRules.errorRateThreshold) {
triggerAlert(`API错误率过高: ${(errorRate * 100).toFixed(1)}%`)
}
if (slowRate > alertRules.slowRateThreshold) {
triggerAlert(`API慢请求比例过高: ${(slowRate * 100).toFixed(1)}%`)
}
}
15. 未来演进方向
15.1 渐进式迁移策略
当需要从Axios迁移到其他库时:
javascript复制// adapter.js
export function createHttpClient(options = {}) {
return options.useFetch
? new FetchAdapter(options)
: new AxiosAdapter(options)
}
class AxiosAdapter {
constructor(options) {
this.instance = axios.create(options)
// 保留原有拦截器逻辑...
}
get(url, config) {
return this.instance.get(url, config)
}
// 其他方法...
}
15.2 WebSocket集成
javascript复制service.interceptors.response.use(response => {
if (response.data?.wsToken) {
setupWebSocket(response.data.wsToken)
}
return response
})
function setupWebSocket(token) {
const ws = new WebSocket(`wss://api.example.com/ws?token=${token}`)
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
// 分发到各个处理模块
}
}
15.3 Serverless环境适配
javascript复制// AWS Lambda环境特殊处理
if (process.env.AWS_LAMBDA_FUNCTION_VERSION) {
service.defaults.adapter = async (config) => {
// 使用Lambda优化过的HTTP客户端
const { data } = await lambdaHttpClient.request(config)
return {
data,
status: 200,
statusText: 'OK',
headers: {},
config
}
}
}
