关于http:得物技术浅谈重复http请求的取消

场景:

作为开发者,咱们接触最多的就是CRUD,各种接口的联调,然而如同会比拟少的去关注咱们发送的http申请,当这个申请没有fulfilled,而又有新的申请发送进来,那么以后这一个申请该当如何解决?咱们晓得为了避免反复的动作,咱们能应用相似于防抖和节流来躲避,然而明天咱们谈谈从申请层面上如何去躲避反复的申请,而不是从用户侧去阻止。其实网上也有很多这方面的开发者的尝试,然而有的也讲得没有那么分明。为此,在调研了这方面的一些常识之后,联合本人平时在开发中遇到的场景。总结进去两个最常见的http申请须要被勾销的场景。

场景一:

雷同的申请须要勾销,这里的雷同的申请指的是对于get 申请来讲,method一样,params一样,url一样,对于Post申请来讲,当然就是method一样,body一样,url一样

场景二:

路由地址发生变化(之前网页的申请曾经无意义)

实现办法:

首先,要想实现反复的勾销,咱们就须要做两步,第一步是首先要晓得如何勾销,第二步就是如何判断出以后是反复的申请

勾销反复申请实现办法:

对于如何勾销,这部分我会从axios以及fetch两个方面来论述,因为这两个申请形式的实现勾销的办法是有差异的

为了不便了解,咱们通过react来演示这个过程更间接,因为咱们晓得钩子函数useEffect,useEffect的特点决定了返回函数会在每一次useEffect执行之前,进行一次,来进行清理操作,因而在这里,咱们把勾销操作放在这儿,就能够模仿每一次都勾销前一次的操作

axios

首先介绍axios,这个大家前端er们必定接触得都要吐了,一句话概括,axios实现勾销的实质就是应用其外部封装的cancelToken。

首先要晓得,token确定了唯一性,这也是确定哪一个申请须要被勾销的标识,它能够由cancelToken.source()生成

source蕴含了cancel办法 ,咱们调用它来实现勾销

useEffect(() => {
  const cancelToken = axios.CancelToken;
  const source = cancelToken.source();
  setAxiosRes("axios request created");
  getReq(source).then((res) => {
    setAxiosRes(res);
  });
  return () => {
    source.cancel("axios request cancelled");
  };
}, [axiosClick]);
export const instance = axios.create({
  baseURL: "http://localhost:4001",
});
export const getReq = async (source) => {
  try {
    const {
      data
    } = await instance.get("/", {
      cancelToken: source.token,
    });
    return data;
  } catch (err) {
    if (axios.isCancel(err)) {
      return "axios request cancelled";
    }
    return err;
  }
};

这里须要留神的是,cancel这个动作,自身就是能够被catch局部给捕捉到的,也是一个err,咱们用它提供的isCancel办法去判断一下是否是勾销操作,这个能够用来验证咱们的勾销是否胜利

fetch:

那么fetch的状况就不一样了,fetch的实现形式则是通过signal来勾销,其外部的AbortController()能勾销掉所有响应signal标记的申请,同样用react来模仿一次,其实实质还是一样的,并且同样也能被catch到

export const instance = axios.create({
      useEffect(() => {
        const controller = new AbortController();
        const signal = controller.signal;
        setFetchRes("fetch request created");
        hitApi(signal).then((res) => {
          setFetchRes(res);
        });
        //cleanup function
        return () => {
          controller.abort();
        };
      }, [fetchClick]);

hitApi函数如下,也就是把signal放进咱们的fetch外面,这样咱们能力abort。

 export const hitApi = async (signal) => {
        try {
          const response = await fetch("http://localhost:4001/", {
            signal
          });
          const data = await response.json();
          return data;
        } catch (err) {
          if (err.name === "AbortError") {
            return "Request Aborted ";
          }
          return err;
        }
      }

这里同样的,’AbortError’能够在catch被捕捉到

判断是否反复

好了,勾销的性能实现了,接下来就要思考如何去判断申请是否反复了。那毋庸置疑,判断是否反复,想要常数级工夫复杂度来找到是否有反复的申请,当然是应用Map,这样能够以O(1)的速度找到那个反复的申请,从而能力决定去勾销。而且,能够想到,整个过程须要向数组外面加货色,而曾经被勾销过的当然就须要拿进去,因而咱们须要一个加的函数,以及一个移除的函数

const addPending = (config) => {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!pending.has(url)) { // If the current request does not exist in pending, add it
      pending.set(url, cancel)
    }
  })
}

