Axios 请求封装

封装 axios 实例

utils/request.ts
/**
 * axios 请求封装模块
 * 提供统一的请求/响应拦截、错误处理、请求重试等功能
 */

import axios, { 
  AxiosInstance, 
  AxiosRequestConfig, 
  AxiosResponse, 
  AxiosError,
  InternalAxiosRequestConfig 
} from 'axios'
import { 
  ElMessage, 
  ElMessageBox 
} from 'element-plus'

// ==================== 类型定义 ====================

/** 请求配置扩展 */
export interface RequestConfig extends AxiosRequestConfig {
  /** 是否显示加载状态 */
  loading?: boolean
  /** 是否显示错误提示 */
  showError?: boolean
  /** 请求重试次数 */
  retry?: number
  /** 请求重试延迟(毫秒) */
  retryDelay?: number
  /** 是否忽略 Token */
  ignoreToken?: boolean
  /** 自定义错误处理 */
  errorHandler?: (error: AxiosError) => void
}

/** 响应数据结构 */
export interface ResponseData<T = any> {
  code: number
  data: T
  message: string
  success: boolean
}

/** 错误响应数据结构 */
export interface ErrorResponse {
  code: number
  message: string
  data?: any
}

/** 请求方法类型 */
export type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'options'

// ==================== 常量配置 ====================

/** 默认配置 */
const DEFAULT_CONFIG = {
  /** 基础 URL */
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  /** 请求超时时间 */
  timeout: 30000,
  /** 默认错误提示 */
  errorMessage: '网络请求失败,请稍后重试',
  /** 请求重试次数 */
  retry: 3,
  /** 请求重试延迟 */
  retryDelay: 1000,
  /** Content-Type */
  headers: {
    'Content-Type': 'application/json;charset=UTF-8',
  },
}

// ==================== 工具函数 ====================

/**
 * 判断是否为开发环境
 */
const isDev = import.meta.env.DEV

/**
 * 获取 Token
 */
const getToken = (): string | null => {
  return localStorage.getItem('token') || sessionStorage.getItem('token')
}

/**
 * 设置 Token
 */
const setToken = (token: string, remember?: boolean): void => {
  if (remember) {
    localStorage.setItem('token', token)
  } else {
    sessionStorage.setItem('token', token)
  }
}

/**
 * 移除 Token
 */
const removeToken = (): void => {
  localStorage.removeItem('token')
  sessionStorage.removeItem('token')
}

/**
 * 检查是否是取消请求错误
 */
const isCancelRequest = (error: any): boolean => {
  return axios.isCancel(error)
}

/**
 * 检查是否是超时错误
 */
const isTimeoutError = (error: AxiosError): boolean => {
  return error.code === 'ECONNABORTED' || error.message.includes('timeout')
}

/**
 * 检查是否是网络错误
 */
const isNetworkError = (error: AxiosError): boolean => {
  return !error.response && !error.request
}

/**
 * 延迟函数
 */
const delay = (ms: number): Promise<void> => {
  return new Promise(resolve => setTimeout(resolve, ms))
}

// ==================== Axios 实例 ====================

