概述

Axios 是一个基于 Promise 的 HTTP 客户端,适用于浏览器和 Node.js。在 Vue3 项目中,Axios 是进行 API 请求的首选库之一。本文将详细介绍 Axios 的安装、使用以及在 Vue3 中的最佳封装实践。

安装 Axios

使用 npm 安装

npm install axios

使用 yarn 安装

yarn add axios

使用 pnpm 安装

pnpm add axios

CDN 方式引入

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

基础使用

简单请求示例

import axios from 'axios'

// GET 请求
axios.get('/api/users')
  .then(response => {
    console.log(response.data)
  })
  .catch(error => {
    console.error(error)
  })

// POST 请求
axios.post('/api/users', {
  name: 'John Doe',
  email: 'john@example.com'
})
  .then(response => {
    console.log(response.data)
  })
  .catch(error => {
    console.error(error)
  })

基本配置

// 设置基础URL
axios.defaults.baseURL = 'https://api.example.com'

// 设置请求头
axios.defaults.headers.common['Authorization'] = 'Bearer token'
axios.defaults.headers.post['Content-Type'] = 'application/json'

// 设置超时时间
axios.defaults.timeout = 10000

Axios 实例封装

基础封装结构

// src/utils/request.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'

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

export default service

完整的请求封装

// src/utils/http.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
import router from '@/router'

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

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 添加 token
    const token = getToken()
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    // 添加请求时间戳(防止缓存)
    if (config.method === 'get') {
      config.params = {
        ...config.params,
        _t: Date.now()
      }
    }
    
    // 开发环境打印请求信息
    if (import.meta.env.DEV) {
      console.log('Request:', {
        url: config.url,
        method: config.method,
        params: config.params,
        data: config.data
      })
    }
    
    return config
  },
  error => {
    console.error('Request error:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const { data, config } = response
    
    // 开发环境打印响应信息
    if (import.meta.env.DEV) {
      console.log('Response:', {
        url: config.url,
        status: response.status,
        data: data
      })
    }
    
    // 根据后端约定的状态码处理
    if (data.code === 200 || data.success === true) {
      return data
    } else {
      // 处理业务错误
      ElMessage.error(data.message || '请求失败')
      return Promise.reject(new Error(data.message || '请求失败'))
    }
  },
  error => {
    const { response } = error
    
    if (response) {
      const { status, data } = response
      
      switch (status) {
        case 401:
          ElMessage.error('登录已过期,请重新登录')
          removeToken()
          router.push('/login')
          break
        case 403:
          ElMessage.error('没有权限访问该资源')
          break
        case 404:
          ElMessage.error('请求的资源不存在')
          break
        case 500:
          ElMessage.error('服务器内部错误')
          break
        default:
          ElMessage.error(data?.message || `请求失败: ${status}`)
      }
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('请求超时,请检查网络连接')
    } else {
      ElMessage.error('网络错误,请检查网络连接')
    }
    
    return Promise.reject(error)
  }
)

export default service

请求方法封装

RESTful API 封装

// src/api/index.js
import request from '@/utils/http'

// GET 请求
export function get(url, params = {}, config = {}) {
  return request({
    url,
    method: 'get',
    params,
    ...config
  })
}

// POST 请求
export function post(url, data = {}, config = {}) {
  return request({
    url,
    method: 'post',
    data,
    ...config
  })
}

// PUT 请求
export function put(url, data = {}, config = {}) {
  return request({
    url,
    method: 'put',
    data,
    ...config
  })
}

// DELETE 请求
export function del(url, params = {}, config = {}) {
  return request({
    url,
    method: 'delete',
    params,
    ...config
  })
}

// PATCH 请求
export function patch(url, data = {}, config = {}) {
  return request({
    url,
    method: 'patch',
    data,
    ...config
  })
}

// 文件上传
export function upload(url, formData, config = {}) {
  return request({
    url,
    method: 'post',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    ...config
  })
}

// 文件下载
export function download(url, params = {}, config = {}) {
  return request({
    url,
    method: 'get',
    params,
    responseType: 'blob',
    ...config
  })
}

具体业务 API 封装

// src/api/user.js
import { get, post, put, del } from './index'

