第81集:Axios封装与配置

概述

Axios是Vue生态中最流行的HTTP客户端库,用于发送HTTP请求。在大型Vue应用中,对Axios进行合理的封装和配置可以提高代码的可维护性、可扩展性和性能。掌握Axios的封装和配置对于构建高效、可靠的API通信层至关重要。

核心知识点

1. Axios基础

1.1 安装Axios

npm install axios
# 或
yarn add axios
# 或
pnpm add axios

1.2 基本使用

// 简单使用
import axios from 'axios'

async function fetchUsers() {
  try {
    const response = await axios.get('https://api.example.com/users')
    console.log(response.data)
  } catch (error) {
    console.error('Error fetching users:', error)
  }
}

// 发送POST请求
async function createUser(user: User) {
  try {
    const response = await axios.post('https://api.example.com/users', user)
    console.log(response.data)
  } catch (error) {
    console.error('Error creating user:', error)
  }
}

2. Axios封装设计

2.1 为什么需要封装Axios

  • 统一API请求配置
  • 集中处理请求和响应拦截
  • 统一错误处理
  • 简化API调用
  • 支持多环境配置
  • 便于扩展和维护

2.2 封装Axios实例

// src/utils/axios.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

class HttpRequest {
  private instance: AxiosInstance

  constructor() {
    // 创建Axios实例
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })

    // 初始化拦截器
    this.initInterceptors()
  }

  // 初始化拦截器
  private initInterceptors(): void {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        // 添加请求拦截逻辑,如添加token
        const token = localStorage.getItem('token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        // 统一处理响应数据
        const { data, status } = response
        if (status === 200) {
          return data
        } else {
          return Promise.reject(new Error('Request failed'))
        }
      },
      (error) => {
        // 统一处理错误
        return Promise.reject(error)
      }
    )
  }

  // 请求方法封装
  request<T = any>(config: AxiosRequestConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      this.instance
        .request<any, T>(config)
        .then((response) => {
          resolve(response)
        })
        .catch((error) => {
          reject(error)
        })
    })
  }

  // GET请求
  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'GET', url })
  }

  // POST请求
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'POST', url, data })
  }

  // PUT请求
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT', url, data })
  }

  // DELETE请求
  delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE', url })
  }

  // PATCH请求
  patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH', url, data })
  }
}

// 导出Axios实例
export const http = new HttpRequest()

3. 环境配置

3.1 Vite环境变量

在Vite项目中,可以使用.env文件配置环境变量:

# .env.development
VITE_API_BASE_URL = http://localhost:3000/api

# .env.production
VITE_API_BASE_URL = https://api.example.com/api

# .env.test
VITE_API_BASE_URL = https://test-api.example.com/api

3.2 动态环境配置

// src/utils/axios.ts
// 根据环境动态设置baseURL
const getBaseUrl = () => {
  switch (import.meta.env.MODE) {
    case 'development':
      return 'http://localhost:3000/api'
    case 'production':
      return 'https://api.example.com/api'
    case 'test':
      return 'https://test-api.example.com/api'
    default:
      return '/api'
  }
}

class HttpRequest {
  private instance: AxiosInstance

  constructor() {
    this.instance = axios.create({
      baseURL: getBaseUrl(),
      // ...
    })
    // ...
  }
  // ...
}

4. API模块化设计

4.1 按功能划分API模块

src/api/
├── index.ts              # 统一出口
├── auth.ts               # 认证相关API
├── user.ts               # 用户管理API
├── product.ts            # 产品管理API
└── order.ts              # 订单管理API

4.2 API模块示例

// src/api/user.ts
import { http } from '../utils/axios'
import type { User, UserListParams, UserListResponse } from '../types/user'

// 获取用户列表
export const getUserList = (params: UserListParams) => {
  return http.get<UserListResponse>('/users', { params })
}

// 获取用户详情
export const getUserById = (id: number) => {
  return http.get<User>(`/users/${id}`)
}

// 创建用户
export const createUser = (data: User) => {
  return http.post<User>('/users', data)
}

// 更新用户
export const updateUser = (id: number, data: Partial<User>) => {
  return http.put<User>(`/users/${id}`, data)
}

// 删除用户
export const deleteUser = (id: number) => {
  return http.delete(`/users/${id}`)
}

4.3 统一API出口

// src/api/index.ts
export * from './auth'
export * from './user'
export * from './product'
export * from './order'

5. 类型定义

5.1 API响应类型

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

// src/types/user.ts
export interface User {
  id: number
  name: string
  email: string
  createdAt: string
  updatedAt: string
}

export interface UserListParams {
  page: number
  limit: number
  keyword?: string
}

export interface UserListResponse {
  list: User[]
  total: number
  page: number
  limit: number
}

5.2 Axios实例类型扩展

// src/types/axios.d.ts
import 'axios'

declare module 'axios' {
  export interface AxiosInstance {
    request<T = any>(config: AxiosRequestConfig): Promise<T>
    get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
    post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
    put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
    delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
    patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
  }
}

6. 高级配置

6.1 超时处理

// src/utils/axios.ts
class HttpRequest {
  constructor() {
    this.instance = axios.create({
      timeout: 10000, // 设置全局超时时间
      // ...
    })
    // ...
  }
  // ...
}

// 单独设置超时时间
export const fetchLargeData = () => {
  return http.get('/large-data', { timeout: 30000 }) // 30秒超时
}

6.2 取消请求

// src/utils/axios.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelTokenSource } from 'axios'

class HttpRequest {
  private instance: AxiosInstance
  private cancelTokenSourceMap: Map<string, CancelTokenSource> = new Map()

  constructor() {
    // ...
  }

  // 取消请求
  cancelRequest(url: string) {
    if (this.cancelTokenSourceMap.has(url)) {
      this.cancelTokenSourceMap.get(url)?.cancel()
      this.cancelTokenSourceMap.delete(url)
    }
  }

  // 取消所有请求
  cancelAllRequests() {
    this.cancelTokenSourceMap.forEach((source) => {
      source.cancel()
    })
    this.cancelTokenSourceMap.clear()
  }

  // 请求方法封装
  request<T = any>(config: AxiosRequestConfig): Promise<T> {
    const url = config.url || ''
    // 创建取消令牌
    const source = axios.CancelToken.source()
    this.cancelTokenSourceMap.set(url, source)
    
    config.cancelToken = source.token
    
    return new Promise((resolve, reject) => {
      this.instance
        .request<any, T>(config)
        .then((response) => {
          this.cancelTokenSourceMap.delete(url)
          resolve(response)
        })
        .catch((error) => {
          this.cancelTokenSourceMap.delete(url)
          reject(error)
        })
    })
  }
  // ...
}

6.3 重试机制

// src/utils/axios.ts
class HttpRequest {
  // 添加重试逻辑的请求方法
  requestWithRetry<T = any>(config: AxiosRequestConfig, retryCount: number = 3): Promise<T> {
    return new Promise((resolve, reject) => {
      let attempt = 0

      const attemptRequest = () => {
        attempt++
        this.instance
          .request<any, T>(config)
          .then((response) => {
            resolve(response)
          })
          .catch((error) => {
            if (attempt < retryCount) {
              // 延迟重试
              setTimeout(attemptRequest, 1000 * attempt)
            } else {
              reject(error)
            }
          })
      }

      attemptRequest()
    })
  }
  // ...
}

最佳实践

1. 统一错误处理

// src/utils/axios.ts
class HttpRequest {
  constructor() {
    // ...
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        // ...
      },
      (error) => {
        // 统一错误处理
        const { response } = error
        if (response) {
          switch (response.status) {
            case 400:
              console.error('请求错误:', response.data.message)
              break
            case 401:
              console.error('未授权,请重新登录')
              // 跳转到登录页面
              // router.push('/login')
              break
            case 403:
              console.error('拒绝访问')
              break
            case 404:
              console.error('请求地址不存在')
              break
            case 500:
              console.error('服务器内部错误')
              break
            default:
              console.error('请求失败')
          }
        } else if (error.request) {
          console.error('网络错误,请检查网络连接')
        } else {
          console.error('请求配置错误')
        }
        return Promise.reject(error)
      }
    )
  }
  // ...
}

2. 加载状态管理

// src/utils/axios.ts
class HttpRequest {
  private pendingRequests: Set<string> = new Set()

  constructor() {
    // ...
    this.instance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        const url = config.url || ''
        this.pendingRequests.add(url)
        // 可以在这里触发全局加载状态
        // eventBus.emit('loading', true)
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )

    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const url = response.config.url || ''
        this.pendingRequests.delete(url)
        if (this.pendingRequests.size === 0) {
          // 可以在这里关闭全局加载状态
          // eventBus.emit('loading', false)
        }
        return response
      },
      (error) => {
        if (error.config) {
          const url = error.config.url || ''
          this.pendingRequests.delete(url)
          if (this.pendingRequests.size === 0) {
            // 可以在这里关闭全局加载状态
            // eventBus.emit('loading', false)
          }
        }
        return Promise.reject(error)
      }
    )
  }
  // ...
}

3. API缓存

// src/utils/axios.ts
class HttpRequest {
  private cache: Map<string, { data: any; timestamp: number }> = new Map()
  private cacheExpireTime = 5 * 60 * 1000 // 5分钟缓存

  // 带缓存的GET请求
  getWithCache<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    const cacheKey = `${url}_${JSON.stringify(config?.params)}`
    const cachedData = this.cache.get(cacheKey)

