Fetch API 是目前最新的異步請求解決方案,它在功能上與 XMLHttpRequest(XHR)類似,都是從服務器端異步獲取數據或者資源的方法。 對於有過 AJAX 開發經驗的讀者應該深有體會,基於 XHR 的異步請求方法在實現上比較複雜。 下面簡單演示如何通過 XHR 發送異步請求:
// 實例化 XMLHttpRequest
let xhr = new XMLHttpRequest()
// 定義加載完成回調函數,打印結果
xhr.onload = function(){
console.log("請求成功");
}
// 定義加載出錯時的回調函數,打印錯誤
xhr.onerror = function(err){
console.error("請求失敗");
}
// 設置請求目標
xhr.open("GET","/path/to/text", true)
// 開始發起請求
xhr.send()
從上面的代碼當中可以感受到,基於事件回調機制的 XHR 在編程實現的思路上非常反思維, 要實現這樣一個簡單的 GET 請求所需代碼較多,一旦功能變得負責會很容易造成混亂。 因此在實際應用當中,一般會選擇封裝好的函數進行使用, 例如 jQuery 提供的 $.ajax
方法。
接下來使用 Fetch API 來實現上述功能:
fetch("/path/to/text", { method: "GET" })
.then((response) => {
console.log("請求成功");
})
.catch((err) => {
console.log("請求失敗");
});
Fetch API 代碼邏輯清晰,代碼量少。
1. fetch()
Fetch API 提供了 fetch()
用來發起網絡請求並獲得資源響應。它的使用方法非常簡單:
fetch(request).then(response=>{/* 響應結果處理 */})
接受一個 Request 對象作爲參數,fetch()
發起網絡請求,由於網絡請求是一個異步過程,因此 fetch()
返回一個 Promise 對象,當請求響應時 Promise 執行 resolve 並傳回一個 Response 對象。
除了直接以 Request 對象作爲參數之外, fetch()
還支持傳入請求 URL 和請求配置項的方式, fetch()
會自動根據這些參數實例化 Request 對象之後再去發起請求,因此以下代碼是等價的:
fetch(new Request('/path/to/resource',{ method: "GET" }))
// 等價於
fetch('/path/to/resource',{ method: "GET" })
需要注意的是,fetch()
只有在網絡請求終端的時候纔會拋出異常,此時 Promise 對象會執行 reject 並返回錯誤信息。 因此對於 fetch()
來說,服務端返回的 HTTP 404、500 等狀態碼並不認爲是網絡錯誤,因此處理檢查 Promise 是否 resolve 之外,還需要檢查 Response.status、Response.ok 等對象屬性以確保請求是否成功響應。 示例:
fetch('/paht/to/resource/').then(response => {
if (response.status === 200){
// 請求成功
}else{
// 請求失敗
}
}).catch(err => {
// 網絡請求失敗或者請求被中斷
})
2. Request
Request 是一個用於描述資源請求的類,通過 Request()
構造函數,可以實例化一個 Request 對象:
let request = new Request(input,init)
其中,input
代表想要請求的資源,可以是 URL, 或者是描述資源請求的 Request 對象;init
爲可選參數,可以用來定義請求中的其他選項。 可以注意到,Request
構造函數所需要的參數和fetch()
方法的參數是一樣的。 示例:
-
GET 請求, 請求參數需要寫到 URL 中:
let getRequest = new Request("/api/hello?name=lilei", { method: "GET", });
-
POST 請求, 請求參數需要寫到 body 中:
let postRequest = new Request("/api/hello", { method: "POST", body: JSON.stringify({ name: "lilei", }), });
-
自定義請求的 Headers 信息
let customeRequest = new Request("/api/hello", { headers: new Headers({ "Content-Type": "text/plain", }), });
-
設置發起資源請求時帶上 cookie
let cookieRequest = new Request('/api/hello',{ credentials:'include' })
init 對象還可配置其他參數,這裏不展開講,介紹以下 Request 對象常用的幾個屬性:
-
url: String 類型,只讀,請求的url;
-
method: String 類型,只讀,請求的方法;
-
headers: Headers 類型,只讀,請求的頭部信息,可通過
get()
方法獲取信息,例如:if(request.headers.get('Content-Type') === 'text/html'){ // ... }
3. Response
Response 類,用於描述請求響應數據,通過 Response()
構造函數實例化,
let response = new Response(body,init)
其中 body 參數代表請求響應的資源內容,可以是字符串,FormData, Blob 等等; init爲可選參數對象,可以用來設置響應的 status、statusText、headers 等內容,示例:
// 如何構造一個 iudex.js 的響應:
let jsResponse = new Response(
// index.js 的內容
'console.log("Hello World!)',
{
status: 200,
headers: new Headers({
"Content-Type": "application/x-javascript",
}),
},
);
在實際應用當中,我們一般會通過 fetch()
、Cache API 等等獲得請求響應對象,然後再對響應對象進行操作。
3.1 讀取響應體
Response 的 body 屬性 暴露了一個 ReadableStream 類型的響應體內容,Response 提供了一些方法來讀取響應體:
text()
: 解析爲字符串;json()
: 解析爲 JSON 對象;blob()
: 解析爲 Blob 對象;formData()
: 解析爲 FormData 對象;arrayBuffer()
: 解析爲 ArrayBauffer 對象;
這些方法讀取並解析響應體的數據流屬於異步操作,因此這些方法均返回 Promise 對象,當讀取數據流並解析完成時, Promise 對象將 resolve 並同時返回解析好的結果。 示例:
// 溝槽 Response 對象
let response = new Response(JSON.stringify({ name: "lilei" }));
// 通過 response.json() 讀取請求體
response.json().then((data) => {
console.log(data.nam); //lilei
});
由於Response 的響應體是以數據流的形式存在的,因此只允許進行一次讀取操作。 通過檢查 bodyUsed 屬性可以知道 當前的 Response 對象是否已經被讀取:
let response = new Response(JSON.stringify({ name: "lilei" }));
console.log(response.bodyUsed); //false
response.json().then((data) => {
console.log(response.bodyUsed);// true
});
由於二次讀取響應體內容會導致報錯,因此爲了保險起見,可以在進行響應體讀取前首先判斷 bodyUsed
屬性再決定下一步操作。
3.2 拷貝 Response
Response 提供了 clone()
方法來實現對 Response 對象的拷貝:
let clonedResponse = response.clone()
clone()
是一個同步方法,克隆得到的 新對象再所有方面與原對象都是相同的。 在這裏需要注意的是,如果 Response 對象的響應體已經被讀取, 那麼在調用 clone()
方法的時候會報錯,因此需要在讀取響應體讀取之前進行克隆操作。
4. Fetch API 處理跨域請求
當涉及到前後端通信問題的時候,就不得不提請求跨域的情況。由於受到 Web 同源策略的影響,在使用 Fetch API 默認配置情況下發送異步請求,會受到跨域訪問限制而導致資源請求失敗。
我們通常採用跨域資源共享機制(CORS)來解決這個問題。在跨域服務端支持 CORS 的前提下,通過將 fetch()
的請求模式設置爲“cors”,就可以簡單地實現跨域請求。在這種請求模式下,返回的請求響應是完全可訪問的:
// 假設當前頁面 URL 爲 https://current.com
fetch('https://other.com/data.json', {
mode: 'cors'
})
.then(response => {
console.log(response.status) // 200
console.log(response.type) // 'cors'
console.log(response.bodyUsed) // false
return response.json()
})
.then(data => {
console.log(data.name) // 'lilei'
})
⭐ 對於圖片、JS、CSS 等等這些類型的靜態資源,如果通過對應的 HTML 標籤加載這類跨域資源,是不會受到同源策略限制的,因此一般來說,存放靜態資源的服務器並不需要設置 CORS。這就會對 Fetch API 請求這類靜態資源帶來影響。在默認情況下 fetch()
的請求模式爲“no-cors”,在這種模式下請求跨域資源並不會報錯,但是返回的 Response 對象將變得不透明,type 屬性將變成“opaque”,無論服務端所返回的真實 status 是多少,在這種情況下都會變成 0,其他屬性也都無法正常訪問:
// 假設當前頁面 URL 爲 https://current.com
fetch('https://other.com/data.json', {
mode: 'no-cors'
})
.then(response => {
console.log(response.status) // 0
console.log(response.type) // 'opaque'
console.log(response.headers) // Headers {}
console.log(response.body) // null
})
此時唯一能正常工作的方法是 clone(),即實現對 Response 對象的拷貝,當然拷貝得到的新對象也同樣是不透明的。這種模式比較適用於在 Service Worker 線程中攔截靜態資源請求並複製一份緩存到本地,只要將這類不透明的請求響應返回主線程,依然是能夠正常工作的。下面的代碼演示了 Service Worker 攔截跨域圖片資源並將資源緩存到本地,然後在 fetch()
出錯的時候再從緩存中讀取資源:
self.addEventListener("fetch", (event) => {
// 判斷當前攔截到的請求爲跨域圖片資源
if (event.request.url === "https://other-site.com/pic.jpg") {
event.respondWith(
// 優先發送網絡請求獲取最新的資源
fetch(event.request.url, { mode: "no-cors" })
.then((response) => {
// 將請求得到的響應進行緩存
// 此時緩存的資源是不透明的
const cloneReq = response.clone();
caches
.open("cache-storage")
.then((cache) => cache.put(event.request.url, cloneReq));
// 返回請求響應結果
return response;
})
.catch(
// 請求失敗時在使用緩存資源進行兜底
() =>
caches
.open("cache-storage")
.then((cache) => cache.match(event.request.url))
.then((e) => e),
),
);
}
});
@jayce 原文這裏寫的有點問題,
cache.match(event.request.url)
返回的不是 Response 對象,而是一個 Promise ,所以要進一步處理。加一個.then()
在這種情況下,圖片資源的 Response 對象是不透明的,因此整個操作過程無法對圖片資源響應做任何檢查判斷,只能直存直取。這就有可能將真實狀態碼爲 404、500 等錯誤響應給緩存下來,因此在“no-cors”模式下緩存的跨域資源的可信度不高,最好作爲各類請求策略的兜底資源進行使用。
上面這段代碼,具體在做什麼?
實際上它希望,我先攔截三方站點的 圖片 請求,然後嘗試去請求,然後緩存下來。但是,如果請求不到, fetch()
會報錯,這時候 被 catch
到後,會從先前的緩存中去取出緩存。 注意,緩存的是一個 Response 對象,所以在取得時候直接返回對應的 Response 對象即可。
爲了更加清楚以上的原理,下面用一個實例來說明。
關於Service Worker 的內容,後面有介紹,這裏提前用一下,爲了說明這個地方在做什麼,爲什麼要這麼做,有什麼效果。
有這樣一個頁面,
<img src="http://placeimg.com/640/480/nature" alt="" />
爲了直觀的觀察,這個 API 也有特殊的地方,每次刷新,都會去加載不同的圖片:
這個API 是一個三方站點的 API, 它並不會觸發跨域。 但是會被下面的 Service Worker 攔截:
self.addEventListener("fetch", (event) => {
// 判斷當前攔截到的請求爲跨域圖片資源
if (event.request.url === "http://placeimg.com/640/480/nature") {
event.respondWith(
// 優先發送網絡請求獲取最新的資源
fetch(event.request.url, { mode: "no-cors" })
.then((response) => {
throw Error()
// 將請求得到的響應進行緩存
// 此時緩存的資源是不透明的
const cloneReq = response.clone();
caches
.open("cache-storage")
.then((cache) => cache.put(event.request.url, cloneReq));
// 返回請求響應結果
return response;
})
.catch(
// 請求失敗時在使用緩存資源進行兜底
() =>
caches
.open("cache-storage")
.then((cache) => cache.match(event.request.url))
.then((e) => e),
),
);
}
});
注意,爲了觸發 catch
中的方法,這裏刻意通過 throw Error()
拋出了異常,
可以看到,現在,請求被攔截並且由錯誤處理,返回了緩存中的資源。