Fetch 進階指南

發表於2017-04-25

導讀

Fetch 是 web非同步通訊的未來. 從chrome42, Firefox39, Opera29, EdgeHTML14(並非Edge版本)起, fetch就已經被支援了. 其中chrome42~45版本, fetch對中文支援有問題, 建議從chrome46起使用fetch. 傳送門: fetch中文亂碼 .

Fetch

先過一遍Fetch原生支援率.


可見要想在IE8/9/10/11中使用fetch還是有些犯難的,畢竟它連 Promise 都不支援, 更別說fetch了. 別急, 這裡有polyfill(墊片).

由於IE8基於ES3, IE9支援大部分ES5, IE11支援少量ES5, 其中只有IE10對ES5支援比較完整. 因此IE8+瀏覽器, 建議依次裝載上述墊片.

嘗試一個fetch

先來看一個簡單的fetch.

fetch執行後返回一個 Promise 物件, 執行成功後, 成功列印出 Response 物件.

response headers

該fetch可以在任何域名的網站直接執行, 且能正常返回百度搜尋的建議詞條. 以下是常規輸入時的是介面截圖.

response headers

以下是剛才fetch到的部分資料. 其中key name 為”s”的欄位的value就是以上的建議詞條.(由於有高亮詞條”12306”, 最後一條資料”12366”被頂下去了, 故上面截圖上看不到)

response headers

看完栗子過後, 就要動真格了. 下面就來扒下 Fetch.

Promise特性

fetch方法返回一個Promise物件, 根據 Promise Api 的特性, fetch可以方便地使用then方法將各個處理邏輯串起來, 使用 Promise.resolve() 或 Promise.reject() 方法將分別返會肯定結果的Promise或否定結果的Promise, 從而呼叫下一個then 或者 catch. 一但then中的語句出現錯誤, 也將跳到catch中.

Promise若有疑問, 請閱讀 Promises .

① 我們不妨在 https://sp0.baidu.com 域名的網頁控制檯執行以下程式碼.

執行截圖如下:

fetch then

② 我們不妨在非 https://sp0.baidu.com 域名的網頁控制檯再次執行以上程式碼.(別忘了給fetch的第二引數傳遞{mode: “no-cors”})

執行截圖如下:

fetch catch

由於第一次進入then分支後, 返回了否定結果的 Promise.reject 物件. 因此程式碼進入到catch分支, 丟擲了錯誤. 此時, 上述 response.typeopaque .

response type

一個fetch請求的響應型別(response.type)為如下三種之一:

  • basic
  • cors
  • opaque

如上情景①, 同域下, 響應型別為 “basic”.

如上情景②中, 跨域下, 伺服器沒有返回CORS響應頭, 響應型別為 “opaque”. 此時我們幾乎不能檢視任何有價值的資訊, 比如不能檢視response, status, url等等等等.

fetch type

同樣是跨域下, 如果伺服器返回了CORS響應頭, 那麼響應型別將為 “cors”. 此時響應頭中除 Cache-Control , Content-Language , Content-Type , Expores , Last-ModifiedProgma 之外的欄位都不可見.

注意: 無論是同域還是跨域, 以上 fetch 請求都到達了伺服器.

mode

fetch可以設定不同的模式使得請求有效. 模式可在fetch方法的第二個引數物件中定義.

可定義的模式如下:

  • same-origin: 表示同域下可請求成功; 反之, 瀏覽器將拒絕傳送本次fetch, 同時丟擲錯誤 “TypeError: Failed to fetch(…)”.
  • cors: 表示同域和帶有CORS響應頭的跨域下可請求成功. 其他請求將被拒絕.
  • cors-with-forced-preflight: 表示在發出請求前, 將執行preflight檢查.
  • no-cors: 常用於跨域請求不帶CORS響應頭場景, 此時響應型別為 “opaque”.

除此之外, 還有兩種不太常用的mode型別, 分別是 navigate , websocket , 它們是 HTML標準 中特殊的值, 這裡不做詳細介紹.

fetch獲取http響應頭非常easy. 如下:

設定http請求頭也一樣簡單.

header的內容也是可以被檢索的.

post

在fetch中傳送post請求, 同樣可以在fetch方法的第二個引數物件中設定.

credentials

跨域請求中需要帶有cookie時, 可在fetch方法的第二個引數物件中新增credentials屬性, 並將值設定為”include”.

