前端領(lǐng)域如何實(shí)現(xiàn)請(qǐng)求中斷
幾乎在所有面向用戶或企業(yè)的應(yīng)用程序中,所呈現(xiàn)出來的信息都不是一成不變的,即數(shù)據(jù)都是動(dòng)態(tài)的,由某個(gè)或者多個(gè)后臺(tái)服務(wù)所提供。那么就不可避免地會(huì)涉及到網(wǎng)絡(luò)請(qǐng)求,而對(duì)于不同企業(yè)肯定有不同的業(yè)務(wù)場(chǎng)景。
在一個(gè)功能完善的應(yīng)用程序呈現(xiàn)給用戶之前,前后端開發(fā)人員必須先根據(jù)產(chǎn)品經(jīng)理提供的業(yè)務(wù)需求文檔協(xié)商建立起格式良好的接口契約,然后再經(jīng)過開發(fā)聯(lián)調(diào)測(cè)試驗(yàn)證部署上線等一系列流程之后才具有可用性,才能展現(xiàn)在用戶面前供用戶使用。
但是可能并不是在任何場(chǎng)景下,我們都需要關(guān)心網(wǎng)絡(luò)請(qǐng)求的響應(yīng)結(jié)果,或者說在某些場(chǎng)景下,我們只需要關(guān)心最新的有效的網(wǎng)絡(luò)請(qǐng)求,對(duì)于老舊的失效的網(wǎng)絡(luò)請(qǐng)求,我們甚至可以忽略它的存在。
我們知道,從瀏覽器發(fā)起一次網(wǎng)絡(luò)請(qǐng)求,到建立TCP鏈接(對(duì)于HTTPS協(xié)議還需要建立額外的TLS連接)以及DNS域名解析,再到發(fā)送請(qǐng)求數(shù)據(jù)報(bào)文,最終服務(wù)器處理請(qǐng)求并響應(yīng)數(shù)據(jù),期間會(huì)不停占用客戶端和服務(wù)器資源。
如果該網(wǎng)絡(luò)請(qǐng)求對(duì)于我們而言已經(jīng)無效,那么我們就可以通過手動(dòng)中斷請(qǐng)求,來提前釋放被占用的資源,減少不必要的資源開銷。
例如考慮以下場(chǎng)景:
- 在Vue或React單頁應(yīng)用中,組件A掛載完畢之后向后臺(tái)服務(wù)發(fā)起請(qǐng)求拉取數(shù)據(jù),但是由于加載過慢,用戶可能期間發(fā)生路由跳轉(zhuǎn)或回退,導(dǎo)致組件A卸載,但是組件內(nèi)部的網(wǎng)絡(luò)請(qǐng)求并沒有立即停止下來,此時(shí)的響應(yīng)數(shù)據(jù)對(duì)于已卸載的組件A而言已經(jīng)無效。若剛好此時(shí)請(qǐng)求響應(yīng)錯(cuò)誤,就可能導(dǎo)致前端實(shí)現(xiàn)的兜底彈窗出現(xiàn)在跳轉(zhuǎn)后的頁面中,造成視覺干擾;
- 頁面存在定時(shí)輪詢業(yè)務(wù),即固定間隔一段時(shí)間再次發(fā)起請(qǐng)求,這樣就可能存在多個(gè)請(qǐng)求間的競(jìng)爭(zhēng)關(guān)系,如果上一個(gè)請(qǐng)求的響應(yīng)速度比最近一次請(qǐng)求的響應(yīng)速度慢,則前者就會(huì)覆蓋后者,從而導(dǎo)致數(shù)據(jù)錯(cuò)亂;
- 類似于關(guān)鍵字搜索或模糊查詢等需要頻繁發(fā)起網(wǎng)絡(luò)請(qǐng)求的相關(guān)業(yè)務(wù),可能在一定程度上為了優(yōu)化程序的執(zhí)行性能,減少冗余的網(wǎng)絡(luò)IO,我們會(huì)使用防抖(debounce)函數(shù)來對(duì)請(qǐng)求邏輯進(jìn)行包裝,減少查詢次數(shù)以降低服務(wù)器壓力,但是依舊避免不了由于加載耗時(shí)過長(zhǎng)導(dǎo)致新老請(qǐng)求數(shù)據(jù)錯(cuò)亂的問題;
- 針對(duì)前端大文件上傳等上傳服務(wù),需要實(shí)現(xiàn)上傳進(jìn)度的暫停與恢復(fù),即斷點(diǎn)續(xù)傳。
還有很多其他沒有列出的應(yīng)用場(chǎng)景,針對(duì)每種應(yīng)用場(chǎng)景,雖然我們都能給出對(duì)應(yīng)的方案來解決實(shí)際問題,但是筆者認(rèn)為最理想的方案還是盡量減少無用請(qǐng)求,減少客戶端和服務(wù)器之間的無效傳輸,鑒于此也就引入了本文中將要講到的中斷請(qǐng)求的方式。
在前端領(lǐng)域,個(gè)人覺得有幾種比較常見的網(wǎng)絡(luò)請(qǐng)求方案:瀏覽器原生支持的XMLHttpRequest對(duì)象,同時(shí)兼容瀏覽器端和NodeJS服務(wù)端的第三方HTTP庫(kù)Axios和大部分瀏覽器最新實(shí)現(xiàn)的Fetch API。本文主要基于以上三種請(qǐng)求方案講解一下各自中斷請(qǐng)求的方式,文中若有錯(cuò)誤,還請(qǐng)指正。
1、XMLHttpRequest
瀏覽器原生實(shí)現(xiàn)的XMLHttpRequest(以下簡(jiǎn)稱XHR)構(gòu)造函數(shù)對(duì)于我們來說已經(jīng)是再熟悉不過了,但是在實(shí)際應(yīng)用中,大部分場(chǎng)景下可能我們并不需要去主動(dòng)實(shí)例化XHR構(gòu)造函數(shù),畢竟實(shí)例化之后還需要通過調(diào)用open和send等一系列的官方API才能實(shí)現(xiàn)與服務(wù)器的數(shù)據(jù)交互,操作細(xì)節(jié)稍微繁瑣。
相反我們一般會(huì)推薦使用社區(qū)實(shí)現(xiàn)的第三方庫(kù)來方便我們簡(jiǎn)化操作流程,提升開發(fā)效率,例如下一節(jié)將要講述的Axios。但即便是Axios,在瀏覽器端其底層依舊是通過XHR構(gòu)造函數(shù)來實(shí)現(xiàn)網(wǎng)絡(luò)IO的,因此這一小節(jié)有必要對(duì)XHR的相關(guān)知識(shí)點(diǎn)進(jìn)行回顧和講解。
首先拋出一個(gè)基礎(chǔ)示例:
/**
* @description: 基于 XHR 封裝的網(wǎng)絡(luò)請(qǐng)求工具函數(shù)
* @param {String} url 請(qǐng)求接口地址
* @param {Document | XMLHttpRequestBodyInit | null} body 請(qǐng)求體
* @param {Object} requestHeader 請(qǐng)求頭
* @param {String} method 請(qǐng)求方法
* @param {String} responseType 設(shè)置響應(yīng)內(nèi)容的解析格式
* @param {Boolean} async 請(qǐng)求是否異步
* @param {Number} timeout 設(shè)置請(qǐng)求超時(shí)時(shí)間(單位:毫秒)
* @param {Boolean} withCredentials 設(shè)置跨域請(qǐng)求是否允許攜帶 cookies 或 Authorization header 等授權(quán)信息
* @return {Promise} 可包含響應(yīng)內(nèi)容的 Promise 實(shí)例
*/
function request({
url,
body = null,
requestHeader = {'Content-Type': 'application/x-www-form-urlencoded'},
method = 'GET',
responseType = 'text',
async = true,
timeout = 30000,
withCredentials = false,
} = {}) {
return new Promise((resolve, reject) => {
if (!url) {
return reject(new TypeError('the required parameter [url] is missing.'));
}
if (method.toLowerCase() === 'get' && body) {
url += `?${request.serialize(body)}`;
body = null;
}
const xhr = new XMLHttpRequest();
xhr.open(method, url, async);
if (async) {
xhr.responseType = responseType;
xhr.timeout = timeout;
}
xhr.withCredentials = withCredentials;
if (requestHeader && typeof requestHeader === 'object') {
Object.keys(requestHeader).forEach(key => xhr.setRequestHeader(key, requestHeader[key]));
}
xhr.onreadystatechange = function onReadyStateChange() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
resolve(xhr.response);
}
}
};
xhr.onerror = function onError(error) {
console.log(error);
reject({ message: '請(qǐng)求出錯(cuò),請(qǐng)稍后重試' });
};
xhr.ontimeout = function onTimeout() {
reject({ message: '接口超時(shí),請(qǐng)稍后重試' });
};
xhr.send(body ? JSON.stringify(body) : null);
});
}
以上示例對(duì)XHR請(qǐng)求操作流程進(jìn)行了一下簡(jiǎn)單的封裝,并未涉及到太多的細(xì)節(jié)和兼容處理。一個(gè)簡(jiǎn)單的調(diào)用方式如下:
request({
url: 'http://www.some-domain.com/path/to/example',
method: 'POST',
requestHeader: {'Content-Type': 'application/json; charset=UTF-8'},
body: {key: value}
}).then(response => console.log(response));
基于以上操作便完成了一次客戶端和服務(wù)器的數(shù)據(jù)交互請(qǐng)求,接下來在此基礎(chǔ)上繼續(xù)完善請(qǐng)求中斷的相關(guān)邏輯。
我們知道,在XHR實(shí)例上為我們提供了一個(gè)abort方法用于終止該請(qǐng)求,并且當(dāng)一個(gè)請(qǐng)求被終止的時(shí)候,該請(qǐng)求所對(duì)應(yīng)的XHR實(shí)例的readyState屬性將會(huì)被設(shè)置為XMLHttpRequest.UNSET(0),同時(shí)status屬性會(huì)被重置為0,因此在本示例中我們同樣使用abort方法來實(shí)現(xiàn)請(qǐng)求中斷。
// 參考以上示例
function request
// 省略入?yún)?br> ...
} = {}) {
return new Promise((resolve, reject) => {
// 省略代碼
...
});
}
// 存儲(chǔ)請(qǐng)求接口地址以及請(qǐng)求體和 XHR 實(shí)例的映射關(guān)系
request.cache = {};
/**
* @description: 根據(jù)提供的鍵名中斷對(duì)應(yīng)的請(qǐng)求
* @param {String} key 存儲(chǔ)在 request.cache 屬性中的鍵名,若未提供則中斷全部請(qǐng)求
* @return {void}
*/
request.clearCache = (key) => {
if (key) {
const instance = request.cache[key];
if (instance) {
instance.abort();
delete request.cache[key];
}
return;
}
Object.keys(request.cache).forEach(cacheKey => {
const instance = request.cache[cacheKey];
instance.abort();
delete request.cache[cacheKey];
});
};
在以上示例中,我們通過request.cache來臨時(shí)存儲(chǔ)請(qǐng)求接口地址以及請(qǐng)求體和XHR實(shí)例的映射關(guān)系,因?yàn)樵谕豁撁嬷幸话憧赡軙?huì)涉及到多個(gè)接口地址不同的請(qǐng)求,或者同一個(gè)請(qǐng)求對(duì)應(yīng)不同的請(qǐng)求體,因此這里考慮加上了請(qǐng)求體以做區(qū)分。當(dāng)然為了作為request.cache中的唯一鍵名,我們還需要對(duì)請(qǐng)求體進(jìn)行序列化操作,因此簡(jiǎn)單封裝一個(gè)序列化工具函數(shù)。
/**
* @description: 將請(qǐng)求體序列化為字符串
* @param {Document | XMLHttpRequestBodyInit | null} data 請(qǐng)求體
* @return {String} 序列化后的字符串
*/
request.serialize = (data) => {
if (data && typeof data === 'object') {
const result = [];
Object.keys(data).forEach(key => {
result.push(`${key}=${JSON.stringify(data[key])}`);
});
return result.join('&');
}
return data;
}
完成以上的基礎(chǔ)代碼之后,接下來我們將其應(yīng)用到request函數(shù)中:
function request({
url,
body = null,
// 省略部分入?yún)?br> ...
} = {}) {
return new Promise((resolve, reject) => {
if (!url) {
return reject(new TypeError('the required parameter [url] is missing.'));
}
// 省略部分代碼
...
const xhr = new XMLHttpRequest();
// 將請(qǐng)求接口地址以及請(qǐng)求體和 XHR 實(shí)例存入 cache 中
let cacheKey = url;
if (body) {
cacheKey += `_${request.serialize(body)}`;
}
// 每次發(fā)送請(qǐng)求之前將上一個(gè)未完成的相同請(qǐng)求進(jìn)行中斷
request.cache[cacheKey] && request.clearCache(cacheKey);
request.cache[cacheKey] = xhr;
// 省略部分代碼
...
xhr.onreadystatechange = function onReadyStateChange() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
// 請(qǐng)求完成之后清除緩存
request.clearCache(cacheKey);
resolve(xhr.response);
}
}
};
xhr.onerror = function onError(error) {
console.log(error);
// 請(qǐng)求報(bào)錯(cuò)之后清除緩存
request.clearCache(cacheKey);
reject({ message: '請(qǐng)求出錯(cuò),請(qǐng)稍后重試' });
};
xhr.ontimeout = function onTimeout() {
// 請(qǐng)求超時(shí)之后清除緩存
request.clearCache(cacheKey);
reject({ message: '接口超時(shí),請(qǐng)稍后重試' });
};
xhr.send(body ? JSON.stringify(body) : null);
});
}
這樣便簡(jiǎn)單實(shí)現(xiàn)了一個(gè)自包含的請(qǐng)求中斷的處理邏輯,每次發(fā)送請(qǐng)求之前自動(dòng)判定未完成的多余請(qǐng)求并將其清除,從而避免性能上的開銷。
當(dāng)然,不僅如此,這里同樣可以通過request.clearCache函數(shù)來在組件卸載或路由跳轉(zhuǎn)的時(shí)候手動(dòng)清除未完成的請(qǐng)求,因?yàn)檫@部分請(qǐng)求對(duì)于卸載后的組件而言沒有太多實(shí)質(zhì)意義,例如以下示例:
// 網(wǎng)頁卸載前清除緩存
window.addEventListener('beforeunload', () => request.clearCache(), false);
// Vue 中路由跳轉(zhuǎn)前清除緩存
router.beforeEach((to, from, next) => { request.clearCache(); next(); });
// React 中路由跳轉(zhuǎn)時(shí)清除緩存
import { Component } from 'react';
import { withRouter } from 'react-router-dom';
class App extends Component {
componentDidMount() {
// 監(jiān)聽路由變化
this.props.history.listen(location => {
// 通過比較 location.pathname 來判定路由是否發(fā)生變化
if (this.props.location.pathname !== location.pathname) {
// 若路由發(fā)生變化,則清除緩存
request.clearCache();
}
});
}
}
export default withRouter(App);
2、Axios
Axios想必是我們使用最多的一個(gè)第三方開源免費(fèi)的HTTP庫(kù),其本身基于Promise的特性使得我們可以很方便地寫出更加優(yōu)雅且易維護(hù)的代碼,從而避免函數(shù)多層嵌套所帶來的一系列問題。
當(dāng)然,它最大的特點(diǎn)在于可以同時(shí)兼容瀏覽器端和NodeJS服務(wù)端。底層通過判定不同的運(yùn)行環(huán)境來自動(dòng)提供不同的適配器,在瀏覽器端通過原生的XHR對(duì)象來發(fā)送請(qǐng)求,而在NodeJS服務(wù)端則通過內(nèi)置的http模塊來發(fā)送請(qǐng)求。
不僅如此,在其底層的Promise管道鏈中還為我們暴露了稱之為攔截器的入口,使得我們可以參與到一個(gè)請(qǐng)求的生命周期中,在請(qǐng)求發(fā)送之前和響應(yīng)接收之后能夠自定義實(shí)現(xiàn)數(shù)據(jù)的裝配和轉(zhuǎn)換操作。帶來的如此之多的人性化操作,使得我們沒有理由不去用它,這也奠定了其長(zhǎng)久以來依舊如此火爆的基礎(chǔ)。
言歸正傳,在Axios中同樣為我們提供了請(qǐng)求中斷的相關(guān)API。首先拋出一個(gè)基礎(chǔ)示例:
// 安裝 axios
npm install --save axios
// 導(dǎo)入 axios
import axios from 'axios';
// 創(chuàng)建 axios 實(shí)例
const instance = axios.create({
baseURL: 'https://www.some-domain.com/path/to/example',
timeout: 30000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
// 設(shè)置 axios 實(shí)例默認(rèn)配置
instance.defaults.headers.common['Authorization'] = '';
instance.defaults.headers.post['Content-Type'] = 'application/json; charset=UTF-8';
// 自定義請(qǐng)求攔截器
instance.interceptors.request.use(config => {
const token = window.localStorage.getItem('token');
token && (config.headers['Authorization'] = token);
return config;
}, error => Promise.reject(error));
// 自定義響應(yīng)攔截器
instance.interceptors.response.use(response => {
if (response.status === 200) {
return Promise.resolve(response.data);
}
return Promise.reject(response);
}, error => Promise.reject(error));
接下來我們結(jié)合Axios提供的CancelToken構(gòu)造函數(shù)來創(chuàng)建一個(gè)簡(jiǎn)單的post請(qǐng)求:
const CancelToken = axios.CancelToken;
let cancel;
instance.post('/api/user/123', {
name: 'new name',
phone: 'new phone',
}, {
// CancelToken 構(gòu)造函數(shù)接收一個(gè) executor 函數(shù)參數(shù),并且該函數(shù)接收一個(gè)取消函數(shù) c 用于取消該次請(qǐng)求
cancelToken: new CancelToken(function executor(c) {
// 將取消函數(shù)賦值到外部變量,方便從外部取消請(qǐng)求
cancel = c;
}),
});
// 手動(dòng)取消請(qǐng)求
cancel();
針對(duì)需要同時(shí)取消多個(gè)請(qǐng)求以及自動(dòng)取消的應(yīng)用場(chǎng)景,上面的示例顯然不能滿足我們的需求。這里我們同樣可以利用上一小節(jié)的思路來維護(hù)一個(gè)請(qǐng)求接口地址以及請(qǐng)求體和取消函數(shù)c之間的映射關(guān)系。
同時(shí)為了避免在每個(gè)請(qǐng)求中都需要手動(dòng)去實(shí)例化CancelToken,我們可以巧妙利用request攔截器來整合這部分的邏輯,實(shí)現(xiàn)邏輯復(fù)用。首先我們將緩存邏輯拆分到一個(gè)單獨(dú)的文件中:
// cacheUtils.js
export const CacheUtils = {
// 存儲(chǔ)請(qǐng)求接口地址以及請(qǐng)求體和取消函數(shù)之間的映射關(guān)系
cache: {},
// 根據(jù)提供的鍵名 key 取消對(duì)應(yīng)的請(qǐng)求,若未提供則取消全部請(qǐng)求
clearCache: function (key) {
if (key) {
const cancel = this.cache[key];
if (cancel && typeof cancel === 'function') {
cancel();
delete this.cache[key];
}
return;
}
Object.keys(this.cache).forEach(cacheKey => {
const cancel = this.cache[cacheKey];
cancel();
delete this.cache[cacheKey];
});
},
};
接下來我們將其應(yīng)用到請(qǐng)求攔截器和響應(yīng)攔截器中:
import qs from 'qs';
import { CacheUtils } from './cacheUtils.js';
// 自定義請(qǐng)求攔截器
instance.interceptors.request.use(config => {
let cacheKey = config.url;
const token = window.localStorage.getItem('token');
token && (config.headers['Authorization'] = token);
const method = config.method.toLowerCase();
if (method === 'get' && config.params && typeof config.params === 'object') {
cacheKey += qs.stringify(config.params, { addQueryPrefix: true });
}
if (['post', 'put', 'patch'].includes(method) && config.data && typeof config.data === 'object') {
config.data = qs.stringify(config.data);
cacheKey += `_${qs.stringify(config.data, { arrayFormat: 'brackets' })}`;
}
// 每次發(fā)送請(qǐng)求之前將上一個(gè)未完成的相同請(qǐng)求進(jìn)行中斷
CacheUtils.cache[cacheKey] && CacheUtils.clearCache(cacheKey);
// 將當(dāng)前請(qǐng)求所對(duì)應(yīng)的取消函數(shù)存入緩存
config.cancelToken = new axios.CancelToken(function executor(c) {
CacheUtils.cache[cacheKey] = c;
});
// 臨時(shí)保存 cacheKey,用于在響應(yīng)攔截器中清除緩存
config.cacheKey = cacheKey;
return config;
}, error => Promise.reject(error));
// 自定義響應(yīng)攔截器
instance.interceptors.response.use(response => {
// 響應(yīng)接收之后清除緩存
const cacheKey = response.config.cacheKey;
delete CacheUtils.cache[cacheKey];
if (response.status === 200) {
return Promise.resolve(response.data);
}
return Promise.reject(response);
}, error => {
// 響應(yīng)異常清除緩存
if (error.config) {
const cacheKey = error.config.cacheKey;
delete CacheUtils.cache[cacheKey];
}
return Promise.reject(error);
});
這里我們同樣提供CacheUtils.clearCache函數(shù)來應(yīng)對(duì)需要手動(dòng)清除未完成請(qǐng)求的應(yīng)用場(chǎng)景,使用方式與上一小節(jié)思路相同,這里就不再重復(fù)多講。
3、Fetch API
作為瀏覽器原生提供的XHR構(gòu)造函數(shù)的理想替代方案,新增的Fetch API為我們提供了Request和Response(以及其他與網(wǎng)絡(luò)請(qǐng)求有關(guān)的)對(duì)象的通用定義,一個(gè)Request對(duì)象表示一個(gè)資源請(qǐng)求,通常包含一些初始數(shù)據(jù)和正文內(nèi)容,例如資源請(qǐng)求路徑、請(qǐng)求方式、請(qǐng)求主體等,而一個(gè)Response對(duì)象則表示對(duì)一次請(qǐng)求的響應(yīng)數(shù)據(jù)。
同時(shí)Fetch API還為我們提供了一個(gè)全局的fetch方法,通過該方法我們可以更加簡(jiǎn)單合理地跨網(wǎng)絡(luò)異步獲取資源。fetch方法不僅原生支持Promise的鏈?zhǔn)讲僮鳎瑫r(shí)還支持直接傳入Request對(duì)象來發(fā)送請(qǐng)求,增加了很強(qiáng)的靈活性。
到目前為止,F(xiàn)etch API的支持程度如下圖:
不難看出IE瀏覽器下的兼容性不容樂觀,但是作為一名有追求的前端開發(fā)人員,當(dāng)然不會(huì)止步于此。一番探索之后,發(fā)現(xiàn)可以通過isomorphic-fetch或者whatwg-fetch這兩個(gè)第三方依賴來解決兼容性問題:
// 安裝依賴
npm install --save whatwg-fetch
// 引入依賴
import {fetch as fetchPolyfill} from 'whatwg-fetch';
接下來同樣先拋出一個(gè)基礎(chǔ)示例:
const url = 'http://www.some-domain.com/path/to/example';
const initData = {
method: 'POST',
body: JSON.stringify({key: value}),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
cache: 'no-cache',
credentials: 'same-origin',
mode: 'cors',
redirect: 'follow',
referrer: 'no-referrer',
};
fetch(url, initData).then(response => response.json()).then(data => console.log(data));
// 也可以直接通過 Request 構(gòu)造函數(shù)來初始化請(qǐng)求數(shù)據(jù)
// Request 構(gòu)造函數(shù)接收兩個(gè)參數(shù)
// 第一個(gè)參數(shù)表示需要獲取的資源 URL 路徑或者另一個(gè)嵌套的 Request 實(shí)例
// 第二個(gè)可選參數(shù)表示需要被包含到請(qǐng)求中的各種自定義選項(xiàng)
const request = new Request(url, initData);
fetch(request).then(response => response.json()).then(data => console.log(data));
可以看到,相比于傳統(tǒng)的XHR方式而言,fetch函數(shù)的使用方式更加簡(jiǎn)潔友好,易用性更強(qiáng),同時(shí)還為我們提供了多種入?yún)⒌男问绞沟贸绦蚬δ茏兊酶拥撵`活可擴(kuò)展。
那么回到本文的主題,上文中提到,在XHR實(shí)例中可以通過abort方法來取消請(qǐng)求,在Axios中可以通過CancelToken構(gòu)造函數(shù)的參數(shù)來獲得取消函數(shù),從而通過取消函數(shù)來取消請(qǐng)求。
但是很遺憾的是,在Fetch API中,并沒有自帶的取消請(qǐng)求的API供我們調(diào)用。不過令人愉悅的是,除了IE瀏覽器外,其他瀏覽器已經(jīng)為Abort API添加了實(shí)驗(yàn)性支持,Abort API允許對(duì)XHR和fetch這樣的請(qǐng)求操作在未完成時(shí)進(jìn)行終止,那么接下來對(duì)Abort API做一下簡(jiǎn)要的介紹。
在Abort API的相關(guān)概念中主要包含了AbortController和AbortSignal兩大接口:
- AbortController:表示一個(gè)控制器對(duì)象,該對(duì)象擁有一個(gè)只讀屬性signal和一個(gè)方法abort。signal屬性表示一個(gè)AbortSignal實(shí)例,當(dāng)我們需要取消某一個(gè)請(qǐng)求時(shí),需要將該signal屬性所對(duì)應(yīng)的AbortSignal實(shí)例與請(qǐng)求進(jìn)行關(guān)聯(lián),然后通過控制器對(duì)象提供的abort方法來取消請(qǐng)求;
- AbortSignal:表示一個(gè)信號(hào)對(duì)象,作為控制器對(duì)象和請(qǐng)求之間通信的橋梁,允許我們通過控制器對(duì)象來對(duì)請(qǐng)求進(jìn)行取消操作。該對(duì)象擁有一個(gè)只讀屬性aborted和一個(gè)方法onabort,aborted屬性體現(xiàn)為一個(gè)布爾值,表示與之通信的請(qǐng)求是否已經(jīng)被終止,而onabort方法會(huì)在控制器對(duì)象終止該請(qǐng)求時(shí)調(diào)用。
通過以上兩個(gè)接口,我們嘗試封裝一個(gè)簡(jiǎn)單加強(qiáng)版的可取消的fetch工具函數(shù):
const abortableFetch = (url, initData) => {
// 實(shí)例化控制器對(duì)象
const abortController = new AbortController();
// 獲取信號(hào)對(duì)象
const signal = abortController.signal;
return {
// 注意這里需要將 signal 信號(hào)對(duì)象與請(qǐng)求進(jìn)行關(guān)聯(lián),關(guān)聯(lián)之后才能通過 abortController.abort 方法取消請(qǐng)求
ready: fetch(url, {...initData, signal}).then(response => response.json()),
// 暴露 cancel 方法,用于在外層手動(dòng)取消請(qǐng)求
cancel: () => abortController.abort(),
};
};
并將其應(yīng)用到之前的基礎(chǔ)示例中:
const url = 'http://www.some-domain.com/path/to/example';
const initData = {
method: 'POST',
body: JSON.stringify({key: value}),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
cache: 'no-cache',
credentials: 'same-origin',
mode: 'cors',
redirect: 'follow',
referrer: 'no-referrer',
};
const {ready, cancel} = abortableFetch(url, initData);
ready
.then(response => console.log(response))
.catch(err => {
if (err.name === 'AbortError') {
console.log('請(qǐng)求已被終止');
}
});
// 手動(dòng)取消請(qǐng)求
cancel();
至此我們便成功完成了基于Abort API的請(qǐng)求中斷邏輯,當(dāng)然如果針對(duì)需要同時(shí)取消多個(gè)請(qǐng)求以及自動(dòng)取消的應(yīng)用場(chǎng)景,在abortableFetch函數(shù)中我們已經(jīng)對(duì)外暴露了cancel方法,是不是想起來在第二小節(jié)介紹Axios的過程中,同樣出現(xiàn)過cancel方法,
所以這里完全可以借助上文中的思路,構(gòu)建出請(qǐng)求路徑與請(qǐng)求體以及cancel取消函數(shù)之間的映射關(guān)系,對(duì)緩存進(jìn)行集中管理并對(duì)外提供清空緩存的工具方法,由于實(shí)現(xiàn)思路與上文中的大同小異,這里就不再展開細(xì)講,感興趣的小伙伴兒可以自己嘗試下。
總結(jié)
這里我們?cè)俅位仡櫼幌卤疚闹饕v解的內(nèi)容,本文主要是基于目前前端領(lǐng)域使用的幾種比較常見的網(wǎng)絡(luò)請(qǐng)求方案,講解了一下在代碼層面各自實(shí)現(xiàn)請(qǐng)求中斷的處理方式。
在瀏覽器原生提供的XHR對(duì)象中,我們通過實(shí)例上的abort方法來終止請(qǐng)求。在Axios庫(kù)中,我們借助于其提供的CancelToken構(gòu)造函數(shù)同樣實(shí)現(xiàn)了請(qǐng)求中斷。
最后,我們通過fetch函數(shù)和Abort API的相互配合,實(shí)現(xiàn)了在現(xiàn)代主流瀏覽器的Fetch API中請(qǐng)求中斷的方式。通過這些優(yōu)化操作可以提前釋放被占用的資源,一定程度上減少了不必要的資源開銷。