前端网络请求实战 | Axios 从入门到封装(拦截器 / 错误处理 / 重试)

一、为什么选择 Axios?

在项目开发中,网络请求是必不可少的一环。虽然浏览器提供了 Fetch API 和 XMLHttpRequest,但 Axios 凭借其强大的功能和友好的 API,成为最受欢迎的请求库。

1.1 Axios 核心优势

js 复制代码
// 1. 支持浏览器和 Node.js 环境
// 浏览器:XMLHttpRequest
// Node.js:http 模块

// 2. 自动转换 JSON 数据
axios.get('/api/user').then(res => {
  console.log(res.data) // 自动解析为 JavaScript 对象
})

// 3. 请求拦截和响应拦截
// 4. 取消请求
// 5. 超时处理
// 6. 并发请求
// 7. CSRF 防护
// 8. 上传/下载进度监控

二、Axios 基础入门

2.1 安装与引入

js 复制代码
# 使用 npm
npm install axios

# 使用 yarn
yarn add axios

# 使用 CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

2.2 基本请求方法

js 复制代码
import axios from 'axios'

// GET 请求
axios.get('/api/users', {
  params: {
    page: 1,
    limit: 10
  }
})
.then(response => {
  console.log('用户列表:', response.data)
})
.catch(error => {
  console.error('请求失败:', error)
})

// POST 请求
axios.post('/api/users', {
  name: '张三',
  email: 'zhangsan@example.com',
  age: 25
})
.then(response => {
  console.log('创建成功:', response.data)
})

// PUT 请求(更新)
axios.put('/api/users/1', {
  name: '张三丰',
  age: 26
})

// DELETE 请求
axios.delete('/api/users/1')

// PATCH 请求(部分更新)
axios.patch('/api/users/1', {
  age: 27
})

2.3 请求配置详解