除此之外, credentials 還可以取以下值:

  • omit: 預設值, 預設為該值.
  • same-origin: 同源, 表示同域請求才傳送cookie.

catch

同 XMLHttpRequest 一樣, 無論伺服器返回什麼樣的狀態碼(chrome中除407之外的其他狀態碼), 它們都不會進入到錯誤捕獲裡. 也就是說, 此時, XMLHttpRequest 例項不會觸發 onerror 事件回撥, fetch 不會觸發 reject. 通常只在網路出現問題時或者ERR_CONNECTION_RESET時, 它們才會進入到相應的錯誤捕獲裡. (其中, 請求返回狀態碼為407時, chrome瀏覽器會觸發onerror或者reject掉fetch.)

cache

cache表示如何處理快取, 遵守http規範, 擁有如下幾種值:

  • default: 表示fetch請求之前將檢查下http的快取.
  • no-store: 表示fetch請求將完全忽略http快取的存在. 這意味著請求之前將不再檢查下http的快取, 拿到響應後, 它也不會更新http快取.
  • no-cache: 如果存在快取, 那麼fetch將傳送一個條件查詢request和一個正常的request, 拿到響應後, 它會更新http快取.
  • reload: 表示fetch請求之前將忽略http快取的存在, 但是請求拿到響應後, 它將主動更新http快取.
  • force-cache: 表示fetch請求不顧一切的依賴快取, 即使快取過期了, 它依然從快取中讀取. 除非沒有任何快取, 那麼它將傳送一個正常的request.
  • only-if-cached: 表示fetch請求不顧一切的依賴快取, 即使快取過期了, 它依然從快取中讀取. 如果沒有快取, 它將丟擲網路錯誤(該設定只在mode為”same-origin”時有效).

如果fetch請求的header裡包含 If-Modified-Since, If-None-Match, If-Unmodified-Since, If-Match, 或者 If-Range 之一, 且cache的值為 default , 那麼fetch將自動把 cache的值設定為 "no-store" .

async/await

為什麼是async/await

回撥深淵一直是jser的一塊心病, 雖然ES6提供了 Promise, 將巢狀平鋪, 但使用起來依然不便.

要說ES6也提供了generator/yield, 它將一個函式執行暫停, 儲存上下文, 再次呼叫時恢復當時的狀態.(學習可參考 Generator 函式的含義與用法 – 阮一峰的網路日誌) 無論如何, 總感覺彆扭. 如下摘自推庫的一張圖.

我們不難看出其中的差距, callback簡單粗暴, 層層回撥, 回撥越深入, 越不容易捋清楚邏輯. Promise 將非同步操作規範化.使用then連線, 使用catch捕獲錯誤, 堪稱完美, 美中不足的是, then和catch中傳遞的依然是回撥函式, 與心目中的同步程式碼不是一個套路.

為此, ES7 提供了更標準的解決方案 — async/await. async/await 幾乎沒有引入新的語法, 表面上看起來, 它就和alert一樣易用, 雖然它尚處於ES7的草案中, 不過這並不影響我們提前使用它.

async/await語法

async 用於宣告一個非同步函式, 該函式需返回一個 Promise 物件. 而 await 通常後接一個 Promise物件, 需等待該 Promise 物件的 resolve() 方法執行並且返回值後才能繼續執行. (如果await後接的是其他物件, 便會立即執行)

因此, async/await 天生可用於處理 fetch請求(毫無違和感). 如下:

自然, async/await 也可處理 Promise 物件.

可見使用await後, 可以直接得到返回值, 不必寫 .then(callback) , 也不必寫 .catch(error) 了, 更可以使用 try catch 標準語法捕獲錯誤.

由於await採用的是同步的寫法, 看起來它就和alert函式一樣, 可以自動阻塞上下文. 因此它可以重複執行多次, 就像上述程式碼②一樣.

可以看到, await/async 同步阻塞式的寫法解決了完全使用 Promise 的一大痛點——不同Promise之間共享資料問題. Promise 需要設定上層變數從而實現資料共享, 而 await/async 就不存在這樣的問題, 只需要像寫alert一樣書寫就可以了.

值得注意的是, await 只能用於 async 宣告的函式上下文中. 如下 forEach 中, 是不能直接使用await的.

如果是試圖將async宣告的函式作為回撥傳給forEach,該回撥將同時觸發多次,回撥內部await依然有效,只是多次的await隨著回撥一起同步執行了,這便不符合我們阻塞迴圈的初衷。如下:

正確的寫法如下:

如何試執行async/await

鑑於目前只有Edge支援 async/await, 我們可以使用以下方法之一執行我們的程式碼.

  1. 隨著node7.0的釋出, node中可以使用如下方式直接執行:

  2. babel線上編譯並執行 Babel · The compiler for writing next generation JavaScript .
  3. 本地使用babel編譯es6或更高版本es.

1) 安裝.

由於Babel5預設自帶各種轉換外掛, 不需要手動安裝. 然而從Babel6開始, 外掛需要手動下載, 因此以下安裝babel後需要再順便安裝兩個外掛.

2) 書寫.babelrc配置檔案.

3) 如果不配置.babelrc. 也可在命令列顯式指定外掛.

4) 編譯.

5) 實時編譯

6) 編譯目錄輸出到其他目錄

7) 編譯目錄輸出到單個檔案

8) 想要直接執行es6.js, 可使用babel-node.

9) 如需在程式碼中使用fetch, 且使用babel-node執行, 需引入 node-fetch 模組.

然後在es6.js中require node-fetch 模組.

4.本地使用traceur編譯es6或更高版本es.請參考 在專案開發中優雅地使用ES6:Traceur & Babel .

如何彌補Fetch的不足

fetch基於Promise, Promise受限, fetch也難倖免. ES6的Promise基於 Promises/A+ 規範 (對規範感興趣的同學可選讀 剖析原始碼理解Promises/A規範 ), 它只提供極簡的api, 沒有 timeout 機制, 沒有 progress 提示, 沒有 deferred 處理 (這個可以被async/await替代).

fetch-jsonp

除此之外, fetch還不支援jsonp請求. 不過辦法總比問題多, 萬能的開源作者提供了 fetch-jsonp 庫, 解決了這個問題.

fetch-jsonp 使用起來非常簡單. 如下是安裝:

如下是使用:

abort

由於Promise的限制, fetch 並不支援原生的abort機制, 但這並不妨礙我們使用 Promise.race() 實現一個.

Promise.race(iterable) 方法返回一個Promise物件, 只要 iterable 中任意一個Promise 被 resolve 或者 reject 後, 外部的Promise 就會以相同的值被 resolve 或者 reject.

支援性: 從 chrome33, Firefox29, Safari7.1, Opera20, EdgeHTML12(並非Edge版本) 起, Promise就被完整的支援. Promise.race()也隨之可用. 下面我們來看下實現.

然後, 使用如下方法測試新的fetch.

以上, fetch請求後, 立即呼叫abort方法, 該promise被拒絕, 符合預期. 細心的同學可能已經注意到了, “p.abort();” 該語句我是單獨寫一行的, 沒有鏈式寫在then方法之後. 為什麼這麼幹呢? 這是因為then方法呼叫後, 返回的是新的promise物件. 該物件不具有abort方法, 因此使用時要注意繞開這個坑.

timeout

同上, 由於Promise的限制, fetch 並不支援原生的timeout機制, 但這並不妨礙我們使用 Promise.race() 實現一個.

下面是一個簡易的版本.

實際上, 無論超時時間設定為多長, 控制檯都將輸出log “timeout”. 這是因為, 即使fetch執行成功, 外部的promise執行完畢, 此時 setTimeout 所在的那個promise也不會reject.

下面我們來看一個類似xhr版本的timeout.

然後, 使用如下方法測試新的fetch.

progress

xhr的 onprogress 讓我們可以掌控下載進度, fetch顯然沒有提供原生api 做類似的事情. 不過 Fetch中的Response.body 中實現了getReader()方法用於讀取原始位元組流, 該位元組流可以迴圈讀取, 直到body下載完成. 因此我們完全可以模擬fetch的progress.

以下是 stackoverflow 上的一段程式碼, 用於模擬fetch的progress事件. 為了方便測試, 請求url已改為本地服務.(原文請戳 javascript – Progress indicators for fetch? – Stack Overflow)

以下是日誌截圖:

剛好github上有個fetch progress的demo, 感興趣的小夥伴請參看這裡: Fetch Progress DEMO .

我們不妨來對比下, 使用xhr的onprogress事件回撥, 輸出如下:

我試著適當增加響應body的size, 發現xhr的onprogress事件回撥依然只執行兩次. 通過多次測試發現其執行頻率比較低, 遠不及fetch progress.


本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.

參考文章

相關文章