如何取消 HTTP 请求

如何取消 HTTP 请求?是在面试过程中经常遇到的一个问题,并且在日常开发中也会遇到,比如不使用防抖的情况下如何取消请求,再比如一个请求等待时间过长,用户不想等了,可以取消这个请求。

最近在使用 Angular 的过程中,用到了 switchMap 操作符,发现每次发送新的请求时,上一个请求都会被自动取消掉,为了探究原理,就有了本文章。

我们知道,浏览器能发送请求,靠两个重要的 API,一个是比较老旧的 XHR,另一个比较新的 Fetch。所以本文会分别介绍使用 XHR 和 Fetch 如何取消请求,并且会分析一下 Angular 内置的 http 模块是怎样取消请求的。

XHR 取消请求

为了测试,首先创建一个 index.html 文件,内容如下,并且我们会使用 http://httpbin.org/ 提供的开放 API 来发送和取消请求。

<!DOCTYPE html>
<html lang="en">
  <body>
    <script>
      // 测试代码
    </script>
    <hr />
    <button onclick="beginFetching();">Begin</button>
    <button onclick="abortFetching();">Abort</button>
  </body>
</html>

XHR 使用实例的 abort() 方法来取消请求。

function beginFetching() {
  console.log('Now fetching');
  xhr.send();
}
 
function abortFetching() {
  console.log('Now aborting');
  xhr.abort();
  console.log('aborted!');
}
 
const xhr = new XMLHttpRequest();
 
const urlToFetch = 'https://httpbin.org/delay/3';
xhr.open('GET', urlToFetch, true);
 
xhr.onreadystatechange = (state) => {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};

先点击 Begin,并且在 3 秒内点击 Abort,效果如下:

xhr

可以看到,在点击 Abort 的时候,当前的 HTTP 请求的 Status 会变成 canceled 的状态。

Fetch 取消请求

Fetch 使用 AbortController 来取消请求。

// Create an instance.
const controller = new AbortController();
const signal = controller.signal;
 
// Register a listenr.
signal.addEventListener('abort', () => {
  console.log('aborted!');
});
 
function beginFetching() {
  console.log('Now fetching');
  var urlToFetch = 'https://httpbin.org/delay/3';
 
  fetch(urlToFetch, {
    method: 'get',
    signal: signal,
  })
    .then(function (response) {
      console.log(`Fetch complete. (Not aborted)`);
    })
    .catch(function (err) {
      console.error(` Err: ${err}`);
    });
}
 
function abortFetching() {
  console.log('Now aborting');
  // Abort.
  controller.abort();
}

同样先点击 Begin,并且在 3 秒内点击 Abort,效果如下:

fetch

可以看到,当点击 Abort 之后会调用 controller 实例的 Abort 方法,并且在 singal 上订阅的 abort event 也会被执行。与 XHR 不同的是,控制台会多打印一条 Error 信息。

Angular 中的 http 模块取消请求

了解了 XHR 和 Fetch 取消请求的方式之后,我们来看一下 Angular 中是使用的哪种方式。

首先在 sandbox 上创建一个 Angular 项目,地址:演示代码

通过多次点击按钮可以发现,每次点击都会先取消上一个请求,并且发送新的请求。那么 Angular 和 RxJS 内部是怎么实现的呢?

带着这个问题,我们首先来看 switchMap 的代码:switchMap

其中有这么几行是我们关心的:

// Cancel the previous inner subscription if there was one
innerSubscriber?.unsubscribe();

我们知道 switchMap 操作符的行为是每次上游产生数据的时候都会去调用函数参数 project,只要有新的内部 Observable 产生,就会立刻退订之前的内部 Observable 对象,所以 Angular 中取消 http 请求的代码应该是该请求内部 Observable 的返回值,比如对应到演示代码中就是 this.http.get(url)方法内部 Observable 的返回值。

接下来我们去查看 Angular http 模块的 get 方法,地址:client.ts

通过查看代码,可以发现 get 方法会调用一个名为 handle 的函数,这个函数返回了一个 Observable,而这个 Observable 的返回值也是一个函数,代码如下:

// This is the return from the Observable function, which is the
// request cancellation handler.
return () => {
  // On a cancellation, remove all registered event listeners.
  xhr.removeEventListener('error', onError);
  xhr.removeEventListener('abort', onError);
  xhr.removeEventListener('load', onLoad);
  xhr.removeEventListener('timeout', onError);
  if (req.reportProgress) {
    xhr.removeEventListener('progress', onDownProgress);
    if (reqBody !== null && xhr.upload) {
      xhr.upload.removeEventListener('progress', onUpProgress);
    }
  }
 
  // Finally, abort the in-flight request.
  if (xhr.readyState !== xhr.DONE) {
    xhr.abort();
  }
};

通过 abort 和变量命名也能猜出来,Angular 中内置的 http 模块是使用的 XHR 来发送请求,更详细的过程可以阅读 Angular http 模块的源码。