/** 创建 axios 实例 */
const createAxiosInstance = (): AxiosInstance => {
  const instance = axios.create({
    baseURL: DEFAULT_CONFIG.baseURL,
    timeout: DEFAULT_CONFIG.timeout,
    headers: DEFAULT_CONFIG.headers,
    // 允许跨域携带凭证
    withCredentials: false,
  })

  // 配置请求拦截器
  instance.interceptors.request.use(
    (config: InternalAxiosRequestConfig & RequestConfig) => {
      // 获取请求配置
      const requestConfig = config as RequestConfig

      // 如果不是忽略 Token 的请求,添加 Token
      if (!requestConfig.ignoreToken) {
        const token = getToken()
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
      }

      // 设置 Loading(如果需要)
      if (requestConfig.loading) {
        // 可以在这里触发全局 Loading
        // 例如:store.dispatch('setLoading', true)
      }

      // 处理请求参数
      if (config.method === 'get' && config.params) {
        // GET 请求参数序列化优化
        config.paramsSerializer = {
          serialize: (params: any) => {
            return Object.keys(params)
              .map(key => {
                const value = params[key]
                if (Array.isArray(value)) {
                  return value.map((v: any) => `${key}=${encodeURIComponent(v)}`).join('&')
                }
                return `${key}=${encodeURIComponent(value)}`
              })
              .join('&')
          },
        }
      }

      // 开发环境打印请求信息
      if (isDev) {
        console.group(`🚀 ${config.method?.toUpperCase()} ${config.url}`)
        console.log('请求参数:', config.params || config.data)
        console.log('请求头:', config.headers)
        console.groupEnd()
      }

      return config
    },
    (error: AxiosError) => {
      console.error('请求配置错误:', error)
      return Promise.reject(error)
    }
  )

  // 配置响应拦截器
  instance.interceptors.response.use(
    async (response: AxiosResponse) => {
      const config = response.config as RequestConfig

      // 关闭 Loading
      if (config.loading) {
        // store.dispatch('setLoading', false)
      }

      const { data, status } = response

      // 处理业务逻辑错误(根据后端返回的 code 判断)
      if (data && typeof data.code === 'number') {
        // 请求成功
        if (data.code === 200 || data.code === 0 || data.success) {
          return response
        }

        // Token 过期或无效
        if (data.code === 401 || data.message?.includes('登录')) {
          // 清除 Token
          removeToken()
          
          // 提示用户登录过期
          await ElMessageBox.alert('登录已过期,请重新登录', '提示', {
            confirmButtonText: '去登录',
            type: 'warning',
          }).catch(() => {})
          
          // 跳转到登录页
          // router.push('/login')
          return Promise.reject(new Error(data.message || '登录已过期'))
        }

        // 其他业务错误
        if (config.showError !== false) {
          ElMessage.error(data.message || DEFAULT_CONFIG.errorMessage)
        }

        return Promise.reject(new Error(data.message || '请求失败'))
      }

      // HTTP 状态码处理
      if (status >= 200 && status < 300) {
        return response
      }

      // HTTP 错误处理
      const errorMsg = getHttpErrorMessage(status)
      if (config.showError !== false) {
        ElMessage.error(errorMsg)
      }

      return Promise.reject(new Error(errorMsg))
    },
    async (error: AxiosError) => {
      const config = error.config as RequestConfig & { _retryCount?: number }

      // 关闭 Loading
      if (config?.loading) {
        // store.dispatch('setLoading', false)
      }

      // 如果是取消请求,不处理
      if (isCancelRequest(error)) {
        return Promise.reject(error)
      }

      // 请求重试逻辑
      if (config && isNetworkError(error) || isTimeoutError(error)) {
        const retryCount = config._retryCount || 0
        const maxRetries = config.retry ?? DEFAULT_CONFIG.retry

        if (retryCount < maxRetries) {
          config._retryCount = retryCount + 1
          const retryDelay = config.retryDelay ?? DEFAULT_CONFIG.retryDelay
          
          if (isDev) {
            console.warn(`请求失败,${retryDelay}ms 后进行第 ${retryCount + 1} 次重试...`)
          }

          await delay(retryDelay)
          return instance(config)
        }
      }

      // 处理 HTTP 错误
      const errorMessage = handleError(error, config)

      // 自定义错误处理
      if (config?.errorHandler) {
        config.errorHandler(error)
      } else if (config?.showError !== false) {
        ElMessage.error(errorMessage)
      }

      return Promise.reject(error)
    }
  )

  return instance
}

/**
 * 获取 HTTP 错误消息
 */
const getHttpErrorMessage = (status: number): string => {
  const errorMap: Record<number, string> = {
    400: '请求参数错误',
    401: '登录已过期,请重新登录',
    403: '没有权限访问该资源',
    404: '请求的资源不存在',
    405: '请求方法不被允许',
    408: '请求超时',
    409: '请求冲突',
    422: '请求参数验证失败',
    429: '请求过于频繁,请稍后再试',
    500: '服务器内部错误',
    501: '服务未实现',
    502: '网关错误',
    503: '服务不可用',
    504: '网关超时',
  }

  return errorMap[status] || DEFAULT_CONFIG.errorMessage
}

/**
 * 处理错误
 */
const handleError = (error: AxiosError, config?: RequestConfig): string => {
  // 有响应结果
  if (error.response) {
    return getHttpErrorMessage(error.response.status)
  }

  // 无响应结果(网络错误)
  if (error.request) {
    if (isTimeoutError(error)) {
      return '请求超时,请检查网络连接'
    }
    return '网络连接失败,请检查网络设置'
  }

  // 其他错误
  return error.message || DEFAULT_CONFIG.errorMessage
}

// ==================== 请求封装 ====================

/** 创建 axios 实例 */
const request = createAxiosInstance()

/**
 * 封装 GET 请求
 */
export const get = <T = any>(
  url: string,
  params?: any,
  config?: RequestConfig
): Promise<ResponseData<T>> => {
  return request.get(url, { ...config, params })
}

/**
 * 封装 POST 请求
 */
export const post = <T = any>(
  url: string,
  data?: any,
  config?: RequestConfig
): Promise<ResponseData<T>> => {
  return request.post(url, data, config)
}

/**
 * 封装 PUT 请求
 */
export const put = <T = any>(
  url: string,
  data?: any,
  config?: RequestConfig
): Promise<ResponseData<T>> => {
  return request.put(url, data, config)
}

/**
 * 封装 DELETE 请求
 */
export const del = <T = any>(
  url: string,
  params?: any,
  config?: RequestConfig
): Promise<ResponseData<T>> => {
  return request.delete(url, { ...config, params })
}

/**
 * 封装 PATCH 请求
 */
