处理前端接口重复请求

处理前端接口重复请求

在前端开发中,经常会遇到这样的情况:用户在操作页面时,不小心多次点击了某个按钮或者触发了某个事件,导致同一个接口被重复请求多次。这不仅浪费了服务器资源,还可能导致页面数据的混乱。

那么,我们应该如何解决这个问题呢?

常见的处理方式有以下几种:

  • 页面添加loading效果,禁止用户多次点击。
  • 给按钮添加防抖节流
  • 封装axios函数,进行全局的拦截和处理。

我们今天主要讲述第三种方式。

如果是根据第三种方式进行处理,那我们要清楚以下几个问题:

  • 如何判断是重复请求
  • axios如何取消请求
  • 如何处理异常情况

由于本项目是基于react+mobx,接口请求除了上传和下载全封装在mobx里面,本篇文章就不处理重复请求结果共享的问题。

流程是:

预备知识

如何判断是重复请求

一个请求中包含的内容有请求方法地址参数 等,我们可以根据这几个数据把这个请求生成一个key 来作为这个请求的标识

js 复制代码
// 根据请求生成对应的key
function generateReqKey(config) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
}

现在我们生成了请求对应的标识值,然后我们可以根据这个标识作为key值保存在请求Map中。

Axios如何取消网络请求

我们从Axios官网中可以知道,可以通过AbortController 取消请求,CancelToken方式已经不被axios推荐,

那如何通过AbortController 取消网络请求呢,我们可以根据官网示例来了解:

js 复制代码
const controller = new AbortController();
​
axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()

然后我们就可以直接来处理接口请求问题了。

处理网络重复请求

根据上述流程步骤,我们需要封装以下的方法:

  • 生成请求标识符,用来判断是否是重复请求
  • 如果不在请求Map中,我们要给config赋值AbortController参数,用于在后面的判断中取消网络请求
  • 如果在请求Map中,要取消网络请求,并做异常的提示
  • 清楚所有的网络请求和清空请求Map

然后我们根据以上,将其封装:

ts 复制代码
import type { AxiosRequestConfig } from "axios";
import qs from "qs";
​
class RequestCanceler {
  // 存储每个请求的标志和取消函数
  pendingRequest: Map<string, AbortController>;
  constructor() {
    this.pendingRequest = new Map<string, AbortController>();
  }
​
  generateReqKey(config: AxiosRequestConfig): string {
    const { method, url, params, data } = config;
    return [method, url, JSON.stringify(params), JSON.stringify(data)].join("&");
  }
​
  addPendingRequest(config: AxiosRequestConfig) {
    const requestKey: string = this.generateReqKey(config);
    if (this.pendingRequest.has(requestKey)) {
      const controller = new AbortController();
      // 给config挂载signal
      config.signal = controller.signal;
      this.pendingRequest.set(requestKey, controller);
    } else {
      // 如果requestKey已经存在,则获取之前设置的controller,并挂signal
      config.signal = (this.pendingRequest.get(requestKey) as AbortControlle).signal;
    }
  }
  removePendingRequest(config: AxiosRequestConfig, flag?: boolean) {
    const requestKey = this.generateReqKey(config);
    if (this.pendingRequest.has(requestKey)) {
      //取消请求
      if (flag) {
        (this.pendingRequest.get(requestKey) as AbortController).abort();
        Promise.reject(new Error(`请求的重复请求:${config.url}`)).then();
      }
      //从pendingRequest中删掉
      this.pendingRequest.delete(requestKey);
    }
    setTimeout(() => {}, 100);
  }
  removeAllPendingRequest() {
    [...this.pendingRequest.keys()].forEach((requestKey) => {
      (this.pendingRequest.get(requestKey) as AbortController).abort();
    });
    this.pendingRequest.clear();
  }
  checkRequest(config: AxiosRequestConfig) {
    // 检查是否存在重复请求,若存在则取消己发的请求
    this.removePendingRequest(config, true);
    // 把当前的请求信息添加到pendingRequest
    this.addPendingRequest(config);
  }
}
​
export { RequestCanceler };

然后我们来封装axios:

