Ts封装Axios并实现类似于节流效果

1,308 阅读5分钟

本文已参与[新人创作礼]活动,一起开启掘金创作之路

由于Typescript越来越普及化,许多项目都引入了Ts,所以使用Ts对Axios进行封装势在必行,所谓一人封装,整体受益

先谈谈为什么要使用Ts,以及Ts相对于Js有什么优点?

Typescript特点

  1. 图灵完备。(虽然不太清楚这意味着什么)
  2. 渐进的类型系统,所有类型标注都是可选的,既是天使又是恶魔的 any 类型。(被 Haskell 大牛 Colliot 称为 ts 类型系统的漏洞)
  3. 支持局部类型推导
  4. 丰富的类型层面的计算,如 index types, mapped types, conditional types 等等
  5. 支持鸭子类型。(或叫结构子类型?)
  6. 像 js 支持对象字面量一样支持方便的对象字面类型(object literal type),字符串和数字还有布尔值字面类型。
  7. 空安全。
  8. 基于控制流的类型分析。

还有比如支持类型别名,泛型,协变逆变双变等等...

优点:

其一,静态类型检查可以做到early fail,即你编写的代码即使没有被执行到,一旦你编写代码时发生类型不匹配,语言在编译阶段(解释执行也一样,可以在运行前)即可发现。针对大型应用,测试调试分支覆盖困难,很多代码并不一定能够在所有条件下执行到。而假如你的代码简单到任何改动都可以从UI体现出来,这确实跟大型应用搭不上关系,那么静态类型检查确实没什么作用。

其二,静态类型对阅读代码是友好的,比如我们举个例子 jQuery API Documentation 这是大家都非常喜欢用的jQuery.ajax,在这份文档中,详尽地解释了类型为object的唯一一个参数settings,它是如此之复杂,如果没有文档,我们只看这个函数声明的话,根本不可能有人把这个用法猜对。针对大型应用,方法众多,调用关系复杂,不可能每个函数都有人编写细致的文档,所以静态类型就是非常重要的提示和约束。而假如你的代码像jQuery这样所有函数基本全是API,根本没什么内部函数,而且逻辑关系看起来显而易见,这确实跟大型应用搭不上关系,那么静态类型对阅读代码确实也没什么帮助

总结:

  1. Ts提供类型判断,提早告知错误,不像Js运行后再报错
  2. 代码友好性高

Axios封装

这边封装Axios的同时还实现了类似于节流效果,过滤后续重复请求

Axios封装

// axios.ts
// 封装axios
import axios, { AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
// import { setToken, getToken, getTokenKey, removeToken, } from "./cookie";
​
import { addPending, removePending, checkRequest } from './pending';
​
const showStatus = (status: number) => {
  let message = '';
  switch (status) {
    case 400:
      message = '请求错误(400)';
      break;
    case 401:
      message = '未授权,请重新登录(401)';
      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 = `连接出错(${status})!`;
  }
  return `${message},请检查网络或联系管理员!`;
};
​
export class Interceptors {
  instance: AxiosInstance;
  constructor() {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL as string, // 看个人配置
      timeout: 30 * 1000,
      withCredentials: true,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/json;charset=utf-8',
      },
    });
    this.setupInterceptors();
  }
  // 初始化拦截器
  setupInterceptors() {
    // 请求接口拦截器
    this.instance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        if (!checkRequest(config)) {
          // 如果pending中没有该请求,则加入pending
          // 如果有的话,会在该方法中取消
          addPending(config)
        }
        // 判断一下是否有cookie 如果有的话则把cookie放入请求头中
        // if (getToken()) {
        //   config.headers[getTokenKey()] = getToken();
        // }
        return config;
      },
      () => {
        // 错误抛到业务代码
        const error = { data: { msg: '服务器异常,请联系管理员!' } };
        return Promise.resolve(error);
      },
    );
    // 响应拦截器
    this.instance.interceptors.response.use((response: AxiosResponse) => {
      removePending(response); // 在请求结束后,移除本次请求
      return response.data;
    }, (error: any) => {
      let msg;
      let code = -1;
      if (axios.isCancel(error)) {
        msg = '';
        code = -2;
      } else {
        const { status } = error?.response;
        if (status < 200 || status >= 300) {
          // 处理http错误,抛到业务代码
          msg = showStatus(status);
        }
      }
      return Promise.resolve({ msg, code });
    });
  }
  // 返回一下
  getInterceptors() {
    return this.instance;
  }
}
 

