fetch使用的常見問題及解決辦法

引言

說道fetch就不得不提XMLHttpRequest了,XHR在發送web請求時須要開發者配置相關請求信息和成功後的回調,儘管開發者只關心請求成功後的業務處理,可是也要配置其餘繁瑣內容,致使配置和調用比較混亂,也不符合關注分離的原則;fetch的出現正是爲了解決XHR存在的這些問題。例以下面代碼:javascript

fetch(url).then(function(response) { return response.json(); }).then(function(data) { console.log(data); }).catch(function(e) { console.log("Oops, error"); });

上面這段代碼讓開發者只關注請求成功後的業務邏輯處理,其餘的不用關心,至關簡單;也比較符合現代Promise形式,比較友好。css

fetch是基於Promise設計的,從上面代碼也能看得出來,這就要求fetch要配合Promise一塊兒使用。正是這種設計,fetch所帶來的優勢正如傳統 Ajax 已死,Fetch 永生總結的同樣:html

  • 語法簡單,更加語義化前端

  • 基於標準的Promise實現,支持async/awaitjava

  • 使用isomorphic-fetch能夠方便同構git

不過話說回來,fetch雖然有不少優勢,可是使用fetch來進行項目開發時,也是有一些常見問題的,下面就來講說fetch使用的常見問題。es6

fetch兼容性

fetch是相對較新的技術,固然就會存在瀏覽器兼容性的問題,借用上面應用文章的一幅圖加以說明fetch在各類瀏覽器的原生支持狀況:
github

從上圖能夠看出,在各個瀏覽器低版本的狀況下都是不被支持的。web

那麼問題來了,如何在全部瀏覽器中通用fetch呢,固然就要考慮fetch的polyfill了。面試

上面說過,fetch是基於Promise來實現的,因此在低版本瀏覽器中Promise可能也未被原生支持,因此還須要Promise的polyfill;大多數狀況下,實現fetch的polyfill須要涉及到的:

  • promise的polyfill,例如es6-promise、babel-polyfill提供的promise實現。
  • fetch的polyfill實現,例如isomorphic-fetch和whatwg-fetch

這樣是否就能夠安全的使用fetch來進行先後端通訊了?上面說了在大多數狀況下是這樣,可是IE8/9則比較特殊:IE8它使用的是ES3,而IE9則對ES5部分支持。這種狀況下還須要ES5的polyfill es5-shim支持了。

上述有關promise的polyfill實現,須要說明的是:

babel-runtime是不能做爲Promise的polyfill的實現的,不然在IE8/9下使用fetch會報Promise未定義。爲何?我想你們猜到了,由於babel-runtime實現的polyfill是局部實現而不是全局實現,fetch底層實現用到Promise就是從全局中去取的,拿不到這報上述錯誤。

另外,順便補充一下fetch的polyfill實現思路是:

首先判斷瀏覽器是否原生支持fetch,不然結合Promise使用XMLHttpRequest的方式來實現;這正是whatwg-fetch的實現思路,而同構應用中使用的isomorphic-fetch,其客戶端fetch的實現是直接require whatwg-fetch來實現的。

fetch默認不攜帶cookie

fetch發送請求默認是不發送cookie的,不論是同域仍是跨域;那麼問題就來了,對於那些須要權限驗證的請求就可能沒法正常獲取數據,這時能夠配置其credentials項,其有3個值:

  • omit: 默認值,忽略cookie的發送
  • same-origin: 表示cookie只能同域發送,不能跨域發送
  • include: cookie既能夠同域發送,也能夠跨域發送

credentials所表達的含義,其實與XHR2中的withCredentials屬性相似,表示請求是否攜帶cookie;具體能夠參考阮一峯老師的跨域資源共享 CORS 詳解中withCredentials一節的介紹;

這樣,若要fetch請求攜帶cookie信息,只需設置一下credentials選項便可,例如fetch(url, {credentials: 'include'});

另外補充一點:

fetch默認對服務端經過Set-Cookie頭設置的cookie也會忽略,若想選擇接受來自服務端的cookie信息,也必需要配置credentials選項;

fetch請求對某些錯誤http狀態不會reject

這主要是由fetch返回promise致使的,由於fetch返回的promise在某些錯誤的http狀態下如400、500等不會reject,相反它會被resolve;只有網絡錯誤會致使請求不能完成時,fetch 纔會被 reject;因此通常會對fetch請求作一層封裝,例以下面代碼所示:

