Vue项目Axios封装全攻略:从零到一打造优雅的HTTP请求层

今天我们来聊聊Vue项目中一个看似基础却至关重要的技能------Axios封装。相信不少前端开发者都有过这样的困惑:为什么我的接口请求代码总是重复?为什么错误处理如此混乱?如何统一管理API地址?

别担心,读完这篇文章,你将彻底掌握Axios封装的精髓,打造出属于你自己的优雅HTTP请求层!

为什么要封装Axios?

在开始之前,我们先思考一个问题:为什么要封装Axios?

不封装的痛苦:

  • • 每个请求都要重复写axios.get()axios.post()
  • • 每个请求都要单独处理错误
  • • 切换环境时,需要手动修改大量baseURL
  • • 缺乏统一的loading处理
  • • 难以管理接口和统一添加认证信息

封装的好处:

  • • 统一管理API地址和接口
  • • 统一处理错误和loading
  • • 减少重复代码,提高开发效率
  • • 方便维护和更新
  • • 增强代码的可读性和可维护性

封装设计思路

我们先来看一下整体设计思路:

javascript 复制代码
添加token

显示loading

序列化数据

处理错误

隐藏loading

转换数据

发起HTTP请求请求拦截器拦截器处理添加认证信息显示加载状态数据处理发送请求接收响应响应拦截器拦截器处理统一错误处理隐藏加载状态数据转换返回Promise

第一步:基础封装

1. 安装依赖

首先,确保已经安装了axios:

csharp 复制代码
npm install axios
# 或
yarn add axios

2. 创建Axios实例

src/utils/request.js中创建基础的axios实例:

javascript 复制代码
import axios from 'axios'
import { Message } from 'element-ui' // 以Element UI为例,可根据项目使用UI库调整

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 从环境变量读取API地址
  timeout: 10000, // 请求超时时间
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么
    // 1. 添加token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`
    }
    
    // 2. 显示loading(如果需要)
    if (config.showLoading) {
      // 显示loading组件
      showLoading()
    }
    
    return config
  },
  error => {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    // 对响应数据做点什么
    // 隐藏loading
    if (response.config.showLoading) {
      hideLoading()
    }
    
    const res = response.data
    
    // 假设后端返回的数据格式为 { code: 200, data: {}, message: 'success' }
    if (res.code !== 200) {
      // 处理业务错误
      Message.error(res.message || '请求失败')
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res.data
    }
  },
  error => {
    // 对响应错误做点什么
    // 隐藏loading
    if (error.config && error.config.showLoading) {
      hideLoading()
    }
    
    // 处理HTTP错误
    handleHttpError(error)
    
    return Promise.reject(error)
  }
)

// HTTP错误处理函数
function handleHttpError(error) {
  if (error.response) {
    // 请求成功发出且服务器也响应了状态码,但状态码超出了 2xx 的范围
    switch (error.response.status) {
      case 400:
        Message.error('请求错误')
        break
      case 401:
        Message.error('未授权,请重新登录')
        // 跳转到登录页
        router.push('/login')
        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(`请求失败: ${error.response.status}`)
    }
  } else if (error.request) {
    // 请求已经成功发起,但没有收到响应
    Message.error('网络异常,请检查网络连接')
  } else {
    // 发送请求时出了点问题
    Message.error('请求失败,请稍后重试')
  }
}

// loading计数器
let loadingCount = 0

// 显示loading
function showLoading() {
  if (loadingCount === 0) {
    // 这里根据项目使用的UI库来显示loading
    // Element UI示例
    // Loading.service({ fullscreen: true })
  }
  loadingCount++
}

// 隐藏loading
function hideLoading() {
  loadingCount--
  if (loadingCount <= 0) {
    // 关闭loading
    // Loading实例的close方法
  }
}

export default service

第二步:高级封装 - 创建API管理层

1. 创建API管理文件

src/api目录下创建接口管理文件:

javascript 复制代码
// src/api/index.js
import request from '@/utils/request'

