Skip to content

Latest commit

 

History

History
777 lines (659 loc) · 16.9 KB

File metadata and controls

777 lines (659 loc) · 16.9 KB

request.js HTTP请求工具文档

概述

request.js是基于axios封装的HTTP请求工具,提供统一的请求配置、拦截器处理、错误处理等功能。

文件位置: src/utils/request.js

核心功能

1. 基础配置

import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/stores/user'
import router from '@/router'

// 创建axios实例
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

// 请求配置
const requestConfig = {
  // 基础URL
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  
  // 超时时间
  timeout: 10000,
  
  // 是否携带凭证
  withCredentials: true,
  
  // 请求头
  headers: {
    'Content-Type': 'application/json;charset=UTF-8',
    'X-Requested-With': 'XMLHttpRequest'
  },
  
  // 响应类型
  responseType: 'json',
  
  // 最大重定向次数
  maxRedirects: 5,
  
  // 最大内容长度
  maxContentLength: 2000,
  
  // 最大请求体长度
  maxBodyLength: 2000
}

// 应用配置
Object.assign(request.defaults, requestConfig)

2. 请求拦截器

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    // 添加时间戳防止缓存
    if (config.method === 'get') {
      config.params = {
        ...config.params,
        _t: Date.now()
      }
    }
    
    // 添加认证Token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = `Bearer ${userStore.token}`
    }
    
    // 添加请求ID用于追踪
    config.headers['X-Request-ID'] = generateRequestId()
    
    // 添加语言标识
    config.headers['Accept-Language'] = getLanguage()
    
    // 请求开始时间
    config.startTime = Date.now()
    
    // 显示加载状态
    if (config.showLoading !== false) {
      showLoading()
    }
    
    // 请求去重处理
    const requestKey = generateRequestKey(config)
    if (pendingRequests.has(requestKey)) {
      const cancelToken = pendingRequests.get(requestKey)
      cancelToken.cancel('重复请求')
    }
    
    // 创建取消令牌
    const source = axios.CancelToken.source()
    config.cancelToken = source.token
    pendingRequests.set(requestKey, source)
    
    return config
  },
  (error) => {
    hideLoading()
    console.error('请求拦截器错误:', error)
    return Promise.reject(error)
  }
)

3. 响应拦截器

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    const config = response.config
    
    // 计算请求耗时
    const duration = Date.now() - config.startTime
    console.log(`API请求耗时: ${config.url} - ${duration}ms`)
    
    // 隐藏加载状态
    if (config.showLoading !== false) {
      hideLoading()
    }
    
    // 移除待处理请求
    const requestKey = generateRequestKey(config)
    pendingRequests.delete(requestKey)
    
    // 处理响应数据
    const { data } = response
    
    // 统一响应格式处理
    if (data && typeof data === 'object') {
      // 检查业务状态码
      if (data.status === false) {
        handleBusinessError(data, config)
        return Promise.reject(new Error(data.msg || '请求失败'))
      }
      
      // 返回数据
      return data
    }
    
    // 直接返回响应数据
    return data
  },
  (error) => {
    const config = error.config
    
    // 隐藏加载状态
    if (config && config.showLoading !== false) {
      hideLoading()
    }
    
    // 移除待处理请求
    if (config) {
      const requestKey = generateRequestKey(config)
      pendingRequests.delete(requestKey)
    }
    
    // 处理HTTP错误
    handleHttpError(error)
    
    return Promise.reject(error)
  }
)

4. 错误处理