js 复制代码
axios({
  method: 'post',                    // 请求方法
  url: '/api/users',                  // 请求地址
  baseURL: 'https://api.example.com', // 基础URL
  headers: {                           // 请求头
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  params: {                            // URL参数
    page: 1,
    limit: 10
  },
  data: {                              // 请求体
    name: '张三'
  },
  timeout: 5000,                       // 超时时间(ms)
  withCredentials: true,                // 跨域请求时携带cookie
  responseType: 'json',                 // 响应数据类型
  maxContentLength: 2000,               // 最大响应长度
  validateStatus: function (status) {   // 定义哪些状态码是成功的
    return status >= 200 && status < 300
  },
  proxy: {                              // 代理配置
    host: '127.0.0.1',
    port: 9000
  }
})

2.4 响应数据结构

js 复制代码
axios.get('/api/user').then(response => {
  // response 对象包含:
  console.log(response.data)       // 服务器返回的数据
  console.log(response.status)     // HTTP 状态码
  console.log(response.statusText) // 状态消息
  console.log(response.headers)    // 响应头
  console.log(response.config)     // 请求配置
  console.log(response.request)    // 原生XMLHttpRequest对象
})

三、项目中的 Axios 封装

在实际项目中,我们通常会对 Axios 进行二次封装,统一处理请求配置、拦截器、错误处理等。

3.1 基础封装结构

js 复制代码
// service/index.js
import axios from 'axios'

class RequestService {
  constructor() {
    // 创建 axios 实例
    this.service = axios.create({
      baseURL: process.env.VUE_APP_BASE_API || '/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json;charset=UTF-8'
      }
    })
    
    // 初始化拦截器
    this.setupInterceptors()
  }
  
  setupInterceptors() {
    // 请求拦截器
    this.service.interceptors.request.use(
      this.handleRequestSuccess,
      this.handleRequestError
    )
    
    // 响应拦截器
    this.service.interceptors.response.use(
      this.handleResponseSuccess,
      this.handleResponseError
    )
  }
  
  handleRequestSuccess(config) {
    console.log('请求配置:', config)
    return config
  }
  
  handleRequestError(error) {
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
  
  handleResponseSuccess(response) {
    console.log('响应数据:', response)
    return response
  }
  
  handleResponseError(error) {
    console.error('响应错误:', error)
    return Promise.reject(error)
  }
  
  // 请求方法封装
  get(url, params = {}, config = {}) {
    return this.service.get(url, { params, ...config })
  }
  
  post(url, data = {}, config = {}) {
    return this.service.post(url, data, config)
  }
  
  put(url, data = {}, config = {}) {
    return this.service.put(url, data, config)
  }
  
  delete(url, params = {}, config = {}) {
    return this.service.delete(url, { params, ...config })
  }
  
  patch(url, data = {}, config = {}) {
    return this.service.patch(url, data, config)
  }
}

export default new RequestService()

3.2 完整的拦截器实现

js 复制代码
// service/interceptors.js
import { message, Modal } from 'ant-design-vue'
import router from '@/router'
import store from '@/store'

// 请求拦截器
export function requestSuccess(config) {
  // 1. 添加 token
  const token = store.state.user.token
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`
  }
  
  // 2. 添加时间戳防止缓存(GET请求)
  if (config.method === 'get') {
    config.params = {
      ...config.params,
      _t: Date.now()
    }
  }
  
  // 3. 请求日志(开发环境)
  if (process.env.NODE_ENV === 'development') {
    console.log('🚀 请求信息:', {
      url: config.url,
      method: config.method,
      params: config.params,
      data: config.data,
      headers: config.headers
    })
  }
  
  return config
}

export function requestError(error) {
  console.error('❌ 请求发送失败:', error)
  message.error('网络请求失败,请检查网络连接')
  return Promise.reject(error)
}

// 响应拦截器
export function responseSuccess(response) {
  // 可以统一处理业务状态码
  const { code, data, message: msg } = response.data
  
  // 根据后端约定的状态码处理
  switch (code) {
    case 200: // 成功
      return data
    case 401: // 未授权
      handleUnauthorized()
      return Promise.reject(new Error('未授权,请重新登录'))
    case 403: // 禁止访问
      message.error('没有权限访问')
      return Promise.reject(new Error('禁止访问'))
    case 500: // 服务器错误
      message.error('服务器错误,请稍后重试')
      return Promise.reject(new Error('服务器错误'))
    default:
      // 其他错误
      message.error(msg || '请求失败')
      return Promise.reject(new Error(msg || '请求失败'))
  }
}

export function responseError(error) {
  // 处理 HTTP 状态码错误
  if (error.response) {
    // 服务器返回了错误状态码
    const { status, data } = error.response
    
    switch (status) {
      case 400:
        message.error(data?.message || '请求参数错误')
        break
      case 401:
        handleUnauthorized()
        break
      case 403:
        message.error('没有权限访问')
        break
      case 404:
        message.error('请求的资源不存在')
        break
      case 500:
        message.error('服务器内部错误')
        break
      case 502:
        message.error('网关错误')
        break
      case 503:
        message.error('服务不可用')
        break
      case 504:
        message.error('网关超时')
        break
      default:
        message.error(`网络错误: ${status}`)
    }
  } else if (error.request) {
    // 请求已发送但没有收到响应
    message.error('服务器无响应,请检查网络')
  } else {
    // 请求配置出错
    message.error('请求配置错误')
  }
  
  return Promise.reject(error)
}

// 处理未授权
function handleUnauthorized() {
  Modal.confirm({
    title: '登录已过期',
    content: '您的登录信息已过期,请重新登录',
    okText: '去登录',
    cancelText: '取消',
    onOk: () => {
      store.dispatch('user/logout')
      router.push('/login')
    }
  })
}

3.3 增强版封装(支持取消请求、重试)

js 复制代码
// service/advanced.js
import axios from 'axios'
import qs from 'qs'

class AdvancedRequest {
  constructor() {
    this.service = axios.create({
      baseURL: process.env.VUE_APP_API_URL,
      timeout: 30000,
      paramsSerializer: params => {
        // 处理复杂参数序列化
        return qs.stringify(params, { indices: false })
      }
    })
    
    // 存储取消请求的控制器
    this.pendingRequests = new Map()
    
    this.setupInterceptors()
  }
  
  setupInterceptors() {
    // 请求拦截器
    this.service.interceptors.request.use(
      config => {
        // 添加取消请求功能
        this.addCancelToken(config)
        
        // 请求签名
        if (config.needSign) {
          config.data = this.signRequest(config)
        }
        
        // 加密敏感数据
        if (config.encrypt) {
          config.data = this.encryptData(config.data)
        }
        
        return config
      },
      error => Promise.reject(error)
    )
    
    // 响应拦截器
    this.service.interceptors.response.use(
      response => {
        // 请求完成后移除 pending 记录
        this.removePendingRequest(response.config)
        
        // 处理文件下载
        if (response.config.responseType === 'blob') {
          return this.handleFileResponse(response)
        }
        
        return response.data
      },
      error => {
        // 如果是取消请求,不处理错误
        if (axios.isCancel(error)) {
          console.log('请求已取消:', error.message)
          return Promise.reject(error)
        }
        
        // 移除 pending 记录
        if (error.config) {
          this.removePendingRequest(error.config)
        }
        
        return Promise.reject(error)
      }
    )
  }
  
  // 取消请求管理
  addCancelToken(config) {
    // 避免重复请求
    const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
    
    // 如果已有相同请求,取消之前的请求
    if (this.pendingRequests.has(requestKey)) {
      const cancel = this.pendingRequests.get(requestKey)
      cancel('重复请求已取消')
      this.pendingRequests.delete(requestKey)
    }
    
    // 创建新的取消令牌
    config.cancelToken = new axios.CancelToken(cancel => {
      this.pendingRequests.set(requestKey, cancel)
    })
  }
  
  removePendingRequest(config) {
    const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
    if (this.pendingRequests.has(requestKey)) {
      this.pendingRequests.delete(requestKey)
    }
  }
  
  // 取消所有请求
  cancelAllRequests() {
    this.pendingRequests.forEach(cancel => cancel('主动取消所有请求'))
    this.pendingRequests.clear()
  }
  
  // 带重试机制的请求
  async requestWithRetry(config, retries = 3) {
    let lastError
    
    for (let i = 0; i < retries; i++) {
      try {
        const response = await this.service(config)
        return response
      } catch (error) {
        lastError = error
        
        // 是否应该重试
        if (this.shouldRetry(error, i, retries)) {
          // 指数退避延迟
          const delay = Math.pow(2, i) * 1000
          console.log(`第${i + 1}次请求失败,${delay}ms后重试...`)
          await this.sleep(delay)
          continue
        }
        break
      }
    }
    
    throw lastError
  }
  
  shouldRetry(error, currentRetry, maxRetries) {
    // 只有特定错误才重试
    if (axios.isCancel(error)) return false
    
    // 网络错误或超时重试
    const retryableErrors = [
      'ECONNABORTED',  // 超时
      'ETIMEDOUT',      // 连接超时
      'ECONNREFUSED',   // 连接被拒绝
      'ECONNRESET',     // 连接重置
      'ENOTFOUND'       // DNS解析失败
    ]
    
    const shouldRetry = (
      currentRetry < maxRetries - 1 &&
      (error.code && retryableErrors.includes(error.code)) ||
      (error.response && error.response.status >= 500)
    )
    
    return shouldRetry
  }
  
  // 文件上传(支持进度)
  uploadFile(url, file, onProgress) {
    const formData = new FormData()
    formData.append('file', file)
    
    return this.service.post(url, formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      onUploadProgress: progressEvent => {
        if (onProgress) {
          const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
          onProgress(percentCompleted)
        }
      }
    })
  }
  
  // 文件下载
  async downloadFile(url, filename) {
    const response = await this.service.get(url, {
      responseType: 'blob'
    })
    
    // 创建下载链接
    const blob = new Blob([response.data])
    const downloadUrl = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = filename || 'download'
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    window.URL.revokeObjectURL(downloadUrl)
  }
  
  handleFileResponse(response) {
    const contentDisposition = response.headers['content-disposition']
    let filename = 'download'
    
    if (contentDisposition) {
      const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
      if (match && match[1]) {
        filename = match[1].replace(/['"]/g, '')
        // 处理中文文件名
        try {
          filename = decodeURIComponent(escape(filename))
        } catch (e) {
          console.error('文件名解码失败', e)
        }
      }
    }
    
    return {
      data: response.data,
      filename,
      type: response.headers['content-type']
    }
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
  
  signRequest(config) {
    // 实现请求签名逻辑
    const timestamp = Date.now()
    const nonce = Math.random().toString(36).substring(7)
    const data = { ...config.data, timestamp, nonce }
    
    // 签名计算...
    // data.sign = generateSignature(data)
    
    return data
  }
  
  encryptData(data) {
    // 实现数据加密逻辑
    return data
  }
}

export default new AdvancedRequest()

四、业务层封装

4.1 API 模块化管理

js 复制代码
// api/modules/user.js
import request from '@/service'

// 用户相关接口
export const userApi = {
  // 登录
  login(data) {
    return request.post('/auth/login', data, {
      needSign: true  // 需要签名
    })
  },
  
  // 登出
  logout() {
    return request.post('/auth/logout')
  },
  
  // 获取用户信息
  getUserInfo() {
    return request.get('/user/info', {}, {
      retry: 3  // 失败重试3次
    })
  },
  
  // 更新用户信息
  updateUserInfo(data) {
    return request.put('/user/info', data)
  },
  
  // 上传头像
  uploadAvatar(file, onProgress) {
    return request.uploadFile('/user/avatar', file, onProgress)
  },
  
  // 获取用户列表
  getUserList(params) {
    return request.get('/user/list', params, {
      cache: true  // 启用缓存
    })
  },
  
  // 导出用户数据
  exportUsers(params) {
    return request.get('/user/export', params, {
      responseType: 'blob'
    })
  }
}

// api/modules/product.js
export const productApi = {
  getProductList(params) {
    return request.get('/product/list', params)
  },
  
  getProductDetail(id) {
    return request.get(`/product/detail/${id}`)
  },
  
  createProduct(data) {
    return request.post('/product', data)
  },
  
  updateProduct(id, data) {
    return request.put(`/product/${id}`, data)
  },
  
  deleteProduct(id) {
    return request.delete(`/product/${id}`)
  }
}

// api/index.js
export { userApi } from './modules/user'
export { productApi } from './modules/product'

4.2 请求缓存管理

js 复制代码
// service/cache.js
class RequestCache {
  constructor() {
    this.cache = new Map()
    this.maxAge = 5 * 60 * 1000 // 默认5分钟
  }
  
  // 生成缓存key
  generateKey(config) {
    return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
  }
  
  // 设置缓存
  set(key, data, maxAge = this.maxAge) {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      maxAge
    })
  }
  
  // 获取缓存
  get(key) {
    const cached = this.cache.get(key)
    if (!cached) return null
    
    // 检查是否过期
    if (Date.now() - cached.timestamp > cached.maxAge) {
      this.cache.delete(key)
      return null
    }
    
    return cached.data
  }
  
  // 清除缓存
  clear() {
    this.cache.clear()
  }
  
  // 清除指定缓存
  delete(key) {
    this.cache.delete(key)
  }
  
  // 清除匹配模式的缓存
  clearPattern(pattern) {
    const regex = new RegExp(pattern)
    for (const key of this.cache.keys()) {
      if (regex.test(key)) {
        this.cache.delete(key)
      }
    }
  }
}

export default new RequestCache()

4.3 在 Vue/React 中使用

js 复制代码
// Vue 3 中使用
import { userApi } from '@/api'
import { ref, onMounted } from 'vue'

export default {
  setup() {
    const userList = ref([])
    const loading = ref(false)
    
    const fetchUserList = async () => {
      loading.value = true
      try {
        const res = await userApi.getUserList({
          page: 1,
          limit: 10
        })
        userList.value = res
      } catch (error) {
        console.error('获取用户列表失败:', error)
      } finally {
        loading.value = false
      }
    }
    
    onMounted(() => {
      fetchUserList()
    })
    
    return {
      userList,
      loading,
      fetchUserList
    }
  }
}

// React 中使用
import { useState, useEffect } from 'react'
import { userApi } from '@/api'

function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(false)
  
  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true)
      try {
        const data = await userApi.getUserList()
        setUsers(data)
      } catch (error) {
        console.error('Failed to fetch users:', error)
      } finally {
        setLoading(false)
      }
    }
    
    fetchUsers()
  }, [])
  
  return (
    <div>
      {loading ? <Spin /> : (
        <Table dataSource={users} />
      )}
    </div>
  )
}

五、高级功能实现

5.1 请求队列管理

js 复制代码
// service/queue.js
class RequestQueue {
  constructor(concurrency = 5) {
    this.concurrency = concurrency
    this.queue = []
    this.running = 0
  }
  
  // 添加请求到队列
  add(request) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        request,
        resolve,
        reject
      })
      this.next()
    })
  }
  
  // 执行下一个请求
  next() {
    while (this.running < this.concurrency && this.queue.length) {
      const { request, resolve, reject } = this.queue.shift()
      this.running++
      
      request()
        .then(resolve)
        .catch(reject)
        .finally(() => {
          this.running--
          this.next()
        })
    }
  }
  
  // 清空队列
  clear() {
    this.queue = []
    this.running = 0
  }
  
  // 获取队列状态
  getStatus() {
    return {
      queueLength: this.queue.length,
      running: this.running,
      concurrency: this.concurrency
    }
  }
}

export default RequestQueue

5.2 请求节流防抖

js 复制代码
// service/throttle.js
class RequestThrottle {
  constructor() {
    this.pendingRequests = new Map()
  }
  
  // 防抖:最后一次请求有效
  debounce(key, fn, delay = 300) {
    if (this.pendingRequests.has(key)) {
      clearTimeout(this.pendingRequests.get(key))
    }
    
    const timeout = setTimeout(() => {
      fn()
      this.pendingRequests.delete(key)
    }, delay)
    
    this.pendingRequests.set(key, timeout)
  }
  
  // 节流:限制请求频率
  throttle(key, fn, limit = 1000) {
    const now = Date.now()
    const lastCall = this.pendingRequests.get(key)
    
    if (!lastCall || now - lastCall > limit) {
      fn()
      this.pendingRequests.set(key, now)
    }
  }
  
  // 取消所有待执行的请求
  cancelAll() {
    this.pendingRequests.forEach(timeout => {
      clearTimeout(timeout)
    })
    this.pendingRequests.clear()
  }
}

export default new RequestThrottle()

5.3 断网重连机制

js 复制代码
// service/reconnect.js
class ReconnectManager {
  constructor(requestService) {
    this.requestService = requestService
    this.isOnline = navigator.onLine
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
    this.pendingRequests = []
    
    this.initEventListeners()
  }
  
  initEventListeners() {
    window.addEventListener('online', () => {
      this.handleOnline()
    })
    
    window.addEventListener('offline', () => {
      this.handleOffline()
    })
  }
  
  handleOnline() {
    console.log('网络已恢复,开始重连...')
    this.isOnline = true
    this.reconnectAttempts = 0
    
    // 重试所有待处理的请求
    this.processPendingRequests()
  }
  
  handleOffline() {
    console.log('网络已断开')
    this.isOnline = false
  }
  
  async processPendingRequests() {
    while (this.pendingRequests.length > 0) {
      const request = this.pendingRequests.shift()
      try {
        const result = await this.requestService(request.config)
        request.resolve(result)
      } catch (error) {
        request.reject(error)
      }
    }
  }
  
  // 添加请求到待处理队列
  addPendingRequest(config) {
    return new Promise((resolve, reject) => {
      this.pendingRequests.push({
        config,
        resolve,
        reject
      })
      
      // 尝试重新连接
      this.attemptReconnect()
    })
  }
  
  attemptReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.log('重连失败次数过多,请手动刷新')
      return
    }
    
    this.reconnectAttempts++
    setTimeout(() => {
      if (navigator.onLine) {
        this.handleOnline()
      }
    }, Math.pow(2, this.reconnectAttempts) * 1000)
  }
}

六、错误处理与日志

6.1 统一错误处理

js 复制代码
// service/errorHandler.js
class ErrorHandler {
  constructor() {
    this.errorListeners = []
  }
  
  // 处理错误
  handle(error, context = {}) {
    // 格式化错误信息
    const errorInfo = this.formatError(error, context)
    
    // 记录错误日志
    this.logError(errorInfo)
    
    // 触发错误监听器
    this.notifyListeners(errorInfo)
    
    // 根据错误类型进行处理
    this.processByType(errorInfo)
    
    return errorInfo
  }
  
  formatError(error, context) {
    return {
      timestamp: new Date().toISOString(),
      type: this.getErrorType(error),
      message: error.message,
      code: error.code,
      status: error.response?.status,
      url: error.config?.url,
      method: error.config?.method,
      params: error.config?.params,
      data: error.config?.data,
      stack: error.stack,
      context
    }
  }
  
  getErrorType(error) {
    if (error.response) {
      // 服务器返回错误状态码
      const status = error.response.status
      if (status >= 500) return 'SERVER_ERROR'
      if (status === 401) return 'UNAUTHORIZED'
      if (status === 403) return 'FORBIDDEN'
      if (status === 404) return 'NOT_FOUND'
      if (status >= 400) return 'CLIENT_ERROR'
    } else if (error.request) {
      // 请求已发送但没有响应
      return 'NETWORK_ERROR'
    } else {
      // 请求配置错误
      return 'CONFIG_ERROR'
    }
    return 'UNKNOWN_ERROR'
  }
  
  logError(errorInfo) {
    // 开发环境打印到控制台
    if (process.env.NODE_ENV === 'development') {
      console.group('❌ 请求错误')
      console.log('时间:', errorInfo.timestamp)
      console.log('类型:', errorInfo.type)
      console.log('信息:', errorInfo.message)
      console.log('状态码:', errorInfo.status)
      console.log('URL:', errorInfo.url)
      console.log('方法:', errorInfo.method)
      console.log('参数:', errorInfo.params)
      console.log('数据:', errorInfo.data)
      console.trace('堆栈:', errorInfo.stack)
      console.groupEnd()
    }
    
    // 生产环境发送到日志服务
    if (process.env.NODE_ENV === 'production') {
      this.sendToLogService(errorInfo)
    }
  }
  
  sendToLogService(errorInfo) {
    // 发送错误日志到服务器
    fetch('/api/log/error', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorInfo),
      keepalive: true // 即使页面卸载也发送
    }).catch(() => {
      // 静默失败
    })
  }
  
  processByType(errorInfo) {
    switch (errorInfo.type) {
      case 'UNAUTHORIZED':
        // 跳转到登录页
        this.redirectToLogin()
        break
      case 'SERVER_ERROR':
        // 显示服务器错误提示
        this.showErrorMessage('服务器开小差了,请稍后重试')
        break
      case 'NETWORK_ERROR':
        // 显示网络错误提示
        this.showErrorMessage('网络连接失败,请检查网络设置')
        break
      default:
        // 显示通用错误提示
        this.showErrorMessage('操作失败,请重试')
    }
  }
  
  addListener(listener) {
    this.errorListeners.push(listener)
  }
  
  notifyListeners(errorInfo) {
    this.errorListeners.forEach(listener => {
      try {
        listener(errorInfo)
      } catch (e) {
        console.error('Error listener failed:', e)
      }
    })
  }
  
  redirectToLogin() {
    // 跳转到登录页
    if (window.location.pathname !== '/login') {
      window.location.href = '/login'
    }
  }
  
  showErrorMessage(message) {
    // 使用UI库的提示组件
    if (window.$message) {
      window.$message.error(message)
    } else {
      alert(message)
    }
  }
}

export default new ErrorHandler()

6.2 请求监控

js 复制代码
// service/monitor.js
class RequestMonitor {
  constructor() {
    this.metrics = {
      totalRequests: 0,
      successRequests: 0,
      failedRequests: 0,
      totalTime: 0,
      slowRequests: [],
      errorStats: {}
    }
    
    this.slowThreshold = 3000 // 慢请求阈值(ms)
  }
  
  // 记录请求开始
  startRequest(config) {
    const requestId = this.generateRequestId()
    const startTime = Date.now()
    
    config.metadata = {
      requestId,
      startTime
    }
    
    return config
  }
  
  // 记录请求结束
  endRequest(config, response, error) {
    const endTime = Date.now()
    const startTime = config.metadata?.startTime || endTime
    const duration = endTime - startTime
    
    // 更新总请求数
    this.metrics.totalRequests++
    
    if (error) {
      // 记录失败请求
      this.metrics.failedRequests++
      this.recordError(config, error)
    } else {
      // 记录成功请求
      this.metrics.successRequests++
      this.metrics.totalTime += duration
    }
    
    // 检查慢请求
    if (duration > this.slowThreshold) {
      this.recordSlowRequest(config, duration, error)
    }
    
    // 打印性能日志
    this.logPerformance(config, duration, error)
    
    // 清理metadata
    delete config.metadata
  }
  
  recordError(config, error) {
    const errorType = error.response?.status || 'NETWORK_ERROR'
    this.metrics.errorStats[errorType] = (this.metrics.errorStats[errorType] || 0) + 1
  }
  
  recordSlowRequest(config, duration, error) {
    this.metrics.slowRequests.push({
      url: config.url,
      method: config.method,
      duration,
      timestamp: new Date().toISOString(),
      success: !error,
      error: error?.message
    })
    
    // 保留最近100条慢请求记录
    if (this.metrics.slowRequests.length > 100) {
      this.metrics.slowRequests.shift()
    }
  }
  
  logPerformance(config, duration, error) {
    const status = error ? '❌' : '✅'
    const slow = duration > this.slowThreshold ? '🐢' : ''
    
    console.log(
      `${status} ${slow} [${config.method.toUpperCase()}] ${config.url} - ${duration}ms`
    )
  }
  
  generateRequestId() {
    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  }
  
  // 获取监控报告
  getReport() {
    const avgTime = this.metrics.successRequests > 0
      ? Math.round(this.metrics.totalTime / this.metrics.successRequests)
      : 0
    
    return {
      ...this.metrics,
      avgTime,
      successRate: this.metrics.totalRequests > 0
        ? `${Math.round((this.metrics.successRequests / this.metrics.totalRequests) * 100)}%`
        : '0%'
    }
  }
  
  // 重置监控数据
  reset() {
    this.metrics = {
      totalRequests: 0,
      successRequests: 0,
      failedRequests: 0,
      totalTime: 0,
      slowRequests: [],
      errorStats: {}
    }
  }
}

export default new RequestMonitor()

七、测试与调试

7.1 单元测试

js 复制代码
// service/__tests__/request.test.js
import MockAdapter from 'axios-mock-adapter'
import request from '../index'
import axios from 'axios'

describe('Request Service', () => {
  let mock
  
  beforeEach(() => {
    mock = new MockAdapter(axios)
  })
  
  afterEach(() => {
    mock.reset()
  })
  
  test('should handle GET request successfully', async () => {
    const mockData = { id: 1, name: '张三' }
    mock.onGet('/api/user/1').reply(200, mockData)
    
    const result = await request.get('/api/user/1')
    expect(result).toEqual(mockData)
  })
  
  test('should handle request error', async () => {
    mock.onGet('/api/user/1').reply(500)
    
    await expect(request.get('/api/user/1')).rejects.toThrow()
  })
  
  test('should add token to headers', async () => {
    const token = 'test-token'
    localStorage.setItem('token', token)
    
    mock.onGet('/api/user').reply(config => {
      expect(config.headers.Authorization).toBe(`Bearer ${token}`)
      return [200, {}]
    })
    
    await request.get('/api/user')
  })
  
  test('should handle timeout', async () => {
    mock.onGet('/api/user').timeout()
    
    await expect(request.get('/api/user')).rejects.toThrow('timeout')
  }, 10000)
})

7.2 调试技巧

js 复制代码
// 调试配置
if (process.env.NODE_ENV === 'development') {
  // 开启调试模式
  axios.defaults.debug = true
  
  // 拦截所有请求并打印详细信息
  axios.interceptors.request.use(config => {
    console.group(`🌐 请求 [${config.method}] ${config.url}`)
    console.log('参数:', config.params)
    console.log('数据:', config.data)
    console.log('头信息:', config.headers)
    console.groupEnd()
    return config
  })
  
  // 模拟慢网络
  if (process.env.VUE_APP_SLOW_NETWORK) {
    axios.interceptors.request.use(async config => {
      await new Promise(resolve => setTimeout(resolve, 2000))
      return config
    })
  }
  
  // 模拟随机失败
  if (process.env.VUE_APP_RANDOM_FAIL) {
    axios.interceptors.response.use(
      response => response,
      error => {
        if (Math.random() < 0.1) { // 10% 概率失败
          return Promise.reject(new Error('模拟网络错误'))
        }
        return Promise.reject(error)
      }
    )
  }
}

八、最佳实践总结

8.1 项目结构推荐

text 复制代码
src/
├── api/
│   ├── modules/
│   │   ├── user.js
│   │   ├── product.js
│   │   └── order.js
│   ├── index.js
│   └── config.js
├── service/
│   ├── index.js           # 请求服务主入口
│   ├── interceptors.js    # 拦截器
│   ├── errorHandler.js    # 错误处理
│   ├── cache.js          # 缓存管理
│   ├── monitor.js        # 监控
│   └── utils.js          # 工具函数
└── utils/
    └── request.js        # 导出封装的请求方法

8.2 配置管理

js 复制代码
// service/config.js
const config = {
  development: {
    baseURL: 'http://localhost:3000/api',
    timeout: 10000,
    withCredentials: true
  },
  test: {
    baseURL: 'https://test-api.example.com/api',
    timeout: 15000
  },
  production: {
    baseURL: 'https://api.example.com/api',
    timeout: 30000,
    withCredentials: true
  }
}

export default config[process.env.NODE_ENV || 'development']

8.3 安全建议

js 复制代码
// 1. 防止 CSRF 攻击
axios.defaults.xsrfCookieName = 'csrf-token'
axios.defaults.xsrfHeaderName = 'X-CSRF-Token'

// 2. 敏感信息加密
import CryptoJS from 'crypto-js'

function encryptRequest(data) {
  const key = CryptoJS.enc.Utf8.parse(process.env.VUE_APP_SECRET_KEY)
  const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  })
  return encrypted.toString()
}

// 3. HTTPS 强制
if (window.location.protocol !== 'https:' && process.env.NODE_ENV === 'production') {
  window.location.href = 'https://' + window.location.host + window.location.pathname
}

九、常见面试题

Q1: 如何取消重复请求?

js 复制代码
// 使用 CancelToken
const cancelTokenSource = axios.CancelToken.source()

axios.get('/api/user', {
  cancelToken: cancelTokenSource.token
})

// 取消请求
cancelTokenSource.cancel('操作取消')

Q2: 如何实现请求缓存?

js 复制代码
// 使用 Map 存储响应结果
const cache = new Map()

async function requestWithCache(url) {
  if (cache.has(url)) {
    return cache.get(url)
  }
  
  const response = await axios.get(url)
  cache.set(url, response.data)
  return response.data
}

Q3: 如何统一处理错误?

js 复制代码
// 响应拦截器中统一处理
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          // 处理未授权
          break
        case 404:
          // 处理未找到
          break
        case 500:
          // 处理服务器错误
          break
      }
    }
    return Promise.reject(error)
  }
)

Q4: 如何监控请求性能?

js 复制代码
// 使用 Performance API
const start = performance.now()

axios.get('/api/data').then(() => {
  const end = performance.now()
  console.log(`请求耗时: ${end - start}ms`)
})

十、总结

Axios 封装的核心要点:

  1. 统一配置:baseURL、超时、请求头等
  2. 拦截器:请求/响应拦截,统一处理 token、日志、错误
  3. 错误处理:分类处理、友好提示、日志记录
  4. 取消请求:防止重复提交、组件卸载时取消
  5. 重试机制:网络错误时自动重试
  6. 缓存管理:减少重复请求
  7. 监控告警:性能监控、错误上报
  8. 测试覆盖:单元测试保证稳定性

通过合理的封装,可以让项目中的网络请求更加健壮、可维护,提升开发效率和用户体验。

相关推荐
进击的尘埃1 小时前
组合式函数 Composables 的设计模式:如何写出可复用的 Vue3 Hooks
javascript
进击的尘埃2 小时前
浏览器渲染管线深度拆解:从 Parse HTML 到 Composite Layers 的每一帧发生了什么
javascript
大雨还洅下2 小时前
前端手写: Promise封装Ajax
javascript
codeniu2 小时前
@logicflow/vue-node-registry 在 Vite 中无法解析的踩坑记录与解决方案
前端·javascript
Heo2 小时前
深入 React19 Diff 算法
前端·javascript·面试
滕青山2 小时前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
颜酱3 小时前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
炫饭第一名3 小时前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune13 小时前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript