第11章 HTTP请求与API交互

第30节 Axios封装

11.30.1 Axios基础配置

什么是Axios?

Axios是一个基于Promise的HTTP客户端,用于浏览器和Node.js环境。它具有以下特点:

  • 从浏览器中创建XMLHttpRequest请求
  • 从Node.js创建HTTP请求
  • 支持Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XSRF攻击

安装Axios

使用npm安装:

npm install axios

使用yarn安装:

yarn add axios

使用pnpm安装:

pnpm add axios

创建Axios实例

我们通常会创建一个Axios实例来配置一些默认选项,如基础URL、超时时间、请求头信息等。

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

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量中获取基础URL
  timeout: 10000, // 请求超时时间,单位ms
  headers: {
    'Content-Type': 'application/json' // 默认请求头
  }
})

export default request

在上面的代码中,我们使用了import.meta.env.VITE_API_BASE_URL来获取环境变量中的基础URL。这是Vite项目中获取环境变量的方式,需要在项目根目录下创建.env文件:

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

请求拦截器

请求拦截器可以在发送请求之前对请求进行处理,例如添加认证令牌、修改请求头信息等。

// src/utils/request.js
import axios from 'axios'
import router from '@/router'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
request.interceptors.request.use(
  config => {
    // 从本地存储中获取认证令牌
    const token = localStorage.getItem('token')
    if (token) {
      // 添加Authorization请求头
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => {
    // 处理请求错误
    return Promise.reject(error)
  }
)

export default request

响应拦截器

响应拦截器可以在收到响应之后对响应进行处理,例如统一处理错误、转换响应数据等。

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

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

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

// 响应拦截器
request.interceptors.response.use(
  // 成功响应处理
  response => {
    // 直接返回响应数据,简化使用
    return response.data
  },
  // 错误响应处理
  error => {
    // 处理网络错误
    if (!error.response) {
      ElMessage.error('网络错误,请检查网络连接')
      return Promise.reject(error)
    }

    // 处理HTTP错误状态码
    const { status, data } = error.response
    switch (status) {
      case 400:
        ElMessage.error(data.message || '请求参数错误')
        break
      case 401:
        // 未授权,清除token并跳转到登录页
        localStorage.removeItem('token')
        router.push('/login')
        ElMessage.error('登录已过期,请重新登录')
        break
      case 403:
        ElMessage.error('没有权限访问该资源')
        break
      case 404:
        ElMessage.error('请求的资源不存在')
        break
      case 500:
        ElMessage.error('服务器内部错误')
        break
      default:
        ElMessage.error(`请求失败,状态码:${status}`)
    }
    return Promise.reject(error)
  }
)

export default request

11.30.2 统一错误处理

在实际项目中,我们需要对各种错误进行统一处理,包括网络错误、HTTP错误状态码、业务逻辑错误等。

业务逻辑错误处理

除了HTTP错误状态码之外,我们还需要处理业务逻辑错误。例如,当请求成功(状态码200)但业务逻辑失败时,后端可能会返回一个包含错误信息的响应。

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

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

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

// 响应拦截器
request.interceptors.response.use(
  response => {
    const { code, message, data } = response.data
    
    // 假设后端返回的响应格式为:{ code: number, message: string, data: any }
    // code为0表示成功,非0表示失败
    if (code === 0) {
      // 业务逻辑成功,返回数据
      return data
    } else {
      // 业务逻辑失败,显示错误信息
      ElMessage.error(message || '请求失败')
      return Promise.reject(new Error(message || '请求失败'))
    }
  },
  error => {
    if (!error.response) {
      ElMessage.error('网络错误,请检查网络连接')
      return Promise.reject(error)
    }

    const { status, data } = error.response
    switch (status) {
      case 400:
        ElMessage.error(data.message || '请求参数错误')
        break
      case 401:
        localStorage.removeItem('token')
        router.push('/login')
        ElMessage.error('登录已过期,请重新登录')
        break
      case 403:
        ElMessage.error('没有权限访问该资源')
        break
      case 404:
        ElMessage.error('请求的资源不存在')
        break
      case 500:
        ElMessage.error('服务器内部错误')
        break
      default:
        ElMessage.error(`请求失败,状态码:${status}`)
    }
    return Promise.reject(error)
  }
)

export default request

错误类型定义

为了更好地处理错误,我们可以定义一些错误类型:

// src/utils/errorTypes.js

// 网络错误
export class NetworkError extends Error {
  constructor(message = '网络错误') {
    super(message)
    this.name = 'NetworkError'
  }
}

// 认证错误
export class AuthError extends Error {
  constructor(message = '认证失败') {
    super(message)
    this.name = 'AuthError'
  }
}

// 权限错误
export class PermissionError extends Error {
  constructor(message = '没有权限') {
    super(message)
    this.name = 'PermissionError'
  }
}

// 业务逻辑错误
export class BusinessError extends Error {
  constructor(message = '业务逻辑错误', code = -1) {
    super(message)
    this.name = 'BusinessError'
    this.code = code
  }
}

然后在响应拦截器中使用这些错误类型:

// src/utils/request.js
import axios from 'axios'
import router from '@/router'
import { ElMessage } from 'element-plus'
import { NetworkError, AuthError, PermissionError, BusinessError } from './errorTypes'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

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

// 响应拦截器
request.interceptors.response.use(
  response => {
    const { code, message, data } = response.data
    
    if (code === 0) {
      return data
    } else {
      ElMessage.error(message || '请求失败')
      return Promise.reject(new BusinessError(message || '请求失败', code))
    }
  },
  error => {
    if (!error.response) {
      ElMessage.error('网络错误,请检查网络连接')
      return Promise.reject(new NetworkError())
    }

    const { status, data } = error.response
    let errorInstance
    
    switch (status) {
      case 400:
        errorInstance = new Error(data.message || '请求参数错误')
        break
      case 401:
        localStorage.removeItem('token')
        router.push('/login')
        errorInstance = new AuthError('登录已过期,请重新登录')
        break
      case 403:
        errorInstance = new PermissionError('没有权限访问该资源')
        break
      case 404:
        errorInstance = new Error('请求的资源不存在')
        break
      case 500:
        errorInstance = new Error('服务器内部错误')
        break
      default:
        errorInstance = new Error(`请求失败,状态码:${status}`)
    }
    
    ElMessage.error(errorInstance.message)
    return Promise.reject(errorInstance)
  }
)

export default request

11.30.3 取消请求与防抖处理

取消请求

Axios支持取消请求,可以用于取消正在进行的请求,例如当用户快速切换页面时,取消之前页面的请求。

使用CancelToken(Axios v0.x和v1.x兼容)
// src/utils/request.js
import axios from 'axios'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 存储取消请求的控制器
const cancelTokens = new Map()

// 请求拦截器
request.interceptors.request.use(
  config => {
    // 生成请求标识
    const requestKey = `${config.method}-${config.url}`
    
    // 取消之前的相同请求
    if (cancelTokens.has(requestKey)) {
      cancelTokens.get(requestKey).cancel()
      cancelTokens.delete(requestKey)
    }
    
    // 创建新的CancelToken
    const source = axios.CancelToken.source()
    config.cancelToken = source.token
    cancelTokens.set(requestKey, source)
    
    // 添加认证令牌
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  response => {
    // 移除已完成请求的取消控制器
    const requestKey = `${response.config.method}-${response.config.url}`
    cancelTokens.delete(requestKey)
    
    const { code, message, data } = response.data
    if (code === 0) {
      return data
    } else {
      return Promise.reject(new Error(message || '请求失败'))
    }
  },
  error => {
    // 移除已完成请求的取消控制器
    if (error.config) {
      const requestKey = `${error.config.method}-${error.config.url}`
      cancelTokens.delete(requestKey)
    }
    
    // 处理取消请求的错误
    if (axios.isCancel(error)) {
      console.log('请求已取消:', error.message)
      return Promise.reject(error)
    }
    
    // 处理其他错误
    return Promise.reject(error)
  }
)

export default request
使用AbortController(Axios v1.x推荐)

Axios v1.x推荐使用AbortController来取消请求,这是浏览器原生支持的API。

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

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 存储取消控制器
const abortControllers = new Map()

// 请求拦截器
request.interceptors.request.use(
  config => {
    // 生成请求标识
    const requestKey = `${config.method}-${config.url}`
    
    // 取消之前的相同请求
    if (abortControllers.has(requestKey)) {
      abortControllers.get(requestKey).abort()
      abortControllers.delete(requestKey)
    }
    
    // 创建新的AbortController
    const controller = new AbortController()
    config.signal = controller.signal
    abortControllers.set(requestKey, controller)
    
    // 添加认证令牌
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
request.interceptors.response.use(
  response => {
    // 移除已完成请求的取消控制器
    const requestKey = `${response.config.method}-${response.config.url}`
    abortControllers.delete(requestKey)
    
    const { code, message, data } = response.data
    if (code === 0) {
      return data
    } else {
      return Promise.reject(new Error(message || '请求失败'))
    }
  },
  error => {
    // 移除已完成请求的取消控制器
    if (error.config) {
      const requestKey = `${error.config.method}-${error.config.url}`
      abortControllers.delete(requestKey)
    }
    
    // 处理取消请求的错误
    if (error.name === 'AbortError') {
      console.log('请求已取消')
      return Promise.reject(error)
    }
    
    // 处理其他错误
    return Promise.reject(error)
  }
)

export default request

防抖处理

防抖处理可以防止用户频繁触发请求,例如在搜索框中输入时,只有在用户停止输入一段时间后才发送请求。

使用useDebounceFn(VueUse)

我们可以使用VueUse的useDebounceFn函数来实现防抖:

<template>
  <div class="demo-debounce">
    <h3>防抖搜索</h3>
    <el-input 
      v-model="searchText" 
      placeholder="输入搜索内容"
      style="width: 100%; margin-bottom: 20px;"
    ></el-input>
    <div class="search-result" v-if="searchResult.length > 0">
      <h4>搜索结果:</h4>
      <ul>
        <li v-for="item in searchResult" :key="item.id">
          {{ item.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useDebounceFn } from '@vueuse/core'
import request from '@/utils/request'

const searchText = ref('')
const searchResult = ref([])

// 防抖搜索函数,延迟500ms执行
const debouncedSearch = useDebounceFn(async (text) => {
  if (!text) {
    searchResult.value = []
    return
  }
  
  try {
    const result = await request.get('/search', {
      params: { keyword: text }
    })
    searchResult.value = result
  } catch (error) {
    console.error('搜索失败:', error)
  }
}, 500)

// 监听搜索文本变化
watch(searchText, (newText) => {
  debouncedSearch(newText)
})
</script>

<style scoped>
.demo-debounce {
  max-width: 400px;
  margin: 0 auto;
}

.search-result {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  background-color: #f5f7fa;
}

.search-result ul {
  padding-left: 20px;
}

.search-result li {
  margin: 5px 0;
}
</style>
自定义防抖函数

我们也可以自己实现一个防抖函数:

// src/utils/debounce.js

/**
 * 防抖函数
 * @param {Function} func - 要执行的函数
 * @param {number} delay - 延迟时间,单位ms
 * @returns {Function} - 防抖处理后的函数
 */
export function debounce(func, delay) {
  let timeoutId
  return function (...args) {
    // 清除之前的定时器
    clearTimeout(timeoutId)
    // 设置新的定时器
    timeoutId = setTimeout(() => {
      func.apply(this, args)
    }, delay)
  }
}

然后在组件中使用:

<template>
  <div class="demo-debounce">
    <h3>防抖搜索</h3>
    <el-input 
      v-model="searchText" 
      placeholder="输入搜索内容"
      style="width: 100%; margin-bottom: 20px;"
      @input="handleInput"
    ></el-input>
    <div class="search-result" v-if="searchResult.length > 0">
      <h4>搜索结果:</h4>
      <ul>
        <li v-for="item in searchResult" :key="item.id">
          {{ item.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { debounce } from '@/utils/debounce'
import request from '@/utils/request'

const searchText = ref('')
const searchResult = ref([])

// 防抖搜索函数,延迟500ms执行
const handleInput = debounce(async () => {
  if (!searchText.value) {
    searchResult.value = []
    return
  }
  
  try {
    const result = await request.get('/search', {
      params: { keyword: searchText.value }
    })
    searchResult.value = result
  } catch (error) {
    console.error('搜索失败:', error)
  }
}, 500)
</script>

节流处理

节流处理可以限制函数的执行频率,例如在滚动事件中,只在一定时间间隔内执行一次函数。

自定义节流函数
// src/utils/throttle.js

/**
 * 节流函数
 * @param {Function} func - 要执行的函数
 * @param {number} delay - 时间间隔,单位ms
 * @returns {Function} - 节流处理后的函数
 */
export function throttle(func, delay) {
  let lastExecTime = 0
  return function (...args) {
    const currentTime = Date.now()
    if (currentTime - lastExecTime >= delay) {
      func.apply(this, args)
      lastExecTime = currentTime
    }
  }
}

然后在组件中使用:

<template>
  <div class="demo-throttle">
    <h3>节流滚动</h3>
    <div class="scroll-area" ref="scrollArea">
      <div v-for="i in 100" :key="i" class="scroll-item">
        项目 {{ i }}
      </div>
    </div>
    <div class="scroll-info">
      <p>滚动位置: {{ scrollPosition }}px</p>
      <p>执行次数: {{ execCount }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { throttle } from '@/utils/throttle'

const scrollArea = ref(null)
const scrollPosition = ref(0)
const execCount = ref(0)

// 节流滚动处理函数,每200ms执行一次
const handleScroll = throttle(() => {
  if (scrollArea.value) {
    scrollPosition.value = scrollArea.value.scrollTop
    execCount.value++
  }
}, 200)

onMounted(() => {
  // 添加滚动事件监听
  if (scrollArea.value) {
    scrollArea.value.addEventListener('scroll', handleScroll)
  }
})

onUnmounted(() => {
  // 移除滚动事件监听
  if (scrollArea.value) {
    scrollArea.value.removeEventListener('scroll', handleScroll)
  }
})
</script>

<style scoped>
.demo-throttle {
  max-width: 400px;
  margin: 0 auto;
}

.scroll-area {
  height: 300px;
  overflow-y: auto;
  border: 1px solid #eee;
  border-radius: 4px;
  margin-bottom: 20px;
}

.scroll-item {
  padding: 10px;
  border-bottom: 1px solid #f0f0f0;
}

.scroll-info {
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
  background-color: #f5f7fa;
}

.scroll-info p {
  margin: 5px 0;
}
</style>

最佳实践

  1. 统一配置

    • 创建一个Axios实例,统一配置基础URL、超时时间、请求头等
    • 使用环境变量管理不同环境的基础URL
  2. 拦截器处理

    • 使用请求拦截器添加认证令牌、处理请求数据等
    • 使用响应拦截器统一处理响应数据、错误信息等
    • 区分HTTP错误和业务逻辑错误
  3. 取消请求

    • 为相同请求添加取消机制,防止重复请求
    • 使用AbortController(Axios v1.x)或CancelToken(兼容旧版本)
  4. 防抖和节流

    • 对频繁触发的请求使用防抖处理,如搜索框、 autocomplete等
    • 对高频事件使用节流处理,如滚动、 resize等
  5. 错误处理

    • 定义清晰的错误类型,便于调试和处理
    • 统一显示错误信息,提高用户体验
    • 记录错误日志,便于排查问题
  6. 类型安全

    • 使用TypeScript定义请求和响应的类型
    • 为Axios实例添加类型定义,提高开发体验

小结

本节我们学习了Axios的封装,包括:

  • Axios的基础配置,包括创建实例、配置请求头、超时时间等
  • 请求拦截器和响应拦截器的使用
  • 统一错误处理,包括HTTP错误和业务逻辑错误
  • 取消请求的实现,包括使用CancelToken和AbortController
  • 防抖和节流处理,提高性能和用户体验

通过合理封装Axios,我们可以提高开发效率,减少重复代码,同时提高代码的可维护性和可扩展性。在实际项目中,我们可以根据具体需求调整Axios的封装方式,以满足项目的需要。

思考与练习

  1. 安装Axios并创建一个Axios实例。
  2. 实现请求拦截器,添加认证令牌。
  3. 实现响应拦截器,统一处理错误。
  4. 实现取消请求的功能,防止重复请求。
  5. 使用防抖函数实现一个搜索框。
  6. 使用节流函数实现滚动事件处理。
« 上一篇 28-vueuse-utils 下一篇 » 30-api-interface-management