vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue3+ts封装axios,无感刷新

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() // 不需要登录,直接访问
  }
}

总结

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

您可能感兴趣的文章:
阅读全文