第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 request11.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 request11.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>最佳实践
统一配置:
- 创建一个Axios实例,统一配置基础URL、超时时间、请求头等
- 使用环境变量管理不同环境的基础URL
拦截器处理:
- 使用请求拦截器添加认证令牌、处理请求数据等
- 使用响应拦截器统一处理响应数据、错误信息等
- 区分HTTP错误和业务逻辑错误
取消请求:
- 为相同请求添加取消机制,防止重复请求
- 使用AbortController(Axios v1.x)或CancelToken(兼容旧版本)
防抖和节流:
- 对频繁触发的请求使用防抖处理,如搜索框、 autocomplete等
- 对高频事件使用节流处理,如滚动、 resize等
错误处理:
- 定义清晰的错误类型,便于调试和处理
- 统一显示错误信息,提高用户体验
- 记录错误日志,便于排查问题
类型安全:
- 使用TypeScript定义请求和响应的类型
- 为Axios实例添加类型定义,提高开发体验
小结
本节我们学习了Axios的封装,包括:
- Axios的基础配置,包括创建实例、配置请求头、超时时间等
- 请求拦截器和响应拦截器的使用
- 统一错误处理,包括HTTP错误和业务逻辑错误
- 取消请求的实现,包括使用CancelToken和AbortController
- 防抖和节流处理,提高性能和用户体验
通过合理封装Axios,我们可以提高开发效率,减少重复代码,同时提高代码的可维护性和可扩展性。在实际项目中,我们可以根据具体需求调整Axios的封装方式,以满足项目的需要。
思考与练习
- 安装Axios并创建一个Axios实例。
- 实现请求拦截器,添加认证令牌。
- 实现响应拦截器,统一处理错误。
- 实现取消请求的功能,防止重复请求。
- 使用防抖函数实现一个搜索框。
- 使用节流函数实现滚动事件处理。