Axios类的二次封装

一、功能

  • 存储封装:处理浏览器存储(localStorage 和 sessionStorage)以及 cookie 的工具类
  • Pinia: 状态管理模块的封装
  • 双token:双Token机制,特别是结合无感刷新登录状态的设计,主要用来增强用户的访问安全性,同时保持用户体验的流畅性,避免频繁登录
  • 取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
  • 请求失败自动重试: 接口请求后台异常时候,自动重新发起多次请求,直到达到所设次数

二、基础

axios请求

  • axios 是一个基于 promise 的 HTTP 库,广泛用于浏览器和node.js中执行HTTP 请求。它支持 XMLHttpRequests(XHR)作为浏览器环境下的基础技术,并在 Node.js 环境下使用 http/https模块
    1. 安装 Axios: 首先,你需要在你的项目中安装 Axios。如果你正在使用 npm 或 yarn 管理包,可以在命令行中运行以下命令

       	npm install axios
       	# 或者
      	yarn add axios
      
    2. 基本用法:

      • GET 请求:Axios.get 方法用于发送 GET 请求
        	import axios from 'axios';
        	axios.get('https://api.example.com/data')
        	  .then(response => {
        	    console.log(response.data); // 打印服务器返回的数据
        	  })
        	  .catch(error => {
        	    if (error.response) {
        	      // 请求已发出但服务器响应的状态码不在 2xx 范围内
        	      console.error(`请求失败, 原因: ${error.response.status}`);
        	    } else if (error.request) {
        	      // 请求已发出,但没有收到响应(如网络故障)
        	      console.error('请求未能发出,请检查网络连接');
        	    } else {
        	      // 发生了其他错误,如设置适配器时出错
        	      console.error('Error', error.message);
        	    }
        	    console.error(error.config); // 打印完整的请求配置信息
          	});
        
      • POST 请求:Axios.post 方法用于发送 POST 请求,通常用来提交数据到服务器
        const postData = { key: 'value' };
        axios.post('https://api.example.com/submit-data', postData, {
        	  headers: {
        	    'Content-Type': 'application/json'
        	  }
        	})
        	  .then(response => {
        	    console.log(response.data);
        	  })
        	  .catch(error => {
        	    // 同样的错误处理逻辑...
        	 });		 	
        
      • PUT、DELETE 等其他方法:Axios 提供了对应的方法来发起 PUT、DELETE、HEAD、OPTIONS 等 HTTP 请求。
    3. 配置选项

      • axios 支持丰富的配置选项,包括但不限于 baseURL、headers、timeout、params、responseType 和transformRequest/transformResponse 等
        const config = {
          method: 'post',
          url: 'https://api.example.com/user',
          data: { id: 123, name: 'John Doe' },
          headers: { Authorization: 'Bearer your_token_here' },
          timeout: 5000, // 设置超时时间(毫秒)
          responseType: 'json', // 指定响应的数据类型
          ...
        };
        
        axios(config)
          .then(response => {
            console.log(response.data);
          })
          .catch(error => {
            // 错误处理逻辑...
          });
        
    4. 拦截器

      • axios 允许你添加全局或实例级别的请求和响应拦截器。这些拦截器可以用来统一处理认证、日志记录、错误处理等操作
        // 添加请求拦截器
        axios.interceptors.request.use(config => {
          // 在每个请求之前进行一些操作
          config.headers.common['X-Custom-Header'] = 'foobar';
          return config;
        });
        
        // 添加响应拦截器
        axios.interceptors.response.use(response => {
          // 对任何成功的响应做一些预处理
          return response;
        }, error => {
          // 对任何错误响应做一些预处理,如自动重试、统一错误提示等
          if (error.response.status === 401) {
            // 处理未授权情况,比如跳转到登录页面
          }
          return Promise.reject(error);
        });
        

Pinia状态管理

