如何优雅地abort XHR/Fetch请求?

3,054 阅读9分钟

前言

XMLHttpRequest 在 AJAX 编程中被大量使用并被开发者熟知。所以现在大家也不再严格区分AJAXXMLHttpRequest,认为他们在可以出现的场合完全可以互换。

通过交互式网站和现代 Web 标准的建立,AJAX正在逐渐被 JavaScript 框架中的函数和官方的 Fetch API 标准取代。关于XMLHttpRequestFetch API使用方法的文档很多,本文重点是给出如何abort(终止)XMLHttpRequestFetch API请求的方法。欢迎大家阅读并给出自己实践中的好例子。

Ajax

1998年前后,Outlook Web Access小组写成了允许客户端脚本发送HTTP请求(XMLHTTP)的第一个组件。该组件原属于微软Exchange Server,并且迅速地成为了Internet Explorer 4.0[2]的一部分。因此大部分人认为,Outlook Web Access是第一个应用了Ajax技术的成功的商业应用程序,并成为包括Oddpost的网络邮件产品在内的许多产品的领头羊。

然而直到2005年初,杰西·詹姆士·贾瑞特发表的一篇谈论阐述了让网页透过JavascriptXML格式来回传资料、达到异步更新网页内容技术的文章,并将此技术以“AJAX”来简称之、来代表原先含意“Asynchronous JavaScript and XML”的缩写,AJAX才被大众所接受。

采用了AJAX动态修改网页的应用称为AJAX应用,AJAX应用可以仅向服务器发送并取回必须的数据,并在客户端采用JavaScript处理来自服务器的回应。因为在服务器和浏览器之间交换的数据大量减少,服务器回应更快了。同时,很多的处理工作可以在发出请求的客户端机器上完成,因此Web服务器的负荷也减少了。

AJAX不是指一种单一的技术,而是有机地利用了一系列相关的技术。虽然其名称包含XML,但实际上数据格式可以由JSON代替以进一步减少数据量。而客户端与服务器也并不需要异步。一些基于AJAX的“派生/合成”式(derivative/composite)的技术也正在出现,如AFLAX

XMLHttpRequest

简介

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。

image.png

从上面的对象原型继承图可知,XMLHttpRequest其实继承了EventTarget的原型对象:

image.png

兼容性

XMLHttpRequest 最初是由微软公司发明的,在Internet Explorer 5.0中用作ActiveX对象,可通过JavaScriptVBScript或其它浏览器支持的脚本语言访问。Mozilla的开发人员后来在Mozilla 1.0中实现了一个兼容的版本。之后苹果电脑公司在Safari 1.2中开始支持XMLHttpRequest,而Opera从8.0版开始也宣布支持XMLHttpRequest。所以在IE6及以前(稳妥点可以放宽到IE9及以前)的IE浏览器中使用 XMLHttpRequest 需要这样做:

if (window.XMLHttpRequest) {
    //Firefox、 Opera、 IE7 和其它浏览器使用本地 JavaScript 对象
    var request = new XMLHttpRequest();
} else {
    //IE 5 和 IE 6 使用 ActiveX 控件
    var request = new ActiveXObject("Microsoft.XMLHTTP");
}

通过查看 XMLHttpRequest 的兼容性也可知道,在IE6-IE9中,XMLHttpRequest都是不支持的:

image.png

使用

XMLHttpRequest 的使用也很简单:

const xhr = new XMLHttpRequest();// 创建一个xhr对象
const method = 'GET';// 设置请求类型
const url = 'https://developer.mozilla.org/';

xhr.open(method, url, true);
xhr.onreadystatechange = () => {
  if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    console.log(xhr.responseText)
  } else {
    console.error(xhr.responseText)
  }
}
xhr.send();

通过上述代码可以在网页中发起一个异步请求。当请求响应时,会触发 onreadystatechange 事件,在事件会调里判断 readyState属性的值,来判定请求是否正常响应,然后做相应操作。

XMLHttpRequest 的 abort

XMLHttpRequest自带有abort方法:

如果该请求已被发出,XMLHttpRequest.abort()  方法将终止该请求。当一个请求被终止,它的  readyState 将被置为 XMLHttpRequest.UNSENT (0),并且请求的 status 置为 0。

调用语法:

xhrInstance.abort();

因此想要abort一个XMLHttpRequest请求只需在请求未返回前调用一下abort就行,当然,在ES6时代的开发中http请求响应默认都会封装为一个Promise对象,直接操作xhrInstance的场景很少。下面贴出的代码源自stack overflow上的一段自带abort的get请求工具函数:

function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

abort 后响应结果

请求代码:

const sendXhrRequest = () => {
    const xhr = new XMLHttpRequest(); // 创建一个xhr对象
    ref.current = xhr;
    const method = 'GET'; // 设置请求类型
    const url = 'demo url';

    xhr.open(method, url, true);
    xhr.onreadystatechange = () => {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        setUser(JSON.parse(xhr.responseText));
      } else {
        console.log(xhr.readyState, xhr.status);
      }
    };
    xhr.send();
  };

结果如下图所示,第一个请求为正常响应的xhr请求,第二个请求为调用中执行了abort()的请求(为了方便测试,作者对接口增加了3s的响应延时)。

image.png

通过打印状态码可知正常请求经历了完整的 HEADERS_RECEIVEDLOADING状态,并且http状态为200;abort请求则直接进入DONE状态,http状态码为0。 image.png

Fetch

诞生

