axios 封装

4 阅读1分钟

这里提供几种常见的 Axios 封装方案,从简单到复杂:

方案一:基础封装(适合小型项目)

// request.js
import axios from 'axios'

// 创建实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000
})

// 请求拦截
service.interceptors.request.use(
  config => {
    // 添加token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截
service.interceptors.response.use(
  response => {
    const res = response.data
    
    // 根据自定义状态码处理
    if (res.code !== 200) {
      // 处理错误
      return Promise.reject(new Error(res.message || 'Error'))
    }
    return res
  },
  error => {
    // 处理HTTP错误状态
    if (error.response) {
      switch (error.response.status) {
        case 401:
          // 未授权处理
          break
        case 404:
          // 404处理
          break
        default:
          // 其他错误
      }
    }
    return Promise.reject(error)
  }
)

export default service

方案二:模块化封装(适合中大型项目)

// http/axios.js
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'

class AxiosService {
  constructor() {
    this.service = axios.create({
      baseURL: process.env.VUE_APP_BASE_API,
      timeout: 30000,
      withCredentials: true
    })
    
    this.setInterceptors()
  }
  
  setInterceptors() {
    // 请求拦截
    this.service.interceptors.request.use(
      config => {
        // 添加loading
        if (config.showLoading) {
          this.showLoading()
        }
        
        // 添加token
        const token = this.getToken()
        if (token) {
          config.headers['Authorization'] = `Bearer ${token}`
        }
        
        return config
      },
      error => {
        this.hideLoading()
        return Promise.reject(error)
      }
    )
    
    // 响应拦截
    this.service.interceptors.response.use(
      response => {
        this.hideLoading()
        
        const res = response.data
        
        // 下载文件直接返回
        if (response.config.responseType === 'blob') {
          return res
        }
        
        // 业务状态码处理
        if (res.code !== 200) {
          this.handleBusinessError(res)
          return Promise.reject(new Error(res.message || 'Error'))
        }
        
        return res
      },
      error => {
        this.hideLoading()
        this.handleHttpError(error)
        return Promise.reject(error)
      }
    )
  }
  
  // 业务错误处理
  handleBusinessError(res) {
    Message({
      message: res.message || '业务错误',
      type: 'error',
      duration: 5 * 1000
    })
  }
  
  // HTTP错误处理
  handleHttpError(error) {
    if (error.response) {
      const { status } = error.response
      
      if (status === 401) {
        MessageBox.confirm('登录已过期,请重新登录', '提示', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.clearToken()
          location.reload()
        })
      } else {
        Message({
          message: error.response.data.message || `HTTP错误: ${status}`,
          type: 'error',
          duration: 5 * 1000
        })
      }
    } else if (error.code === 'ECONNABORTED') {
      Message({
        message: '请求超时',
        type: 'error',
        duration: 5 * 1000
      })
    } else {
      Message({
        message: '网络错误',
        type: 'error',
        duration: 5 * 1000
      })
    }
  }
  
  // 获取token
  getToken() {
    return localStorage.getItem('token')
  }
  
  // 清除token
  clearToken() {
    localStorage.removeItem('token')
  }
  
  // 显示loading
  showLoading() {
    // 实现loading逻辑,比如使用Element UI的Loading
  }
  
  // 隐藏loading
  hideLoading() {
    // 隐藏loading
  }
  
  // 封装请求方法
  request(config) {
    return this.service.request(config)
  }
  
  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 })
  }
}

export default new AxiosService()

方案三:TypeScript 封装(适合大型项目)

// http/index.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

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

// 定义请求配置
export interface RequestConfig extends AxiosRequestConfig {
  showLoading?: boolean
  showError?: boolean
  retry?: number
  retryDelay?: number
}

class HttpClient {
  private instance: AxiosInstance
  private retryCount: number = 3
  private retryDelay: number = 1000
  