导出http

// index.ts
// 导出
import { AxiosPromise } from 'axios';
import { Interceptors } from './axios';
import { message } from 'ant-design-vue';
​
export interface HttpRequest {
  method?: string
  url: string
  data?: any
  timeout?: number
}
// 接口响应通过格式 (由各自响应格式定义)
export interface HttpResponse {
  code: number
  data: any
  msg: string
}
// 请求配置
export class HttpServer {
  axios: any;
  // 获取axios实例
  constructor() {
    this.axios = new Interceptors().getInterceptors();
  }
  // 简单封装一下方法
  request(config: HttpRequest): AxiosPromise {
    return new Promise((resolve, reject) => {
      this.axios(config).then((res: HttpResponse) => {
        if (res.code === 0) {
          resolve(res?.data);
        } else if (res.code === -2) { // 多次请求取消
          console.log('请求多次。');
        } else {
          const msg = res?.msg || '系统繁忙,请稍后重试';
          message.error(msg);
          reject(msg);
        }
      })
        .catch((err: any) => {
          err?.msg && message.error(err?.msg);
          reject(err);
        });
    });
  }
}
​
const http = new HttpServer();
​
export default http;

实现去重

// pending.ts
// 实现去重
import axios, { AxiosRequestConfig } from 'axios';
// CancelToken 使用说明 https://github.com/axios/axios
// 参考文章 https://segmentfault.com/a/1190000039806000// 声明一个 Map 用于存储每个请求的标识 和 取消函数
export const pending = new Map();
​
export const getKey = (config: AxiosRequestConfig) => JSON.stringify([config.method, config.url, config.params, config.data]);
/**
 * 添加请求
 * @param {Object} config
 */
export const addPending = (config: AxiosRequestConfig): void => {
  const url = getKey(config);
  // eslint-disable-next-line no-param-reassign
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
      pending.set(url, cancel);
    }
  });
};
/**
 * 移除请求
 * @param {Object} config
 */
export const removePending = (config: AxiosRequestConfig): void => {
  const url = getKey(config);
  if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
    const cancel = pending.get(url);
    cancel(url);
    pending.delete(url);
  }
};
​
/**
 * 清空 pending 中的请求(在路由跳转时调用)
 */
export const clearPending = (): void => {
  for (const [url, cancel] of pending) {
    cancel(url);
  }
  pending.clear();
};
​
// 校验是否有相同请求在请求中
export const checkRequest = (config: AxiosRequestConfig): boolean => {
  const url = getKey(config);
  if (pending.has(url)) {
    new axios.CancelToken((cancel) => {
      cancel(url);
    })
    return true;
  } else return false;
}
​
// cookie.ts
import Cookies from 'js-cookie';
​
const tokenKey = 'token';
​
export const getToken = () => Cookies.get(tokenKey);
export const getTokenKey = () => tokenKey;
export const setToken = (token: string) => Cookies.set(tokenKey, token);
export const removeToken = () => Cookies.remove(tokenKey);

在路由跳转时撤销所有请求

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Login from '@/views/Login/Login.vue'
//引入在axios暴露出的clearPending函数
import { clearPending } from "@/utils/http/pending"
​
....
....
....
​
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})
​
router.beforeEach((to, from, next) => {
  //在跳转路由之前,先清除所有的请求
  clearPending()
  // ...
  next()
})
​
export default router

请求例子

// promise式
http.request({ url: /api/xxx/xxx, method: 'get', data: { username, password } }).then((res: any) => {
    ...
}).catch((err) => {
    ...
})
​
// async/await式
// 函数头要加async
const res = await http.request({ url: /api/xxx/xxx, method: 'get', data: { username, password } });

参考链接

TypeScript 的好处都有啥?和 JavaScript 的区别在哪?

axios说明

Vue3+TypeScript封装axios并进行请求调用