ts 复制代码
import type {
  AxiosError,
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
} from "axios";
import axios from "axios";
import { RequestCanceler } from "./requestCanceler";
​
export const requestCanceler = new RequestCanceler();
​
export interface RequestConfig extends AxiosRequestConfig {
  prefix?: string;
  expireError?: {
    codes: number[];
    url?: string;
  };
  userErrorCodes?: number[];
  errorHandle?: (data: any) => void;
  allData?: boolean;
  [args: string]: any;
  callback?: () => void;
}
​
const request = ({
  timeout = 50000,
  withCredentials = true,
  prefix = "",
  expireError = {
    codes: [],
  },
  userErrorCodes = [],
  headers = {
    "Content-Type": "application/json;charset=utf-8",
    timestamp: true, // loading时间为接口请求时间, 默认为true
  },
  ...args
}: RequestConfig) =>
  new RequestHttp({
    timeout,
    withCredentials,
    prefix,
    expireError,
    userErrorCodes,
    headers: {
      "Content-Type": "application/json;charset=utf-8",
      timestamp: true, // loding时间为接口请求时间, 默认为true
      ...headers,
    },
    ...args,
  });
​
class RequestHttp {
  service: AxiosInstance;
​
  public constructor(configs: RequestConfig) {
    this.service = axios.create(configs);
    console.log("configs: ", configs);
    /**
     * @description 请求拦截器
     * 客户端发送请求 -> [请求拦截器] -> 服务器
     * token校验(JWT) : 接受服务器返回的token,存储到redux/本地储存当中
     */
    this.service.interceptors.request.use(
      (config: any) => {
        const token: any = localStorage.getItem("token");
        // 检查请求信息
        config.url = (config.headers.prefix ?? config.prefix) + config.url;
        requestCanceler.checkRequest(config);
        if (config.headers.timestamp) {
          config.headers.timestamp = new Date().valueOf();
        } else {
          config.headers.timestamp = 0;
        }
        return { ...config, headers: { ...config.headers, token: `${token}` } };
      },
      async (error: AxiosError) => {
        throw error;
      }
    );
    configs.requestUse?.forEach((request: any) =>
      this.service.interceptors.request.use(...request)
    );
    /**
     * @description 响应拦截器
     *  服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
     */
    this.service.interceptors.response.use(
      async (response: AxiosResponse) => {
        const { data, config } = response;
        //  检查是否存在重复请求
        requestCanceler.removePendingRequest(config);
        configs.callback?.();
        if (configs.errorHandle) {
          configs.errorHandle(data);
        }
        return response;
      },
      async (error: any) => {
        requestCanceler.removePendingRequest(error.config || {});
        configs.callback?.();
        // 服务器结果都没有返回(可能服务器错误可能客户端断网) 断网处理:可以跳转到断网页面
        if (!window.navigator.onLine) {
          requestCanceler.removeAllPendingRequest();
        }
        throw error;
      }
    );
    configs.responseUse?.forEach((response: any) =>
      this.service.interceptors.response.use(...response)
    );
  }
​
  async get<T>(url: string, params?: object, _object: any = {}): Promise<T> {
    return await this.service.get(url, { params, ..._object });
  }
​
  async post<T>(url: string, data?: object, _object: any = {}): Promise<T> {
    return await this.service.post(url, data, _object);
  }
​
  async put<T>(url: string, params?: object, _object = {}): Promise<T> {
    return await this.service.put(url, params, _object);
  }
​
  async delete<T>(url: string, params?: any, _object = {}): Promise<T> {
    return await this.service.delete(url, { params, ..._object });
  }
​
  async patch<T>(url: string, data?: object, _object = {}): Promise<T> {
    return await this.service.patch(url, data, _object);
  }
}
​
export default request;
相关推荐
理想不理想v22 分钟前
高级前端开发工程师--掌握的技术
java·前端·javascript·typescript
贺今宵41 分钟前
vue使用vite-plugin-svg-icons插件组件化svg图片
前端·javascript·vue.js
linzhisong42 分钟前
LayUI组件国际化多国语言版本脚本-下篇根据语种替换
前端·javascript·python·layui
LAY家的奶栗子是德云女孩1 小时前
HTML5+CSS前端开发[保姆级教学]+基本文本控制标签介绍
前端·css·html·学习方法
遗憾何来2 小时前
第9章综合案例————众成远程教育
前端·javascript·css
开心工作室_kaic2 小时前
ssm117网络教学平台的设计与实现+vue(论文+源码)_kaic
前端·javascript·vue.js
OEC小胖胖2 小时前
Vue 3 中的 v-bind 完全指南
前端·javascript·vue.js·前端框架·web
Dreams°1232 小时前
【Vue组件中使用数据绑定】
前端·javascript·vue.js
叶子_o2 小时前
vue 获取摄像头拍照,并旋转、裁剪生成新的图片
前端·javascript·vue.js
xcLeigh2 小时前
VUE3实现简洁的特色美食网站源码
前端·源码·vue3