终于找到 Axios 最优雅的封装方式了,再也不用写重复代码了!

大家好,我是大华!
在干前端时,每次写接口调用,没完没了的.then.catch,每个请求都要写一遍错误处理,重复代码写了一堆又一堆。
想写个防重复提交,页面数据都乱成一锅粥!

直到我把Axios彻底封装了一遍。现在不仅代码清爽了,连后端同事都跑来问:“你这接口调用怎么写得这么优雅?”。

1. 为啥要封装?不就是发个请求吗?

原生Axios是能用,但不好用。

比如:

  • • 每次都要写完整的URL
  • • token得手动塞headers
  • • 用户狂点按钮,生成一堆重复请求
  • • 网络抖一下,接口就挂了
  • • 上传文件还得手动设 Content-Type

所以我们封装的目标就仨字:省事、统一。

2. 看看我之前写的屎山代码

// 以前的我:每个请求都要写一堆重复代码
axios.post('/api/submit', formData)
  .then(response => {
    if (response.data.success) {
      alert('提交成功!')
    } else {
      alert('提交失败:' + response.data.message)
    }
  })
  .catch(error => {
    if (error.response.status === 401) {
      alert('请先登录!')
      router.push('/login')
    } else if (error.response.status === 500) {
      alert('服务器开小差了,请重试')
    } else {
      alert('网络异常,请检查网络连接')
    }
  })
  .finally(() => {
    this.loading = false
  })

每个请求都要写一遍错误处理,每个请求都要处理loading状态,每个请求都要判断状态码… 写到怀疑人生!

3. 封装后

来看看封装后的使用方式:

// 调用接口变得如此简单
const submitForm = async () => {
  try {
    const result = await api.post('/submit', formData, {
      showSuccess: true,    // 自动显示成功提示
      retry: 3,            // 失败自动重试3次
      preventDuplicate: true // 防止重复提交
    })
    // 直接拿到业务数据,不用再解构response
    console.log(result)
  } catch (error) {
    // 错误已经统一处理了,这里基本不用写代码
  }
}

是不是清爽多了?下面我来拆解这个封装是怎么做的。

4. 核心封装代码

基础封装:创建axios实例

// src/utils/request.js
import axios from 'axios'
import { message } from 'antd'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.REACT_APP_BASE_API,
  timeout: 15000, // 超时时间
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 存储pending请求
const pendingRequests = new Map()

// 生成请求的唯一key
const generateReqKey = (config) => {
  const { url, method, params, data } = config
  return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}

// 添加请求到pending池
const addPendingRequest = (config) => {
  const key = generateReqKey(config)
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!pendingRequests.has(key)) {
      pendingRequests.set(key, cancel)
    }
  })
}

// 移除pending请求
const removePendingRequest = (config) => {
  const key = generateReqKey(config)
  if (pendingRequests.has(key)) {
    pendingRequests.delete(key)
  }
}

// 取消重复请求
const cancelPendingRequest = (config) => {
  const key = generateReqKey(config)
  if (pendingRequests.has(key)) {
    const cancel = pendingRequests.get(key)
    cancel('重复请求,自动取消')
    pendingRequests.delete(key)
  }
}

5. 请求拦截器:统一处理

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    // 取消重复请求
    if (config.preventDuplicate !== false) {
      cancelPendingRequest(config)
      addPendingRequest(config)
    }

    // 显示loading
    if (config.showLoading !== false) {
      showLoading()
    }

    // 自动添加token
    const token = localStorage.getItem('token')
    if (token && config.needToken !== false) {
      config.headers.Authorization = `Bearer ${token}`
    }

    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

6. 响应拦截器:核心逻辑

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    // 移除pending请求
    removePendingRequest(response.config)
    
    // 隐藏loading
    hideLoading()

    // 根据后端返回结构处理数据
    const { data } = response
    if (data.code === 200) {
      // 自动显示成功提示
      if (response.config.showSuccess) {
        message.success(data.message || '操作成功')
      }
      return data
    } else {
      // 业务逻辑错误
      message.error(data.message || '操作失败')
      return Promise.reject(new Error(data.message))
    }
  },
  async (error) => {
    // 移除pending请求
    removePendingRequest(error.config)
    
    // 隐藏loading
    hideLoading()

    // 如果是重复请求被取消,不报错
    if (axios.isCancel(error)) {
      return Promise.reject(error)
    }

    // 重试机制
    const { config } = error
    if (config && config.retry > 0) {
      config.retryCount = config.retryCount || 0
      if (config.retryCount < config.retry) {
        config.retryCount++
        // 延迟重试
        await new Promise(resolve => setTimeout(resolve, 1000))
        return service(config)
      }
    }

    // 统一错误处理
    handleError(error)
    return Promise.reject(error)
  }
)