2015年,随着ES6的正式发布,Promise这一前端异步编程神器横空出世。

一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知的值。它让您能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

同年Google也正式在Chrome上推出了符合Promise规范的Fetch API,并最终作为了ECMAScript标准特性。

兼容性

image.png

可以看到所有”现代“浏览器对fetch都是支持的。

与XHR区别

Fetch API与旧的XMLHttpRequest相比,Fetch更容易发出web请求和处理响应,后者通常需要额外的逻辑(例如:用于处理重定向)。
Fetch 提供了对 Request 和 Response (以及其他与网络请求有关的)对象的通用定义。使之今后可以被使用到更多地应用场景中:无论是 service worker、Cache API、又或者是其他处理请求和响应的方式,甚至是任何一种需要你自己在程序中生成响应的方式。

使用

fetch 与 XMLHttpRequest 主要有两点行为方式的不同:

  1. 当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject,  即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
  2. fetch 不会发送 cookies。除非你使用了credentials 的初始化选项。(自 2017 年 8 月 25 日以后,默认的 credentials 政策变更为 same-origin。Firefox 也在 61.0b13 版本中进行了修改)

fetch的基本语法为:

Promise<Response> fetch(input[, init]);

一个基本的 fetch 请求设置起来很简单:

fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

上面代码发起http协议获取一个 JSON 文件并将其打印到控制台。fetch() 只写入一个参数:资源url地址,然后fetch()返回一个包含响应结果的promise(一个 Response 对象)。

fetch() 接受第二个可选参数,一个init对象:

  • method: 请求使用的方法,如 GET、POST
  • headers: 请求的头信息。
  • body: 请求的 body 信息:可能是一个 BlobBufferSource 、FormDataURLSearchParams]或者 USVString 对象。
  • mode: 请求的模式,如 cors、no-cors 或者same-origin
  • credentials: 请求的 credentials,如 omit、``same-origin 或者 include。为了在当前域名内自动发送 cookie ,必须提供这个选项。
  • cache:  请求的 cache 模式: default、 no-store、 reload 、 no-cache 、 force-cache 或者 only-if-cached 。
  • redirect: 可用的 redirect 模式: follow (自动重定向), error (如果产生重定向将自动终止并且抛出一个错误), 或者 manual (手动处理重定向)。
  • referrer: 一个 USVString 可以是 no-referrer、``client或一个 URL。默认是 client。
  • referrerPolicy: 指定了HTTP头部referer字段的值。可能为以下值之一: no-referrer、 no-referrer-when-downgrade、 origin、 origin-when-cross-origin、 unsafe-url
  • integrity: 包括请求的  subresource integrity值( 例如:sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=。

fetch 的 abort

这是跟XMLHttpRequest相比是稍显不足的地方,目前fetch还未真正支持abort方法,只是提供实验性的AbortControllerAbortSignal接口以abort Fetch请求

浏览器已经开始为 AbortController 和 AbortSignal 接口(也就是Abort API)添加实验性支持,允许像 Fetch 和 XHR 这样的操作在还未完成时被终止 。

image.png

通过兼容性测试可以发现,国产浏览器对齐兼容性要差些,国外较新版本的主流浏览器兼容性都是OK的。所以,如果能确保兼容指定浏览器的话,AbortControllerAbortSignal接口现阶段还是可以使用的。其使用方法也很简单。

来看一个例子:

  const fetchRef = useRef<AbortController>();

  const sendFetchRequest = () => {
    const controller = new AbortController();
    const signal = controller.signal;
    fetchRef.current = controller;

    fetch(url, { signal })
      .then(function (res) {
        return res.json();
      })
      .then(function (data) {
        setUser(data ?? []);
      })
      .catch(function (e) {
        console.error('Download error: ' + e.message);
      });
  };
  const abortFetchRequest = () => {
    console.log('Fetch aborted');
    fetchRef.current?.abort();
  };

首先通过AbortController() 构造函数来创建一个controller实例, 然后通过AbortController.signal 属性获取到它的关联对象AbortSignal的引用.

当 fetch request 初始化后, 将 AbortSignal 作为一个选项传入请求的选项参数中 (上面代码的 {signal}). 这将signal、controller与fetch请求关联起来, 允许我们通过调用AbortController.abort()来取消fetch请求, 这样就可以用abortBtn的事件监听abort已经发出的fetch请求。

执行结果:

image.png

第一个请求是正常响应的请求,第二个是调用AbortController.abort()的请求,打印出错误信息:

image.png

提示

  • abort() 被调用,  fetch() promise 将会抛出一个AbortError对象.
  • 执行abort后的AbortController将一直处于abort态,此时需要重新创建AbortController,否则被abort的fetch请求将一直无法执行,强行执行会报:Failed to execute 'fetch' on 'Window': The user aborted a request.异常

总结

  • XMLHttpRequest提供了abort方法,只需要拿到xhrInstance就可以很方便的终止xhr请求;
  • Fetch目前只提供了实验性的AbortControllerAbortSignal接口来终止发出的请求,如果浏览器支持该实验性的特性话,使用起来还是没问题的,那如果不支持呢? 解决方案很简单粗暴:丢弃”过时“的响应即可,具体方法可以参见作者下一篇文章。
  • Promise是有限状态机,状态的流转是不可逆的,基于Promise+规范的Fecth API能终止的是它自身的请求过程,而不是终止Promise,事实上Fecth请求终止后返回的仍是一个 处于rejectedPromise

参考