掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
幾乎在所有面向用戶或企業(yè)的應(yīng)用程序中,所呈現(xiàn)出來的信息都不是一成不變的,即數(shù)據(jù)都是動態(tài)的,由某個或者多個后臺服務(wù)所提供。那么就不可避免地會涉及到網(wǎng)絡(luò)請求,而對于不同企業(yè)肯定有不同的業(yè)務(wù)場景。

創(chuàng)新互聯(lián)建站是一家專注于成都網(wǎng)站設(shè)計、成都網(wǎng)站制作與策劃設(shè)計,雙遼網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)建站做網(wǎng)站,專注于網(wǎng)站建設(shè)十年,網(wǎng)設(shè)計領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:雙遼等地區(qū)。雙遼做網(wǎng)站價格咨詢:18980820575
在一個功能完善的應(yīng)用程序呈現(xiàn)給用戶之前,前后端開發(fā)人員必須先根據(jù)產(chǎn)品經(jīng)理提供的業(yè)務(wù)需求文檔協(xié)商建立起格式良好的接口契約,然后再經(jīng)過開發(fā)聯(lián)調(diào)測試驗證部署上線等一系列流程之后才具有可用性,才能展現(xiàn)在用戶面前供用戶使用。
但是可能并不是在任何場景下,我們都需要關(guān)心網(wǎng)絡(luò)請求的響應(yīng)結(jié)果,或者說在某些場景下,我們只需要關(guān)心最新的有效的網(wǎng)絡(luò)請求,對于老舊的失效的網(wǎng)絡(luò)請求,我們甚至可以忽略它的存在。
我們知道,從瀏覽器發(fā)起一次網(wǎng)絡(luò)請求,到建立TCP鏈接(對于HTTPS協(xié)議還需要建立額外的TLS連接)以及DNS域名解析,再到發(fā)送請求數(shù)據(jù)報文,最終服務(wù)器處理請求并響應(yīng)數(shù)據(jù),期間會不停占用客戶端和服務(wù)器資源。
如果該網(wǎng)絡(luò)請求對于我們而言已經(jīng)無效,那么我們就可以通過手動中斷請求,來提前釋放被占用的資源,減少不必要的資源開銷。
例如考慮以下場景:
還有很多其他沒有列出的應(yīng)用場景,針對每種應(yīng)用場景,雖然我們都能給出對應(yīng)的方案來解決實際問題,但是筆者認為最理想的方案還是盡量減少無用請求,減少客戶端和服務(wù)器之間的無效傳輸,鑒于此也就引入了本文中將要講到的中斷請求的方式。
在前端領(lǐng)域,個人覺得有幾種比較常見的網(wǎng)絡(luò)請求方案:瀏覽器原生支持的XMLHttpRequest對象,同時兼容瀏覽器端和NodeJS服務(wù)端的第三方HTTP庫Axios和大部分瀏覽器最新實現(xiàn)的Fetch API。本文主要基于以上三種請求方案講解一下各自中斷請求的方式,文中若有錯誤,還請指正。
1、XMLHttpRequest
瀏覽器原生實現(xiàn)的XMLHttpRequest(以下簡稱XHR)構(gòu)造函數(shù)對于我們來說已經(jīng)是再熟悉不過了,但是在實際應(yīng)用中,大部分場景下可能我們并不需要去主動實例化XHR構(gòu)造函數(shù),畢竟實例化之后還需要通過調(diào)用open和send等一系列的官方API才能實現(xiàn)與服務(wù)器的數(shù)據(jù)交互,操作細節(jié)稍微繁瑣。
相反我們一般會推薦使用社區(qū)實現(xiàn)的第三方庫來方便我們簡化操作流程,提升開發(fā)效率,例如下一節(jié)將要講述的Axios。但即便是Axios,在瀏覽器端其底層依舊是通過XHR構(gòu)造函數(shù)來實現(xiàn)網(wǎng)絡(luò)IO的,因此這一小節(jié)有必要對XHR的相關(guān)知識點進行回顧和講解。
首先拋出一個基礎(chǔ)示例:
/**
* @description: 基于 XHR 封裝的網(wǎng)絡(luò)請求工具函數(shù)
* @param {String} url 請求接口地址
* @param {Document | XMLHttpRequestBodyInit | null} body 請求體
* @param {Object} requestHeader 請求頭
* @param {String} method 請求方法
* @param {String} responseType 設(shè)置響應(yīng)內(nèi)容的解析格式
* @param {Boolean} async 請求是否異步
* @param {Number} timeout 設(shè)置請求超時時間(單位:毫秒)
* @param {Boolean} withCredentials 設(shè)置跨域請求是否允許攜帶 cookies 或 Authorization header 等授權(quán)信息
* @return {Promise} 可包含響應(yīng)內(nèi)容的 Promise 實例
*/
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: '請求出錯,請稍后重試' });
};
xhr.ontimeout = function onTimeout() {
reject({ message: '接口超時,請稍后重試' });
};
xhr.send(body ? JSON.stringify(body) : null);
});
}
以上示例對XHR請求操作流程進行了一下簡單的封裝,并未涉及到太多的細節(jié)和兼容處理。一個簡單的調(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ù)交互請求,接下來在此基礎(chǔ)上繼續(xù)完善請求中斷的相關(guān)邏輯。
我們知道,在XHR實例上為我們提供了一個abort方法用于終止該請求,并且當一個請求被終止的時候,該請求所對應(yīng)的XHR實例的readyState屬性將會被設(shè)置為XMLHttpRequest.UNSET(0),同時status屬性會被重置為0,因此在本示例中我們同樣使用abort方法來實現(xiàn)請求中斷。
// 參考以上示例
function request
// 省略入?yún)?br> ...
} = {}) {
return new Promise((resolve, reject) => {
// 省略代碼
...
});
}
// 存儲請求接口地址以及請求體和 XHR 實例的映射關(guān)系
request.cache = {};
/**
* @description: 根據(jù)提供的鍵名中斷對應(yīng)的請求
* @param {String} key 存儲在 request.cache 屬性中的鍵名,若未提供則中斷全部請求
* @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來臨時存儲請求接口地址以及請求體和XHR實例的映射關(guān)系,因為在同一頁面中一般可能會涉及到多個接口地址不同的請求,或者同一個請求對應(yīng)不同的請求體,因此這里考慮加上了請求體以做區(qū)分。當然為了作為request.cache中的唯一鍵名,我們還需要對請求體進行序列化操作,因此簡單封裝一個序列化工具函數(shù)。
/**
* @description: 將請求體序列化為字符串
* @param {Document | XMLHttpRequestBodyInit | null} data 請求體
* @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();
// 將請求接口地址以及請求體和 XHR 實例存入 cache 中
let cacheKey = url;
if (body) {
cacheKey += `_${request.serialize(body)}`;
}
// 每次發(fā)送請求之前將上一個未完成的相同請求進行中斷
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) {
// 請求完成之后清除緩存
request.clearCache(cacheKey);
resolve(xhr.response);
}
}
};
xhr.onerror = function onError(error) {
console.log(error);
// 請求報錯之后清除緩存
request.clearCache(cacheKey);
reject({ message: '請求出錯,請稍后重試' });
};
xhr.ontimeout = function onTimeout() {
// 請求超時之后清除緩存
request.clearCache(cacheKey);
reject({ message: '接口超時,請稍后重試' });
};
xhr.send(body ? JSON.stringify(body) : null);
});
}這樣便簡單實現(xiàn)了一個自包含的請求中斷的處理邏輯,每次發(fā)送請求之前自動判定未完成的多余請求并將其清除,從而避免性能上的開銷。
當然,不僅如此,這里同樣可以通過request.clearCache函數(shù)來在組件卸載或路由跳轉(zhuǎn)的時候手動清除未完成的請求,因為這部分請求對于卸載后的組件而言沒有太多實質(zhì)意義,例如以下示例:
// 網(wǎng)頁卸載前清除緩存
window.addEventListener('beforeunload', () => request.clearCache(), false);
// Vue 中路由跳轉(zhuǎn)前清除緩存
router.beforeEach((to, from, next) => { request.clearCache(); next(); });
// React 中路由跳轉(zhuǎn)時清除緩存
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想必是我們使用最多的一個第三方開源免費的HTTP庫,其本身基于Promise的特性使得我們可以很方便地寫出更加優(yōu)雅且易維護的代碼,從而避免函數(shù)多層嵌套所帶來的一系列問題。
當然,它最大的特點在于可以同時兼容瀏覽器端和NodeJS服務(wù)端。底層通過判定不同的運行環(huán)境來自動提供不同的適配器,在瀏覽器端通過原生的XHR對象來發(fā)送請求,而在NodeJS服務(wù)端則通過內(nèi)置的http模塊來發(fā)送請求。
不僅如此,在其底層的Promise管道鏈中還為我們暴露了稱之為攔截器的入口,使得我們可以參與到一個請求的生命周期中,在請求發(fā)送之前和響應(yīng)接收之后能夠自定義實現(xiàn)數(shù)據(jù)的裝配和轉(zhuǎn)換操作。帶來的如此之多的人性化操作,使得我們沒有理由不去用它,這也奠定了其長久以來依舊如此火爆的基礎(chǔ)。
言歸正傳,在Axios中同樣為我們提供了請求中斷的相關(guān)API。首先拋出一個基礎(chǔ)示例:
// 安裝 axios
npm install --save axios
// 導(dǎo)入 axios
import axios from 'axios';
// 創(chuàng)建 axios 實例
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 實例默認配置
instance.defaults.headers.common['Authorization'] = '';
instance.defaults.headers.post['Content-Type'] = 'application/json; charset=UTF-8';
// 自定義請求攔截器
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)建一個簡單的post請求:
const CancelToken = axios.CancelToken;
let cancel;
instance.post('/api/user/123', {
name: 'new name',
phone: 'new phone',
}, {
// CancelToken 構(gòu)造函數(shù)接收一個 executor 函數(shù)參數(shù),并且該函數(shù)接收一個取消函數(shù) c 用于取消該次請求
cancelToken: new CancelToken(function executor(c) {
// 將取消函數(shù)賦值到外部變量,方便從外部取消請求
cancel = c;
}),
});
// 手動取消請求
cancel();
針對需要同時取消多個請求以及自動取消的應(yīng)用場景,上面的示例顯然不能滿足我們的需求。這里我們同樣可以利用上一小節(jié)的思路來維護一個請求接口地址以及請求體和取消函數(shù)c之間的映射關(guān)系。
同時為了避免在每個請求中都需要手動去實例化CancelToken,我們可以巧妙利用request攔截器來整合這部分的邏輯,實現(xiàn)邏輯復(fù)用。首先我們將緩存邏輯拆分到一個單獨的文件中:
// cacheUtils.js
export const CacheUtils = {
// 存儲請求接口地址以及請求體和取消函數(shù)之間的映射關(guān)系
cache: {},
// 根據(jù)提供的鍵名 key 取消對應(yī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)用到請求攔截器和響應(yīng)攔截器中:
import qs from 'qs';
import { CacheUtils } from './cacheUtils.js';
// 自定義請求攔截器
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ā)送請求之前將上一個未完成的相同請求進行中斷
CacheUtils.cache[cacheKey] && CacheUtils.clearCache(cacheKey);
// 將當前請求所對應(yīng)的取消函數(shù)存入緩存
config.cancelToken = new axios.CancelToken(function executor(c) {
CacheUtils.cache[cacheKey] = c;
});
// 臨時保存 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)對需要手動清除未完成請求的應(yīng)用場景,使用方式與上一小節(jié)思路相同,這里就不再重復(fù)多講。
3、Fetch API
作為瀏覽器原生提供的XHR構(gòu)造函數(shù)的理想替代方案,新增的Fetch API為我們提供了Request和Response(以及其他與網(wǎng)絡(luò)請求有關(guān)的)對象的通用定義,一個Request對象表示一個資源請求,通常包含一些初始數(shù)據(jù)和正文內(nèi)容,例如資源請求路徑、請求方式、請求主體等,而一個Response對象則表示對一次請求的響應(yīng)數(shù)據(jù)。
同時Fetch API還為我們提供了一個全局的fetch方法,通過該方法我們可以更加簡單合理地跨網(wǎng)絡(luò)異步獲取資源。fetch方法不僅原生支持Promise的鏈式操作,同時還支持直接傳入Request對象來發(fā)送請求,增加了很強的靈活性。
到目前為止,F(xiàn)etch API的支持程度如下圖:
不難看出IE瀏覽器下的兼容性不容樂觀,但是作為一名有追求的前端開發(fā)人員,當然不會止步于此。一番探索之后,發(fā)現(xiàn)可以通過isomorphic-fetch或者whatwg-fetch這兩個第三方依賴來解決兼容性問題:
// 安裝依賴
npm install --save whatwg-fetch
// 引入依賴
import {fetch as fetchPolyfill} from 'whatwg-fetch';
接下來同樣先拋出一個基礎(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ù)來初始化請求數(shù)據(jù)
// Request 構(gòu)造函數(shù)接收兩個參數(shù)
// 第一個參數(shù)表示需要獲取的資源 URL 路徑或者另一個嵌套的 Request 實例
// 第二個可選參數(shù)表示需要被包含到請求中的各種自定義選項
const request = new Request(url, initData);
fetch(request).then(response => response.json()).then(data => console.log(data));
可以看到,相比于傳統(tǒng)的XHR方式而言,fetch函數(shù)的使用方式更加簡潔友好,易用性更強,同時還為我們提供了多種入?yún)⒌男问绞沟贸绦蚬δ茏兊酶拥撵`活可擴展。
那么回到本文的主題,上文中提到,在XHR實例中可以通過abort方法來取消請求,在Axios中可以通過CancelToken構(gòu)造函數(shù)的參數(shù)來獲得取消函數(shù),從而通過取消函數(shù)來取消請求。
但是很遺憾的是,在Fetch API中,并沒有自帶的取消請求的API供我們調(diào)用。不過令人愉悅的是,除了IE瀏覽器外,其他瀏覽器已經(jīng)為Abort API添加了實驗性支持,Abort API允許對XHR和fetch這樣的請求操作在未完成時進行終止,那么接下來對Abort API做一下簡要的介紹。
在Abort API的相關(guān)概念中主要包含了AbortController和AbortSignal兩大接口:
通過以上兩個接口,我們嘗試封裝一個簡單加強版的可取消的fetch工具函數(shù):
const abortableFetch = (url, initData) => {
// 實例化控制器對象
const abortController = new AbortController();
// 獲取信號對象
const signal = abortController.signal;
return {
// 注意這里需要將 signal 信號對象與請求進行關(guān)聯(lián),關(guān)聯(lián)之后才能通過 abortController.abort 方法取消請求
ready: fetch(url, {...initData, signal}).then(response => response.json()),
// 暴露 cancel 方法,用于在外層手動取消請求
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('請求已被終止');
}
});
// 手動取消請求
cancel();
至此我們便成功完成了基于Abort API的請求中斷邏輯,當然如果針對需要同時取消多個請求以及自動取消的應(yīng)用場景,在abortableFetch函數(shù)中我們已經(jīng)對外暴露了cancel方法,是不是想起來在第二小節(jié)介紹Axios的過程中,同樣出現(xiàn)過cancel方法,
所以這里完全可以借助上文中的思路,構(gòu)建出請求路徑與請求體以及cancel取消函數(shù)之間的映射關(guān)系,對緩存進行集中管理并對外提供清空緩存的工具方法,由于實現(xiàn)思路與上文中的大同小異,這里就不再展開細講,感興趣的小伙伴兒可以自己嘗試下。
總結(jié)
這里我們再次回顧一下本文主要講解的內(nèi)容,本文主要是基于目前前端領(lǐng)域使用的幾種比較常見的網(wǎng)絡(luò)請求方案,講解了一下在代碼層面各自實現(xiàn)請求中斷的處理方式。
在瀏覽器原生提供的XHR對象中,我們通過實例上的abort方法來終止請求。在Axios庫中,我們借助于其提供的CancelToken構(gòu)造函數(shù)同樣實現(xiàn)了請求中斷。
最后,我們通過fetch函數(shù)和Abort API的相互配合,實現(xiàn)了在現(xiàn)代主流瀏覽器的Fetch API中請求中斷的方式。通過這些優(yōu)化操作可以提前釋放被占用的資源,一定程度上減少了不必要的資源開銷。

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流