    // 检查缓存是否有效
    if (cachedData && Date.now() - cachedData.timestamp < this.cacheExpireTime) {
      return Promise.resolve(cachedData.data as T)
    }

    // 缓存无效,重新请求
    return this.get<T>(url, config).then((data) => {
      // 更新缓存
      this.cache.set(cacheKey, { data, timestamp: Date.now() })
      return data
    })
  }

  // 清除缓存
  clearCache(url?: string) {
    if (url) {
      // 清除指定URL的缓存
      this.cache.forEach((_, key) => {
        if (key.startsWith(url)) {
          this.cache.delete(key)
        }
      })
    } else {
      // 清除所有缓存
      this.cache.clear()
    }
  }
  // ...
}

常见问题与解决方案

1. 跨域问题

问题:浏览器出现跨域错误。

解决方案

  • 配置CORS(跨域资源共享)
  • 使用代理服务器
  • 使用JSONP(仅支持GET请求)

在Vite项目中配置代理:

// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

2. Token过期处理

问题:Token过期导致请求失败。

解决方案

  • 在响应拦截器中检测Token过期
  • 自动刷新Token
  • 跳转到登录页面
// src/utils/axios.ts
class HttpRequest {
  private isRefreshing = false
  private refreshTokenQueue: Array<(token: string) => void> = []

  constructor() {
    // ...
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        // ...
      },
      async (error) => {
        const { response } = error
        if (response && response.status === 401) {
          if (!this.isRefreshing) {
            this.isRefreshing = true
            try {
              // 刷新Token
              const refreshToken = localStorage.getItem('refreshToken')
              const { token: newToken } = await http.post('/auth/refresh', { refreshToken })
              
              // 更新Token
              localStorage.setItem('token', newToken)
              
              // 执行队列中的请求
              this.refreshTokenQueue.forEach((callback) => callback(newToken))
              this.refreshTokenQueue = []
              
              // 重试当前请求
              return this.instance(error.config)
            } catch (refreshError) {
              // 刷新Token失败,跳转到登录页面
              // router.push('/login')
              return Promise.reject(refreshError)
            } finally {
              this.isRefreshing = false
            }
          } else {
            // 等待Token刷新完成
            return new Promise((resolve) => {
              this.refreshTokenQueue.push((token: string) => {
                error.config.headers.Authorization = `Bearer ${token}`
                resolve(this.instance(error.config))
              })
            })
          }
        }
        return Promise.reject(error)
      }
    )
  }
  // ...
}

3. 重复请求问题

问题:短时间内发送多个相同请求,导致性能问题。

解决方案

  • 取消重复请求
  • 使用缓存
  • 使用防抖和节流
// src/utils/axios.ts
class HttpRequest {
  private pendingRequests: Set<string> = new Set()

  constructor() {
    // ...
    this.instance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        const requestKey = `${config.method}_${config.url}_${JSON.stringify(config.params)}_${JSON.stringify(config.data)}`
        if (this.pendingRequests.has(requestKey)) {
          return Promise.reject(new Error('Duplicate request'))
        }
        this.pendingRequests.add(requestKey)
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )

    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const requestKey = `${response.config.method}_${response.config.url}_${JSON.stringify(response.config.params)}_${JSON.stringify(response.config.data)}`
        this.pendingRequests.delete(requestKey)
        return response
      },
      (error) => {
        if (error.config) {
          const requestKey = `${error.config.method}_${error.config.url}_${JSON.stringify(error.config.params)}_${JSON.stringify(error.config.data)}`
          this.pendingRequests.delete(requestKey)
        }
        return Promise.reject(error)
      }
    )
  }
  // ...
}

进一步学习资源

  1. Axios官方文档
  2. Vite官方文档 - 环境变量
  3. RESTful API设计指南
  4. HTTP状态码
  5. CORS(跨域资源共享)

课后练习

  1. 基础练习

    • 安装Axios并创建Axios实例
    • 配置请求拦截器和响应拦截器
    • 实现基本的GET和POST请求
  2. 进阶练习

    • 封装Axios类,支持多种HTTP方法
    • 实现API模块化设计
    • 添加环境配置支持
    • 实现TypeScript类型定义
  3. 高级练习

    • 实现取消请求功能
    • 添加重试机制
    • 实现API缓存
    • 处理Token过期问题
  4. 实战练习

    • 集成到Vue项目中
    • 实现用户管理API
    • 添加加载状态管理
    • 实现统一错误处理

通过本节课的学习,你应该能够掌握Axios的封装和配置,理解API模块化设计,掌握环境配置和类型定义,以及Axios的高级特性如取消请求、重试机制和缓存。这些知识将帮助你构建高效、可靠的API通信层,提高应用的性能和可维护性。

« 上一篇 大型应用状态架构 - Pinia企业级设计 下一篇 » 82-request-response-interceptors