// 处理HTTP错误
const handleHttpError = (error) => {
  if (axios.isCancel(error)) {
    console.log('请求被取消:', error.message)
    return
  }
  
  const { response, code, message } = error
  
  if (response) {
    // 服务器响应错误
    const { status, data } = response
    
    switch (status) {
      case 400:
        ElMessage.error(data?.msg || '请求参数错误')
        break
      case 401:
        handleUnauthorized()
        break
      case 403:
        ElMessage.error('权限不足,拒绝访问')
        break
      case 404:
        ElMessage.error('请求的资源不存在')
        break
      case 408:
        ElMessage.error('请求超时')
        break
      case 500:
        ElMessage.error('服务器内部错误')
        break
      case 502:
        ElMessage.error('网关错误')
        break
      case 503:
        ElMessage.error('服务不可用')
        break
      case 504:
        ElMessage.error('网关超时')
        break
      default:
        ElMessage.error(data?.msg || `请求失败 (${status})`)
    }
  } else if (code === 'ECONNABORTED') {
    // 请求超时
    ElMessage.error('请求超时,请稍后重试')
  } else if (code === 'ERR_NETWORK') {
    // 网络错误
    ElMessage.error('网络连接失败,请检查网络设置')
  } else {
    // 其他错误
    ElMessage.error(message || '网络请求失败')
  }
  
  // 发送错误报告
  reportError(error)
}

// 处理业务错误
const handleBusinessError = (data, config) => {
  const { code, msg } = data
  
  // 根据业务错误码处理
  switch (code) {
    case 40001:
      ElMessage.error('参数验证失败')
      break
    case 40003:
      ElMessage.error('数据不存在')
      break
    case 50001:
      ElMessage.error('服务器内部错误')
      break
    default:
      if (config.showError !== false) {
        ElMessage.error(msg || '操作失败')
      }
  }
}