// 用户相关API
export const userApi = {
  // 登录
  login(data) {
    return request({
      url: '/user/login',
      method: 'post',
      data,
      showLoading: true // 可配置是否显示loading
    })
  },
  
  // 获取用户信息
  getUserInfo(params) {
    return request({
      url: '/user/info',
      method: 'get',
      params
    })
  },
  
  // 退出登录
  logout() {
    return request({
      url: '/user/logout',
      method: 'post'
    })
  }
}

// 商品相关API
export const productApi = {
  // 获取商品列表
  getProductList(params) {
    return request({
      url: '/product/list',
      method: 'get',
      params
    })
  },
  
  // 获取商品详情
  getProductDetail(id) {
    return request({
      url: `/product/detail/${id}`,
      method: 'get'
    })
  },
  
  // 创建商品
  createProduct(data) {
    return request({
      url: '/product/create',
      method: 'post',
      data
    })
  },
  
  // 更新商品
  updateProduct(id, data) {
    return request({
      url: `/product/update/${id}`,
      method: 'put',
      data
    })
  },
  
  // 删除商品
  deleteProduct(id) {
    return request({
      url: `/product/delete/${id}`,
      method: 'delete'
    })
  }
}

// 订单相关API
export const orderApi = {
  // 获取订单列表
  getOrderList(params) {
    return request({
      url: '/order/list',
      method: 'get',
      params
    })
  },
  
  // 创建订单
  createOrder(data) {
    return request({
      url: '/order/create',
      method: 'post',
      data,
      showLoading: true
    })
  }
}

2. 在Vue组件中使用

xml 复制代码
<template>
  <div>
    <el-button @click="getUserInfo">获取用户信息</el-button>
    <el-button @click="getProductList">获取商品列表</el-button>
  </div>
</template>

<script>
import { userApi, productApi } from '@/api'

export default {
  methods: {
    async getUserInfo() {
      try {
        const userInfo = await userApi.getUserInfo({ userId: 123 })
        console.log('用户信息:', userInfo)
        this.$message.success('获取用户信息成功')
      } catch (error) {
        console.error('获取用户信息失败:', error)
      }
    },
    
    async getProductList() {
      try {
        const params = {
          page: 1,
          pageSize: 10,
          category: 'electronics'
        }
        const productList = await productApi.getProductList(params)
        console.log('商品列表:', productList)
        this.$message.success('获取商品列表成功')
      } catch (error) {
        console.error('获取商品列表失败:', error)
      }
    }
  }
}
</script>

第三步:进阶功能 - 添加更多特性

1. 请求重试机制

javascript 复制代码
// 在request.js中添加重试机制
const RETRY_COUNT = 3 // 重试次数
const RETRY_DELAY = 1000 // 重试延迟

// 重试请求函数
async function retryRequest(config, retryCount = 0) {
  try {
    return await service(config)
  } catch (error) {
    if (shouldRetry(error) && retryCount < RETRY_COUNT) {
      // 延迟后重试
      await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
      return retryRequest(config, retryCount + 1)
    }
    throw error
  }
}

// 判断是否需要重试
function shouldRetry(error) {
  // 只在网络错误或特定状态码时重试
  return !error.response || 
         error.code === 'ECONNABORTED' || 
         error.response.status >= 500
}

// 修改原始的request函数
export default function request(config) {
  if (config.retry) {
    return retryRequest(config)
  }
  return service(config)
}

2. 请求缓存

kotlin 复制代码
// 请求缓存工具
class RequestCache {
  constructor() {
    this.cache = new Map()
    this.maxSize = 100 // 最大缓存数量
  }
  
  // 生成缓存key
  generateKey(config) {
    const { method, url, params, data } = config
    return JSON.stringify({ method, url, params, data })
  }
  
  // 获取缓存
  get(config) {
    const key = this.generateKey(config)
    const cached = this.cache.get(key)
    
    if (!cached) return null
    
    // 检查缓存是否过期
    if (Date.now() > cached.expire) {
      this.cache.delete(key)
      return null
    }
    
    return cached.data
  }
  
  // 设置缓存
  set(config, data, cacheTime = 5 * 60 * 1000) { // 默认5分钟
    const key = this.generateKey(config)
    
    // 清理最旧的缓存如果达到最大限制
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    this.cache.set(key, {
      data,
      expire: Date.now() + cacheTime
    })
  }
  
  // 清除缓存
  clear() {
    this.cache.clear()
  }
  
