重复请求取消(不发请求)

重复请求取消(不发请求)

axios 有自带的取消请求方式, 但是 在请求已发出时,取消只是状态取消, 其实请求已经发出,消耗了服务器资源(方式1)。本文探究的是 在调用请求A时, 在A未响应之前,所有的A重复请求都应该不发出,待A响应之后,将响应结果给到之前的所有A请求!(方式2,3)

基础代码

// request.js
export const request = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

// api/user.js  获取数据函数, request 就是axios实例
export function getCrypto1(params) {
  return request({
    url: "/echo",
    method: "post",
    params,
    encryption: false,
    hideMsg: true,
    notSign: true,
    notToken: true
  });
}

// App.vue created
function sleep(time) {
   return new Promise((resolve) => setTimeout(resolve, time));
}

for (let i = 0; i < 5; i++) {
  console.log("for: ", i);
  // await sleep(500);
  getCrypto1({ a: 11111 }).then((res) => {
    console.log("res--P: ", res);
  }).catch((err) => {
    console.log("res--E: ", err);
  });
}

#1 axios自带的取消请求(AbortController)

完整代码(request.js):

// 用于存储请求标识和对应的 AbortController
const pendingRequests = new Map();

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    cancelRequestHandler(config);
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    cancelRequestHandler(response.config, "repsonse");
  },
  (error) => {
    if (error.config) {
      cancelRequestHandler(error.config, "repsonse");
    }
    if (error.name === "CanceledError") {
      return;
    }
    return Promise.reject(error.response);
  }
);

// 生成唯一请求标识的方法
function generateRequestKey(config) {
  const { method, url, params, data } = config;
  return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
}

// axios AbortController取消请求(取消前面的,只保留最后一个)
function cancelRequestHandler(config, requestType = "request") {
  // 创建请求标识
  const requestKey = generateRequestKey(config);

  if (requestType === "request") {
    // 如果已存在相同请求,取消之前的请求
    if (pendingRequests.has(requestKey)) {
      const controller = pendingRequests.get(requestKey);
      controller.abort();
      pendingRequests.delete(requestKey);
    }

    // 为当前请求创建新的 AbortController
    const controller = new AbortController();
    config.signal = controller.signal;

    // 存储当前请求的控制器
    pendingRequests.set(requestKey, controller);
  } else {
    pendingRequests.delete(requestKey);
  }
}

请添加图片描述

#2 不发请求(axios)

大概思路就是 将重复请求在请求拦截器里用 return Promise.reject 直接绕过请求阶段进入响应拦截器错误里,再将结果返回即可

完整代码(request.js):

export const request = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

// 用于存储请求标识
const pendingRequests = new Map();

// 请求拦截器配置
request.interceptors.request.use(
  (config) => {
    const { shouldIntercept, promise } = handleDuplicateRequest(config);
    if (shouldIntercept) {
      return promise; // 如果是重复请求,返回挂起的 Promise
    }
    // 其它逻辑...
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    resolvePendingQueue(response);
    // 其它逻辑...
    return Promise.resolve(decrypts);
  },
 (error) => {
    console.log("resp error: ", error);
    if (error.cachedResponse) {
      if (error.cachedResponse.status === 200) {
        return Promise.resolve(error.cachedResponse?.data);
      } else {
        return Promise.reject(error.cachedResponse?.data);
      }
    }
    if (error.config) {
      rejectPendingQueue(error.response);
    }
    // 其它逻辑...
    return Promise.reject(error.response);
  }
);

// 生成唯一请求标识的方法
function generateRequestKey(config) {
  const { method, url, params, data } = config;
  return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`;
}

// 重复请求缓存
function handleDuplicateRequest(config) {
  const requestKey = generateRequestKey(config);
  if (pendingRequests.has(requestKey)) {
    return {
      shouldIntercept: true,
      promise: new Promise((resolve, reject) => {
        pendingRequests.get(requestKey).push({ resolve, reject });
      })
    };
  }
  // 如果是首次请求,初始化队列
  pendingRequests.set(requestKey, []);
  return { shouldIntercept: false, promise: null };
}

// 处理成功响应队列
function resolvePendingQueue(response) {
  const requestKey = generateRequestKey(response.config);
  const pendingQueue = pendingRequests.get(requestKey) || [];
  pendingQueue.forEach(({ reject }) => reject({ cachedResponse: response }));
  pendingRequests.delete(requestKey);
}

// 处理失败响应队列
function rejectPendingQueue(error) {
  const requestKey = generateRequestKey(error.config);
  const pendingQueue = pendingRequests.get(requestKey) || [];
  pendingQueue.forEach(({ reject }) => reject({ cachedResponse: error }));
  pendingRequests.delete(requestKey);
}

示例((成功):

请添加图片描述

示例(失败):
请添加图片描述

以上只发了一次请求

#3 不发请求(推荐,适合所有请求库)

基本思路: 对于第一个请求, 将 promise 存起来, 在该请求响应之前, 所有的相同请求都公用一个 promise, 当第一个请求响应时,该 promise 被释放,所有阻塞的相同请求都能拿到相同的结果!

完整代码(request.js)

export const request = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

// 创建缓存请求
export function createCacheedRequest(config) {
  // 根据请求的 URL 和参数生成唯一的请求 key
  const requestKey = generateRequestKey(config);
  // 判断请求是否已经在进行中
  if (!pendingRequests.has(requestKey)) {
    // 如果没有该请求,发起请求并缓存该请求的 Promise
    const requestPromise = service(config).finally(() => {
      // 请求完成后,从 pendingRequests 中删除该请求
      pendingRequests.delete(requestKey);
    })

    // 缓存该请求的 Promise
    pendingRequests.set(requestKey, requestPromise);
  }

  // 返回正在进行的请求 Promise
  return pendingRequests.get(requestKey);
}

// 不用再拦截器添加代码

// 调用的方式需要改一下
// 获取数据
export function getCrypto1(params) {
  return createCacheedRequest({
    url: "/echo",
    method: "post",
    params,
    encryption: false,
    hideMsg: true,
    notSign: true,
    notToken: true
  });
}

请添加图片描述

补充一下 Promise.finally 的实现方式

finally(cb) {
  return this.then(
    value => MyPromise.resolve(cb()).then(() => value),
    reason =>
     MyPromise.resolve(cb()).then(() => {
        throw reason;
      })
  );
}

当打开睡眠时间时,控制请求发出时延

for (let i = 0; i < 5; i++) {
  await sleep(500); // 每过500ms发出一个请求
  console.log("for: ", i);
  getCrypto1({ a: 1111 }).then((res) => {
    console.log("res--P: ", res);
  }).catch((err) => {
    console.log("res--E: ", err);
  });
  // this.getCryptoData();
}

node服务

// 接口耗时1500ms
app.post('/echo', (req, res) => {
  console.log('received : ', req.body)
  const data = req.body;
  setTimeout(() => {
    res.json({ code: 0, msg: 'success', data: 22222 });
    // res.status(500).json({ code: -1, msg: 'error', data: null });
  }, 1500)
});

这样就会第1个请求结束后,和第2,3,4 一起返回, 第五个请求重新发,不会被取消,单独返回。总共发出了两次请求

请添加图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值