  constructor() {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_APP_BASE_API,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json;charset=UTF-8'
      }
    })
    
    this.setupInterceptors()
  }
  
  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config: RequestConfig) => {
        // 添加token
        const token = this.getToken()
        if (token) {
          config.headers!['Authorization'] = `Bearer ${token}`
        }
        
        // 添加时间戳防止缓存
        if (config.method === 'get') {
          config.params = {
            ...config.params,
            _t: Date.now()
          }
        }
        
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse<ResponseData>) => {
        const { data } = response
        
        // 处理下载文件
        if (response.config.responseType === 'blob') {
          return response
        }
        
        // 业务状态码处理
        if (data.code !== 200) {
          return Promise.reject(new Error(data.message || '请求失败'))
        }
        
        return data.data
      },
      async (error) => {
        const { config } = error
        
        // 重试机制
        if (config && config.retry !== undefined) {
          config.retryCount = config.retryCount || 0
          
          if (config.retryCount < this.retryCount) {
            config.retryCount++
            
            // 延迟重试
            await new Promise(resolve => 
              setTimeout(resolve, this.retryDelay)
            )
            
            return this.instance(config)
          }
        }
        
        // 错误处理
        this.handleError(error)
        return Promise.reject(error)
      }
    )
  }
  
  private handleError(error: any) {
    if (error.response) {
      // 服务器返回错误状态码
      const { status } = error.response
      
      switch (status) {
        case 401:
          this.handleUnauthorized()
          break
        case 403:
          console.error('没有权限')
          break
        case 404:
          console.error('请求资源不存在')
          break
        case 500:
          console.error('服务器错误')
          break
        default:
          console.error(`HTTP错误: ${status}`)
      }
    } else if (error.request) {
      // 请求发送失败
      console.error('网络错误,请检查网络连接')
    } else {
      // 请求配置错误
      console.error('请求配置错误:', error.message)
    }
  }
  
  private handleUnauthorized() {
    // 清除token
    this.clearToken()
    
    // 跳转到登录页
    window.location.href = '/login'
  }
  
  private getToken(): string | null {
    return localStorage.getItem('token')
  }
  
  private clearToken() {
    localStorage.removeItem('token')
  }
  
  // 通用请求方法
  public async request<T = any>(config: RequestConfig): Promise<T> {
    try {
      const response = await this.instance.request(config)
      return response as T
    } catch (error) {
      return Promise.reject(error)
    }
  }
  
  public async get<T = any>(
    url: string,
    params?: any,
    config?: RequestConfig
  ): Promise<T> {
    return this.request({
      method: 'get',
      url,
      params,
      ...config
    })
  }
  
  public async post<T = any>(
    url: string,
    data?: any,
    config?: RequestConfig
  ): Promise<T> {
    return this.request({
      method: 'post',
      url,
      data,
      ...config
    })
  }
  
  public async put<T = any>(
    url: string,
    data?: any,
    config?: RequestConfig
  ): Promise<T> {
    return this.request({
      method: 'put',
      url,
      data,
      ...config
    })
  }
  
  public async delete<T = any>(
    url: string,
    params?: any,
    config?: RequestConfig
  ): Promise<T> {
    return this.request({
      method: 'delete',
      url,
      params,
      ...config
    })
  }
  
  // 上传文件
  public async upload<T = any>(
    url: string,
    file: File,
    config?: RequestConfig
  ): Promise<T> {
    const formData = new FormData()
    formData.append('file', file)
    
    return this.post(url, formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      ...config
    })
  }
  
  // 下载文件
  public async download(url: string, filename?: string): Promise<void> {
    const response = await this.get(url, {
      responseType: 'blob'
    })
    
    const blob = new Blob([response])
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    link.download = filename || 'download'
    link.click()
    URL.revokeObjectURL(link.href)
  }
}

export default new HttpClient()

方案四:API模块化管理

// api/modules/user.js
import http from '@/http'

export const userApi = {
  // 登录
  login(data) {
    return http.post('/user/login', data)
  },
  
  // 获取用户信息
  getUserInfo() {
    return http.get('/user/info')
  },
  
  // 更新用户信息
  updateUserInfo(data) {
    return http.put('/user/info', data)
  },
  
  // 获取用户列表
  getUserList(params) {
    return http.get('/user/list', params, {
      showLoading: true  // 显示loading
    })
  }
}

