vue3+ts封装axios,无感刷新问题

 更新时间:2025年12月06日 08:56:17   作者:音洛  
文章详细介绍了前端项目开发中常见的技术栈,包括安装依赖、创建类型定义、状态管理、核心封装(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() // 不需要登录,直接访问
  }
}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 探索Vue中组合式API和选项式API的用法与比较

    探索Vue中组合式API和选项式API的用法与比较

    Vue3为我们开发提供了两种组件逻辑实现方式:选项式API和组合式API,本文将尝试为大家分析什么是选项式API和组合式API,以及两种API的优缺点,希望对大家有所帮助
    2023-12-12
  • vue中@click绑定多个事件问题(教你避坑)

    vue中@click绑定多个事件问题(教你避坑)

    这篇文章主要介绍了vue中@click绑定多个事件问题(教你避坑),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • 详解vue axios中文文档

    详解vue axios中文文档

    本篇文章主要介绍了详解axios中文文档,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • vue利用axios来完成数据的交互

    vue利用axios来完成数据的交互

    这篇文章主要介绍了vue利用axios来完成数据的交互,本文通过实例代码给大家讲解数据交互方法及安装方法,需要的朋友可以参考下
    2018-03-03
  • 解析Vue 2.5的Diff算法

    解析Vue 2.5的Diff算法

    本文将对于Vue 2.5.3版本中使用的Virtual Dom进行分析。updataChildren是Diff算法的核心,所以本文对updataChildren进行了图文的分析。下面通过本文给大家分享Vue 2.5的Diff算法,需要的朋友参考下吧
    2017-11-11
  • vue打包后显示空白正确处理方法

    vue打包后显示空白正确处理方法

    很多朋友遇到这样的问题当vue打包后显示空白问题,遇到这样的问题怎么处理呢?下面脚本之家小编给大家分享下vue打包后显示空白正确处理方法,感兴趣的朋友一起看看吧
    2017-11-11
  • Mint UI组件库CheckList使用及踩坑总结

    Mint UI组件库CheckList使用及踩坑总结

    这篇文章主要介绍了Mint UI组件库CheckList使用及踩坑总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • vue3实现无缝滚动列表效果(大屏数据轮播场景)

    vue3实现无缝滚动列表效果(大屏数据轮播场景)

    vue3-scroll-seamless 是一个用于 Vue 3 的插件,用于实现无缝滚动的组件,它可以让内容在水平或垂直方向上无缝滚动,适用于展示轮播图、新闻滚动、图片展示等场景,本文就给大家介绍了vue3实现无缝滚动列表效果,需要的朋友可以参考下
    2024-07-07
  • vue学习笔记之Vue中css动画原理简单示例

    vue学习笔记之Vue中css动画原理简单示例

    这篇文章主要介绍了vue学习笔记之Vue中css动画原理,结合简单实例形式分析了Vue中css样式变换动画效果实现原理与相关操作技巧,需要的朋友可以参考下
    2020-02-02
  • Vue-cli3.x + axios 跨域方案踩坑指北

    Vue-cli3.x + axios 跨域方案踩坑指北

    这篇文章主要介绍了Vue-cli3.x + axios 跨域方案踩坑指北,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-07-07

最新评论