1. Vue 与 Axios 整合实战:从零构建企业级 HTTP 请求方案
在当今前后端分离的开发模式下,HTTP 请求处理是前端工程师的必修课。作为 Vue 开发者,我们经常需要与后端 API 进行数据交互。虽然浏览器原生提供了 fetch API,但在实际项目中,我们往往需要更强大的功能和更便捷的开发体验。这就是 Axios 大显身手的地方。
我在多个 Vue 项目中深度使用 Axios 后,总结出了一套完整的请求封装方案。这套方案不仅能提升开发效率,还能显著增强代码的可维护性。今天,我将分享如何从零开始,在 Vue 项目中实现 Axios 的深度整合,包括请求封装、拦截器配置以及最佳实践。
2. Axios 核心优势解析
2.1 为什么选择 Axios 而非原生 fetch?
Axios 之所以成为 Vue 生态中的 HTTP 请求首选,主要得益于以下几个核心优势:
-
Promise 风格的 API 设计:Axios 天然支持 Promise,这意味着我们可以使用 async/await 语法糖,让异步代码看起来像同步代码一样清晰。相比回调地狱或者 fetch 的 then 链式调用,这种写法更加直观和易于维护。
-
强大的拦截器机制:这是 Axios 最具特色的功能之一。我们可以在请求发出前和响应返回后插入自定义逻辑,实现诸如自动添加认证头、统一错误处理等功能。这种 AOP(面向切面编程)的思想大大提升了代码的复用性。
-
自动 JSON 数据转换:与 fetch 需要手动调用 .json() 方法不同,Axios 会自动将请求和响应的数据转换为 JSON 格式。这不仅减少了样板代码,还避免了因忘记调用 .json() 而导致的 bug。
-
请求取消功能:在复杂的单页应用中,我们经常需要处理组件卸载时取消未完成的请求。Axios 提供了 CancelToken 机制,可以优雅地实现这一需求,避免内存泄漏和潜在的错误。
-
浏览器和 Node.js 双环境支持:Axios 是一个同构库,这意味着我们可以在浏览器和 Node.js 环境中使用相同的 API。对于需要服务端渲染(SSR)的项目来说,这一点尤为重要。
2.2 Axios 与其他 HTTP 库的对比
除了原生 fetch,前端领域还有其他 HTTP 客户端库,如 jQuery 的 $.ajax、SuperAgent 等。相比之下,Axios 具有以下独特优势:
- 体积更小:Axios 压缩后仅有 13KB 左右,比 jQuery 轻量得多
- 专注单一功能:不像 jQuery 那样大而全,Axios 专注于 HTTP 请求处理
- 更现代的 API 设计:基于 Promise 的设计比回调风格的 API 更符合现代前端开发习惯
- 活跃的社区支持:作为最流行的 HTTP 库之一,Axios 有着丰富的文档和社区资源
3. 项目集成与基础配置
3.1 安装与初始化
在 Vue 项目中集成 Axios 非常简单。首先,我们需要通过 npm 或 yarn 安装它:
bash复制npm install axios
# 或
yarn add axios
安装完成后,我建议不要直接使用全局的 axios 实例,而是创建一个专属实例。这样做的好处是:
- 避免全局配置污染
- 可以为不同的 API 端点创建多个实例
- 便于单元测试和 mock
3.2 创建 Axios 实例的最佳实践
在 src/utils 目录下创建 request.js 文件,这是我们封装 Axios 的核心文件:
javascript复制import axios from 'axios'
// 创建 axios 实例
const service = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL, // 从环境变量读取 API 基础路径
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
这里有几个关键配置需要注意:
- baseURL:建议通过环境变量配置,这样在不同环境(开发、测试、生产)中可以轻松切换 API 地址
- timeout:根据项目实际情况设置合理的超时时间,我一般设置为 10 秒
- headers:设置默认的请求头,特别是 Content-Type
3.3 环境变量配置技巧
为了在不同环境中使用不同的 API 地址,我们可以在项目根目录下创建以下环境文件:
.env.development:开发环境.env.staging:测试环境.env.production:生产环境
例如,在 .env.development 中:
code复制VUE_APP_API_BASE_URL=https://dev-api.example.com
然后在代码中通过 process.env.VUE_APP_API_BASE_URL 访问。注意,自定义环境变量必须以 VUE_APP_ 开头才能在客户端代码中使用。
4. 拦截器深度解析与实战
4.1 请求拦截器:前置处理的艺术
请求拦截器是 Axios 最强大的功能之一。它允许我们在请求发送前对配置进行修改或执行某些操作。以下是几个典型的使用场景:
javascript复制// 请求拦截器
service.interceptors.request.use(
config => {
// 在发送请求之前做些什么
// 1. 添加认证 token
const token = getToken() // 从 store 或 localStorage 获取
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 2. 处理特定请求的配置
if (config.method === 'post') {
// 对 POST 请求做特殊处理
}
// 3. 显示加载状态
showLoading()
return config
},
error => {
// 对请求错误做些什么
hideLoading()
return Promise.reject(error)
}
)
在实际项目中,我总结了几个请求拦截器的最佳实践:
- 认证信息处理:自动从 store 或 localStorage 中获取 token 并添加到请求头
- 请求日志:在开发环境中记录请求信息,便于调试
- 数据加密:对敏感数据进行加密处理
- 请求取消:为某些特殊请求添加取消 token
4.2 响应拦截器:后置处理的智慧
响应拦截器允许我们在响应到达 then/catch 之前对响应数据进行处理。这是统一处理错误和数据的绝佳位置:
javascript复制// 响应拦截器
service.interceptors.response.use(
response => {
// 对响应数据做点什么
hideLoading()
const res = response.data
// 自定义状态码验证
if (res.code !== 200) {
// 处理业务逻辑错误
if (res.code === 401) {
// 处理未授权
redirectToLogin()
return Promise.reject(new Error('未授权,请重新登录'))
}
return Promise.reject(new Error(res.message || 'Error'))
}
// 直接返回核心数据
return res.data
},
error => {
// 对响应错误做点什么
hideLoading()
// HTTP 状态码处理
if (error.response) {
switch (error.response.status) {
case 400:
error.message = '请求错误'
break
case 401:
error.message = '未授权,请登录'
redirectToLogin()
break
case 403:
error.message = '拒绝访问'
break
case 404:
error.message = `请求地址出错: ${error.response.config.url}`
break
case 500:
error.message = '服务器内部错误'
break
default:
error.message = `连接错误 ${error.response.status}`
}
} else if (error.request) {
// 请求已发出但没有收到响应
error.message = '网络异常,请检查网络连接'
} else {
// 发送请求时出错
error.message = '请求发送失败'
}
// 统一错误提示
showErrorMessage(error.message)
return Promise.reject(error)
}
)
在响应拦截器中,我们需要处理两种类型的错误:
- 业务逻辑错误:由后端返回的自定义错误码(如 code !== 200)
- HTTP 错误:如 404、500 等状态码
4.3 拦截器执行顺序与原理
理解拦截器的执行顺序对于调试和排查问题非常重要。Axios 的拦截器遵循以下顺序:
- 请求拦截器(后添加的先执行)
- 发送请求
- 响应拦截器(先添加的先执行)
- then/catch 处理
这种洋葱圈模型的设计使得我们可以在不同阶段插入处理逻辑,非常灵活。
5. API 模块化封装策略
5.1 为什么要进行 API 封装?
直接在每个组件中使用 axios 实例虽然可行,但会带来以下问题:
- 代码重复:相同的 API 路径和参数需要在多个地方重复编写
- 难以维护:当 API 变更时,需要在多个地方修改
- 缺乏统一性:不同开发者可能采用不同的请求方式
通过 API 封装,我们可以:
- 集中管理所有 API 接口
- 统一处理请求参数和响应数据
- 便于 mock 数据和单元测试
5.2 按业务模块组织 API
我建议按照业务模块来组织 API 文件。例如:
code复制src/
api/
auth.js # 认证相关 API
user.js # 用户相关 API
product.js # 产品相关 API
order.js # 订单相关 API
index.js # 统一导出
以用户模块为例,user.js 的内容可能是这样的:
javascript复制import request from '@/utils/request'
// 登录
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data
})
}
// 获取用户信息
export function getInfo() {
return request({
url: '/user/info',
method: 'get'
})
}
// 更新用户信息
export function updateUser(data) {
return request({
url: '/user/update',
method: 'post',
data
})
}
5.3 高级封装技巧
对于大型项目,我们可以进一步优化 API 封装:
- 类型提示:使用 JSDoc 或 TypeScript 为 API 添加类型定义
- 参数校验:使用 joi 等库对请求参数进行校验
- 自动重试:对某些特定错误实现自动重试逻辑
- 缓存策略:为频繁请求但数据变化不大的 API 添加缓存
例如,带有类型提示的 API:
javascript复制/**
* 用户登录
* @param {Object} params - 登录参数
* @param {string} params.username - 用户名
* @param {string} params.password - 密码
* @returns {Promise<{token: string, userInfo: Object}>}
*/
export function login(params) {
return request({
url: '/auth/login',
method: 'post',
data: params
})
}
6. 组件中的 API 调用实践
6.1 基础调用方式
在 Vue 组件中调用封装好的 API 非常简单:
javascript复制import { login, getInfo } from '@/api/user'
export default {
methods: {
async handleLogin() {
try {
const { token, userInfo } = await login({
username: 'admin',
password: '123456'
})
// 处理登录成功逻辑
this.$store.commit('SET_TOKEN', token)
this.$store.commit('SET_USER_INFO', userInfo)
} catch (error) {
console.error('登录失败:', error)
}
},
async fetchUserInfo() {
try {
const userInfo = await getInfo()
this.user = userInfo
} catch (error) {
console.error('获取用户信息失败:', error)
}
}
}
}
6.2 高级调用模式
对于复杂场景,我们可以采用以下模式:
- 组合式 API:在 setup 中使用
- 加载状态管理:统一处理加载状态
- 防抖/节流:防止重复提交
例如,使用组合式 API:
javascript复制import { ref } from 'vue'
import { getProductList } from '@/api/product'
export default {
setup() {
const products = ref([])
const loading = ref(false)
const error = ref(null)
const fetchProducts = async () => {
try {
loading.value = true
products.value = await getProductList()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
return {
products,
loading,
error,
fetchProducts
}
}
}
7. 错误处理与调试技巧
7.1 错误分类与处理策略
在前端开发中,我们需要处理多种类型的错误:
- 网络错误:如断网、超时等
- HTTP 错误:如 404、500 等状态码
- 业务逻辑错误:后端返回的自定义错误码
- 前端逻辑错误:如参数验证失败
针对不同类型的错误,我们应该采取不同的处理策略:
javascript复制// 在响应拦截器中统一处理错误
service.interceptors.response.use(
response => {
const res = response.data
// 业务逻辑错误
if (res.code !== 0) {
// 特定错误码处理
if (res.code === 1001) {
// 处理令牌过期
return refreshTokenAndRetry(response.config)
}
// 通用错误提示
showToast(res.message || '操作失败')
return Promise.reject(new Error(res.message || 'Error'))
}
return res.data
},
error => {
// HTTP 错误
if (error.response) {
// 根据状态码处理
} else if (error.request) {
// 网络错误
showToast('网络异常,请检查网络连接')
} else {
// 其他错误
showToast(error.message || '请求失败')
}
return Promise.reject(error)
}
)
7.2 调试技巧与工具
调试 Axios 请求时,以下工具和技巧非常有用:
- 浏览器开发者工具:查看 Network 面板中的请求和响应
- axios-debug-log:一个专门用于调试 Axios 的库
- 请求/响应日志:在开发环境中打印详细的请求信息
例如,添加请求日志:
javascript复制// 请求拦截器
service.interceptors.request.use(config => {
if (process.env.NODE_ENV === 'development') {
console.log('请求发出:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data
})
}
return config
})
// 响应拦截器
service.interceptors.response.use(response => {
if (process.env.NODE_ENV === 'development') {
console.log('响应收到:', {
url: response.config.url,
status: response.status,
data: response.data
})
}
return response
})
8. 性能优化与安全实践
8.1 请求性能优化
- 合理设置超时时间:根据 API 的响应速度设置不同的超时时间
- 请求取消:在组件卸载时取消未完成的请求
- 请求去重:避免短时间内重复发送相同请求
- 数据缓存:对不常变化的数据进行本地缓存
例如,实现请求取消:
javascript复制// 在组件中使用
export default {
data() {
return {
cancelToken: null
}
},
methods: {
fetchData() {
// 取消之前的请求
if (this.cancelToken) {
this.cancelToken.cancel('取消重复请求')
}
// 创建新的 cancelToken
this.cancelToken = axios.CancelToken.source()
getSomeData(null, {
cancelToken: this.cancelToken.token
}).then(response => {
// 处理响应
})
}
},
beforeUnmount() {
// 组件卸载时取消请求
if (this.cancelToken) {
this.cancelToken.cancel('组件卸载,取消请求')
}
}
}
8.2 安全最佳实践
- CSRF 防护:确保正确配置 CSRF token
- 敏感信息保护:不要在请求和响应中暴露敏感数据
- HTTPS 强制:生产环境必须使用 HTTPS
- 请求限流:防止恶意频繁请求
例如,添加 CSRF 防护:
javascript复制// 请求拦截器
service.interceptors.request.use(config => {
const csrfToken = getCookie('csrfToken')
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken
}
return config
})
9. 测试与 Mock 策略
9.1 单元测试方案
为 API 模块编写单元测试非常重要。我们可以使用以下工具:
- Jest:测试框架
- axios-mock-adapter:模拟 Axios 请求
- msw:更强大的 API mock 方案
示例测试代码:
javascript复制import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
import { login } from '@/api/auth'
describe('auth API', () => {
let mock
beforeEach(() => {
mock = new MockAdapter(axios)
})
afterEach(() => {
mock.restore()
})
it('login success', async () => {
const mockData = { token: 'mock-token' }
mock.onPost('/auth/login').reply(200, {
code: 0,
data: mockData
})
const result = await login({
username: 'test',
password: '123456'
})
expect(result).toEqual(mockData)
})
it('login failed', async () => {
mock.onPost('/auth/login').reply(200, {
code: 1001,
message: '用户名或密码错误'
})
await expect(login({
username: 'wrong',
password: 'wrong'
})).rejects.toThrow('用户名或密码错误')
})
})
9.2 Mock 数据方案
在开发前期,后端 API 可能还未就绪,我们可以使用以下方案 mock 数据:
- 本地 mock:使用 axios-mock-adapter 或 json-server
- 在线 mock 服务:如 fastmock、yapi 等
- 环境切换:通过环境变量区分 mock 和真实 API
例如,使用 json-server:
javascript复制// mock/db.json
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
]
}
// 启动命令
json-server --watch mock/db.json --port 3004
然后在 axios 配置中根据环境切换 baseURL:
javascript复制const service = axios.create({
baseURL: process.env.VUE_APP_MOCK === 'true'
? 'http://localhost:3004'
: process.env.VUE_APP_API_BASE_URL
})
10. 常见问题与解决方案
10.1 跨域问题处理
在开发环境中,我们经常会遇到跨域问题。解决方案包括:
- 代理配置:在 vue.config.js 中配置 devServer.proxy
- CORS:确保后端正确配置 CORS 头
- JSONP:仅适用于 GET 请求(不推荐)
vue.config.js 代理配置示例:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
10.2 文件上传下载
处理文件上传下载时需要注意:
- 上传:使用 FormData 和正确的 Content-Type
- 下载:处理 blob 响应并触发浏览器下载
文件上传示例:
javascript复制export function uploadFile(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
文件下载示例:
javascript复制export function downloadFile(fileId) {
return request({
url: `/download/${fileId}`,
method: 'get',
responseType: 'blob'
}).then(response => {
const url = window.URL.createObjectURL(new Blob([response]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'filename.ext')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
}
10.3 大文件分片上传
对于大文件上传,我们可以实现分片上传:
javascript复制export async function uploadLargeFile(file, chunkSize = 5 * 1024 * 1024) {
const chunks = Math.ceil(file.size / chunkSize)
const fileMd5 = await calculateFileMd5(file) // 计算文件 MD5
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkIndex', i)
formData.append('totalChunks', chunks)
formData.append('fileMd5', fileMd5)
await request({
url: '/upload/chunk',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 通知后端合并分片
await request({
url: '/upload/merge',
method: 'post',
data: {
fileMd5,
fileName: file.name,
totalChunks: chunks
}
})
}
11. 项目实战:完整请求封装示例
11.1 高级封装方案
结合前面介绍的各种技巧,这里给出一个更完整的请求封装方案:
javascript复制// src/utils/request.js
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// 创建 axios 实例
const service = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 显示 loading
if (config.showLoading !== false) {
store.dispatch('app/showLoading')
}
// 添加 token
const token = getToken()
if (token && config.needAuth !== false) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 开发环境打印请求日志
if (process.env.NODE_ENV === 'development') {
console.log(`%c ${config.method.toUpperCase()} ${config.url}`, 'color: #1890ff; font-weight: bold', config)
}
return config
},
error => {
store.dispatch('app/hideLoading')
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
store.dispatch('app/hideLoading')
// 开发环境打印响应日志
if (process.env.NODE_ENV === 'development') {
console.log(`%c ${response.status} ${response.config.url}`, 'color: #52c41a; font-weight: bold', response.data)
}
const res = response.data
// 业务逻辑错误处理
if (res.code !== undefined && res.code !== 0) {
// 登录过期处理
if (res.code === 401) {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
return Promise.reject(new Error('登录状态已过期,请重新登录'))
}
// 其他错误提示
if (config.showError !== false) {
Message.error(res.message || '操作失败')
}
return Promise.reject(new Error(res.message || 'Error'))
}
return res.data !== undefined ? res.data : res
},
error => {
store.dispatch('app/hideLoading')
// HTTP 错误处理
if (error.response) {
const status = error.response.status
let message = ''
switch (status) {
case 400: message = '请求参数错误'; break
case 401:
message = '登录状态已过期,请重新登录'
store.dispatch('user/resetToken').then(() => {
location.reload()
})
break
case 403: message = '没有权限访问该资源'; break
case 404: message = `请求地址不存在: ${error.response.config.url}`; break
case 500: message = '服务器内部错误'; break
case 502: message = '网关错误'; break
case 503: message = '服务不可用'; break
case 504: message = '网关超时'; break
default: message = `网络错误 ${status}`
}
if (error.config.showError !== false) {
Message.error(message)
}
} else if (error.request) {
// 请求已发出但没有收到响应
if (error.config.showError !== false) {
Message.error('网络异常,请检查网络连接')
}
} else {
// 发送请求时出错
if (error.config.showError !== false) {
Message.error(error.message || '请求发送失败')
}
}
return Promise.reject(error)
}
)
export default service
11.2 API 模块示例
配套的 API 模块示例:
javascript复制// src/api/user.js
import request from '@/utils/request'
/**
* 用户登录
* @param {Object} data - 登录参数
* @param {string} data.username - 用户名
* @param {string} data.password - 密码
* @returns {Promise<{token: string, userInfo: Object}>}
*/
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data,
showLoading: true // 显示 loading
})
}
/**
* 获取用户信息
* @returns {Promise<Object>}
*/
export function getUserInfo() {
return request({
url: '/user/info',
method: 'get'
})
}
/**
* 分页获取用户列表
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.size - 每页数量
* @returns {Promise<{list: Array, total: number}>}
*/
export function getUserList(params) {
return request({
url: '/user/list',
method: 'get',
params
})
}
11.3 组件中使用示例
在 Vue 组件中使用封装好的 API:
javascript复制<template>
<div>
<el-button @click="handleLogin">登录</el-button>
<el-table :data="userList" v-loading="loading">
<!-- 表格内容 -->
</el-table>
</div>
</template>
<script>
import { login, getUserList } from '@/api/user'
export default {
data() {
return {
userList: [],
loading: false
}
},
methods: {
async handleLogin() {
try {
const { token, userInfo } = await login({
username: 'admin',
password: '123456'
})
this.$store.commit('user/SET_TOKEN', token)
this.$store.commit('user/SET_USER_INFO', userInfo)
this.$message.success('登录成功')
// 登录后获取用户列表
this.fetchUserList()
} catch (error) {
console.error('登录失败:', error)
}
},
async fetchUserList() {
this.loading = true
try {
const { list } = await getUserList({
page: 1,
size: 10
})
this.userList = list
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
this.loading = false
}
}
}
}
</script>
12. 进阶技巧与最佳实践
12.1 请求重试机制
对于某些临时性错误(如网络波动),我们可以实现自动重试机制:
javascript复制// 在 request.js 中添加重试逻辑
const MAX_RETRY_COUNT = 3
service.interceptors.response.use(null, async error => {
const config = error.config
// 如果是特定错误且未设置重试次数,则进行重试
if (!config.__retryCount && shouldRetry(error)) {
config.__retryCount = 0
return retryRequest(config)
}
return Promise.reject(error)
})
function shouldRetry(error) {
return (
!error.response ||
(error.response.status >= 500 && error.response.status <= 599) ||
error.code === 'ECONNABORTED'
)
}
async function retryRequest(config) {
config.__retryCount = config.__retryCount || 0
if (config.__retryCount >= MAX_RETRY_COUNT) {
return Promise.reject(new Error('超过最大重试次数'))
}
config.__retryCount += 1
// 延迟一段时间再重试
await new Promise(resolve => setTimeout(resolve, 1000 * config.__retryCount))
return service(config)
}
12.2 并发请求控制
在某些场景下,我们需要控制并发请求数量:
javascript复制class RequestQueue {
constructor(maxConcurrent = 5) {
this.maxConcurrent = maxConcurrent
this.queue = []
this.activeCount = 0
}
add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject })
this.next()
})
}
next() {
if (this.activeCount >= this.maxConcurrent || !this.queue.length) return
this.activeCount++
const { requestFn, resolve, reject } = this.queue.shift()
requestFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.activeCount--
this.next()
})
}
}
// 使用示例
const requestQueue = new RequestQueue(3)
// 替代直接调用 request
requestQueue.add(() => request({ url: '/api/test' }))
12.3 请求缓存策略
对于不常变化的数据,我们可以实现请求缓存:
javascript复制const cacheMap = new Map()
export function cachedRequest(config, cacheKey, cacheTime = 5 * 60 * 1000) {
const now = Date.now()
if (cacheMap.has(cacheKey)) {
const { expire, data } = cacheMap.get(cacheKey)
if (now < expire) {
return Promise.resolve(data)
}
}
return request(config).then(response => {
cacheMap.set(cacheKey, {
expire: now + cacheTime,
data: response
})
return response
})
}
// 使用示例
cachedRequest(
{ url: '/api/constant-data' },
'constant-data',
30 * 60 * 1000 // 缓存30分钟
)
13. 与 Vuex/Pinia 的集成
13.1 在 Vuex 中管理 API 状态
在大型项目中,我们通常会在 Vuex 中管理 API 相关状态:
javascript复制// store/modules/user.js
const actions = {
async login({ commit }, userInfo) {
try {
commit('SET_LOADING', true)
const { token, userInfo } = await login(userInfo)
commit('SET_TOKEN', token)
commit('SET_USER_INFO', userInfo)
return userInfo
} catch (error) {
commit('SET_ERROR', error.message)
throw error
} finally {
commit('SET_LOADING', false)
}
},
async getUserInfo({ commit }) {
try {
commit('SET_LOADING', true)
const userInfo = await getInfo()
commit('SET_USER_INFO', userInfo)
return userInfo
} catch (error) {
commit('SET_ERROR', error.message)
throw error
} finally {
commit('SET_LOADING', false)
}
}
}
13.2 在 Pinia 中使用 API
对于使用 Pinia 的项目,我们可以创建专门的 store 来管理 API:
javascript复制// stores/userStore.js
import { defineStore } from 'pinia'
import { login, getInfo } from '@/api/user'
export const useUserStore = defineStore('user', {
state: () => ({
token: null,
userInfo: null,
loading: false,
error: null
}),
actions: {
async login(userInfo) {
try {
this.loading = true
const { token, userInfo } = await login(userInfo)
this.token = token
this.userInfo = userInfo
return userInfo
} catch (error) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async fetchUserInfo() {
try {
this.loading = true
this.userInfo = await getInfo()
return this.userInfo
} catch (error) {
this.error = error.message
throw error
} finally {
this.loading = false
}
}
}
})
14. TypeScript 支持
14.1 为 Axios 添加类型定义
在使用 TypeScript 的项目中,我们可以为 Axios 添加类型支持:
typescript复制// src/utils/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
interface IResponseData<T = any> {
code: number
data: T
message?: string
}
class Request {
private instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config)
this.setupInterceptors()
}
private setupInterceptors() {
// 拦截器逻辑...
}
public request<T>(config: AxiosRequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
this.instance
.request<IResponseData<T>>(config)
.then(res => resolve(res.data.data))
.catch(err => reject(err))
})
}
public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'GET', url })
}
public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'POST', url, data })
}
// 其他方法...
}
const service = new Request({
baseURL: process.env.VUE_APP