有关于《vue 项目axios和useRequest的二次封装整合 及 Api统一管理》-- 实践经验总结

286 阅读5分钟

有关于《vue 项目axios和useRequest的二次封装整合 及 Api统一管理》

在项目实际开发应用中 与接口打交道的次数最多了 如何优雅又高效的调用接口就显得尤为重要了

话不多说直接上干货

request.ts -- axios封装

import axios, { type AxiosRequestConfig } from "axios"
import storage from "good-storage"
import { useRequest, setGlobalOptions } from "vue-request"
// vue-router 本地封装后的入口文件
import { router } from "@/plugins/components/vue3-router"
import { message } from "ant-design-vue"
import httpsResponseCode from "./httpsResponseCode"

/** 接口响应通过格式 */
export interface HttpResponse {
  code: number;
  cost: number;
  data: any[] | { page: number, size: number, total: number, list: [] | null } | null;
  msg: string;
}

// 用于存储请求的标识,便于路由切换时取消请求
const cancelTokenStore: any = {
  source: {
    token: null,
    cancel: null
  }
};

const axiosConfig: AxiosRequestConfig = {
  // 代理模式使用本方式命名接口域名
  // baseURL: import.meta.env.VITE_NODE_ENV === "dev" ? undefined : `${import.meta.env.VITE_APP_API_URL}`,
  baseURL: `${import.meta.env.VITE_APP_API_URL}`,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  // 是否跨站点访问控制请求
  // withCredentials: true,
  timeout: 5000, //超时时间,单位毫秒
  timeoutErrorMessage: '超时,请稍后再尝试',
  validateStatus() {
    // 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
    return true;
  },
  transformRequest: [
    data => {
      data = JSON.stringify(data);
      return data;
    }
  ],
  transformResponse: [
    data => {
      if (typeof data === 'string' && data.startsWith('{')) {
        data = JSON.parse(data);
      }
      return data;
    }
  ]
}

/**
 * 清空所有请求(通常在路由跳转时调用)
 */
export const clearAllPending = () => {
  const CancelToken = axios.CancelToken;
  cancelTokenStore.source.cancel && cancelTokenStore.source.cancel();
  cancelTokenStore.source = CancelToken.source();
};

// 创建 axios 实例
const request = axios.create(axiosConfig)

// 异常拦截处理器
const errorHandler = (error: any) => {
  // console.log(error)

  const errMsgMap = {
    "Network Error": "无法连接网络,请确认网络连接是否正常",
    "Request failed with status code 500": "服务器响应异常 - 请联系管理员",
    "Request failed with status code 400": "参数格式不正确 - 请联系管理员",
    [`timeout of ${axiosConfig.timeout}ms exceeded`]: "请求超时 请重试",
  }

  const errMsg = errMsgMap[error?.response?.data?.message || error?.message] || error?.response?.data?.message || error?.message

  const ret = { code: 999999, msg: errMsg, data: error?.response?.data }

  if (errMsg !== "中断请求") {
    message.destroy()
    message.error(errMsg)
    console.error("错误拦截器", "\n", ret, "\n", error)
  }
  return ret
}