  // 删除特定缓存
  delete(config) {
    const key = this.generateKey(config)
    this.cache.delete(key)
  }
}

const requestCache = new RequestCache()

// 在request函数中添加缓存逻辑
export default function request(config) {
  const {
    useCache = false,
    cacheTime = 5 * 60 * 1000,
    ...requestConfig
  } = config
  
  // 如果启用缓存且是GET请求,先检查缓存
  if (useCache && requestConfig.method?.toLowerCase() === 'get') {
    const cachedData = requestCache.get(requestConfig)
    if (cachedData) {
      return Promise.resolve(cachedData)
    }
  }
  
  return service(requestConfig).then(response => {
    // 缓存响应数据
    if (useCache && requestConfig.method?.toLowerCase() === 'get') {
      requestCache.set(requestConfig, response, cacheTime)
    }
    return response
  })
}

3. 并发请求控制

kotlin 复制代码
// 并发请求控制器
class RequestController {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent
    this.queue = []
    this.activeCount = 0
  }
  
  // 添加请求到队列
  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject })
      this.next()
    })
  }
  
  // 执行下一个请求
  next() {
    if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {
      return
    }
    
    this.activeCount++
    const { requestFn, resolve, reject } = this.queue.shift()
    
    Promise.resolve(requestFn())
      .then(resolve)
      .catch(reject)
      .finally(() => {
        this.activeCount--
        this.next()
      })
  }
  
  // 清空队列
  clear() {
    this.queue = []
  }
}

// 创建全局请求控制器
const requestController = new RequestController(5)

// 支持并发控制的request函数
export function controlledRequest(config) {
  return requestController.add(() => request(config))
}

第四步:完整封装示例

下面是完整的、生产环境可用的Axios封装:

javascript 复制代码
// src/utils/request.js
import axios from 'axios'
import router from '@/router'
import { Message, Loading } from 'element-ui'

// 配置
const config = {
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000,
  withCredentials: true, // 跨域请求时是否需要使用凭证
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
}

// 创建axios实例
const service = axios.create(config)

// 请求缓存
class RequestCache {
  constructor(maxSize = 100) {
    this.cache = new Map()
    this.maxSize = maxSize
  }
  