export const patch = <T = any>(
  url: string,
  data?: any,
  config?: RequestConfig
): Promise<ResponseData<T>> => {
  return request.patch(url, data, config)
}

/**
 * 封装上传文件请求
 */
export const upload = <T = any>(
  url: string,
  formData: FormData,
  config?: RequestConfig
): Promise<ResponseData<T>> => {
  return request.post(url, formData, {
    ...config,
    headers: {
      ...config?.headers,
      'Content-Type': 'multipart/form-data',
    },
  })
}

/**
 * 封装下载文件请求
 */
export const download = (
  url: string,
  params?: any,
  config?: RequestConfig
): Promise<Blob> => {
  return request.get(url, {
    ...config,
    params,
    responseType: 'blob',
  }) as Promise<any>
}

// ==================== 请求管理 ====================

/** 存储所有正在进行的请求 */
const pendingRequests = new Map<string, AbortController>()

/**
 * 生成请求唯一标识
 */
const generateRequestKey = (config: RequestConfig): string => {
  const { url, method, params, data } = config
  return `${method}-${url}-${JSON.stringify(params)}-${JSON.stringify(data)}`
}

/**
 * 添加请求到 pending
 */
export const addPendingRequest = (config: RequestConfig): void => {
  const requestKey = generateRequestKey(config)
  
  // 取消之前的同名请求
  if (pendingRequests.has(requestKey)) {
    const controller = pendingRequests.get(requestKey)
    controller?.abort()
    pendingRequests.delete(requestKey)
  }

  // 创建新的 AbortController
  const controller = new AbortController()
  pendingRequests.set(requestKey, controller)
  
  // 将 signal 绑定到 config
  config.signal = controller.signal
}

/**
 * 移除请求从 pending
 */
export const removePendingRequest = (config: RequestConfig): void => {
  const requestKey = generateRequestKey(config)
  pendingRequests.delete(requestKey)
}

/**
 * 取消所有 pending 请求
 */
export const cancelAllRequests = (): void => {
  pendingRequests.forEach(controller => {
    controller.abort()
  })
  pendingRequests.clear()
}

/**
 * 创建一个可以取消的请求
 */
export const createCancellableRequest = <T = any>(
  url: string,
  config?: RequestConfig
): { request: Promise<ResponseData<T>>; cancel: () => void } => {
  const controller = new AbortController()
  const cancel = () => controller.abort()
  
  const requestConfig: RequestConfig = {
    ...config,
    signal: controller.signal,
  }

  const method = (config?.method as RequestMethod) || 'get'
  let requestPromise: Promise<ResponseData<T>>

  switch (method) {
    case 'post':
      requestPromise = post(url, config?.data, requestConfig)
      break
    case 'put':
      requestPromise = put(url, config?.data, requestConfig)
      break
    case 'delete':
      requestPromise = del(url, config?.params, requestConfig)
      break
    case 'patch':
      requestPromise = patch(url, config?.data, requestConfig)
      break
    default:
      requestPromise = get(url, config?.params, requestConfig)
  }

  return { request: requestPromise, cancel }
}

// ==================== 导出 ====================

/** axios 实例(可直接使用) */
export { request as axiosInstance }

/** axios 原生方法 */
export { axios }

export default request

使用示例

api/index.ts
// 基础 GET 请求
import { get, post } from '@/utils/request'

// 简单请求
const fetchUserInfo = async () => {
  const res = await get('/user/info')
  return res.data
}

// 带参数请求
const searchUsers = async (keyword: string) => {
  const res = await get('/user/list', { keyword, page: 1, size: 10 })
  return res.data
}

// POST 请求
const login = async (username: string, password: string) => {
  const res = await post('/auth/login', { username, password })
  return res.data
}

// 显示 Loading 的请求
const fetchDataWithLoading = async () => {
  const res = await get('/data', null, { loading: true })
  return res.data
}

// 不显示错误提示
const fetchDataNoError = async () => {
  const res = await get('/data', null, { showError: false })
  return res.data
}

// 自定义错误处理
const fetchDataCustomError = async () => {
  const res = await get('/data', null, { 
    errorHandler: (error) => {
      console.error('自定义错误处理:', error)
    }
  })
  return res.data
}

// 文件上传
import { upload } from '@/utils/request'

const uploadFile = async (file: File) => {
  const formData = new FormData()
  formData.append('file', file)
  const res = await upload('/upload', formData)
  return res.data
}

// 文件下载
import { download } from '@/utils/request'

const downloadFile = async (fileId: string) => {
  const res = await download('/file/download', { fileId })
  // 处理下载
  const blob = new Blob([res])
  const url = window.URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.href = url
  link.download = 'file.pdf'
  link.click()
  window.URL.revokeObjectURL(url)
}

// 取消请求
import { createCancellableRequest } from '@/utils/request'

// 在组件中
const { request, cancel } = createCancellableRequest('/api/data')
request.then(res => console.log(res))

// 在需要取消时调用
cancel()