fetch雖好用,但也有缺陷。它先天缺失超時機制,終止機制,進度反饋等。本文在講解fetch的同時,手把手教你寫fetch的timeout,abort以及progress。javascript
Fetch 是 web異步通訊的將來. 從chrome42, Firefox39, Opera29, EdgeHTML14(並不是Edge版本)起, fetch就已經被支持了. 其中chrome42~45版本, fetch對中文支持有問題, 建議從chrome46起使用fetch. 傳送門: fetch中文亂碼 .html
先過一遍Fetch原生支持率.前端
可見要想在IE8/9/10/11中使用fetch仍是有些犯難的,畢竟它連 Promise 都不支持, 更別說fetch了. 別急, 這裏有polyfill(墊片). java
es5-shim, es5-sham
.es6-promise
.fetch-ie8
.因爲IE8基於ES3, IE9支持大部分ES5, IE11支持少許ES5, 其中只有IE10對ES5支持比較完整. 所以IE8+瀏覽器, 建議依次裝載上述墊片.node
先來看一個簡單的fetch.react
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
對象.git
該fetch能夠在任何域名的網站直接運行, 且能正常返回百度搜索的建議詞條. 如下是常規輸入時的是界面截圖.es6
如下是剛纔fetch到的部分數據. 其中key name 爲"s"的字段的value就是以上的建議詞條.(因爲有高亮詞條"12306", 最後一條數據"12366"被頂下去了, 故上面截圖上看不到)github
看完栗子事後, 就要動真格了. 下面就來扒下 Fetch.web
fetch方法返回一個Promise對象, 根據 Promise Api
的特性, fetch能夠方便地使用then方法將各個處理邏輯串起來, 使用 Promise.resolve() 或 Promise.reject() 方法將分別返會確定結果的Promise或否認結果的Promise, 從而調用下一個then 或者 catch. 一但then中的語句出現錯誤, 也將跳到catch中.
Promise如有疑問, 請閱讀 Promises .
① 咱們不妨在 sp0.baidu.com 域名的網頁控制檯運行如下代碼.
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);
});複製代碼
運行截圖以下:
② 咱們不妨在非 sp0.baidu.com 域名的網頁控制檯再次運行以上代碼.(別忘了給fetch的第二參數傳遞{mode: "no-cors"})
運行截圖以下:
因爲第一次進入then分支後, 返回了否認結果的 Promise.reject 對象. 所以代碼進入到catch分支, 拋出了錯誤. 此時, 上述 response.type
爲 opaque
.
一個fetch請求的響應類型(response.type)爲以下三種之一:
如上情景①, 同域下, 響應類型爲 "basic".
如上情景②中, 跨域下, 服務器沒有返回CORS響應頭, 響應類型爲 "opaque". 此時咱們幾乎不能查看任何有價值的信息, 好比不能查看response, status, url等等等等.
一樣是跨域下, 若是服務器返回了CORS響應頭, 那麼響應類型將爲 "cors". 此時響應頭中除 Cache-Control
, Content-Language
, Content-Type
, Expores
, Last-Modified
和 Progma
以外的字段都不可見.
注意: 不管是同域仍是跨域, 以上 fetch 請求都到達了服務器.
fetch能夠設置不一樣的模式使得請求有效. 模式可在fetch方法的第二個參數對象中定義.
fetch(url, {mode: 'cors'});複製代碼
可定義的模式以下:
除此以外, 還有兩種不太經常使用的mode類型, 分別是 navigate
, websocket
, 它們是 HTML標準 中特殊的值, 這裏不作詳細介紹.
fetch獲取http響應頭很是easy. 以下:
fetch(url).then(function(response) {
console.log(response.headers.get('Content-Type'));
});複製代碼
設置http請求頭也同樣簡單.
var headers = new Headers();
headers.append("Content-Type", "text/html");
fetch(url,{
headers: headers
});複製代碼
header的內容也是能夠被檢索的.
var header = new Headers({
"Content-Type": "text/plain"
});
console.log(header.has("Content-Type")); //true
console.log(header.has("Content-Length")); //false複製代碼
在fetch中發送post請求, 一樣能夠在fetch方法的第二個參數對象中設置.
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'
})
});複製代碼
跨域請求中須要帶有cookie時, 可在fetch方法的第二個參數對象中添加credentials屬性, 並將值設置爲"include".
fetch(url,{
credentials: 'include'
});複製代碼
除此以外, credentials 還能夠取如下值:
同 XMLHttpRequest 同樣, 不管服務器返回什麼樣的狀態碼(chrome中除407以外的其餘狀態碼), 它們都不會進入到錯誤捕獲裏. 也就是說, 此時, XMLHttpRequest 實例不會觸發 onerror
事件回調, fetch 不會觸發 reject. 一般只在網絡出現問題時或者ERR_CONNECTION_RESET時, 它們纔會進入到相應的錯誤捕獲裏. (其中, 請求返回狀態碼爲407時, chrome瀏覽器會觸發onerror或者reject掉fetch.)
cache表示如何處理緩存, 遵照http規範, 擁有以下幾種值:
若是fetch請求的header裏包含 If-Modified-Since
, If-None-Match
, If-Unmodified-Since
, If-Match
, 或者 If-Range
之一, 且cache的值爲 default
, 那麼fetch將自動把 cache的值設置爲 "no-store"
.
回調深淵一直是jser的一塊心病, 雖然ES6提供了 Promise, 將嵌套平鋪, 但使用起來依然不便.
要說ES6也提供了generator/yield, 它將一個函數執行暫停, 保存上下文, 再次調用時恢復當時的狀態.(學習可參考 Generator 函數的含義與用法 - 阮一峯的網絡日誌) 不管如何, 總感受彆扭. 以下摘自推庫的一張圖.
咱們不難看出其中的差距, callback簡單粗暴, 層層回調, 回調越深刻, 越不容易捋清楚邏輯. Promise 將異步操做規範化.使用then鏈接, 使用catch捕獲錯誤, 堪稱完美, 美中不足的是, then和catch中傳遞的依然是回調函數, 與心目中的同步代碼不是一個套路.
爲此, ES7 提供了更標準的解決方案 — async/await. async/await 幾乎沒有引入新的語法, 表面上看起來, 它就和alert同樣易用, 雖然它尚處於ES7的草案中, 不過這並不影響咱們提早使用它.
async 用於聲明一個異步函數, 該函數需返回一個 Promise 對象. 而 await 一般後接一個 Promise對象, 需等待該 Promise 對象的 resolve() 方法執行而且返回值後才能繼續執行. (若是await後接的是其餘對象, 便會當即執行)
所以, async/await 天生可用於處理 fetch請求(毫無違和感). 以下:
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 對象.
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的.
let array = [0,1,2,3,4,5];
(async ()=>{
array.forEach(function(item){
console.log(item);
await wait(1000);//這是錯誤的寫法
});
})();
//因await只能用於 async 聲明的函數上下文中, 故不能寫在forEach內.下面咱們來看正確的寫法
(async ()=>{
for(let i=0,len=array.length;i<len;i++){
console.log(array[i]);
await wait(1000);
}
})();複製代碼
鑑於目前只有Edge支持 async/await, 咱們可使用如下方法之一運行咱們的代碼.
隨着node7.0的發佈, node中可使用以下方式直接運行:
node --harmony-async-await test.js複製代碼
babel在線編譯並運行 Babel · The compiler for writing next generation JavaScript .
本地使用babel編譯es6或更高版本es.
1) 安裝.
因爲Babel5默認自帶各類轉換插件, 不須要手動安裝. 然而從Babel6開始, 插件須要手動下載, 所以如下安裝babel後須要再順便安裝兩個插件.
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配置文件.
{
"presets": [
"es2015",
"stage-0"
],
"plugins": []
}複製代碼
3) 若是不配置.babelrc. 也可在命令行顯式指定插件.
babel es6.js -o es5.js --presets es2015 stage-0 # 指定使用插件es2015和stage-0編譯js複製代碼
4) 編譯.
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) 實時編譯
babel es6.js -w -o es5.js # 實時watch es6.js的變化,一旦改變就從新編譯
babel es6.js -watch -o es5.js # -w也可寫全爲--watch複製代碼
6) 編譯目錄輸出到其餘目錄
babel src -d build # 編譯src目錄下全部js,並輸出到build目錄
babel src --out-dir build # -d也可寫全爲--out-dir複製代碼
7) 編譯目錄輸出到單個文件
babel src -o es5.js # 編譯src目錄全部js,合併輸出爲es5.js複製代碼
8) 想要直接運行es6.js, 可以使用babel-node.
npm i babel-node -g # 全局安裝babel-node
babel-node es6.js # 直接運行js文件複製代碼
9) 如需在代碼中使用fetch, 且使用babel-node運行, 需引入 node-fetch
模塊.
npm i node-fetch --save-dev複製代碼
而後在es6.js中require node-fetch
模塊.
var fetch = require('node-fetch');複製代碼
本地使用traceur編譯es6或更高版本es.請參考 在項目開發中優雅地使用ES6:Traceur & Babel .
fetch基於Promise, Promise受限, fetch也難倖免. ES6的Promise基於 Promises/A+ 規範 (對規範感興趣的同窗可選讀 剖析源碼理解Promises/A規範 ), 它只提供極簡的api, 沒有 timeout 機制, 沒有 progress 提示, 沒有 deferred 處理 (這個能夠被async/await替代).
除此以外, fetch還不支持jsonp請求. 不過辦法總比問題多, 萬能的開源做者提供了 fetch-jsonp
庫, 解決了這個問題.
fetch-jsonp
使用起來很是簡單. 以下是安裝:
npm install fetch-jsonp --save-dev複製代碼
以下是使用:
fetchJsonp(url, {
timeout: 3000,
jsonpCallback: 'callback'
}).then(function(response) {
console.log(response.json());
}).catch(function(e) {
console.log(e)
});複製代碼
因爲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()也隨之可用. 下面咱們來看下實現.
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.
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方法, 所以使用時要注意繞開這個坑.
同上, 因爲Promise的限制, fetch 並不支持原生的timeout機制, 但這並不妨礙咱們使用 Promise.race() 實現一個.
下面是一個簡易的版本.
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.
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.
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."複製代碼
xhr的 onprogress 讓咱們能夠掌控下載進度, fetch顯然沒有提供原生api 作相似的事情. 不過 Fetch中的Response.body
中實現了getReader()
方法用於讀取原始字節流, 該字節流能夠循環讀取, 直到body下載完成. 所以咱們徹底能夠模擬fetch的progress.
如下是 stackoverflow 上的一段代碼, 用於模擬fetch的progress事件. 爲了方便測試, 請求url已改成本地服務.(原文請戳 javascript - Progress indicators for fetch? - Stack Overflow)
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.
本次徵文活動的連接: juejin.im/post/58d8e9…
本問就討論這麼多內容,你們有什麼問題或好的想法歡迎在下方參與留言和評論.
本文做者: louis
本文連接: louiszhai.github.io/2016/10/19/…
參考文章