// 统一错误处理函数
const handleError = (error) => {
  if (error.response) {
    const { status } = error.response
    switch (status) {
      case 401:
        message.error('未授权,请重新登录')
        localStorage.removeItem('token')
        window.location.href = '/login'
        break
      case 403:
        message.error('拒绝访问')
        break
      case 404:
        message.error('请求资源不存在')
        break
      case 500:
        message.error('服务器内部错误')
        break
      default:
        message.error('网络异常,请稍后重试')
    }
  } else if (error.request) {
    message.error('网络连接失败,请检查网络')
  } else {
    message.error('请求发送失败')
  }
}

7. 文件上传特别处理

文件上传需要不同的Content-Type,我们单独处理:

// 文件上传方法
export const uploadFile = async (url, file, options = {}) => {
  const formData = new FormData()
  formData.append('file', file)
  
  return service.post(url, formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    showLoading: true,
    showSuccess: true,
    ...options
  })
}

// 多文件上传
export const uploadMultipleFiles = async (url, files, options = {}) => {
  const formData = new FormData()
  files.forEach(file => {
    formData.append('files', file)
  })
  
  return service.post(url, formData, {
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    ...options
  })
}

// 使用示例
const handleUpload = async (file) => {
  try {
    const result = await uploadFile('/api/upload', file, {
      onUploadProgress: (progressEvent) => {
        const percent = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        )
        console.log(`上传进度: ${percent}%`)
      }
    })
    console.log('上传成功', result)
  } catch (error) {
    console.error('上传失败', error)
  }
}

8. 实际使用示例

// src/api/user.js
import api from '@/utils/request'

// 用户登录
export const login = (data) => {
  return api.post('/auth/login', data, {
    showSuccess: true,
    needToken: false // 登录接口不需要token
  })
}

// 获取用户信息
export const getUserInfo = () => {
  return api.get('/user/info', {
    retry: 2, // 失败自动重试2次
    preventDuplicate: true // 防止重复请求
  })
}

// 提交表单数据
export const submitFormData = (formData) => {
  return api.post('/form/submit', formData, {
    showSuccess: true,
    showLoading: true,
    preventDuplicate: true, // 防止重复提交
    retry: 3 // 网络错误时重试3次
  })
}

9. 在组件中的使用

import React, { useState } from 'react'
import { submitFormData, uploadFile } from '@/api/user'

const MyForm = () => {
  const [loading, setLoading] = useState(false)

  const handleSubmit = async (formData) => {
    try {
      setLoading(true)
      const result = await submitFormData(formData)
      console.log('提交成功', result)
    } catch (error) {
      // 错误已经统一处理,这里基本不用写代码
    } finally {
      setLoading(false)
    }
  }

  const handleFileChange = async (event) => {
    const file = event.target.files[0]
    if (file) {
      try {
        const result = await uploadFile(file)
        console.log('上传成功', result)
      } catch (error) {
        console.error('上传失败', error)
      }
    }
  }

  return (
    <div>
      {/* 表单内容 */}
      <input type="file" onChange={handleFileChange} />
    </div>
  )
}

总结

代码复用性:不用每个请求都写错误处理
维护性:修改错误处理逻辑只需要改一个地方
用户体验:自动loading、错误提示、重试机制
网络优化:防止重复请求,减少服务器压力
团队协作:统一代码风格,降低沟通成本

这个Axios封装现在已经成了我们团队的标配,新同事上手就能写出规范的接口调用代码。

封装的关键思路

  • • 能统一的绝不写两遍
  • • 拦截器是最好用的工具
  • • 为同事着想,降低使用成本
  • • 留出灵活配置的空间

你的项目中Axios是怎么封装的?欢迎在评论区分享交流~

本篇文章来源于微信公众号: 程序员刘大华

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容