Web 开发中如何终止一个 Http 请求

在前端开发中,终止一个已经开始的网路请求是有一定应用场景的。尤其是在 SPA (Signal Page Web Application) 单页面 Web 应用中,路由的跳转或场景的切换并不会像多页面跳转一样终止之前未被响应的网络请求。此时就可能会发生数据请求发生时的场景已经不存在,而网络请求成功后,数据被不正确的渲染在新的场景中的问题。因此应确保这些网络请求在场景变化后被终止或不被处理,以避免数据的混乱,提升部分性能。

本文介绍如何终止一个 XHR、Fetch 或 Axios 请求。(Axios 是一个基于 XHR 和 Promise 封装的 HTTP 库,可用于 Web 和 Node.js,如果你不使用它,则不必关心 Axios 的部分。)

本文的完整 Demo 代码见于 GitHub: hooozen/web-demo

终止一个 XHR 请求

XMLHttpRequest (本文中 XHR 为其简写)被浏览器广泛支持,功能完整,稳定性强,在 Ajax 中大量使用。

XHR 终止一个请求的操作是非常简单的,只需要调用该 XHR 实例对象的 abort 方法即可阻断当前未被响应的请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 发送 XHR 请求
*/
function sendXHRRequest() {
xhr.open('POST', url);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('XHR: ', xhr.responseText);
}
}
xhr.send();
}

/**
* 终止 XHR 请求
*/
function abortXHRRequest() {
console.log('尝试终止 XHR 请求');
xhr.abort();
}

XMLHttpRequest 代理有 4 种状态,XHR 的当前状态由 readyState 属性记录。XHR 总处于下列状态之一:

状态 描述
0 UNSET 代理被创建,但尚未调用 open() 方法。
1 OPENED open() 方法已经被调用
2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。
3 LOADING 下载中,responseText 属性已经包含部分数据。
4 DONE 下载操作已完成。

当调用 abort() 方法时,readyState 会被置为 DONE,然后该请求被终止,状态变为 UNSET 状态。因此,在调用 abort() 方法后,再次调用 XHR 发送请求,仍然可以正常响应(除非再次调用了 abort() 方法终止了请求),这一点和 Fetch API 终止请求是不一样的。

终止一个 Fetch 请求

Fetch 请求返回一个 Promise 对象,而 Promise 有一个缺点就是一旦被“许诺”,要么被 resolve 要么被 reject,不可被终止。所以一开始 Fetch 是不支持被终止的,所以开发者们不得不使用其他的方法来达到“终止”的效果。后来新的标准增加了 AbortController 接口,来终止 DOM 请求,但该接口目前仍处于“实验中”,不建议在生产中使用。

接下来分别介绍这两种方法:

使用 AbortController 终止 Fetch 请求

AbortController 是一个实验中的标准接口,提供 abort 方法来终止指定的 DOM 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 开始 fetch 请求
*/
function startFetch() {
var options = {
method: 'POST',
signal,
}
fetch(url, options).then(function (res) {
res.text().then(function (text) {
console.log('Fetch: ', text);
});
}).catch(function (error) {
console.log(error);
});
}

/**
* 终止 fetch 请求
*/
function abortFetch() {
console.log('尝试终止 fetch 请求');
controller.abort();
}

AbortController 实例化对象提供一个 signal 标记,当创建 Fetch 请求时,把该标记作为参数传入 Fetch 方法,则可以调用该实例化对象的 abort() 方法来终止被“标记”的 Fetch 请求。

与 XHR 的 abort() 方法不同的是,AbortController.abort() 方法一旦被调用,则被标记的 Fetch 不可再次发起请求,这一点和 Axios 中终止请求的机制是一致的。

使用 Promise.race() 封装一个可“终止”的 Fetch

因为 Promise 不能被终止,而 AbortController 接口稳定性和兼容性又很差,所以更多情况下需要采取其他方法来模拟“终止”Fetch请求。使用 Promise.race() 方法在需要时跳过对 Fetch 请求结果的处理来达到“终止”请求的效果。

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。)

利用 Promise.race 的“竞速”特性,封装一个可以被“终止”的 _Fetch 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 封装一个模拟 abort 的 fetch
* @param {接口地址} url
* @param {fetch 选项} options
*/
function _fetch(url, options) {
var abort = null;
var abortPromise = new Promise(function(resolve, reject) {
abort = reject;
});
var fetchPromise = fetch(url, options);
var promise = Promise.race([abortPromise, fetchPromise]);
promise.abort = abort;
return promise;
}

var fetchAPI;

/**
* 开始 fetch 请求
*/
function _startFetch() {
fetchAPI = _fetch(url, { method: 'POST' });
fetchAPI.then(function(res) {
res.text().then(function(text) {
console.log('_fetch: ', text);
});
}).catch(function(e) {
console.log(e);
});
}

/**
* 跳过请求处理
*/
function _abortFetch() {
console.log('"终止"_fetch请求');
fetchAPI.abort('abort 竞速成功');
}

上面代码封装了一个 _fetch 方法来进行网络请求,在_fetch().then() 中对网络请求进行处理。_fetch 方法返回了一个 Promise.race() Promise,并在该 race() 中使用了一个 abortPromise Promse 来与 fetch Promise“竞速”。并将 abortPromisereject 方法暴露出来。所以当调用被暴露出的 reject 方法时,如果此时 fetch 请求还没有完成(即没有被 resolve,或 reject),那么由于 Promise.race() 的“竞速”特性,_fetch 将返回 abortPromisereject,fetch Promise 将被忽略。

需要明确的是,上述方法并没有真正意义上终止一个网络请求,实际上网络请求没有受到任何影响,只不过通过封装的方法“跳过”了对希望被终止的请求的处理。这一点是和 XHR 和 AbortController 的 abort() 方法不同的。

终止一个 Axios 请求

Axios 提供了终止网络请求的方法,具体介绍可见官方文档中的 cancellation 部分,下面提供一个例子并对文档中未介绍的内容进行一些补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var CancelToken = axios.CancelToken;
var source = CancelToken.source();

/**
* 开始一个 Axios Post 请求
*/
function axiosPost() {
axios.post(url, undefined, {
cancelToken: source.token,
}).then(function(res) {
console.log('Axios: ', res.data);
}).catch(e => {
console.log('Axios error: ', e);
});
}

/**
* 终止一个 Axios 请求
*/
function abortAxios() {
console.log('尝试终止 axios 请求');
source.cancel('axios 被终止');
}

Axios 的 cancel() 方法实现的效果类似于 AbortController 接口的 abort() 方法,即一旦该方法被调用,则该实例就不再被允许再次发送网络请求。但 Axios 网络请求是基于 XHR 实现的,终止网络请求的方法也是调用了 XHR 的 abort() 方法。之所以其效果和 XHR.abort() 终止后仍可再次请求的效果不一致,是因为 Axios 基于 Promise 进行了封装,一旦调用了 cancel() 方法,Promise 就会抛出异常,并且被记录。下次再次调用该实例进行请求时,会直接抛出异常,不会发起网络请求。具体实现方法请查阅 Axios 源码