axios请求封装讲的太多了,自己都有点腻了,应该是最后一次吧,今天讲vue3+ts版本关于axios的请求封装。实现功能如下:

1、可配置是否展示全局动画

2、可配置同个接口重复请求时是否取消前面未响应的请求

3、可配置路由跳转时,对未响应的接口取消请求

直接上代码吧

// api/axios.tsimport axios from 'axios'import type {  AxiosInstance,  AxiosRequestConfig,  AxiosResponse,  InternalAxiosRequestConfig} from 'axios'import { ElMessage } from 'element-plus'import { useAppStoreHook } from '@/store/modules/app'const useAppStore = useAppStoreHook()import { useUserStoreHook } from '@/store/modules/user'
interface RequestObj {  [key: string]: {    cancelSource: any    noShowLoading: boolean // 是否展示加载遮罩    changeRouteRemove: boolean // 是否路由跳转时 取消未响应的接口请求    isCancel: boolean // 是否是取消请求  }}
interface NewAddConfig {  preventDuplicateRequests?: boolean // 是否配置防止重复请求  noShowLoading?: boolean // 是否展示加载遮罩  changeRouteRemove?: boolean // 是否路由跳转时 取消未响应的接口请求}
export interface UserAxiosRequestConfig extends NewAddConfig, AxiosRequestConfig {}
interface NewAxiosRequestConfig extends NewAddConfig, InternalAxiosRequestConfig {}
interface NewAxiosResponse extends AxiosResponse {  config: NewAxiosRequestConfig}
const CancelToken = axios.CancelTokenclass Axios {  // axios 实例  instance: AxiosInstance
  // 基础配置,url和超时时间  baseConfig: AxiosRequestConfig = { baseURL: '/proxy', timeout: 10000 }
  // 请求对象  requestObj: RequestObj = {}
  // 正在请求的接口数量  requestingNum = 0
  // 取消请求方法,  remove(url: string, isRouteChange: boolean): void {    // 没有这个请求,直接返回    if (!this.requestObj[url]) return
    // 路由处调用的remove方法, 且此接口切换路由时不进行 取消请求操作 直接返回    if (isRouteChange && !this.requestObj[url].changeRouteRemove) return
    this.requestObj[url].cancelSource.cancel({      ...this.requestObj[url]    })
    if (isRouteChange) {      delete this.requestObj[url]    }  }
  constructor(config?: AxiosRequestConfig) {    // 使用axios.create创建axios实例    this.instance = axios.create(Object.assign(this.baseConfig, config))
    this.instance.interceptors.request.use(      (config: NewAxiosRequestConfig) => {        // 如果没配置不展示加载动画 则请求开始 请求数量+1        if (!config.noShowLoading) {          this.requestingNum++          useAppStore.setLoadingStatus(true)        }
        const url = config.url        // 配置 preventDuplicateRequests 为false 则不校验防止重复请求        if (url && config.preventDuplicateRequests !== false) {          this.remove(url, false)          this.requestObj[url] = {            cancelSource: CancelToken.source(),            isCancel: true,            noShowLoading: !!config.noShowLoading,            changeRouteRemove: !(config.changeRouteRemove === false) // 配置为false时才不清除          }          config.cancelToken = this.requestObj[config.url].cancelSource.token        }
        // 一般会请求拦截里面加token        const useUserStore = useUserStoreHook()        const token = useUserStore.gettersToken()        if (token) {          /**           *   当 NewAxiosRequestConfig 继承自 AxiosRequestConfig时 会报 headers不一定存在,于是加了下面的判断,           * 后面查看axios/index.d.ts代码,发现还有一层 InternalAxiosRequestConfig 包含headers的引用类型,然后可去除下面的判断           * */          // if (!config.headers) {          //   config.headers = {}          // }          config.headers['Authorization'] = 'Bearer ' + token        }
        return config      },      (err: any) => {        return Promise.reject(err)      }    )
    this.instance.interceptors.response.use(      async (res: NewAxiosResponse) => {        const { config } = res        // 如果没配置不展示加载动画 则响应结束 请求数量-1        if (!config.noShowLoading) {          this.requestingNum--          if (this.requestingNum <= 0) {            useAppStore.setLoadingStatus(false)          }        }
        // 响应结束 去除 请求对象 的key值        if (config.url && this.requestObj[config.url]) {          delete this.requestObj[config.url]        }
        const data = res.data        // code为200 或 返回的是文件流 直接返回        if (data.code === 200 || config.responseType === 'blob') {          return data        }        ElMessage({          showClose: true,          message: `${data.message}`,          type: 'error'        })        if (data.code === 401) {          const useUserStore = useUserStoreHook()          await useUserStore.logout()          window.location.reload()        }        return Promise.reject(new Error(data.message || 'Error'))      },      (err: any) => {        // 这里用来处理http常见错误,进行全局提示        let message = ''
        // 取消请求中message.noShowLoading不为true 或者 非取消请求中 config.noShowLoading 不为true        if (          (err?.message?.isCancel && !err?.message?.noShowLoading) ||          (err.config && !err.config.noShowLoading)        ) {          this.requestingNum--          if (this.requestingNum <= 0) {            useAppStore.setLoadingStatus(false)          }        }
        message = err?.message?.isCancel ? '' : err.message
        if (err.response && err.response.status) {          switch (err.response.status) {            case 400:              message = '请求错误(400)'              break            case 401:              message = '未授权,请重新登录(401)'              // 这里可以做清空storage并跳转到登录页的操作              break            case 403:              message = '拒绝访问(403)'              break            case 404:              message = '请求出错(404)'              break            case 408:              message = '请求超时(408)'              break            case 500:              message = '服务器错误(500)'              break            case 501:              message = '服务未实现(501)'              break            case 502:              message = '网络错误(502)'              break            case 503:              message = '服务不可用(503)'              break            case 504:              message = '网络超时(504)'              break            case 505:              message = 'HTTP版本不受支持(505)'              break            default:              message = `连接出错(${err.response.status})!`          }        }        // 这里错误消息可以使用全局弹框展示出来        if (message) {          // 比如element plus 可以使用 ElMessage          ElMessage({            showClose: true,            message: `${message},请检查网络或联系管理员!`,            type: 'error'          })        }
        // 这里是AxiosError类型,所以一般我们只reject我们需要的响应即可        return Promise.reject(err)      }    )  }}// 对于使用非此默认配置的,可传参 再暴露出多个配置axiosexport default new Axios()
// /api/modules/a-demo-api.tsimport axios from '@/api/axios'import type { UserAxiosRequestConfig } from '@/api/axios'const xiaobuAxios = axios.instance
// get 请求demo 1export function getDemo1(  data = {},  config: UserAxiosRequestConfig = {    noShowLoading: true // 配置不展示加载动画  }) {  return xiaobuAxios.get(`/xiaobu-admin/getDemo1`, { params: data, ...config })}
// get 请求 demo 2export function getDemo2(  data = {},  config: UserAxiosRequestConfig = {    preventDuplicateRequests: false // 配置不防止重复请求  }) {  return xiaobuAxios(`/xiaobu-admin/getDemo2`, {    method: 'get',    url: 'url',    params: data,    ...config  })}
// post 请求demo 1export function postDemo1(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios.post('/xiaobu-admin/postDemo1', data, config)}
// post 请求demo 2export function postDemo2(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios({    url: '/xiaobu-admin/postDemo2',    method: 'post',    data,    ...config  })}
// delete 请求demo 1  使用params作为参数 参数会拼接在请求路径上export function deleteDemo1(  data = {},  config: UserAxiosRequestConfig = {    noShowLoading: true // 配置不展示加载动画  }) {  return xiaobuAxios.get(`/xiaobu-admin/deleteDemo1`, { params: data, ...config })}
// delete 请求 demo 2 使用params作为参数 参数会拼接在请求路径上export function deleteDemo2(  data = {},  config: UserAxiosRequestConfig = {    preventDuplicateRequests: false // 配置不防止重复请求  }) {  return xiaobuAxios(`/xiaobu-admin/deleteDemo2`, {    method: 'delete',    url: 'url',    params: data,    ...config  })}
// delete 请求demo 3  使用data作为参数 参数不展示在  请求路径上export function deleteDemo3(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios.get(`/xiaobu-admin/deleteDemo3`, { data, ...config })}
// delete 请求 demo 4 使用data作为参数 参数不展示在  请求路径上export function deleteDemo4(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios(`/xiaobu-admin/deleteDemo4`, {    method: 'delete',    url: 'url',    data,    ...config  })}
// put 请求demo 1export function putDemo1(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios.put('/xiaobu-admin/putDemo1', data, config)}
// put 请求demo 2export function putDemo2(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios({    url: '/xiaobu-admin/putDemo2',    method: 'put',    data,    ...config  })}
// patch 请求demo 1export function patchDemo1(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios.patch('/xiaobu-admin/patchDemo1', data, config)}
// patch 请求demo 2export function patchDemo2(data = {}, config: UserAxiosRequestConfig = {}) {  return xiaobuAxios({    url: '/xiaobu-admin/patchDemo2',    method: 'patch',    data,    ...config  })}

store部分

// store/modules/app.tsimport { defineStore } from 'pinia'import { store } from '@/store'
const useAppStore = defineStore('app', {  state: () => {    return {      loadingStatus: false // 是否展示加载动画    }  },
  getters: {    gettersLoadingStatus: (state) => {      return () => state.loadingStatus    }  },
  actions: {    setLoadingStatus(status: boolean) {      this.loadingStatus = status    }  }})
export function useAppStoreHook() {  return useAppStore(store)}
// store/index.tsimport { App } from 'vue'import { createPinia } from 'pinia'const store = createPinia()
export function setupStore(app: App<Element>) {  app.use(store)}
export { store }

router路由拦截部分

import NProgress from 'nprogress' // progress barimport { useUserStoreHook } from '@/store/modules/user'const useUserStore = useUserStoreHook()import { handleAliveRoute } from './utils'import xiaobuAxios from '@/api/axios'
async function beforeEach(to, from, next) {  if (to.meta?.keepAlive) {    const newMatched = to.matched    handleAliveRoute(newMatched, 'add')    // 页面整体刷新和点击标签页刷新    if (from.name === undefined || from.name === 'Redirect') {      handleAliveRoute(newMatched)    }  }  NProgress.start()  // 路由跳转时根据配置判断会否需要取消前面未响应的请求  Object.keys(xiaobuAxios.requestObj).forEach((url) => {    xiaobuAxios.remove(url, true)  })
  const token = useUserStore.gettersToken()  if (token) {    if (to.name === 'Login') {      next({ name: 'Redirect', params: { path: '/' } })    } else {      const userInfo = useUserStore.gettersUserInfo()      if (userInfo) {        next()      } else {        await useUserStore.getUserInfo()        next({ ...to, replace: true })      }    }  } else {    if (to?.meta?.unNeedLogin) {      next()    } else {      next(`/login?redirect=${encodeURIComponent(to.fullPath)}`)    }  }}
export default beforeEach

总结

        上述代码包含了部分其他内容,不展开,主要还是得靠自己理解。此处给对象一个remove方法,判断是否是路由拦截处调用根据情况取消请求。由于使用了ts 因此新增了部分 接口 来声明变量类型及包含的数据。上述a-demo-api.ts中代码并不全,待使用到接口时,还需要声明响应数据的结构。

pinia中接口使用例子

// store/modules/user.tsimport { defineStore } from 'pinia'import { store } from '@/store'import { commonApi } from '@/api'import { UserInfoResult } from '@/api/mudules/resultType'import storage, { tokenKey, userKey } from '@/utils/storage'const storageData = storage.info()
const useUserStore = defineStore('user', {  state: () => {    return {      token: storageData[tokenKey] || '',      userInfo: storageData[userKey] || ''    }  },
  getters: {    gettersToken: (state) => {      return () => state.token    },    gettersUserInfo: (state) => {      return () => state.userInfo    }  },
  actions: {    setToken(data: { token: string }) {      this.token = data.token      storage.set(tokenKey, data.token)    },    // 退出登录    async logout() {      return new Promise<UserInfoResult>((resolve, reject) => {        commonApi          .logout()          .then((data) => {            console.log('🚀 ~ file: user.ts:36 ~ .then ~ data:', data)            this.userInfo = ''            this.token = ''            storage.clearAll()            resolve(data)          })          .catch((err) => {            reject(err)          })      })    },    // 获取用户信息    async getUserInfo() {      return new Promise<UserInfoResult>((resolve, reject) => {        commonApi          .getUserInfo()          .then((data) => {            console.log('🚀 ~ file: user.ts:53 ~ .then ~ data:', data)            this.userInfo = data.data.userInfo            storage.set(userKey, data.data.userInfo)            resolve(data)          })          .catch((err) => {            reject(err)          })      })    }  }})
export function useUserStoreHook() {  return useUserStore(store)}

// api/modules/common-api.tsimport axios from '@/api/axios'import type { UserAxiosRequestConfig } from '@/api/axios'const xiaobuAxios = axios.instance
import { UserInfoResult } from '@/api/mudules/resultType'// 获取用户信息export function getUserInfo(  data: object = {},  config: UserAxiosRequestConfig = {    noShowLoading: true,    changeRouteRemove: false  }) {  return xiaobuAxios.get<UserInfoResult>(`/xiaobu-admin/getUserInfo`, { params: data, ...config })}
// api/modules/resultType.tsexport type UserInfoResult = {  code: number  data?: {    userInfo: any  }}

TS用的时候是真的烦,但是是真的严谨,哪里有漏洞就给你跑不动,确保了项目的严谨及可靠,所以ts真香。

编辑器红色波浪线问题

TypeScript 引入第三方包却报错:"无法找到模块"

下面我拿react-router-dom库做例子

 


解决办法有两种

方法一:安装库的TypeScript声明文件

方法二:添加自己写库的TypeScript类型声明

在方法一可行的情况下,推荐使用方法一,但是不是所有库都有 TypeScript 的声明文件,所以方法一无效才用方法二。


方法一:

根据报错提示哪个库缺TypeScript声明文件安装对应的就是了(建议用淘宝镜像快点)

npm install -D @types/库的名字

举个例子:npm install @types/react-router-dom --save-dev


方法二:自己添加特定库的ts类型声明

(1)新建文件夹typings

(2)新建文件:react-router-dom.d.ts

(3)增加库的声明

(4)然后在TS配置文件里增加typings文件引用

(5)然后路由就正常使用了

Logo

前往低代码交流专区

更多推荐