// 用户相关 API
export const userApi = {
  // 获取用户列表
  getUsers(params) {
    return get('/users', params)
  },
  
  // 获取用户详情
  getUserDetail(id) {
    return get(`/users/${id}`)
  },
  
  // 创建用户
  createUser(data) {
    return post('/users', data)
  },
  
  // 更新用户
  updateUser(id, data) {
    return put(`/users/${id}`, data)
  },
  
  // 删除用户
  deleteUser(id) {
    return del(`/users/${id}`)
  },
  
  // 用户登录
  login(data) {
    return post('/auth/login', data)
  },
  
  // 用户登出
  logout() {
    return post('/auth/logout')
  },
  
  // 获取用户信息
  getUserInfo() {
    return get('/auth/userinfo')
  },
  
  // 修改密码
  changePassword(data) {
    return post('/auth/change-password', data)
  }
}

// 文章相关 API
export const articleApi = {
  // 获取文章列表
  getArticles(params) {
    return get('/articles', params)
  },
  
  // 获取文章详情
  getArticleDetail(id) {
    return get(`/articles/${id}`)
  },
  
  // 创建文章
  createArticle(data) {
    return post('/articles', data)
  },
  
  // 更新文章
  updateArticle(id, data) {
    return put(`/articles/${id}`, data)
  },
  
  // 删除文章
  deleteArticle(id) {
    return del(`/articles/${id}`)
  }
}

Vue3 组合式 API 封装

自定义 Composable

// src/composables/useHttp.js
import { ref, reactive } from 'vue'
import { get, post, put, del } from '@/api'

export function useHttp() {
  const loading = ref(false)
  const error = ref(null)
  
  // 通用请求方法
  const request = async (apiMethod, ...args) => {
    loading.value = true
    error.value = null
    
    try {
      const result = await apiMethod(...args)
      return result
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    loading,
    error,
    request,
    get: (...args) => request(get, ...args),
    post: (...args) => request(post, ...args),
    put: (...args) => request(put, ...args),
    del: (...args) => request(del, ...args)
  }
}

// 分页请求 Composable
export function usePagination(apiMethod, defaultParams = {}) {
  const { loading, error, request } = useHttp()
  
  const data = ref([])
  const total = ref(0)
  const currentPage = ref(1)
  const pageSize = ref(10)
  const params = reactive({ ...defaultParams })
  
  const fetchData = async (page = 1) => {
    currentPage.value = page
    
    const requestParams = {
      page: currentPage.value,
      pageSize: pageSize.value,
      ...params
    }
    
    try {
      const result = await request(apiMethod, requestParams)
      data.value = result.data || result.list || []
      total.value = result.total || result.count || 0
    } catch (err) {
      console.error('Pagination error:', err)
    }
  }
  
  const refresh = () => {
    fetchData(currentPage.value)
  }
  
  const reset = () => {
    currentPage.value = 1
    Object.assign(params, defaultParams)
    fetchData()
  }
  
  return {
    loading,
    error,
    data,
    total,
    currentPage,
    pageSize,
    params,
    fetchData,
    refresh,
    reset
  }
}

在组件中使用

<!-- src/views/UserList.vue -->
<template>
  <div class="user-list">
    <el-table v-loading="loading" :data="data">
      <el-table-column prop="id" label="ID" />
      <el-table-column prop="name" label="姓名" />
      <el-table-column prop="email" label="邮箱" />
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button @click="handleEdit(row)">编辑</el-button>
          <el-button type="danger" @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      @current-change="fetchData"
    />
  </div>
</template>

<script setup>
import { userApi } from '@/api/user'
import { usePagination } from '@/composables/useHttp'

const {
  loading,
  data,
  total,
  currentPage,
  pageSize,
  fetchData
} = usePagination(userApi.getUsers, {
  status: 'active'
})

// 初始化加载数据
fetchData()

const handleEdit = (user) => {
  console.log('Edit user:', user)
}

const handleDelete = async (user) => {
  try {
    await userApi.deleteUser(user.id)
    fetchData() // 刷新列表
  } catch (error) {
    console.error('Delete failed:', error)
  }
}
</script>

高级功能封装

请求重试机制

// src/utils/retry.js
import axios from 'axios'

// 重试配置
const retryConfig = {
  retries: 3,
  retryDelay: 1000,
  retryCondition: (error) => {
    // 网络错误或 5xx 错误时重试
    return !error.response || (error.response.status >= 500)
  }
}

// 重试拦截器
const retryInterceptor = (error) => {
  const { config, response } = error
  
  if (!config || !config.retry) {
    return Promise.reject(error)
  }
  
  config.__retryCount = config.__retryCount || 0
  
  if (config.__retryCount >= config.retries) {
    return Promise.reject(error)
  }
  
  config.__retryCount += 1
  
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(axios(config))
    }, config.retryDelay || 1000)
  })
}