  generateKey(config) {
    const { method, url, params, data } = config
    return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`
  }
  
  get(config) {
    const key = this.generateKey(config)
    const cached = this.cache.get(key)
    
    if (!cached || Date.now() > cached.expire) {
      if (cached) this.cache.delete(key)
      return null
    }
    
    return cached.data
  }
  
  set(config, data, cacheTime = 300000) {
    const key = this.generateKey(config)
    
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    this.cache.set(key, {
      data,
      expire: Date.now() + cacheTime
    })
  }
  
  delete(config) {
    const key = this.generateKey(config)
    this.cache.delete(key)
  }
  
  clear() {
    this.cache.clear()
  }
}

const requestCache = new RequestCache()

// loading控制
let loadingCount = 0
let loadingInstance = null

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

function hideLoading() {
  loadingCount--
  if (loadingCount <= 0 && loadingInstance) {
    loadingInstance.close()
    loadingInstance = null
    loadingCount = 0
  }
}

// 错误处理
function handleError(error) {
  // 请求被取消的错误不提示
  if (axios.isCancel(error)) {
    return
  }
  
  let message = '请求失败,请稍后重试'
  
  if (error.response) {
    switch (error.response.status) {
      case 400:
        message = error.response.data?.message || '请求参数错误'
        break
      case 401:
        message = '未授权,请重新登录'
        // 清除token
        localStorage.removeItem('token')
        // 跳转到登录页
        setTimeout(() => {
          router.replace({
            path: '/login',
            query: { redirect: router.currentRoute.fullPath }
          })
        }, 1000)
        break
      case 403:
        message = '拒绝访问'
        break
      case 404:
        message = '请求的资源不存在'
        break
      case 408:
        message = '请求超时'
        break
      case 500:
        message = '服务器内部错误'
        break
      case 501:
        message = '服务未实现'
        break
      case 502:
        message = '网关错误'
        break
      case 503:
        message = '服务不可用'
        break
      case 504:
        message = '网关超时'
        break
      case 505:
        message = 'HTTP版本不受支持'
        break
      default:
        message = `连接错误 ${error.response.status}`
    }
  } else if (error.request) {
    message = '网络连接异常,请检查网络设置'
  } else {
    message = error.message || '请求失败'
  }
  
  Message.error(message)
  console.error('请求错误:', error)
  
  return Promise.reject(error)
}

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 添加token
    const token = localStorage.getItem('token') || sessionStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    // 显示loading
    if (config.showLoading) {
      showLoading()
    }
    
    // 取消请求配置
    if (config.cancelToken) {
      config.cancelToken = new axios.CancelToken(c => {
        config.cancel = c
      })
    }
    
    return config
  },
  error => {
    // 对请求错误做些什么
    if (error.config?.showLoading) {
      hideLoading()
    }
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    // 隐藏loading
    if (response.config.showLoading) {
      hideLoading()
    }
    
    const res = response.data
    
    // 自定义状态码处理
    if (res.code === undefined) {
      // 如果没有code字段,直接返回数据
      return res
    }
    
    // 根据后端返回的code进行判断
    if (res.code === 200 || res.code === 0) {
      return res.data !== undefined ? res.data : res
    } else {
      // 处理业务错误
      const errorMsg = res.message || '请求失败'
      Message.error(errorMsg)
      
      // 特殊状态码处理
      if (res.code === 401) {
        // token过期,跳转到登录页
        localStorage.removeItem('token')
        router.replace('/login')
      }
      
      return Promise.reject(new Error(errorMsg))
    }
  },
  error => {
    // 隐藏loading
    if (error.config?.showLoading) {
      hideLoading()
    }
    
    // 处理错误
    return handleError(error)
  }
)

// 封装请求方法
const request = {
  /**
   * GET请求
   * @param {string} url 请求地址
   * @param {object} params 请求参数
   * @param {object} options 配置选项
   */
  get(url, params = {}, options = {}) {
    return this.request({
      url,
      method: 'GET',
      params,
      ...options
    })
  },
  
  /**
   * POST请求
   * @param {string} url 请求地址
   * @param {object} data 请求数据
   * @param {object} options 配置选项
   */
  post(url, data = {}, options = {}) {
    return this.request({
      url,
      method: 'POST',
      data,
      ...options
    })
  },
  
  /**
   * PUT请求
   * @param {string} url 请求地址
   * @param {object} data 请求数据
   * @param {object} options 配置选项
   */
  put(url, data = {}, options = {}) {
    return this.request({
      url,
      method: 'PUT',
      data,
      ...options
    })
  },
  
  /**
   * DELETE请求
   * @param {string} url 请求地址
   * @param {object} params 请求参数
   * @param {object} options 配置选项
   */
  delete(url, params = {}, options = {}) {
    return this.request({
      url,
      method: 'DELETE',
      params,
      ...options
    })
  },
  
  /**
   * 通用请求方法
   * @param {object} config 请求配置
   */
  request(config) {
    const {
      useCache = false,
      cacheTime = 300000,
      showLoading = false,
      ...requestConfig
    } = config
    
    // 缓存处理
    if (useCache && requestConfig.method?.toUpperCase() === 'GET') {
      const cachedData = requestCache.get(requestConfig)
      if (cachedData) {
        return Promise.resolve(cachedData)
      }
    }
    
    // 发送请求
    return service(requestConfig).then(response => {
      // 缓存响应
      if (useCache && requestConfig.method?.toUpperCase() === 'GET') {
        requestCache.set(requestConfig, response, cacheTime)
      }
      return response
    })
  },
  
  /**
   * 上传文件
   * @param {string} url 上传地址
   * @param {FormData} formData 表单数据
   * @param {object} options 配置选项
   */
  upload(url, formData, options = {}) {
    return this.request({
      url,
      method: 'POST',
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      ...options
    })
  },
  
  /**
   * 下载文件
   * @param {string} url 下载地址
   * @param {object} params 请求参数
   * @param {string} filename 文件名
   */
  download(url, params = {}, filename = 'download') {
    return this.request({
      url,
      method: 'GET',
      params,
      responseType: 'blob'
    }).then(response => {
      const blob = new Blob([response])
      const downloadUrl = window.URL.createObjectURL(blob)
      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)
    })
  },
  
  /**
   * 清除缓存
   * @param {object} config 可选,清除特定缓存
   */
  clearCache(config = null) {
    if (config) {
      requestCache.delete(config)
    } else {
      requestCache.clear()
    }
  },
  
  /**
   * 创建取消令牌
   */
  createCancelToken() {
    return new axios.CancelToken(c => {
      this.cancel = c
    })
  },
  
  /**
   * 取消请求
   */
  cancelRequest(message = '请求已取消') {
    if (this.cancel) {
      this.cancel(message)
    }
  }
}

// 导出
export default request

第五步:最佳实践和注意事项

1. 环境配置

在项目根目录创建环境配置文件:

ini 复制代码
// .env.development
VUE_APP_BASE_API = '/api'
VUE_APP_ENV = 'development'

// .env.production
VUE_APP_BASE_API = 'https://api.yourdomain.com'
VUE_APP_ENV = 'production'

2. 代理配置(开发环境)

java 复制代码
// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
}

3. TypeScript支持

如果你使用TypeScript,可以添加类型定义:

typescript 复制代码
// src/types/request.d.ts
export interface ResponseData<T = any> {
  code: number
  data: T
  message: string
}

export interface RequestConfig {
  url: string
  method?: string
  data?: any
  params?: any
  headers?: Record<string, string>
  timeout?: number
  showLoading?: boolean
  useCache?: boolean
  cacheTime?: number
  cancelToken?: any
}

export interface RequestInstance {
  request<T = any>(config: RequestConfig): Promise<T>
  get<T = any>(url: string, params?: any, options?: Partial<RequestConfig>): Promise<T>
  post<T = any>(url: string, data?: any, options?: Partial<RequestConfig>): Promise<T>
  put<T = any>(url: string, data?: any, options?: Partial<RequestConfig>): Promise<T>
  delete<T = any>(url: string, params?: any, options?: Partial<RequestConfig>): Promise<T>
  upload<T = any>(url: string, formData: FormData, options?: Partial<RequestConfig>): Promise<T>
  download(url: string, params?: any, filename?: string): Promise<void>
  clearCache(config?: RequestConfig): void
  createCancelToken(): any
  cancelRequest(message?: string): void
}

总结

通过以上完整的封装,我们实现了:

    1. 基础功能:统一的请求/响应拦截器、错误处理、token管理
    1. 高级特性:请求缓存、并发控制、文件上传下载
    1. 用户体验:智能loading、友好的错误提示
    1. 开发体验:TypeScript支持、API模块化管理

这样的封装不仅提高了开发效率,还增强了应用的稳定性和用户体验。记住,好的封装不是为了炫技,而是为了解决问题。希望这篇文章能帮助你在Vue项目中构建出更优雅、更强大的HTTP请求层!

最后的小提示: 封装不是一成不变的,要根据项目的实际需求进行调整。比如,如果你的项目需要支持GraphQL,或者有特殊的认证方式,都需要在封装中做相应调整。

如果你觉得这篇文章对你有帮助,欢迎分享给更多的开发者朋友。我们下期再见!

相关推荐
老华带你飞7 小时前
出行旅游安排|基于springboot出行旅游安排系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring·旅游
JIngJaneIL8 小时前
基于Java饮食营养管理信息平台系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
老华带你飞8 小时前
垃圾分类|基于springboot 垃圾分类系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
bug总结9 小时前
vue+A*算法+canvas解决自动寻路方案
前端·vue.js·算法
LYFlied9 小时前
Vue版本演进:Vue3、Vue2.7与Vue2全面对比
前端·javascript·vue.js
VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue非遗传承文化管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
Dwzun10 小时前
基于SpringBoot+Vue的农产品销售系统【附源码+文档+部署视频+讲解)
数据库·vue.js·spring boot·后端·毕业设计
JIngJaneIL10 小时前
基于Java+ vueOA工程项目管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
爱看书的小沐11 小时前
【小沐学WebGIS】基于Three.JS绘制二三维地图地球晨昏效果(WebGL / vue / react )
javascript·vue.js·gis·webgl·three.js·opengl·晨昏线