function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } const error = new Error(response.statusText); error.response = response; throw error; } function parseJSON(response) { return response.json(); } export default function request(url, options) { let opt = options||{}; return fetch(url, {credentials: 'include', ...opt}) .then(checkStatus) .then(parseJSON) .then((data) => ( data )) .catch((err) => ( err )); }

fetch不支持超時timeout處理

用過fetch的都知道,fetch不像大多數ajax庫那樣對請求設置超時timeout,它沒有有關請求超時的feature,這一點比較蛋疼。因此在fetch標準添加超時feature以前,都須要polyfill該特性。

實際上,咱們真正須要的是abort(), timeout能夠經過timeout+abort方式來實現,起到真正超時丟棄當前的請求。

而在目前的fetch指導規範中,fetch並非一個具體實例,而只是一個方法;其返回的promise實例根據Promise指導規範標準是不能abort的,也不能手動改變promise實例的狀態,只能由內部來根據請求結果來改變promise的狀態。

既然不能手動控制fetch方法執行後返回的promise實例狀態,那麼是否是能夠建立一個能夠手動控制狀態的新Promise實例呢。因此:

實現fetch的timeout功能,其思想就是新建立一個能夠手動控制promise狀態的實例,根據不一樣狀況來對新promise實例進行resolve或者reject,從而達到實現timeout的功能;

根據github上timeout handling上的討論,目前能夠有兩種不一樣的解決方法:

方法一:單純setTimeout方式

var oldFetchfn = fetch; //攔截原始的fetch方法 window.fetch = function(input, opts){//定義新的fetch方法,封裝原有的fetch方法 return new Promise(function(resolve, reject){ var timeoutId = setTimeout(function(){ reject(new Error("fetch timeout")) }, opts.timeout); oldFetchfn(input, opts).then( res=>{ clearTimeout(timeoutId); resolve(res) }, err=>{ clearTimeout(timeoutId); reject(err) } ) }) }

固然在上面基礎上能夠模擬相似XHR的abort功能:

var oldFetchfn = fetch; window.fetch = function(input, opts){ return new Promise(function(resolve, reject){ var abort_promise = function(){ reject(new Error("fetch abort")) }; var p = oldFetchfn(input, opts).then(resolve, reject); p.abort = abort_promise; return p; }) }

方法二:利用Promise.race方法

Promise.race方法接受一個promise實例數組參數,表示多個promise實例中任何一個最早改變狀態,那麼race方法返回的promise實例狀態就跟着改變,具體能夠參考這裏

var oldFetchfn = fetch; //攔截原始的fetch方法 window.fetch = function(input, opts){//定義新的fetch方法,封裝原有的fetch方法 var fetchPromise = oldFetchfn(input, opts); var timeoutPromise = new Promise(function(resolve, reject){ setTimeout(()=>{ reject(new Error("fetch timeout")) }, opts.timeout) }); retrun Promise.race([fetchPromise, timeoutPromise]) }

最後,對fetch的timeout的上述實現方式補充幾點:

timeout不是請求鏈接超時的含義,它表示請求的response時間,包括請求的鏈接、服務器處理及服務器響應回來的時間;

fetch的timeout即便超時發生了,本次請求也不會被abort丟棄掉,它在後臺仍然會發送到服務器端,只是本次請求的響應內容被丟棄而已;

fetch不支持JSONP

fetch是與服務器端進行異步交互的,而JSONP是外鏈一個javascript資源,並非真正ajax,因此fetch與JSONP沒有什麼直接關聯,固然至少目前是不支持JSONP的。

這裏咱們把JSONP與fetch關聯在一塊兒有點差強人意,fetch只是一個ajax庫,咱們不可能使fetch支持JSONP;只是咱們要實現一個JSONP,只不過這個JSONP的實現要與fetch的實現相似,即基於Promise來實現一個JSONP;而其外在表現給人感受是fetch支持JSONP同樣;

目前比較成熟的開源JSONP實現fetch-jsonp給咱們提供瞭解決方案,想了解能夠自行前往。不過再次想嘮叨一下其JSONP的實現步驟,由於在本人面試的前端候選人中大部分人對JSONP的實現語焉不詳;

使用它很是簡單,首先須要用npm安裝fetch-jsonp

npm install fetch-jsonp --save-dev

而後在像下面同樣使用:

fetchJsonp('/users.jsonp', { timeout: 3000, jsonpCallback: 'custom_callback' }) .then(function(response) { return response.json() }).catch(function(ex) { console.log('parsing failed', ex) })

fetch不支持progress事件

XHR是原生支持progress事件的,例以下面代碼這樣:

var xhr = new XMLHttpRequest() xhr.open('POST', '/uploads') xhr.onload = function() {} xhr.onerror = function() {} function updateProgress (event) { if (event.lengthComputable) { var percent = Math.round((event.loaded / event.total) * 100) console.log(percent) } xhr.upload.onprogress =updateProgress; //上傳的progress事件 xhr.onprogress = updateProgress; //下載的progress事件 } xhr.send();

可是fetch是不支持有關progress事件的;不過可喜的是,根據fetch的指導規範標準,其內部設計實現了RequestResponse類;其中Response封裝一些方法和屬性,經過Response實例能夠訪問這些方法和屬性,例如response.json()response.body等等;

值得關注的地方是,response.body是一個可讀字節流對象,其實現了一個getRender()方法,其具體做用是:

getRender()方法用於讀取響應的原始字節流,該字節流是能夠循環讀取的,直至body內容傳輸完成;

所以,利用到這點能夠模擬出fetch的progress,具體能夠參考這篇文章2016 - the year of web streams

代碼實現以下,在線demo請參考fetch progress demo

// fetch() returns a promise that resolves once headers have been received fetch(url).then(response => { // response.body is a readable stream. // Calling getReader() gives us exclusive access to the stream's content var reader = response.body.getReader(); var bytesReceived = 0; // read() returns a promise that resolves when a value has been received reader.read().then(function processResult(result) { // Result objects contain two properties: // done - true if the stream has already given you all its data. // value - some data. Always undefined when done is true. if (result.done) { console.log("Fetch complete"); return; } // result.value for fetch streams is a Uint8Array bytesReceived += result.value.length; console.log('Received', bytesReceived, 'bytes of data so far'); // Read some more, and call this function again return reader.read().then(processResult); }); });

另外,github上也有使用Promise+XHR結合的方式實現類fetch的progress效果(固然這跟fetch徹底不搭邊)能夠參考這裏,具體代碼以下:

function fetchProgress(url, opts={}, onProgress){ return new Promise(funciton(resolve, reject){ var xhr = new XMLHttpRequest(); xhr.open(opts.method || 'get', url); for(var key in opts.headers || {}){ xhr.setRequestHeader(key, opts.headers[key]); } xhr.onload = e => resolve(e.target.responseText) xhr.onerror = reject; if (xhr.upload && onProgress){ xhr.upload.onprogress = onProgress; //上傳 } if ('onprogerss' in xhr && onProgress){ xhr.onprogress = onProgress; //下載 } xhr.send(opts.body) }) } fetchProgress('/upload').then(console.log)

fetch跨域問題

既然是ajax庫,就不可避免與跨域扯上關係;XHR2是支持跨域請求的,只不過要知足瀏覽器端支持CORS,服務器經過Access-Control-Allow-Origin來容許指定的源進行跨域,僅此一種方式。

與XHR2同樣,fetch也是支持跨域請求的,只不過其跨域請求作法與XHR2同樣,須要客戶端與服務端支持;另外,fetch還支持一種跨域,不須要服務器支持的形式,具體能夠經過其mode的配置項來講明。

fetch的mode配置項有3個值,以下:

  • same-origin:該模式是不容許跨域的,它須要遵照同源策略,不然瀏覽器會返回一個error告知不能跨域;其對應的response type爲basic

  • cors: 該模式支持跨域請求,顧名思義它是以CORS的形式跨域;固然該模式也能夠同域請求不須要後端額外的CORS支持;其對應的response type爲cors

  • no-cors: 該模式用於跨域請求可是服務器不帶CORS響應頭,也就是服務端不支持CORS;這也是fetch的特殊跨域請求方式;其對應的response type爲opaque

針對跨域請求,cors模式是常見跨域請求實現,可是fetch自帶的no-cors跨域請求模式則較爲陌生,該模式有一個比較明顯的特色:

該模式容許瀏覽器發送本次跨域請求,可是不能訪問響應返回的內容,這也是其response type爲opaque透明的緣由。

這與<img/>發送的請求相似,只是該模式不能訪問響應的內容信息;可是它能夠被其餘APIs進行處理,例如ServiceWorker。另外,該模式返回的repsonse能夠在Cache API中被存儲起來以便後續的對它的使用,這點對script、css和圖片的CDN資源是很是合適的,由於這些資源響應頭中都沒有CORS頭。

總的來講,fetch的跨域請求是使用CORS方式,須要瀏覽器和服務端的支持。

相關文章
相關標籤/搜索