// 处理未授权
const handleUnauthorized = () => {
  const userStore = useUserStore()
  
  ElMessageBox.confirm(
    '登录状态已过期,您可以继续留在该页面,或者重新登录',
    '系统提示',
    {
      confirmButtonText: '重新登录',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(() => {
    userStore.logout()
  }).catch(() => {
    // 用户取消
  })
}

5. 请求工具方法

// 生成请求ID
const generateRequestId = () => {
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}

// 生成请求键
const generateRequestKey = (config) => {
  const { method, url, params, data } = config
  return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`
}

// 获取语言设置
const getLanguage = () => {
  return localStorage.getItem('language') || 'zh-CN'
}

// 显示加载状态
let loadingInstance = null
let loadingCount = 0

const showLoading = () => {
  loadingCount++
  if (loadingCount === 1) {
    loadingInstance = ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
}

// 隐藏加载状态
const hideLoading = () => {
  loadingCount--
  if (loadingCount <= 0) {
    loadingCount = 0
    if (loadingInstance) {
      loadingInstance.close()
      loadingInstance = null
    }
  }
}

// 待处理请求映射
const pendingRequests = new Map()

// 取消所有待处理请求
const cancelAllRequests = () => {
  pendingRequests.forEach((cancelToken) => {
    cancelToken.cancel('页面切换,取消请求')
  })
  pendingRequests.clear()
}

6. 请求重试机制

// 请求重试配置
const retryConfig = {
  retries: 3,           // 重试次数
  retryDelay: 1000,     // 重试延迟
  retryCondition: (error) => {
    // 重试条件
    return error.code === 'ECONNABORTED' || 
           error.code === 'ERR_NETWORK' ||
           (error.response && error.response.status >= 500)
  }
}

// 添加重试拦截器
request.interceptors.response.use(
  (response) => response,
  async (error) => {
    const config = error.config
    
    // 如果没有配置重试或已达到最大重试次数
    if (!config || !retryConfig.retries || config.__retryCount >= retryConfig.retries) {
      return Promise.reject(error)
    }
    
    // 检查是否满足重试条件
    if (!retryConfig.retryCondition(error)) {
      return Promise.reject(error)
    }
    
    // 增加重试计数
    config.__retryCount = config.__retryCount || 0
    config.__retryCount++
    
    // 延迟重试
    await new Promise(resolve => {
      setTimeout(resolve, retryConfig.retryDelay * config.__retryCount)
    })
    
    console.log(`请求重试 ${config.__retryCount}/${retryConfig.retries}: ${config.url}`)
    
    // 重新发起请求
    return request(config)
  }
)

7. 请求缓存

// 请求缓存
const requestCache = new Map()

// 缓存配置
const cacheConfig = {
  maxAge: 5 * 60 * 1000,  // 缓存时间5分钟
  maxSize: 100            // 最大缓存数量
}

// 获取缓存键
const getCacheKey = (config) => {
  const { method, url, params } = config
  return `${method}:${url}:${JSON.stringify(params)}`
}

// 获取缓存数据
const getCachedResponse = (config) => {
  if (config.method !== 'get' || config.cache === false) {
    return null
  }
  
  const cacheKey = getCacheKey(config)
  const cached = requestCache.get(cacheKey)
  
  if (cached && Date.now() - cached.timestamp < cacheConfig.maxAge) {
    return cached.data
  }
  
  return null
}

// 设置缓存数据
const setCachedResponse = (config, data) => {
  if (config.method !== 'get' || config.cache === false) {
    return
  }
  
  const cacheKey = getCacheKey(config)
  
  // 检查缓存大小
  if (requestCache.size >= cacheConfig.maxSize) {
    // 删除最旧的缓存
    const firstKey = requestCache.keys().next().value
    requestCache.delete(firstKey)
  }
  
  requestCache.set(cacheKey, {
    data,
    timestamp: Date.now()
  })
}

// 在请求拦截器中检查缓存
request.interceptors.request.use((config) => {
  const cachedResponse = getCachedResponse(config)
  if (cachedResponse) {
    // 返回缓存数据
    return Promise.resolve({
      data: cachedResponse,
      status: 200,
      statusText: 'OK',
      headers: {},
      config,
      request: {}
    })
  }
  return config
})

// 在响应拦截器中设置缓存
request.interceptors.response.use((response) => {
  setCachedResponse(response.config, response.data)
  return response
})

8. 文件上传下载

// 文件上传
const uploadFile = (url, file, options = {}) => {
  const formData = new FormData()
  formData.append('file', file)
  
  // 添加额外参数
  if (options.data) {
    Object.keys(options.data).forEach(key => {
      formData.append(key, options.data[key])
    })
  }
  
  return request({
    url,
    method: 'post',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    onUploadProgress: options.onProgress,
    ...options
  })
}

// 文件下载
const downloadFile = (url, params = {}, options = {}) => {
  return request({
    url,
    method: 'get',
    params,
    responseType: 'blob',
    ...options
  }).then(response => {
    // 创建下载链接
    const blob = new Blob([response.data])
    const downloadUrl = window.URL.createObjectURL(blob)
    
    // 获取文件名
    const contentDisposition = response.headers['content-disposition']
    let filename = options.filename || 'download'
    
    if (contentDisposition) {
      const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
      if (filenameMatch && filenameMatch[1]) {
        filename = filenameMatch[1].replace(/['"]/g, '')
      }
    }
    
    // 创建下载元素
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = filename
    document.body.appendChild(link)
    link.click()
    
    // 清理
    document.body.removeChild(link)
    window.URL.revokeObjectURL(downloadUrl)
    
    return response
  })
}

9. Mock数据支持

// Mock数据配置
const mockConfig = {
  enabled: import.meta.env.VITE_USE_MOCK === 'true',
  delay: 500  // 模拟延迟
}

// Mock拦截器
if (mockConfig.enabled) {
  request.interceptors.request.use(async (config) => {
    // 检查是否有对应的Mock数据
    const mockData = await getMockData(config)
    
    if (mockData) {
      // 模拟延迟
      await new Promise(resolve => setTimeout(resolve, mockConfig.delay))
      
      // 返回Mock响应
      return Promise.resolve({
        data: mockData,
        status: 200,
        statusText: 'OK',
        headers: {},
        config,
        request: {}
      })
    }
    
    return config
  })
}

// 获取Mock数据
const getMockData = async (config) => {
  try {
    const mockModule = await import('@/mock/api.js')
    const mockApis = mockModule.default || mockModule
    
    const mockApi = mockApis.find(api => {
      return api.url === config.url && api.method === config.method
    })
    
    return mockApi ? mockApi.response : null
  } catch (error) {
    console.warn('获取Mock数据失败:', error)
    return null
  }
}

10. 性能监控

// 性能监控
const performanceMonitor = {
  // 请求统计
  stats: {
    totalRequests: 0,
    successRequests: 0,
    failedRequests: 0,
    totalTime: 0,
    slowRequests: []
  },
  
  // 记录请求
  recordRequest: (config, response, error) => {
    const duration = Date.now() - config.startTime
    
    performanceMonitor.stats.totalRequests++
    performanceMonitor.stats.totalTime += duration
    
    if (error) {
      performanceMonitor.stats.failedRequests++
    } else {
      performanceMonitor.stats.successRequests++
    }
    
    // 记录慢请求
    if (duration > 3000) {
      performanceMonitor.stats.slowRequests.push({
        url: config.url,
        method: config.method,
        duration,
        timestamp: Date.now()
      })
    }
  },
  
  // 获取统计信息
  getStats: () => {
    const stats = performanceMonitor.stats
    return {
      ...stats,
      averageTime: stats.totalRequests > 0 ? stats.totalTime / stats.totalRequests : 0,
      successRate: stats.totalRequests > 0 ? (stats.successRequests / stats.totalRequests) * 100 : 0
    }
  },
  
  // 重置统计
  resetStats: () => {
    performanceMonitor.stats = {
      totalRequests: 0,
      successRequests: 0,
      failedRequests: 0,
      totalTime: 0,
      slowRequests: []
    }
  }
}

// 在响应拦截器中记录性能数据
request.interceptors.response.use(
  (response) => {
    performanceMonitor.recordRequest(response.config, response)
    return response
  },
  (error) => {
    if (error.config) {
      performanceMonitor.recordRequest(error.config, null, error)
    }
    return Promise.reject(error)
  }
)

使用示例

1. 基本使用

import request from '@/utils/request'

// GET请求
const getUsers = (params) => {
  return request({
    url: '/users',
    method: 'get',
    params
  })
}

// POST请求
const createUser = (data) => {
  return request({
    url: '/users',
    method: 'post',
    data
  })
}

// PUT请求
const updateUser = (id, data) => {
  return request({
    url: `/users/${id}`,
    method: 'put',
    data
  })
}

// DELETE请求
const deleteUser = (id) => {
  return request({
    url: `/users/${id}`,
    method: 'delete'
  })
}

2. 高级用法

// 带配置的请求
const getUsersWithConfig = (params) => {
  return request({
    url: '/users',
    method: 'get',
    params,
    timeout: 5000,        // 自定义超时时间
    showLoading: false,   // 不显示加载状态
    showError: false,     // 不显示错误提示
    cache: true,          // 启用缓存
    retry: 3              // 重试次数
  })
}

// 文件上传
const uploadAvatar = (file, onProgress) => {
  return uploadFile('/upload/avatar', file, {
    onProgress,
    data: {
      type: 'avatar'
    }
  })
}

// 文件下载
const downloadReport = (params) => {
  return downloadFile('/reports/export', params, {
    filename: 'report.xlsx'
  })
}

3. 在组件中使用

<script setup>
import { ref } from 'vue'
import { getUsers, createUser } from '@/api/user'

const users = ref([])
const loading = ref(false)

const loadUsers = async () => {
  try {
    loading.value = true
    const result = await getUsers({ page: 1, size: 20 })
    users.value = result.data.list
  } catch (error) {
    console.error('加载用户失败:', error)
  } finally {
    loading.value = false
  }
}

const addUser = async (userData) => {
  try {
    await createUser(userData)
    ElMessage.success('用户创建成功')
    loadUsers() // 重新加载列表
  } catch (error) {
    // 错误已在拦截器中处理
  }
}
</script>

注意事项

  1. 错误处理: 统一的错误处理机制,避免重复处理
  2. 请求去重: 防止重复请求造成的问题
  3. 性能优化: 合理使用缓存和重试机制
  4. 安全性: 敏感信息不要在请求中暴露
  5. 监控: 监控请求性能和错误率

相关文档


最后更新时间:2025-09-19