vue3+ts封装axios,无感刷新问题
作者:音洛
文章详细介绍了前端项目开发中常见的技术栈,包括安装依赖、创建类型定义、状态管理、核心封装(Axios请求)、API接口示例、组件使用以及可选的路由守卫,作者分享了个人经验,希望为开发者提供参考和帮助
1. 安装依赖
npm install axios
2. 创建类型定义
// types/api.ts
// 接口返回数据的统一格式
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
// 请求配置
export interface RequestConfig {
showError?: boolean; // 是否显示错误信息
withToken?: boolean; // 是否携带token
}
3. 创建状态管理
// stores/auth.ts
import { ref } from 'vue'
// 简单的响应式状态管理
export const useAuthStore = () => {
const token = ref(localStorage.getItem('token') || '')
const refreshToken = ref(localStorage.getItem('refreshToken') || '')
const isRefreshing = ref(false) // 是否正在刷新token
// 设置token
const setToken = (newToken: string, newRefreshToken: string) => {
token.value = newToken
refreshToken.value = newRefreshToken
localStorage.setItem('token', newToken)
localStorage.setItem('refreshToken', newRefreshToken)
}
// 清除token
const clearToken = () => {
token.value = ''
refreshToken.value = ''
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
}
return {
token,
refreshToken,
isRefreshing,
setToken,
clearToken
}
}
4. 核心封装 - Axios 请求
// utils/request.ts
import axios from 'axios'
import type { ApiResponse, RequestConfig } from '@/types/api'
import { useAuthStore } from '@/stores/auth'
// 创建axios实例
const request = axios.create({
baseURL: '/api', // 你的API地址
timeout: 10000, // 10秒超时
})
// 存储等待的请求
let waitingRequests: (() => void)[] = []
// 请求拦截器
request.interceptors.request.use(
(config) => {
const authStore = useAuthStore()
const requestConfig = config as any
// 如果配置需要token且存在token,添加到header
if (requestConfig.withToken !== false && authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 直接返回数据
return response
},
async (error) => {
const { config, response } = error
// 如果是401错误(token过期)
if (response?.status === 401 && config) {
return handleTokenExpired(config)
}
// 其他错误
handleError(error)
return Promise.reject(error)
}
)
// 处理token过期
async function handleTokenExpired(originalConfig: any): Promise<any> {
const authStore = useAuthStore()
// 如果已经在刷新token,将请求加入等待队列
if (authStore.isRefreshing) {
return new Promise((resolve) => {
waitingRequests.push(() => {
originalConfig.headers.Authorization = `Bearer ${authStore.token}`
resolve(request(originalConfig))
})
})
}
// 开始刷新token
authStore.isRefreshing = true
try {
// 调用刷新token接口
const refreshResponse = await request.post('/auth/refresh', {
refreshToken: authStore.refreshToken
}, { withToken: false })
const { token: newToken, refreshToken: newRefreshToken } = refreshResponse.data.data
// 更新token
authStore.setToken(newToken, newRefreshToken)
authStore.isRefreshing = false
// 重试所有等待的请求
waitingRequests.forEach(callback => callback())
waitingRequests = []
// 重试原始请求
originalConfig.headers.Authorization = `Bearer ${newToken}`
return request(originalConfig)
} catch (error) {
// 刷新token失败,跳转到登录页
authStore.clearToken()
authStore.isRefreshing = false
waitingRequests = []
// 跳转到登录页
window.location.href = '/login'
return Promise.reject(error)
}
}
// 处理错误
function handleError(error: any) {
if (error.response) {
// 服务器返回错误
const { status, data } = error.response
switch (status) {
case 400:
console.error('请求参数错误:', data.message)
break
case 403:
console.error('没有权限:', data.message)
break
case 404:
console.error('请求地址不存在:', data.message)
break
case 500:
console.error('服务器错误:', data.message)
break
default:
console.error('请求错误:', data.message)
}
} else if (error.request) {
// 网络错误
console.error('网络错误,请检查网络连接')
} else {
// 其他错误
console.error('请求配置错误:', error.message)
}
}
// 封装常用的请求方法
export const http = {
// GET 请求
get: <T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> => {
return request.get(url, config).then(res => res.data)
},
// POST 请求
post: <T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> => {
return request.post(url, data, config).then(res => res.data)
},
// PUT 请求
put: <T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> => {
return request.put(url, data, config).then(res => res.data)
},
// DELETE 请求
delete: <T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> => {
return request.delete(url, config).then(res => res.data)
}
}
export default request
5. API 接口示例
// api/user.ts
import { http } from '@/utils/request'
// 用户相关接口
export const userApi = {
// 登录
login: (username: string, password: string) => {
return http.post<{ token: string; refreshToken: string }>('/user/login', {
username,
password
}, { withToken: false }) // 登录接口不需要token
},
// 获取用户信息
getUserInfo: () => {
return http.get<{ name: string; email: string }>('/user/info')
// 默认withToken为true,会自动携带token
},
// 更新用户信息
updateUserInfo: (data: { name?: string; email?: string }) => {
return http.put('/user/info', data)
}
}
// 商品相关接口
export const productApi = {
getList: (page: number = 1, size: number = 10) => {
return http.get<{ list: any[]; total: number }>('/products', {
params: { page, size }
})
},
getDetail: (id: number) => {
return http.get(`/products/${id}`)
}
}
6. 在组件中使用
<template>
<div>
<h2>用户信息</h2>
<div v-if="loading">加载中...</div>
<div v-else-if="userInfo">
<p>姓名: {{ userInfo.name }}</p>
<p>邮箱: {{ userInfo.email }}</p>
</div>
<div v-else>加载失败</div>
<button @click="handleLogin">登录</button>
<button @click="handleLogout">退出</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import { useAuthStore } from '@/stores/auth'
const loading = ref(false)
const userInfo = ref<{ name: string; email: string } | null>(null)
const authStore = useAuthStore()
// 加载用户信息
const loadUserInfo = async () => {
try {
loading.value = true
const response = await userApi.getUserInfo()
userInfo.value = response.data
} catch (error) {
console.error('获取用户信息失败')
} finally {
loading.value = false
}
}
// 登录
const handleLogin = async () => {
try {
const response = await userApi.login('admin', '123456')
// 保存token
authStore.setToken(response.data.token, response.data.refreshToken)
// 重新加载用户信息
loadUserInfo()
} catch (error) {
console.error('登录失败')
}
}
// 退出
const handleLogout = () => {
authStore.clearToken()
userInfo.value = null
}
onMounted(() => {
// 如果有token,加载用户信息
if (authStore.token) {
loadUserInfo()
}
})
</script>
7. 路由守卫(可选)
// router/guards.ts
import { useAuthStore } from '@/stores/auth'
export const authGuard = (to: any, from: any, next: any) => {
const authStore = useAuthStore()
// 检查是否需要登录
if (to.meta.requiresAuth) {
if (authStore.token) {
next() // 已登录,允许访问
} else {
next('/login') // 未登录,跳转到登录页
}
} else {
next() // 不需要登录,直接访问
}
}
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