此次以Vue的Pinia为例,如有React同学可以使用Redux,再此小编为你们推荐一个状态管理zustand老好使了,感兴趣的同学可以去看看

  • Pinia 是 Vue.js 生态中的一个轻量级状态管理库,它为 Vue 应用程序提供了一种声明式、可预测且易于测试的状态管理模式。在 Vue 3 中,Pinia 被官方推荐作为 Vuex 的替代品,充分利用了 Vue 3 的Composition API 特性,使得状态管理变得更加直观和高效
    1. 安装 Pinia: 首先,你需要在你的项目中安装 Pinia。如果你正在使用 npm 或 yarn 管理包,可以在命令行中运行以下命令
       	npm install pinia
       	# 或者
      	yarn add pinia
      
    2. 核心概念与特性
      • 定义 Store

        • 使用 defineStore 函数创建 store,可以定义 state(状态)、getters(计算属性)、actions(异步操作)和 mutations(同步状态修改)
          import { defineStore } from 'pinia';
          
          export const useUserStore = defineStore('user', {
            state: () => ({
              id: null,
              name: '',
              isLoggedIn: false,
            }),
          
            getters: {
              fullName: (state) => `${state.name} (${state.id})`,
            },
          
            actions: {
              async login(username, password) {
                // 异步登录逻辑...
                this.isLoggedIn = true;
              },
              
              logout() {
                this.isLoggedIn = false;
              },
              
              updateName(newName) {
                this.name = newName;
              },
            },
            
            // 在 Pinia v2 中移除了 mutations,直接在 actions 或其他函数中修改 state 即可触发响应式更新
          });
          
      • 使用 Store

        • 在组件中通过调用 useStore 函数来获取并使用 store。由于利用了 Vue 3 的组合式 API,可以直接访问并操作 store 内部的 state 和方法
          import { useUserStore } from '@/stores/user';
          
          export default {
            setup() {
              const userStore = useUserStore();
              
              function handleLogin() {
                userStore.login('username', 'password');
              }
          
              return {
                handleLogin,
                fullName: computed(() => userStore.fullName),
                isLoggedIn: computed(() => userStore.isLoggedIn),
              };
            }
          };```
          
          
      • 类型安全

        • Pinia 支持 TypeScript,可以为 store 的各个部分添加类型注解,确保代码具有更好的静态检查和智能提示
      • 模块化

        • 可以根据业务需求创建多个独立的 store,每个 store 都可以单独引入并在需要的地方使用,有助于保持应用的结构清晰和模块化
      • 插件系统

        • Pinia 提供了一个插件机制,允许开发者扩展或修改 store 的行为,比如添加全局的中间件来处理所有 store 的 action。
      • 简单易用

        • 相比于 Vuex,Pinia 简化了状态管理流程,无需手动调用 commit方法来修改状态,而是直接修改 state 对象。同时,action 不再区分同步和异步,都可通过返回 Promise 来支持异步操作。

二、封装

Axios.ts

封装了 Axios 库的功能以进行 HTTP 请求,并添加了诸如令牌管理、请求取消以及在服务器错误时自动重试等额外功能。同时,它还与 Vue.js 存储集成以便处理用户认证令牌。

  • Axios 实例

    • Request 类创建了一个 Axios 实例,接收提供的选项(如 baseURL 和 timeout),并配置了 axios-retry 中间件,以便根据特定条件重试失败的请求。
  • 请求和响应拦截器

    • requestInterceptor: 如果从用户存储中获取到了访问令牌,则添加授权头。此外,在发送请求前使用 TokenManager 确保刷新令牌有效,并在需要时注册请求以供取消。
    • requestErrorInterceptor: 当请求阶段出现问题时记录错误信息。
    • responseSuccessInterceptor: 从待处理请求列表中移除已完成的请求,检查状态码,并相应地处理成功消息。
    • responseErrorInterceptor: 处理将失败请求从待处理请求列表中移除,并根据axios-retry配置决定是否重试请求。
  • 方法

    • get、post、put 、delete),简化常见的 HTTP 操作,每个方法都返回一个自定义的 Response 类型,这是一个 Promise,解析为一个元组,包含一个表示成功的布尔值、响应数据以及 AxiosResponse 对象
  • 导出

    • 创建了一个具有默认选项的 Request 类实例并将其作为 http 导出。在整个应用中可以使用这个实例来进行 HTTP 请求,同时利用所有已配置的功能
    import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from "axios"
    import axios from "axios"
    import axiosRetry from "axios-retry"
    import TokenManager from "@/utils/http/token-manager"
    import { checkStatus } from "@/utils/http/check-status"
    import { AxiosCancel } from "@/utils/http/axios-cancel"
    import { useUser } from "@/stores/index.ts"
    
    export type Response<T> = Promise<[boolean, T, AxiosResponse<T>]>
    
    export const TokenOrManager = new TokenManager()
    export const axiosCancel = new AxiosCancel()
    
    export class Request {
      private readonly axiosInstance: AxiosInstance
    
      constructor(options: AxiosRequestConfig) {
        this.axiosInstance = axios.create(options)
        // 配置axios-retry
        axiosRetry(this.axiosInstance, {
          retries: 2, // 设置最大重试次数
          retryDelay: (retryCount) => retryCount * 1000, // 设置每次重试间隔时间
          retryCondition: (error: AxiosError): boolean => {
            // 自定义重试条件
            return Boolean(error?.response && error?.response?.status >= 500)
          }
        })
        this.axiosInstance.interceptors.request.use(
          (axiosConfig: AxiosRequestConfig) => this.requestInterceptor(axiosConfig),
          (error) => this.requestErrorInterceptor(error)
        )
        this.axiosInstance.interceptors.response.use(
          (response: AxiosResponse) => this.responseSuccessInterceptor(response),
          (error) => this.responseErrorInterceptor(error)
        )
      }
    
      async requestInterceptor(config: AxiosRequestConfig): Promise<any> {
        const userStore = useUser()
        axiosCancel.addPending(config)
        if (userStore.getToken.accessToken) {
          ;(config.headers as Recordable).Authorization = `Bearer ${userStore.getToken.accessToken}`
        }
        // 这里可以添加逻辑,如果需要的话
        await TokenOrManager.ensureValidRefreshTokens(config)
        return config
      }
    
      requestErrorInterceptor(error: AxiosError): Promise<AxiosError> {
        console.error("Request Error:", error.message)
        return Promise.reject(error)
      }
    
      responseSuccessInterceptor(response: AxiosResponse): any {
        response && axiosCancel.removePending(response.config)
        const { status, data } = response
        if (status !== 200) return Promise.reject(data)
        ElMessage.success(data.message)
        console.log("Response Success:", response)
        return data
      }
    
      private async responseErrorInterceptor(error: any): Promise<any> {
        axiosCancel.removePending(error?.config)
        const { status } = error?.response || {}
        try {
          await checkStatus(status)
          await this.axiosInstance(error?.config)
        } catch (error) {
          console.error("Response Error:", error)
        }
        return Promise.reject(error)
      }
    
      /**
       * 发起一个请求并返回响应
       * @param config 请求配置
       * @returns 响应对象
       */
      request<T, D = any>(config: AxiosRequestConfig<D>): Response<T> {
        return this.axiosInstance(config)
      }
    
      /**
       * 发起一个GET请求并返回响应
       * @param url 请求的URL
       * @param config 请求配置
       * @returns 响应对象
       */
      get<T, D = any>(url: string, config?: AxiosRequestConfig<D>): Response<T> {
        return this.axiosInstance.get(url, config)
      }
    
      /**
       * 发起一个POST请求并返回响应
       * @param url 请求的URL
       * @param data 请求的数据
       * @param config 请求配置
       * @returns 响应对象
       */
      post<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Response<T> {
        return this.axiosInstance.post(url, data, config)
      }
    
      /**
       * 发起一个PUT请求并返回响应
       * @param url 请求的URL
       * @param data 请求的数据
       * @param config 请求配置
       * @returns 响应对象
       */
      put<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Response<T> {
        return this.axiosInstance.put(url, data, config)
      }
    
      /**
       * 发起一个DELETE请求并返回响应
       * @param url 请求的URL
       * @param config 请求配置
       * @returns 响应对象
       */
      delete<T, D = any>(url: string, config?: AxiosRequestConfig<D>): Response<T> {
        return this.axiosInstance.delete(url, config)
      }
    }
    
    const http = new Request({
      baseURL: import.meta.env.VITE_BASE_URL,
      timeout: 60 * 1000 * 5
    })
    
    export default http
    
    

Check-status.ts

定义了一个名为 checkStatus 的异步函数,用于处理 HTTP 请求返回的错误状态码。该函数从 @/stores/index.ts 导入了 useUser 方法以访问用户存储,为响应提供了一种统一处理 HTTP 状态错误的方式,并且在需要用户重新登录时自动触发登出操作。

根据传入的错误状态码(errStatus),函数会设置相应的错误信息(errMessage),并根据不同情况执行不同操作:

  • 对于 401 错误码(未授权),除了显示错误消息“未授权,请重新登录”之外,还会调用 userStore.loginOutTo() 来登出当前用户。
  • 对于其他错误状态码,仅显示对应的错误信息。
import { useUser } from "@/stores/index.ts"
export const checkStatus = async (errStatus: number) => {
  const userStore = useUser()
  let errMessage = "未知错误"
  if (errStatus) {
    switch (errStatus) {
      case 400:
        errMessage = "错误的请求"
        break
      case 401:
        errMessage = "未授权,请重新登录"
        await userStore.loginOutTo()
        break
      case 403:
        errMessage = "拒绝访问"
        break
      case 404:
        errMessage = "请求错误,未找到该资源"
        break
      case 405:
        errMessage = "请求方法未允许"
        break
      case 408:
        errMessage = "请求超时"
        break
      case 500:
        errMessage = "服务器端出错, 请联系管理员"
        break
      case 501:
        errMessage = "网络未实现"
        break
      case 502:
        errMessage = "网络错误"
        break
      case 503:
        errMessage = "服务不可用"
        break
      case 504:
        errMessage = "网络超时"
        break
      case 505:
        errMessage = "http版本不支持该请求"
        break
      default:
        errMessage = `其他连接错误 --${errStatus}`
    }
  } else {
    errMessage = `无法连接到服务器!`
  }
   ElMessage.error(errMessage)
}

Token-manager.ts

用于管理应用程序中的访问令牌(accessToken)和刷新令牌(refreshToken)

  • refreshTokens
    • 通过调用常量的 REFRESH_TOKEN_URL 来刷新令牌。当 access token 即将过期时,使用 refresh token 向服务器请求新的 access token 和 refresh token
  • isAccessTokenExpired
    • 检查给定的 accessToken 的过期时间(以秒为单位),判断是否已过期。如果传入的 expiresIn 不存在或当前时间大于该值,则返回 true,表示已过期。
  • validateTokens
    • 验证提供的 tokens 是否包含必要的字段(accessToken、refreshToken 和 expiresIn)。如果不完整,会触发用户登出操作。
  • setTokens
    • 将新的令牌数据保存到全局状态管理(Vuex store)中,这里使用了从 “@/stores/index.ts” 导入的 useUser() 函数来访问并设置用户的 token 数据。
  • addAuthorizationHeader
    • 将 access token 添加到 Axios 请求配置的 headers 中,以便发送带有身份验证信息的 HTTP 请求。
  • ensureValidRefreshTokens
    • 在发出 HTTP 请求前确保 refresh token 有效。首先验证令牌的有效性,然后检查 access token 是否即将过期。若未过期则直接添加 Authorization 头;若已过期,则先调用 refreshTokens 更新令牌,并将更新后的令牌添加到请求头中。这个方法通常会在请求拦截器中调用,以确保每个请求都有有效的 access token。
import { Token } from "@/types"
import type { AxiosRequestConfig, AxiosResponse } from "axios"
import axios from "axios"
import { useUser } from "@/stores/index.ts"
// 配置额外的请求头接口
const REFRESH_TOKEN_URL = "/api/refresh-token"
// Token管理类
class TokenManager {
  async refreshTokens(refreshToken: string): Promise<void> {
    const response: AxiosResponse = await axios.post(REFRESH_TOKEN_URL, {
      refreshToken: refreshToken
    })
    return response.data.result
  }
  // 判断accessToken是否过期的方法 true 代表过期,false代表未过期
  private isAccessTokenExpired(expiresIn: number): boolean {
    if (!expiresIn) return true
    return Math.floor(Date.now() / 1000) < expiresIn
  }

  async validateTokens(tokens: Token) {
    if (!tokens.refreshToken || !tokens.accessToken || !tokens.expiresIn) {
      await useUser().loginOutTo()
    }
  }
  setTokens(newTokens: any) {
    useUser().setToken(newTokens)
  }

  // 将访问令牌添加到请求头中
  addAuthorizationHeader(config: AxiosRequestConfig, tokens: Token) {
    ;(config.headers as Recordable).Authorization = `Bearer ${tokens.accessToken}`
  }

  // 确保请求配置中包含有效的refreshToken
  async ensureValidRefreshTokens(config: AxiosRequestConfig): Promise<void> {
    const { accessToken, expiresIn, refreshToken } = useUser().getToken
    await this.validateTokens({ accessToken, expiresIn, refreshToken })
    if (!this.isAccessTokenExpired(expiresIn as number)) {
      const refreshedTokens = await this.refreshTokens(refreshToken as string)
      const token = Object.assign({}, refreshedTokens, { refreshToken })
      this.setTokens(token)
      this.addAuthorizationHeader(config, token)
    } else {
      this.addAuthorizationHeader(config, { accessToken, expiresIn, refreshToken })
    }
  }
}

// 导出TokenManager类
export default TokenManager

Axios-cancel.ts

用于管理并取消重复的 HTTP 请求。在高并发场景下,当同一个接口有多个完全相同的请求同时发出时,为了防止服务器收到重复数据或客户端接收到过时响应,可以采用此方案来取消后发出的重复请求

  • getPendingUrl
    • 函数将 Axios 请求配置中的方法(method)、URL、参数(params)和数据(data)拼接成一个字符串,作为请求标识符
  • addPending
    • 该方法接收一个 Axios 请求配置对象,并根据 getPendingUrl 得到请求标识符。如果标识符已经存在于 pendingMap 中,则为当前请求创建一个新的 CancelToken,并在其回调中执行取消操作,附带错误信息。若不存在,则将新的 CancelToken 回调函数存入 pendingMap。
  • removeAllPending
    • 移除所有挂起请求的 CancelToken,并清除 pendingMap。
  • removePending
    • 根据传入的请求配置获取请求标识符,若标识符存在pendingMap中,则执行对应的取消函数,并从 pendingMap 中移除此项
import axios, { AxiosRequestConfig, Canceler } from "axios"
import { isFunction } from "lodash-es"
import { isJsonString } from "@/utils/is-method.ts"
import { stringify } from "qs"
// 取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
export const getPendingUrl = (config: AxiosRequestConfig) => {
  if (config && config.data && isJsonString(config.data)) {
    config.data = JSON.parse(config.data)
  }
  const { method, url, params, data } = config // 请求方式,参数,请求地址,
  return [method, url, stringify(params), stringify(data)].join("&") // 拼接
}
const pendingMap = new Map<string, Canceler>() // 取消重复请求
export class AxiosCancel {
  /**
   * 添加挂起的请求配置
   * @param config Axios的请求配置
   */
  addPending(config: AxiosRequestConfig) {
    const requestKey = getPendingUrl(config)
    if (pendingMap.has(requestKey)) {
      config.cancelToken = new axios.CancelToken((cancel) => {
        // cancel 函数的参数会作为 promise 的 error 被捕获
        cancel(`${config.url} 请求已取消`)
      })
    } else {
      config.cancelToken =
        config.cancelToken ||
        new axios.CancelToken((cancel) => {
          pendingMap.set(requestKey, cancel)
        })
    }
  }

  /**
   * 移除所有挂起的请求配置
   */
  removeAllPending() {
    pendingMap.forEach((cancel) => {
      cancel && isFunction(cancel) && cancel()
    })
    pendingMap.clear()
  }

  /**
   * 移除指定的挂起的请求配置
   * @param config Axios的请求配置
   */
  removePending(config: AxiosRequestConfig) {
    if (!config) return
    const requestKey = getPendingUrl(config)
    if (pendingMap.has(requestKey)) {
      // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
      const cancel = pendingMap.get(requestKey)
      cancel && cancel(requestKey)
      pendingMap.delete(requestKey)
    }
  }
}

Store/index.ts

使用 Pinia 创建的一个 Vue.js 状态管理模块,用于处理用户相关的状态和操作,如登录、获取用户信息、登出以及存储和读取用户的访问令牌(access token)和刷新令牌(refresh token)

useUserStore 是定义的用户存储模块,它包含了以下部分:

  • State
    • 定义了 IUserState 接口,描述了用户状态数据结构,包括 tokenuserIdrefreshTokenuserInfo。在初始化时,从本地存储中读取 access token,并设置其他属性为空或默认值。
  • Getters
    • 提供了一系列 getter 方法,用于获取用户状态中的各个属性
  • Actions
    • setToken, setRefreshToken, setUserInfo, setUserId:分别用于更新用户状态中的对应属性。
    • login:异步方法,接收登录参数并调用 API 进行登录操作。登录成功后,将返回的 token 信息保存到本地存储,并更新用户状态。
    • getUserInfoTo:异步方法,调用 API 获取用户信息,成功后更新用户状态的 userInfo 属性。
    • loginOutTo:登出方法,弹窗提示用户重新登录,并在确认后清除本地存储的数据。
  • 导出
    • useUser 函数导出了一个便捷的方法,以便在 Vue 组件中轻松地注入和使用这个用户存储模块。
import { defineStore } from "pinia"
import { store } from "@/stores/store.ts"
import { Token, UserInfo, Login } from "@/types"
import { login, getUserInfo } from "@/api"
import { storage } from "@/utils/storage.ts" // 

export const ACCESS_TOKEN = "ACCESS-TOKEN" // 用户token
export enum ResultEnum {
  SUCCESS = 200,
  ERROR = -1,
  TIMEOUT = 10042,
  TYPE = "success"
}

export interface IUserState {
  token: Token
  userId: string
  refreshToken: string
  userInfo: UserInfo
}

export const useUserStore = defineStore({
  id: "user",
  state: (): IUserState => ({
    token: storage.get(ACCESS_TOKEN, {}),
    userId: "",
    refreshToken: "",
    userInfo: {} as UserInfo
  }),
  getters: {
    getToken(): Token {
      return this.token
    },
    getRefreshToken(): string {
      return this.refreshToken
    },
    getUserId(): string {
      return this.userId
    },
    getUserInfo(): UserInfo {
      return this.userInfo
    }
  },
  actions: {
    setToken(token: Token) {
      this.token = token
    },
    setRefreshToken(refreshToken: string) {
      this.refreshToken = refreshToken
    },
    setUserInfo(userInfo: UserInfo) {
      this.userInfo = userInfo
    },
    setUserId(userId: string) {
      this.userId = userId
    },
    async login(params: Login) {
      try {
        const response = await login(params)
        const { result, code } = response as unknown as { result: any; code: string | number }
        if (code === ResultEnum.SUCCESS) {
          const token = {
            accessToken: result.accessToken,
            refreshToken: result.refreshToken,
            expiresIn: result.expiresIn
          }
          storage.set(ACCESS_TOKEN, token)
          this.setRefreshToken(result.refreshToken)
          this.setToken(token)
          this.setUserId(result.userId)
        } else {
          console.error("登录失败:code = " + code)
          // 可以考虑在这里添加一个错误处理的逻辑,比如提示用户
        }
      } catch (error) {
        console.error("登录失败:", error)
        // 对于 API 调用失败,可以考虑提示用户或者重试逻辑
      }
    },
    async getUserInfoTo() {
      try {
        const response = await getUserInfo()
        const { result, code } = response as unknown as { result: any; code: string | number }
        if (code === ResultEnum.SUCCESS) {
          this.setUserInfo(result)
        } else {
          throw new Error("getInfo: permissionsList must be a non-null array !")
        }
        return result
      } catch (error) {
        console.error("获取用户信息失败:", error)
        // 对于 API 调用失败,可以考虑提示用户或者重试逻辑
        throw error
      }
    },
    async loginOutTo() {
      // 重新登录应该由组件的用户交互逻辑来触发
      // 这里应该仅负责执行登出的逻辑,而不是进行导航
      await ElMessageBox.alert("登录已失效,请重新登录", "提示", {
        confirmButtonText: "重新登录",
        callback: () => {
          storage.clear()
        }
      })
    }
  }
})

// 导出
export const useUser = () => {
  return useUserStore(store)
}

Utils/Storage

用于处理浏览器存储(localStoragesessionStorage)以及 cookie 的工具类。它提供了对存储数据的设置、获取、删除及清空操作,并且支持为存储项添加前缀以避免命名冲突,同时也支持缓存过期时间的管理。

  • 构造函数
    • 接受两个参数,一个是存储键名的前缀(prefixKey),另一个是使用的存储类型,默认使用 localStorage。
  • getKey
    • 将传入的 key 与前缀结合并转为大写形式,生成实际在存储中使用的键名。
  • set
    • 用于设置存储项,接受一个 key、一个 value 和一个 expire 参数表示缓存过期时间(单位为秒)。如果设置了过期时间,则会把当前时间戳加上过期秒数作为缓存的实际失效时间。
  • get
    • 读取存储项,若找到对应的存储值且未过期,则返回其内容;否则返回默认值或 null。
  • remove
    • 从存储中移除指定 key 的项。
  • clear
    • 清除所有带有前缀的存储项
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 // 默认缓存期限为7天
type StorageType = typeof localStorage | typeof sessionStorage
export default class Storage {
  private storage: StorageType // StorageType 应该是具体的存储类型,如 localStorage 或 sessionStorage
  private prefixKey?: string

  constructor(prefixKey = "", storage: StorageType = localStorage) {
    this.storage = storage
    this.prefixKey = prefixKey
  }

  private getKey(key: string): string {
    return `${this.prefixKey}${key}`.toUpperCase()
  }

  /**
   * 设置缓存
   * @param {string} key 缓存键
   * @param {*} value 缓存值
   * @param expire 缓存过期时间,默认为7天
   */
  set(key: string, value: any, expire: number | null = DEFAULT_CACHE_TIME): void {
    const data = {
      value,
      expire: expire !== null ? Date.now() + expire * 1000 : null
    }
    const stringData = JSON.stringify(data)
    this.storage.setItem(this.getKey(key), stringData)
  }

  /**
   * 读取缓存
   * @param {string} key 缓存键
   * @param {*=} def 默认值
   */
  get(key: string, def: any = null): any {
    const item = this.storage.getItem(this.getKey(key))
    if (item) {
      try {
        const data = JSON.parse(item)
        const { value, expire } = data
        if (expire === null || expire >= Date.now()) {
          return value
        }
        this.remove(key)
      } catch (e) {
        return def
      }
    }
    return def
  }

  /**
   * 从缓存删除某项
   * @param {string} key
   */
  remove(key: string): void {
    this.storage.removeItem(this.getKey(key))
  }

  /**
   * 清空所有缓存
   */
  clear(): void {
    this.storage.clear()
  }

  /**
   * 设置cookie
   * @param {string} name cookie 名称
   * @param {*} value cookie 值
   * @param {number=} expire 过期时间
   * 如果过期时间未设置,默认关闭浏览器自动删除
   */
  setCookie(name: string, value: any, expire: number | null = DEFAULT_CACHE_TIME): void {
    const maxAge = expire !== null ? `; Max-Age=${expire}` : ""
    document.cookie = `${this.getKey(name)}=${value}${maxAge}`
  }

  /**
   * 根据名字获取cookie值
   * @param name
   */
  getCookie(name: string): string {
    const cookies = document.cookie.split("; ")
    for (const cookie of cookies) {
      const [key, value] = cookie.split("=")
      if (this.getKey(key) === name) {
        return value
      }
    }
    return ""
  }

  /**
   * 根据名字删除指定的cookie
   * @param {string} key
   */
  removeCookie(key: string): void {
    this.setCookie(key, "", -1)
  }

  /**
   * 清空cookie,使所有cookie失效
   */
  clearCookie(): void {
    const keys = document.cookie.match(/[^ =;]+(?==)/g)
    if (keys) {
      for (let i = keys.length; i--; ) {
        const key = keys[i]
        this.setCookie(key, "", -1)
      }
    }
  }
}

export const storage = new Storage("")

三、封装使用

import http from "@/utils/http"
import { Login, BasicResponse, UserInfo } from "@/types"

// 登录
export const login = async (data: Login) => http.post<BasicResponse>("/api/login", data)
// 登出
export const loginOut = async () => http.get<BasicResponse>("/api/logout")

// 获取用户信息
export const getUserInfo = async () => http.get<UserInfo>("/api/user/list")

四、结语

本篇只是实现了上述的功能,其实还有其他的优化:

  • 请求接口数据缓存:接口在设定时间内或后不会向后台获取数据,而是直接拿本地缓存(拦截器配置,以及proxy的配置)
  • access token过期处理: 当access token过期后,可将接口暂时存储,当access token刷新成功继续进行请求
  • .env环境配置

    后续小编会继续完善,同时也请关注下一篇文章:RBAC权限管理

参考思路掘金前端小付阿飞飞飞
雷同代码:如有雷同请告知我这边添加地址,少量代码就算了毕竟千千万万程序猿同学中咱们两个思路相同,新手小编写文不容易,当然欢迎品论~~

好的,我可以为你提供关于Axios二次封装的一些建议。在进行二次封装之前,你需要先确保已经安装了Axios。以下是一个基本的Axios二次封装示例: ```javascript import axios from 'axios'; // 创建一个Axios实例 const instance = axios.create({ baseURL: 'http://api.example.com', // 设置请求的基本URL timeout: 5000 // 设置请求超时时间 }); // 请求拦截器 instance.interceptors.request.use( config => { // 在请求发送之前做一些处理,比如添加token等 return config; }, error => { // 请求错误时的处理 return Promise.reject(error); } ); // 响应拦截器 instance.interceptors.response.use( response => { // 对响应数据进行处理 return response.data; }, error => { // 响应错误时的处理 return Promise.reject(error); } ); // 封装GET请求 export function get(url, params) { return instance.get(url, { params }); } // 封装POST请求 export function post(url, data) { return instance.post(url, data); } // 其他HTTP方法的封装似,如put、delete等 export default instance; ``` 上述示例中,我们创建了一个基于Axios的实例,然后通过拦截器对请求和响应进行处理。你可以根据实际需求在拦截器中添加自定义逻辑,比如统一处理错误信息、添加请求头等。 封装的get和post函数可以直接调用,例如: ```javascript import request from './axios'; // 上述代码放在axios.js文件中 request.get('/api/users', { params: { page: 1 } }) .then(response => { console.log(response); }) .catch(error => { console.error(error); }); ``` 这样你就可以通过调用封装好的函数来发起请求,而无需每次都编写重复的代码。希望这对你有所帮助!如果有任何问题,请随时提问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值