// 定义全局参数
// window["axiosCancel"] = []
request.interceptors.request.use(async (config: AxiosRequestConfig) => {

  // 去除请求地址的末尾斜杠 
  config.url = config.url.replaceAll(/(\/$)/g, "")

  // 当前界面router链路
  const matched = router.currentRoute.value.matched

  // 接口访问埋点统计
  const buriedPoint = {
    // request_url: config.url.replace(/(\/[\d\w]+)/, "/(\\W+)"),
    request_method: config.method,
    request_url: config.url,
    request_url_replace: config.url.replaceAll(/(\/[\w]{32})/g, "/(\\w+)").replaceAll(/(\/\d+)/g, "/(\\d+)"),
    page_path: matched[matched.length - 1]?.path,
    page_name: matched[matched.length - 1]?.meta?.title || matched[matched.length - 1]?.name,
    page_button_name: storage.session.get("buttonName"),
    matched_str: matched.filter((e: any) => !!(e?.meta?.title || e?.name)).map((e: any) => e?.meta?.title || e?.name).join("/") + `${storage.session.get("buttonName") ? "/" + storage.session.get("buttonName") : ""}`,
    // matched,
  }

  if (config.method === "get") {
  
    // 该模式之后  get post方式请求 统一传 { data : any } 即可
    const data = config?.data
    
    if (data) {
      // 无效参数剔除
      Object.keys(data).forEach(e => {
        if (data[e] === undefined) delete data[e]
      })
      config.params = data
    }
  }

  const method = ["post", "put", "patch", "delete"]
  if (method.includes(config.method)) {
    console.warn(`调用了 ${config.method} 请求`)
    // config.headers['Content-Type'] = 'application/json;charset=utf-8'

    // 该模式之后  get post方式请求 统一传 { data : any } 即可
    const data = config?.data?.data || config?.data

    if (data) {
      // 无效参数剔除
      Object.keys(data).forEach(e => {
        if (data[e] === undefined) delete data[e]
      })
      config.data = data
    }
  }

  const user = storage.get("TOKEN")
  const token = user && (user.authorization || user.token || "test")
  config.headers["Authorization"] = token
  if (import.meta.env.VITE_HTTP_POINT == true || import.meta.env.VITE_HTTP_POINT == 'true') {
    config.headers["bury-point"] = btoa(encodeURI(JSON.stringify(buriedPoint)))
  }

  // console.log("请求拦截器\n", { config, buriedPoint, matched })

  // 本地token 续签
  if (user?.exp - 300000 < new Date().getTime()) {
    console.warn("监测到 token 失效, 开始重新获取");
    return new Promise((resolve, reject) => {
      // 让每个请求携带自定义 token 请根据实际情况自行修改
      // token 失效重新拉取 (以后台给的时间-5分钟为准 <25分钟>)
      axios.create({
        ...axiosConfig,
        headers: { "Authorization": token }
      }).put(`/api/token`).then(success => {
        const { data: res } = success
        console.log(res);
        if (!res.code) {
          storage.set("TOKEN", res.data)
          console.log("老token", config.headers["Authorization"]);
          config.headers["Authorization"] = res.data.token
          console.log("新token", config.headers["Authorization"]);
          resolve(config)
        } else {
          console.log("token获取失败 本次token以过期");
          localStorage.clear()
          location.reload()
        }
      }).catch(err => {
        console.log("重新获取token接口请求失败: ", err);
        localStorage.clear()
        location.reload()
      });
    })
  } else {
    return config
  }
}, errorHandler)

request.interceptors.response.use((response) => {
  const _response = httpsResponseCode(response.data)

  // console.log(`响应拦截器`, { response })
  return Promise.resolve(_response)
}, errorHandler)

setGlobalOptions({
  // 当 manual 设置为 true 时,你需要手动触发 run 才会发起请求
  manual: false,
  // 通过设置延迟的毫秒数,可以延迟 loading 变成 true 的时间,有效防止闪烁。
  loadingDelay: 10,
  // 当设置为 true 时,则在浏览器窗口触发 focus 和 visibilitychange 时,会重新发起请求。
  refreshOnWindowFocus: true,
  // 当 refreshOnWindowFocus 设置为 true 时,你可以通过设置间隔的毫秒数,来限制 refresh 的执行间隔,默认为 5000ms
  refocusTimespan: 10000,
  // 通过设置需要延迟的毫秒数,进入防抖模式。此时如果频繁触发 run ,则会以防抖策略进行请求。
  debounceInterval: 300,
  // 通过设置需要节流的毫秒数,进入节流模式。此时如果频繁触发 run ,则会以节流策略进行请求。
  throttleInterval: 300,
  // 当开启缓存后,你可以通过设置 cacheTime 来告诉我们缓存的过期时间。当缓存过期后,我们会将其删除。默认为 600000 毫秒,即 10 分钟
  // cacheTime: 600000, 
  // 如果你能确保缓存下来的数据,在一定时间内不会有任何更新的,我们建议你设置一个合理的毫秒数
  // staleTime: 60000, 
  // it will retry 5 times
  errorRetryCount: 5,
  // The retry interval is 5 seconds
  errorRetryInterval: 1500,
})
const refRequest = {
  /** 返回指定头的url链接 */
  obtainUrl: (url: string, data?: { [key: string]: any }) => {
    let newUrl = `${import.meta.env.VITE_APP_API_URL}/${url.replace(/^\//, "")}`
    if (data) {
      let urlCode = data ? "?" : ""
      for (const k in data) {
        const v = data[k]
        if (v !== undefined) {
          urlCode += `${k}=${v}&`
        }
      }
      urlCode = urlCode.substring(0, urlCode.length - 1)
      newUrl += urlCode
    }
    console.log(newUrl);
    return encodeURI(newUrl)
  },
  refGet: (url: string, data?: any, options?: any) => useRequest(() => request.get(url, data ? { data } : undefined), options),
  refPost: (url: string, data?: any, options?: any) => useRequest(() => request.post(url, data ? { data } : undefined), options),
  refPut: (url: string, data?: any, options?: any) => useRequest(() => request.put(url, data ? { data } : undefined), options),
  refPatch: (url: string, data?: any, options?: any) => useRequest(() => request.patch(url, data ? { data } : undefined), options),
  refDelete: (url: string, data?: any, options?: any) => useRequest(() => request.delete(url, data ? { data } : undefined), options),
  retTest: (url: string, data?: any, options?: any) => useRequest(() => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        return Math.random() > 0.5 ? resolve({ code: 0, data: Math.random(), msg: "success" }) : reject({ code: 500, data: Math.random(), msg: "error" })
      }, 1000);
    })
  }, options),
}