// api/index.js
import { userApi } from './modules/user'
import { productApi } from './modules/product'
import { orderApi } from './modules/order'

export default {
  user: userApi,
  product: productApi,
  order: orderApi
}

// 使用示例
// import api from '@/api'
// api.user.login({ username, password })

方案五:带缓存和取消请求的高级封装

// http/advanced.js
import axios from 'axios'
import qs from 'qs'

class AdvancedHttp {
  constructor() {
    this.instance = axios.create({/* 配置 */})
    this.pendingRequests = new Map()  // 存储待取消的请求
    this.cache = new Map()  // 缓存
    this.setupInterceptors()
  }
  
  // 生成请求key
  generateRequestKey(config) {
    const { method, url, params, data } = config
    return [method, url, qs.stringify(params), qs.stringify(data)].join('&')
  }
  
  // 添加待取消请求
  addPendingRequest(config) {
    const key = this.generateRequestKey(config)
    config.cancelToken = new axios.CancelToken((cancel) => {
      if (!this.pendingRequests.has(key)) {
        this.pendingRequests.set(key, cancel)
      }
    })
  }
  
  // 取消重复请求
  removePendingRequest(config) {
    const key = this.generateRequestKey(config)
    if (this.pendingRequests.has(key)) {
      const cancel = this.pendingRequests.get(key)
      cancel('取消重复请求')
      this.pendingRequests.delete(key)
    }
  }
  
  // 清除所有待取消请求
  clearPendingRequests() {
    this.pendingRequests.forEach(cancel => cancel('取消请求'))
    this.pendingRequests.clear()
  }
  
  // 设置缓存
  setCache(key, data, ttl = 60000) {
    const cacheItem = {
      data,
      expire: Date.now() + ttl
    }
    this.cache.set(key, cacheItem)
  }
  
  // 获取缓存
  getCache(key) {
    const cacheItem = this.cache.get(key)
    if (!cacheItem) return null
    
    if (Date.now() > cacheItem.expire) {
      this.cache.delete(key)
      return null
    }
    
    return cacheItem.data
  }
  
  setupInterceptors() {
    this.instance.interceptors.request.use(
      config => {
        // 取消重复请求
        this.removePendingRequest(config)
        this.addPendingRequest(config)
        
        // 检查缓存
        if (config.useCache && config.method === 'get') {
          const key = this.generateRequestKey(config)
          const cachedData = this.getCache(key)
          if (cachedData) {
            config.cached = true
            return Promise.reject({
              config,
              cached: true,
              data: cachedData
            })
          }
        }
        
        return config
      },
      error => Promise.reject(error)
    )
    
    this.instance.interceptors.response.use(
      response => {
        this.removePendingRequest(response.config)
        
        // 设置缓存
        if (response.config.cache && response.config.method === 'get') {
          const key = this.generateRequestKey(response.config)
          this.setCache(key, response.data)
        }
        
        return response
      },
      error => {
        // 处理缓存命中
        if (error.cached) {
          return Promise.resolve(error.data)
        }
        
        if (axios.isCancel(error)) {
          console.log('请求取消:', error.message)
          return Promise.reject(error)
        }
        
        return Promise.reject(error)
      }
    )
  }
}

export default new AdvancedHttp()

使用示例

// main.js或App.vue中使用
import http from './http'

// 基础使用
async function fetchData() {
  try {
    const data = await http.get('/api/data', { id: 1 })
    console.log(data)
  } catch (error) {
    console.error(error)
  }
}

// 带loading的请求
http.post('/api/save', formData, {
  showLoading: true,
  showError: true
})

// 带缓存和取消的请求
http.get('/api/list', { page: 1 }, {
  useCache: true,
  cacheTime: 60000  // 缓存1分钟
})

// 上传文件
http.upload('/api/upload', file, {
  onUploadProgress: (progressEvent) => {
    const percent = Math.round(
      (progressEvent.loaded * 100) / progressEvent.total
    )
    console.log(`上传进度: ${percent}%`)
  }
})

根据项目规模和需求选择合适的封装方案。小项目用基础封装,大项目用TypeScript封装或高级封装。