当给 config.cancelToken赋值的时候该当留神,以后这个 config.cancelToken是否曾经有值了

为了不便咱们平铺参数,咱们能够用qs来转换Object到string

const removePending = (config) => {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  if (pending.has(url)) { // If the current request identity exists in pending, you need to cancel the current request and remove it
    const cancel = pending.get(url)
    cancel(url)
    pending.delete(url)
  }
}

axios拦截器

然而在理论我的项目中,咱们通常有axios拦截器来对立治理咱们的申请,所以这里也有很多人喜爱间接把这两个办法加进axios拦截器里,这样就一劳永逸了。

axios.interceptors.request.use(config => {
  removePending(options) // Check previous requests to cancel before the request starts
  addPending(options) // Add current request to pending
  // other code before request
  return config
}, error => {
  return Promise.reject(error)
})
axios.interceptors.response.use(response => {
  removePending(response) // Remove this request at the end of the request
  return response
}, error => {
  if (axios.isCancel(error)) {
    console.log('repeated request: ' + error.message)
  } else {
    // handle error code
  }
  return Promise.reject(error)
})

路由切换应用形式

最初再来说一说第二个场景,路由切换了的状况,比较简单,间接清空咱们的pending队列就好,间接怼:

export const clearPending = () => {
  for (const [url, cancel] of pending) {
    cancel(url)
  }
  pending.clear()
}
router.beforeEach((to, from, next) => {
  clearPending()
  // ...
  next()
})

成果演示:

能够看到,反复点击,申请是cancelled,而如果是胜利返回,则是200。

原理剖析:

function CancelToken(executor) { 
 if (typeof executor !== 'function') { 
 throw new TypeError('executor must be a function.'); 
 } 
 var resolvePromise; 
 this.promise = new Promise(function promiseExecutor(resolve) { 
 resolvePromise = resolve; //把外部裸露进去 
 }); 
 var token = this; 
 //executor(cancel办法); 
 executor(function cancel(message) { 
 if (token.reason) { 
 // Cancellation has already been requested 
 return; 
 } 
 //token.reason是Cancel的实例 
 token.reason = new Cancel(message); 
 resolvePromise(token.reason);//扭转promise的状态 
 }); 
 } 

CancelToken的外围实质上来讲其实是将promise挂载下来,而后本人不去被动resolve或者reject,而是把这个主动权先裸露进去,也就是代码里的resolvePromise,而后在勾销的函数中去扭转这个promise的状态。

扭转这个状态有什么用呢,须要联合xhrAdapter源码来了解,在这里咱们就能够看到是在什么中央abort的了,标红局部就是通过上文扭转promise状态,.then外面被执行的过程。

function xhrAdapter(config) { 
 return new Promise(function dispatchXhrRequest(resolve, reject) { 
 if (config.cancelToken) { 
 //申请中,监听cancelToken中promise状态扭转 
 config.cancelToken.promise.then(function onCanceled(cancel) {
 if (!request) {
 return;
 }
 request.abort();
 reject(cancel); 
 request = null; 
 }); 
 } 
 }) 
} 

结语:

其实http申请勾销不是什么很稀奇的事件,要实现相似的申请勾销,有很多种其余的办法,甚至很多办法是优于这一种实现办法的,比如说勾销以后这个申请代替勾销前一个,仿佛更合乎逻辑一些,然而此文着重的是这一种裸露resolve的思维,十分值得一鉴。

文/Lily

关注得物技术,做最潮技术人!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理