手把手教你玩轉Fetch | 掘金技術徵文

fetch雖好用,但也有缺陷。它先天缺失超時機制,終止機制,進度反饋等。本文在講解fetch的同時,手把手教你寫fetch的timeout,abort以及progress。javascript

關於

導讀

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

Fetch

先過一遍Fetch原生支持率.前端

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

因爲IE8基於ES3, IE9支持大部分ES5, IE11支持少許ES5, 其中只有IE10對ES5支持比較完整. 所以IE8+瀏覽器, 建議依次裝載上述墊片.node

嘗試一個fetch

先來看一個簡單的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

response headers

該fetch能夠在任何域名的網站直接運行, 且能正常返回百度搜索的建議詞條. 如下是常規輸入時的是界面截圖.es6

response headers

如下是剛纔fetch到的部分數據. 其中key name 爲"s"的字段的value就是以上的建議詞條.(因爲有高亮詞條"12306", 最後一條數據"12366"被頂下去了, 故上面截圖上看不到)github

response headers

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

Promise特性

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);
});複製代碼

運行截圖以下:

fetch then

② 咱們不妨在非 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方法的第二個參數對象中定義.

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標準 中特殊的值, 這裏不作詳細介紹.

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複製代碼

post

在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'
  })
});複製代碼

credentials

跨域請求中須要帶有cookie時, 可在fetch方法的第二個參數對象中添加credentials屬性, 並將值設置爲"include".

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請求(毫無違和感). 以下:

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);
  }
})();複製代碼
如何試運行async/await

鑑於目前只有Edge支持 async/await, 咱們可使用如下方法之一運行咱們的代碼.

  1. 隨着node7.0的發佈, node中可使用以下方式直接運行:

    node --harmony-async-await test.js複製代碼
  2. babel在線編譯並運行 Babel · The compiler for writing next generation JavaScript .

  3. 本地使用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');複製代碼
  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 使用起來很是簡單. 以下是安裝:

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)
});複製代碼

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()也隨之可用. 下面咱們來看下實現.

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方法, 所以使用時要注意繞開這個坑.

timeout

同上, 因爲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."複製代碼

progress

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/…

參考文章

相關文章
相關標籤/搜索