开发过程中,我们有几种场景的比较容易忽视。

重复请求:

  1. 用户提交表单时按钮重复点击。
  2. 移动端的列表下拉和上拉加载时,意味着两个相同路径的请求(仅页码参数不同)先后发出。我们必须保留后一个请求,否则就可能出现,上拉的数据和下拉的数据先后返回并渲染在列表里,且可能二者顺序错误。

请求需要取消:

  1. 页面请求尚未结束,路由已切换,此时请求仍在发送中。

针对1这种按钮重复点击问题,有三种方案解决:

  1. 最简单粗暴的方法,在代码中对提交过程加锁,按钮点击后加锁知道请求结束才能重新点击。
  2. 利用防抖,对短时间内产生的点击只响应其最后一次。具体可参见 防抖和节流

防抖虽然可以解决短时间内的连击问题,但是考虑一种场景:假设请求由于各种原因响应非常慢,那么依然可以在防抖时间后再次发出请求,依然可以制造出重复请求,所以根本上还是要解决重复请求的问题。

3.利用请求拦截器,对重复请求进行拦截。这里以最常用的axios来举例子。

// 使用map来保存每个请求的CancelToken
const pendingRequest = new Map();

const getRequestKey = (config) => {
  // let key = {
  //   method: config.method,
  //   url: config.url,
  //   params: config.params,
  //   data: config.data
  // }
  // key = qs.stringify(key)
  // 注意这里是用来判断重复请求的标准(比如路径相同且请求方法相同即视为重复请求),也可以根据业务需要进行更改。
  const key = `${config.url}/request_type=${config.method}`;
  return key;
};

const isRequestExist = (config) => {
  const requestKey = getRequestKey(config);
  return pendingRequest.has(requestKey);
};

const addPendingRequest = (config) => {
  const requestKey = getRequestKey(config);
  config.requestKey = requestKey;
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!isRequestExist(config)) {
      pendingRequest.set(requestKey, cancel);
    }
  });
};

const cancelRequest = (requestKey) => {
  const cancel = pendingRequest.get(requestKey);
  cancel(requestKey);
};

const removePendingRequest = (config) => {
  const requestKey = config.requestKey;
  if (pendingRequest.has(requestKey)) {
    pendingRequest.delete(requestKey);
  }
  // cancelRequest(requestKey)
};

const clearPending = () => {
  for (const [requestKey, cancel] of pendingRequest) {
    cancel(requestKey);
  }
  pendingRequest.clear();
};

const allowRepeatRequest = (url) => {
  const result = false;
  for (const path of allowRepeatRequestPaths) {
    if (url.indexOf(path) > -1) {
      return true;
    }
  }
  return result;
};

const service = axios.create({
  baseURL: process.env.VUE_APP_BASIC_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 60000, // request timeout
});

// HTTPrequest拦截
service.interceptors.request.use((config) => {
  if (config.showLoading) {
    context.$showLoading();
  }
  if (!allowRepeatRequest(config.url)) {
	// 注意这里是取消旧的重复请求,重新发送新的,根据业务需要也可以取消新请求,保留旧请求。
    if (isRequestExist(config)) {
      cancelRequest(getRequestKey(config));
      removePendingRequest(config);
    }
    addPendingRequest(config);
  }
  const isToken = (config.headers || {}).isToken === false;
  const token = getToken();
  if (token && !isToken) {
    config.headers.Authorization = `Bearer ${token}`;// token
  }
  return config;
}, (error) => {
  // 网络波动导致错误
  console.warn('request error');
  clearPending();
  return Promise.reject(error);
});

service.interceptors.response.use((response) => {
  removePendingRequest(response.config);
  if (response.config && response.config.showLoading) {
    context.$closeLoading();
  }

  return response;
}, (error) => {
  if (axios.isCancel(error)) {
    console.warn(`repeated request: ${error.message}`);
    return Promise.reject(error);
  }
  if (error && error.response && error.response.status === 401) {
    EventBus.$emit(EventType.UNAUTHORIZED);
  }
  clearPending();
  return Promise.reject(error);
});

所以,对于重复请求,我们可以简单粗暴采取方案1进行处理,但是缺点是重复代码比较多,代码侵入性比较强,不推荐使用。更推荐方案2+3结合来使用,其实单单3已经可以拦截所有重复请求了,2可以降低3的拦截次数。 至于请求取消,已经在上述代码中包含了,有需要的话直接在路由守卫里进行处理就好。

标签: 请求取消

添加新评论

0%