// 添加重试功能
const addRetryToRequest = (config) => {
  return {
    ...config,
    retry: retryConfig.retries,
    retryDelay: retryConfig.retryDelay
  }
}

export { retryConfig, retryInterceptor, addRetryToRequest }

请求取消

// src/utils/cancel.js
import axios from 'axios'

const pendingRequests = new Map()

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

// 添加请求到待处理列表
const addPendingRequest = (config) => {
  const requestKey = generateRequestKey(config)
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingRequests.has(requestKey)) {
      pendingRequests.set(requestKey, cancel)
    }
  })
}

// 移除待处理的请求
const removePendingRequest = (config) => {
  const requestKey = generateRequestKey(config)
  if (pendingRequests.has(requestKey)) {
    const cancel = pendingRequests.get(requestKey)
    cancel(requestKey)
    pendingRequests.delete(requestKey)
  }
}

// 取消所有待处理的请求
const cancelAllRequests = () => {
  pendingRequests.forEach((cancel) => {
    cancel('Component unmounted')
  })
  pendingRequests.clear()
}

export {
  addPendingRequest,
  removePendingRequest,
  cancelAllRequests
}

完整的请求工具类

// src/utils/httpClient.js
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import { addRetryToRequest, retryInterceptor } from './retry'
import { addPendingRequest, removePendingRequest } from './cancel'

class HttpClient {
  constructor(baseConfig = {}) {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
      timeout: 15000,
      headers: {
        'Content-Type': 'application/json;charset=utf-8'
      },
      ...baseConfig
    })
    
    this.setupInterceptors()
  }
  
  setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      config => {
        // 添加 token
        const token = getToken()
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        
        // 添加请求取消
        addPendingRequest(config)
        
        // 添加重试配置
        addRetryToRequest(config)
        
        // 开发环境打印
        if (import.meta.env.DEV) {
          console.log('Request:', config)
        }
        
        return config
      },
      error => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.instance.interceptors.response.use(
      response => {
        // 移除待处理请求
        removePendingRequest(response.config)
        
        const { data } = response
        
        if (import.meta.env.DEV) {
          console.log('Response:', data)
        }
        
        if (data.code === 200 || data.success === true) {
          return data
        } else {
          ElMessage.error(data.message || '请求失败')
          return Promise.reject(new Error(data.message || '请求失败'))
        }
      },
      error => {
        // 移除待处理请求
        if (error.config) {
          removePendingRequest(error.config)
        }
        
        // 重试机制
        if (error.config && error.config.retry) {
          return retryInterceptor(error)
        }
        
        this.handleError(error)
        return Promise.reject(error)
      }
    )
  }
  
  handleError(error) {
    if (axios.isCancel(error)) {
      console.log('Request canceled:', error.message)
      return
    }
    
    const { response } = error
    
    if (response) {
      const { status, data } = response
      
      switch (status) {
        case 401:
          ElMessage.error('登录已过期')
          // 跳转到登录页
          break
        case 403:
          ElMessage.error('没有权限')
          break
        case 404:
          ElMessage.error('资源不存在')
          break
        case 500:
          ElMessage.error('服务器错误')
          break
        default:
          ElMessage.error(data?.message || `请求失败: ${status}`)
      }
    } else if (error.code === 'ECONNABORTED') {
      ElMessage.error('请求超时')
    } else {
      ElMessage.error('网络错误')
    }
  }
  
  // 请求方法
  request(config) {
    return this.instance.request(config)
  }
  
  get(url, params = {}, config = {}) {
    return this.request({
      url,
      method: 'get',
      params,
      ...config
    })
  }
  
  post(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'post',
      data,
      ...config
    })
  }
  
  put(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'put',
      data,
      ...config
    })
  }
  
  delete(url, params = {}, config = {}) {
    return this.request({
      url,
      method: 'delete',
      params,
      ...config
    })
  }
  
  upload(url, formData, config = {}) {
    return this.request({
      url,
      method: 'post',
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      ...config
    })
  }
  
  download(url, params = {}, config = {}) {
    return this.request({
      url,
      method: 'get',
      params,
      responseType: 'blob',
      ...config
    })
  }
}

// 创建实例
const httpClient = new HttpClient()

export default httpClient

环境配置

环境变量配置

// .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=开发环境

// .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=生产环境

// .env.staging
VITE_API_BASE_URL=https://staging-api.example.com
VITE_APP_TITLE=测试环境

API 配置文件

