上文連接:也許你對 Fetch 瞭解得不是那麼多(上)javascript
編者按:除創宇前端與做者博客外,本文還在語雀發佈。html
編者還要按:做者也在掘金哦,歡迎關注:@GoDotDotDot前端
Fetch 相對 XHR 來講具備簡潔、易用、聲明式、天生基於 Promise 等特色。XHR 使用方式複雜,接口繁多,最重要的一點我的以爲是它的回調設計,對於實現 try...catch
比較繁瑣。java
可是 Fetch 也有它的不足,相對於 XHR 來講,目前它具備如下劣勢:c++
在瞭解 Fetch 和 XHR 的一些不一樣後,仍是須要根據自身的業務需求來選擇合適的技術,由於技術沒有永遠的好壞,只有合不合適。git
下面章節咱們將介紹如何「優雅」的使用 Fetch 以及如何儘可能避免掉劣勢。github
前面瞭解了這麼多基礎知識,如今終於到了介紹如何使用 Fetch 了。老規矩,咱們先來看下規範定義的接口。web
partial interface mixin WindowOrWorkerGlobalScope {
[NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);
};
複製代碼
規範中定義的接口咱們能夠對應着 MDN 進行查看,你能夠點擊這裏更直觀的看看它的用法。json
從規範中咱們能夠看到 fetch 屬於 WindowOrWorkerGlobalScope 的一部分,暴露在 Window 或 WorkerGlobalScope 對象上。因此在瀏覽器中,你能夠直接調用 fetch。後端
規範中定義了 fetch 返回一個 Promise,它最多可接收兩個參數( input 和 init )。爲了可以對它的使用方法有個更全面的瞭解,下面來說一下這兩個參數。
input 參數類型爲 RequestInfo
,咱們能夠回到前面的 Request
部分,來回顧一下它的定義。
typedef (Request or USVString) RequestInfo;
發現它是一個 Request
對象或者是一個字符串,所以你能夠傳 Request
實例或者資源地址字符串,這裏通常咱們推薦使用字符串。
init 參數類型爲 RequestInit
,咱們回顧前面 Requst
部分,它是一個字典類型。在 JavaScript
中你須要傳遞一個 Object
對象。
dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null };
在本小節以前咱們都沒有介紹 fetch 的使用方式,可是在其餘章節中或多或少出現過它的容貌。如今,咱們終於能夠在這裏正式介紹它的使用方式了。
fetch 它返回一個 Promise,意味着咱們能夠經過 then
來獲取它的返回值,這樣咱們能夠鏈式調用。若是配合 async/await
使用,咱們的代碼可讀性會更高。下面咱們先經過一個簡單的示例來熟悉下它的使用。
示例代碼位置:github.com/GoDotDotDot…
// 客戶端
const headers = new Headers({
'X-Token': 'fe9',
});
setTimeout(() => {
fetch('/data?name=fe', {
method: 'GET', // 默認爲 GET,不寫也能夠
headers,
})
.then(response => response.json())
.then(resData => {
const { status, data } = resData;
if (!status) {
window.alert('發生了一個錯誤!');
return;
}
document.getElementById('fetch').innerHTML = data;
});
}, 1000);
複製代碼
上面的示例中,咱們自定義了一個 headers
。爲了演示方便,這裏咱們設定了一個定時器。在請求成功時,服務器端會返回相應的數據,咱們經過 Response
實例的 json
方法來解析數據。細心的同窗會發現,這裏 fetch 的第一個參數咱們採用的是字符串,在第二個參數咱們提供了一些 RequestInit
配置信息,這裏咱們指定了請求方法(method)和自定義請求頭(headers)。固然你也能夠傳遞一個 Request
實例對象,下面咱們也給出一個示例。
const headers = new Headers({
'X-Token': 'fe9',
});
const request = new Request('/api/request', {
method: 'GET',
headers,
});
setTimeout(() => {
fetch(request)
.then(res => res.json())
.then(res => {
const { status, data } = res;
if (!status) {
alert('服務器處理失敗');
return;
}
document.getElementById('fetch-req').innerHTML = data;
});
}, 1200);
複製代碼
在瀏覽器中打開:http://127.0.0.1:4000/, 若是上面的示例運行成功,你將會看到以下界面:
好,在運行完示例後,相信你應該對如何使用 fetch 有個基本的掌握。在上一章節,咱們講過 fetch 有必定的缺點,下面咱們針對部分缺點來嘗試着處理下。
當網絡出現異常,請求可能已經超時,爲了使咱們的程序更健壯,提供一個較好的用戶 體驗,咱們須要提供一個超時機制。然而,fetch 並不支持,這在上一小節中咱們也聊到過。慶幸的是,咱們有 Promise ,這使得咱們有機可趁。咱們能夠經過自定義封裝來達到支持超時機制。下面咱們嘗試封裝下。
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
};
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const headers = { ...defaultOptions.headers, ...options.headers };
let abortId;
let timeout = false;
if (options.timeout) {
abortId = setTimeout(() => {
timeout = true;
reject(new Error('timeout!'));
}, options.timeout || 6000);
}
fetch(url, { ...defaultOptions, ...options, headers })
.then((res) => {
if (timeout) throw new Error('timeout!');
return res;
})
.then(checkStatus)
.then(parseJSON)
.then((res) => {
clearTimeout(abortId);
resolve(res);
})
.catch((e) => {
clearTimeout(abortId);
reject(e);
});
});
}
複製代碼
上面的代碼中,咱們須要注意下。就是咱們手動根據超時時間來 reject
並不會阻止後續的請求,因爲咱們並無關閉掉這次鏈接,屬因而僞取消。fetch 中若是後續接受到服務器的響應,依然會繼續處理後續的處理。因此這裏咱們在 fetch 的第一個 then
中進行了超時判斷。
const controller = new AbortController();
const signal = controller.signal;
fetch('/data?name=fe', {
method: 'GET',
signal,
})
.then(response => response.json())
.then(resData => {
const { status, data } = resData;
if (!status) {
window.alert('發生了一個錯誤!');
return;
}
document.getElementById('fetch-str').innerHTML = data;
});
controller.abort();
複製代碼
咱們回過頭看下 fetch 的接口,發現有一個屬性 signal
, 類型爲AbortSignal,表示一個信號對象( signal object ),它容許你經過 AbortController 對象與DOM請求進行通訊並在須要時將其停止。你能夠經過調用 AbortController.abort 方法完成取消操做。
當咱們須要取消時,fetch 會 reject 一個錯誤( AbortError DOMException ),中斷你的後續處理邏輯。具體能夠看規範中的解釋。
因爲目前 AbortController 兼容性極差,基本不能使用,可是社區有人幫咱們提供了 polyfill(這裏我不提供連接,由於目前來講還不適合生產使用,會出現下面所述的問題),咱們能夠經過使用它來幫助咱們提早感覺新技術帶來的快樂。可是你可能會在原生支持 Fetch
可是又不支持 AbortController 的狀況下,部分瀏覽器可能會報以下錯誤:
若是出現以上問題,咱們也無能爲力,可能緣由是瀏覽器內部作了嚴格驗證,對比發現咱們提供的 signal
類型不對。
可是咱們能夠經過手動 reject
的方式達到取消,可是這種屬於僞取消,實際上鍊接並無關閉。咱們能夠經過自定義配置,例如在 options
中增長配置,暴露出 reject
,這樣咱們就能夠在外面來取消掉。這裏本人暫時不提供代碼。有興趣的同窗能夠嘗試一下,也能夠在下面的評論區評論。
前面提到過的獲取進度目前咱們還沒法實現。
示例代碼位置:github.com/GoDotDotDot…
下面咱們講一講如何作一個簡單的攔截器,這裏的攔截器指對響應作攔截。假設咱們須要對接口返回的狀態碼進行解析,例如 403 或者 401 須要跳轉到登陸頁面,200 正常放行,其餘報錯。因爲 fetch 返回一個 Promise ,這就使得咱們能夠在後續的 then
中作些簡單的攔截。咱們看一下示例代碼:
function parseJSON(response) {
const { status } = response;
if (status === 204 || status === 205) {
return null;
}
return response.json();
}
function checkStatus(response) {
const { status } = response;
if (status >= 200 && status < 300) {
return response;
}
// 權限不容許則跳轉到登錄頁面
if (status === 403 || status === 401) {
window ? (window.location = '/login.html') : null;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
/** * @description 默認配置 * 設置請求頭爲json */
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
// credentials: 'include', // 跨域傳遞cookie
};
/** * Requests a URL, returning a promise * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * * @return {object} The response data */
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const headers = { ...defaultOptions.headers, ...options.headers };
let abortId;
let timeout = false;
if (options.timeout) {
abortId = setTimeout(() => {
timeout = true;
reject(new Error('timeout!'));
}, options.timeout || 6000);
}
fetch(url, { ...defaultOptions, ...options, headers })
.then((res) => {
if (timeout) throw new Error('timeout!');
return res;
})
.then(checkStatus)
.then(parseJSON)
.then((res) => {
clearTimeout(abortId);
resolve(res);
})
.catch((e) => {
clearTimeout(abortId);
reject(e);
});
});
}
複製代碼
從上面的 checkStatus
代碼中咱們能夠看到,咱們首先檢查了狀態碼。當狀態碼爲 403 或 401 時,咱們將頁面跳轉到了 login 登陸頁面。細心的同窗還會發現,咱們多了一個處理方法就是 parseJSON
,這裏因爲咱們的後端統一返回 json 數據,爲了方便,咱們就直接統一處理了 json 數據。
本系列文章總體闡述了 fetch 的基本概念、和 XHR 的差別、如何使用 fetch 以及咱們常見的解決方案。但願同窗們在讀完整篇文章可以對 fetch 的認識有所加深。
建議:在總體瞭解了 fetch 以後,但願同窗們可以讀一下 github polyfill 源碼。在讀代碼的同時,能夠同時參考 Fetch 規範。
參考:
文 / GoDotDotDot
Less is More.
編 / 熒聲
做者其餘文章:
本文由創宇前端做者受權發佈,版權屬於做者,創宇前端出品。 歡迎註明出處轉載本文。文章連接:blog.godotdotdot.com/2018/12/28/…
想要訂閱更多來自知道創宇開發一線的分享,請搜索關注咱們的微信公衆號:創宇前端(KnownsecFED)。歡迎留言討論,咱們會盡量回復。
感謝您的閱讀。
新年快樂 :)