導讀
Fetch 是 web非同步通訊的未來. 從chrome42, Firefox39, Opera29, EdgeHTML14(並非Edge版本)起, fetch就已經被支援了. 其中chrome42~45版本, fetch對中文支援有問題, 建議從chrome46起使用fetch. 傳送門: fetch中文亂碼 .
Fetch
先過一遍Fetch原生支援率.
可見要想在IE8/9/10/11中使用fetch還是有些犯難的,畢竟它連 Promise 都不支援, 更別說fetch了. 別急, 這裡有polyfill(墊片).
- es5 的 polyfill —
es5-shim, es5-sham
. - Promise 的 polyfill —
es6-promise
. - fetch 的 polyfill —
fetch-ie8
.
由於IE8基於ES3, IE9支援大部分ES5, IE11支援少量ES5, 其中只有IE10對ES5支援比較完整. 因此IE8+瀏覽器, 建議依次裝載上述墊片.
嘗試一個fetch
先來看一個簡單的fetch.
1 2 3 4 5 6 7 8 9 |
var word = '123', url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3'; fetch(url,{mode: "no-cors"}).then(function(response) { return response; }).then(function(data) { console.log(data); }).catch(function(e) { console.log("Oops, error"); }); |
fetch執行後返回一個 Promise
物件, 執行成功後, 成功列印出 Response
物件.
該fetch可以在任何域名的網站直接執行, 且能正常返回百度搜尋的建議詞條. 以下是常規輸入時的是介面截圖.
以下是剛才fetch到的部分資料. 其中key name 為”s”的欄位的value就是以上的建議詞條.(由於有高亮詞條”12306”, 最後一條資料”12366”被頂下去了, 故上面截圖上看不到)
看完栗子過後, 就要動真格了. 下面就來扒下 Fetch.
Promise特性
fetch方法返回一個Promise物件, 根據 Promise Api
的特性, fetch可以方便地使用then方法將各個處理邏輯串起來, 使用 Promise.resolve() 或 Promise.reject() 方法將分別返會肯定結果的Promise或否定結果的Promise, 從而呼叫下一個then 或者 catch. 一但then中的語句出現錯誤, 也將跳到catch中.
Promise若有疑問, 請閱讀 Promises .
① 我們不妨在 https://sp0.baidu.com 域名的網頁控制檯執行以下程式碼.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var word = '123', url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3'; fetch(url).then(function(response){ console.log('第一次進入then...'); if(response.status>=200 && response.status<300){ console.log('Content-Type: ' + response.headers.get('Content-Type')); console.log('Date: ' + response.headers.get('Date')); console.log('status: ' + response.status); console.log('statusText: ' + response.statusText); console.log('type: ' + response.type); console.log('url: ' + response.url); return Promise.resolve(response); }else{ return Promise.reject(new Error(response.statusText)); } }).then(function(data){ console.log('第二次進入then...'); console.log(data); }).catch(function(e){ console.log('丟擲的錯誤如下:'); console.log(e); }); |
執行截圖如下:
② 我們不妨在非 https://sp0.baidu.com 域名的網頁控制檯再次執行以上程式碼.(別忘了給fetch的第二引數傳遞{mode: “no-cors”})
執行截圖如下:
由於第一次進入then分支後, 返回了否定結果的 Promise.reject 物件. 因此程式碼進入到catch分支, 丟擲了錯誤. 此時, 上述 response.type
為 opaque
.
response type
一個fetch請求的響應型別(response.type)為如下三種之一:
- basic
- cors
- opaque
如上情景①, 同域下, 響應型別為 “basic”.
如上情景②中, 跨域下, 伺服器沒有返回CORS響應頭, 響應型別為 “opaque”. 此時我們幾乎不能檢視任何有價值的資訊, 比如不能檢視response, status, url等等等等.
同樣是跨域下, 如果伺服器返回了CORS響應頭, 那麼響應型別將為 “cors”. 此時響應頭中除 Cache-Control
, Content-Language
, Content-Type
, Expores
, Last-Modified
和 Progma
之外的欄位都不可見.
注意: 無論是同域還是跨域, 以上 fetch 請求都到達了伺服器.
mode
fetch可以設定不同的模式使得請求有效. 模式可在fetch方法的第二個引數物件中定義.
1 |
fetch(url, {mode: 'cors'}); |
可定義的模式如下:
- same-origin: 表示同域下可請求成功; 反之, 瀏覽器將拒絕傳送本次fetch, 同時丟擲錯誤 “TypeError: Failed to fetch(…)”.
- cors: 表示同域和帶有CORS響應頭的跨域下可請求成功. 其他請求將被拒絕.
- cors-with-forced-preflight: 表示在發出請求前, 將執行preflight檢查.
- no-cors: 常用於跨域請求不帶CORS響應頭場景, 此時響應型別為 “opaque”.
除此之外, 還有兩種不太常用的mode型別, 分別是 navigate
, websocket
, 它們是 HTML標準 中特殊的值, 這裡不做詳細介紹.
header
fetch獲取http響應頭非常easy. 如下:
1 2 3 |
fetch(url).then(function(response) { console.log(response.headers.get('Content-Type')); }); |
設定http請求頭也一樣簡單.
1 2 3 4 5 |
var headers = new Headers(); headers.append("Content-Type", "text/html"); fetch(url,{ headers: headers }); |
header的內容也是可以被檢索的.
1 2 3 4 5 |
var header = new Headers({ "Content-Type": "text/plain" }); console.log(header.has("Content-Type")); //true console.log(header.has("Content-Length")); //false |
post
在fetch中傳送post請求, 同樣可以在fetch方法的第二個引數物件中設定.
1 2 3 4 5 6 7 8 9 10 |
var headers = new Headers(); headers.append("Content-Type", "application/json;charset=UTF-8"); fetch(url, { method: 'post', headers: headers, body: JSON.stringify({ date: '2016-10-08', time: '15:16:00' }) }); |
credentials
跨域請求中需要帶有cookie時, 可在fetch方法的第二個引數物件中新增credentials屬性, 並將值設定為”include”.
1 2 3 |
fetch(url,{ 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請求(毫無違和感). 如下:
1 2 3 4 5 6 7 8 9 10 |
var word = '123', url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3'; (async ()=>{ try { let res = await fetch(url, {mode: 'no-cors'});//等待fetch被resolve()後才能繼續執行 console.log(res); } catch(e) { console.log(e); } })(); |
自然, async/await 也可處理 Promise 物件.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let wait = function(ts){ return new Promise(function(resolve, reject){ setTimeout(resolve,ts,'Copy that!'); }); }; (async function(){ try { let res = await wait(1000);//① 等待1s後返回結果 console.log(res); res = await wait(1000);//② 重複執行一次 console.log(res); } catch(e) { console.log(e); } })(); //"Copy that!" |
可見使用await後, 可以直接得到返回值, 不必寫 .then(callback)
, 也不必寫 .catch(error)
了, 更可以使用 try catch
標準語法捕獲錯誤.
由於await採用的是同步的寫法, 看起來它就和alert函式一樣, 可以自動阻塞上下文. 因此它可以重複執行多次, 就像上述程式碼②一樣.
可以看到, await/async 同步阻塞式的寫法解決了完全使用 Promise 的一大痛點——不同Promise之間共享資料問題. Promise 需要設定上層變數從而實現資料共享, 而 await/async 就不存在這樣的問題, 只需要像寫alert一樣書寫就可以了.
值得注意的是, await 只能用於 async 宣告的函式上下文中. 如下 forEach 中, 是不能直接使用await的.
1 2 3 4 5 6 7 |
let array = [0,1,2,3,4,5]; (async ()=>{ array.forEach(function(item){ await wait(1000);//這是錯誤的寫法, 因await不在async宣告的函式上下文中 console.log(item); }); })(); |
如果是試圖將async宣告的函式作為回撥傳給forEach,該回撥將同時觸發多次,回撥內部await依然有效,只是多次的await隨著回撥一起同步執行了,這便不符合我們阻塞迴圈的初衷。如下:
1 2 3 4 5 |
const fn = async (item)=>{ await wait(1000); // 迴圈中的多個await同時執行,因此等待1s後將同時輸出陣列各個元素 console.log(item); }; array.forEach(fn); |
正確的寫法如下:
1 2 3 4 5 6 |
(async ()=>{ for(let i=0,len=array.length;i<len;i++){ await wait(1000); console.log(array[i]); } })(); |
如何試執行async/await
鑑於目前只有Edge支援 async/await, 我們可以使用以下方法之一執行我們的程式碼.
- 隨著node7.0的釋出, node中可以使用如下方式直接執行:
1node --harmony-async-await test.js - babel線上編譯並執行 Babel · The compiler for writing next generation JavaScript .
- 本地使用babel編譯es6或更高版本es.
1) 安裝.
由於Babel5預設自帶各種轉換外掛, 不需要手動安裝. 然而從Babel6開始, 外掛需要手動下載, 因此以下安裝babel後需要再順便安裝兩個外掛.
1 2 3 |
npm i babel-cli -g # babel已更名為babel-cli npm install babel-preset-es2015 --save-dev npm install babel-preset-stage-0 --save-dev |
2) 書寫.babelrc配置檔案.
1 2 3 4 5 6 7 |
{ "presets": [ "es2015", "stage-0" ], "plugins": [] } |
3) 如果不配置.babelrc. 也可在命令列顯式指定外掛.
1 |
babel es6.js -o es5.js --presets es2015 stage-0 # 指定使用外掛es2015和stage-0編譯js |
4) 編譯.
1 2 3 |
babel es6.js -o es5.js # 編譯原始檔es6.js,輸出為es5.js,編譯規則在上述.babelrc中指定 babel es6.js --out-file es5.js # 或者將-o寫全為--out-file也行 bable es6.js # 如果不指定輸出檔案路徑,babel會將編譯生成的文字標準輸出到控制檯 |
5) 實時編譯
1 2 |
babel es6.js -w -o es5.js # 實時watch es6.js的變化,一旦改變就重新編譯 babel es6.js -watch -o es5.js # -w也可寫全為--watch |
6) 編譯目錄輸出到其他目錄
1 2 |
babel src -d build # 編譯src目錄下所有js,並輸出到build目錄 babel src --out-dir build # -d也可寫全為--out-dir |
7) 編譯目錄輸出到單個檔案
1 |
babel src -o es5.js # 編譯src目錄所有js,合併輸出為es5.js |
8) 想要直接執行es6.js, 可使用babel-node.
1 2 |
npm i babel-node -g # 全域性安裝babel-node babel-node es6.js # 直接執行js檔案 |
9) 如需在程式碼中使用fetch, 且使用babel-node執行, 需引入 node-fetch
模組.
1 |
npm i node-fetch --save-dev |
然後在es6.js中require node-fetch
模組.
1 |
var fetch = 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
使用起來非常簡單. 如下是安裝:
1 |
npm install fetch-jsonp --save-dev |
如下是使用:
1 2 3 4 5 6 7 8 |
fetchJsonp(url, { timeout: 3000, jsonpCallback: 'callback' }).then(function(response) { console.log(response.json()); }).catch(function(e) { console.log(e) }); |
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()也隨之可用. 下面我們來看下實現.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var _fetch = (function(fetch){ return function(url,options){ var abort = null; var abort_promise = new Promise((resolve, reject)=>{ abort = () => { reject('abort.'); console.info('abort done.'); }; }); var promise = Promise.race([ fetch(url,options), abort_promise ]); promise.abort = abort; return promise; }; })(fetch); |
然後, 使用如下方法測試新的fetch.
1 2 3 4 5 6 7 8 9 |
var p = _fetch('https://www.baidu.com',{mode:'no-cors'}); p.then(function(res) { console.log('response:', res); }, function(e) { console.log('error:', e); }); p.abort(); //"abort done." //"error: abort." |
以上, fetch請求後, 立即呼叫abort方法, 該promise被拒絕, 符合預期. 細心的同學可能已經注意到了, “p.abort();” 該語句我是單獨寫一行的, 沒有鏈式寫在then方法之後. 為什麼這麼幹呢? 這是因為then方法呼叫後, 返回的是新的promise物件. 該物件不具有abort方法, 因此使用時要注意繞開這個坑.
timeout
同上, 由於Promise的限制, fetch 並不支援原生的timeout機制, 但這並不妨礙我們使用 Promise.race() 實現一個.
下面是一個簡易的版本.
1 2 3 4 5 6 7 8 9 |
function timer(t){ return new Promise(resolve=>setTimeout(resolve, t)) .then(function(res) { console.log('timeout'); }); } var p = fetch('https://www.baidu.com',{mode:'no-cors'}); Promise.race([p, timer(1000)]); //"timeout" |
實際上, 無論超時時間設定為多長, 控制檯都將輸出log “timeout”. 這是因為, 即使fetch執行成功, 外部的promise執行完畢, 此時 setTimeout 所在的那個promise也不會reject.
下面我們來看一個類似xhr版本的timeout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var _fetch = (function(fetch){ return function(url,options){ var abort = null, timeout = 0; var abort_promise = new Promise((resolve, reject)=>{ abort = () => { reject('timeout.'); console.info('abort done.'); }; }); var promise = Promise.race([ fetch(url,options), abort_promise ]); promise.abort = abort; Object.defineProperty(promise, 'timeout',{ set: function(ts){ if((ts=+ts)){ timeout = ts; setTimeout(abort,ts); } }, get: function(){ return timeout; } }); return promise; }; })(fetch); |
然後, 使用如下方法測試新的fetch.
1 2 3 4 5 6 7 8 9 |
var p = _fetch('https://www.baidu.com',{mode:'no-cors'}); p.then(function(res) { console.log('response:', res); }, function(e) { console.log('error:', e); }); p.timeout = 1; //"abort done." //"error: timeout." |
progress
xhr的 onprogress 讓我們可以掌控下載進度, fetch顯然沒有提供原生api 做類似的事情. 不過 Fetch中的Response.body
中實現了getReader()
方法用於讀取原始位元組流, 該位元組流可以迴圈讀取, 直到body下載完成. 因此我們完全可以模擬fetch的progress.
以下是 stackoverflow 上的一段程式碼, 用於模擬fetch的progress事件. 為了方便測試, 請求url已改為本地服務.(原文請戳 javascript – Progress indicators for fetch? – Stack Overflow)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function consume(reader) { var total = 0 return new Promise((resolve, reject) => { function pump() { reader.read().then(({done, value}) => { if (done) { resolve(); return; } total += value.byteLength; console.log(`received ${value.byteLength} bytes (${total} bytes in total)`); pump(); }).catch(reject) } pump(); }); } fetch('http://localhost:10101/notification/',{mode:'no-cors'}) .then(res => consume(res.body.getReader())) .then(() => console.log("consumed the entire body without keeping the whole thing in memory!")) .catch(e => console.log("something went wrong: " + e)); |
以下是日誌截圖:
剛好github上有個fetch progress的demo, 感興趣的小夥伴請參看這裡: Fetch Progress DEMO .
我們不妨來對比下, 使用xhr的onprogress事件回撥, 輸出如下:
我試著適當增加響應body的size, 發現xhr的onprogress事件回撥依然只執行兩次. 通過多次測試發現其執行頻率比較低, 遠不及fetch progress.
本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.
參考文章