也許你對 Fetch 瞭解得不是那麼多(下)
編者按:除創宇前端與作者部落格外,本文還在語雀釋出。
編者還要按:作者也在掘金哦,歡迎關注:@GoDotDotDot
Fetch 與 XHR 比較
Fetch 相對 XHR 來說具有簡潔、易用、宣告式、天生基於 Promise 等特點。XHR 使用方式複雜,介面繁多,最重要的一點個人覺得是它的回撥設計,對於實現 try...catch
比較繁瑣。
但是 Fetch 也有它的不足,相對於 XHR 來說,目前它具有以下劣勢:
- 不能取消(雖然 AbortController 能實現,但是目前相容性基本不能使用,可以使用
- 不能獲取進度
- 不能設定超時(可以通過簡單的封裝來模擬實現)
- 相容性目前比較差(可以使用 polyfill 間接使用 XHR 來優雅降級,這裡推薦使用 isomorphic-fetch )
在瞭解 Fetch 和 XHR 的一些不同後,還是需要根據自身的業務需求來選擇合適的技術,因為技術沒有永遠的好壞,只有合不合適。
下面章節我們將介紹如何“優雅”的使用 Fetch 以及如何儘量避免掉劣勢。
如何使用Fetch
前面瞭解了這麼多基礎知識,現在終於到了介紹如何使用 Fetch 了。老規矩,我們先來看下規範定義的介面。
partial interface mixin WindowOrWorkerGlobalScope {
[NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);
};
複製程式碼
規範中定義的介面我們可以對應著 MDN 進行檢視,你可以點選這裡更直觀的看看它的用法。
從規範中我們可以看到 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
例項物件,下面我們也給出一個示例。
程式碼位置:github.com/GoDotDotDot…
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 的情況下,部分瀏覽器可能會報如下錯誤:
- Chrome: "Failed to execute 'fetch' on 'Window': member signal is not of type AbortSignal."
- Firefox: "'signal' member of RequestInit does not implement interface AbortSignal."
如果出現以上問題,我們也無能為力,可能原因是瀏覽器內部做了嚴格驗證,對比發現我們提供的 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)。歡迎留言討論,我們會盡可能回覆。
感謝您的閱讀。
新年快樂 :)