// src/config/api.js
export const apiConfig = {
  // 开发环境
  development: {
    baseURL: 'http://localhost:3000/api',
    timeout: 15000,
    mock: true
  },
  
  // 生产环境
  production: {
    baseURL: 'https://api.example.com',
    timeout: 10000,
    mock: false
  },
  
  // 测试环境
  staging: {
    baseURL: 'https://staging-api.example.com',
    timeout: 15000,
    mock: false
  }
}

export const currentConfig = apiConfig[import.meta.env.MODE] || apiConfig.development

Mock 数据集成

Mock 服务配置

// src/mock/index.js
import Mock from 'mockjs'
import { userMocks } from './user'
import { articleMocks } from './article'

// 设置延迟时间
Mock.setup({
  timeout: '200-600'
})

// 注册 Mock API
userMocks.forEach(mock => {
  Mock.mock(mock.url, mock.method, mock.response)
})

articleMocks.forEach(mock => {
  Mock.mock(mock.url, mock.method, mock.response)
})

export default Mock

条件性启用 Mock

// src/utils/http.js
import axios from 'axios'

// 开发环境启用 mock
if (import.meta.env.MODE === 'development' && import.meta.env.VITE_USE_MOCK) {
  import('@/mock')
}

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
  timeout: 15000
})

export default service

TypeScript 支持

类型定义

// src/types/api.ts
export interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
  success: boolean
}

export interface PaginationParams {
  page: number
  pageSize: number
  [key: string]: any
}

export interface PaginationResponse<T> {
  data: T[]
  total: number
  page: number
  pageSize: number
}

export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: string
  updatedAt: string
}

export interface Article {
  id: number
  title: string
  content: string
  author: User
  tags: string[]
  createdAt: string
  updatedAt: string
}

类型化的 API 封装

// src/api/user.ts
import { get, post, put, del } from './index'
import type { User, ApiResponse, PaginationParams, PaginationResponse } from '@/types/api'

export const userApi = {
  getUsers(params: PaginationParams): Promise<ApiResponse<PaginationResponse<User>>> {
    return get('/users', params)
  },
  
  getUserDetail(id: number): Promise<ApiResponse<User>> {
    return get(`/users/${id}`)
  },
  
  createUser(data: Partial<User>): Promise<ApiResponse<User>> {
    return post('/users', data)
  },
  
  updateUser(id: number, data: Partial<User>): Promise<ApiResponse<User>> {
    return put(`/users/${id}`, data)
  },
  
  deleteUser(id: number): Promise<ApiResponse<null>> {
    return del(`/users/${id}`)
  }
}

性能优化

请求缓存

// src/utils/cache.js
const cache = new Map()

export function useCache(ttl = 5 * 60 * 1000) { // 默认5分钟
  return {
    get(key) {
      const item = cache.get(key)
      if (item && Date.now() - item.timestamp < ttl) {
        return item.data
      }
      cache.delete(key)
      return null
    },
    
    set(key, data) {
      cache.set(key, {
        data,
        timestamp: Date.now()
      })
    },
    
    clear() {
      cache.clear()
    }
  }
}

请求去重

// src/utils/dedupe.js
const pendingRequests = new Map()

export function dedupeRequest(config) {
  const key = JSON.stringify({ url: config.url, method: config.method, params: config.params, data: config.data })
  
  if (pendingRequests.has(key)) {
    return pendingRequests.get(key)
  }
  
  const request = axios(config).finally(() => {
    pendingRequests.delete(key)
  })
  
  pendingRequests.set(key, request)
  
  return request
}

最佳实践总结

1. 项目结构

src/
├── api/
│   ├── index.js          # 基础请求方法
│   ├── user.js           # 用户相关API
│   ├── article.js        # 文章相关API
│   └── modules/          # 其他模块API
├── utils/
│   ├── http.js           # HTTP客户端封装
│   ├── auth.js           # 认证工具
│   ├── cache.js          # 缓存工具
│   └── cancel.js         # 请求取消工具
├── composables/
│   ├── useHttp.js        # HTTP组合式API
│   └── usePagination.js  # 分页组合式API
└── types/
    └── api.ts            # TypeScript类型定义

2. 配置原则

  • 统一的错误处理
  • 统一的请求/响应拦截
  • 环境变量配置
  • TypeScript 类型支持

3. 使用建议

  • 优先使用组合式 API
  • 合理使用缓存机制
  • 及时取消不需要的请求
  • 做好错误处理和用户提示
  • 保持 API 接口的统一性