export default request
export { request as httpsRequest, refRequest }

httpsResponseCode.ts -- 响应code码解析方法

import { message } from "ant-design-vue"

/** 退出登录 */
const loginOut = (msg: any = '授权以过期 请重新登录') => {
  localStorage.clear()
  setTimeout(() => window.location.reload(), 1500)
  return msg
}

/** code码枚举 */
const codes = {
  // 1: '接口响应异常',
  /** 参数异常 */
  400: (res: any) => Object.keys(res.data).map(e => res.data[e]).join(' , '),
  600: () => loginOut('授权标识丢失 请重新登录'),
  601: () => loginOut(),
  603: '无权限访问',
  10010813: '原始密码错误',
  10010023: '密码过期,请重新设置密码'
}


/** 
 * 非吐司提示 -- 业务流程码 
 * @code 10010029 : 需要二次验证
 * @code 100100XX : 备注内容
 */
const flowCodes: number[] = [10010029]

/**
 * code码 解析方法
 * @param _response 
 * @returns _response
 */
const httpsResponseCode = (_response: any) => {
  const msg = typeof codes[_response.code] === 'function'
    ? codes[_response.code](_response)
    : codes[_response.code] || _response.msg

  if (_response.code) {
    !flowCodes.includes(_response.code) && (message.destroy(), message.warn(`${import.meta.env.VITE_NODE_ENV === 'dev' ? 'code:' + _response.code + ' 一 ' : ''}${msg}`))
  }

  return { ..._response, msg }
}

export default httpsResponseCode

ApiDemo.ts -- 接口清单统一管理

import request from '@/utils/request'

/** 接口名注释 */
export const ApiDemos = {
  /** 获取类别管理列表 */
  getListPage: () => request.get(`/cms/v1/xxxx`),
  /** 选择器 */
  getSelector: () => request.get(`/cms/v1/xxxx`),
  /** 获取数据详情 */
  getDataById: (id: any) => request.get(`/cms/v1/xxxx/${id}`),
  /** 添加数据 */
  addData: (data: any) => request.post(`/cms/v1/xxxx`, { data }),
  /** 编辑数据 */
  editorData: (data: any) => request.put(`/cms/v1/xxxx/${data.id}`, { data }),
  /** 删除数据 */
  deleteData: (data: any) => request.delete(`/v1/xxxx/`, { data }),
}

实际使用示例

@import { ApiDemos } from '@/api/ApiDemo'

...

const demo = async () => {
    // 接口实际响应参数解构 (code默认0位成功)
    const { code, data } = await ApiDemos.getListPage()
    if( !code ){
        const { total, page, size, list } = data
        // 逻辑代码
    }
}