fetch body裏數據爲ReadableStream 解決辦法

前端工程中發送 HTTP 請求歷來都不是一件容易的事,前有駭人的 ActiveXObject ,後有 API 設計十分別扭的 XMLHttpRequest ,甚至這些原生 API 的用法至今還是不少大公司前端校招的考點之一。javascript

也正是如此,fetch 的出如今前端圈子裏一石激起了千層浪,你們歡呼雀躍彈冠相慶巴不得立刻把項目中的 $.ajax 所有幹掉。然而,在新鮮感事後, fetch 真的有你想象的那麼美好嗎?前端

若是你還不瞭解 fetch,能夠參考個人同事 @camsong 在 2015 年寫的文章 《傳統 Ajax 已死,Fetch 永生》java

在開始「批鬥」fetch以前,你們須要明確 fetch 的定位: fetch 是一個 low-level 的 API,它註定不會像你習慣的 $.ajax 或是 axios 等庫幫你封裝各類各樣的功能或實現。 也正是由於這個定位,在學習或使用 fetch API 時,你會遇到很多的挫折。ios

(對於沒有耐心看徹底文的同窗,請先記住本文的主旨不在於批評 fetch,事實上 fetch 的出現絕對是前端領域的進步體現。在瞭解主旨的前提下,關注 加黑 部分便可。)git

發請求,比你想象的要複雜

不少人看到 fetch 的第一眼確定會被它簡潔的 API 吸引:github

fetch('http://abc.com/tiger.png');

原來須要 new XMLHttpRequest 等小十行代碼才能實現的功能現在一行代碼就能搞定,能不讓人動心嗎!ajax

可是當你真正在項目中使用時,少不了須要向服務端發送數據的過程,那麼使用 fetch 發送一個對象到服務端須要幾行代碼呢?(出於兼容性考慮,大部分的項目在發送 POST 請求時都會使用 application/x-www-form-urlencoded 這種 Content-Type )json

先來看看使用 jQuery 如何實現:axios

$.post('/api/add', {name: 'test'});

而後再看看 fetch 如何處理:api

fetch('/api/add', {  
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
  },
  body: Object.keys({name: 'test'}).map((key) => { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&') });

等等, body 字段那一長串代碼在幹什麼? 由於 fetch 是一個 low-level 的 API,因此你須要本身 encode HTTP 請求的 payload,還要本身指定 HTTP Header 中的 Content-Type 字段。

這樣就結束了嗎?若是你在本身的項目中這樣發送 POST 請求,極可能會獲得一個 401 Unauthorized 的結果(視你的服務端如何處理無權限的狀況而定)。若是你在仔細看一遍文檔,會發現 原來 fetch 在發送請求時默認不會帶上 Cookie!

好,咱們讓 fetch 帶上 Cookie:

fetch('/api/add', { method: 'POST', credentials: 'include', ... });

這樣,一個最基礎的 POST 請求才算可以發出去。

同理,若是你須要 POST 一個 JSON 到服務端,你須要這樣作:

fetch('/api/add', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, body: JSON.stringify({name: 'test'}) });

相比於 $.ajax 的封裝,是否是複雜的不是一點半點呢?

錯誤處理,比你想象的複雜

按理說,fetch 是基於 Promise 的 API,每一個 fetch 請求會返回一個 Promise 對象,而 Promise 的異常處理且不管是否方便,起碼你們是比較熟悉的了。然而 fetch 的異常處理,仍是有很多門道。

假如咱們用 fetch 請求一個不存在的資源:

fetch('xx.png')  
.then(() => { console.log('ok'); }) .catch(() => { console.log('error'); });

按照咱們的慣例 console 應該要打印出 「error」纔對,可事實又如何呢?有圖有真相:

爲何會打印出 「ok」呢?

按照 MDN 的 說法 ,fetch 只有在遇到網絡錯誤的時候纔會 reject 這個 promise,好比用戶斷網或請求地址的域名沒法解析等。只要服務器可以返回 HTTP 響應(甚至只是 CORS preflight 的 OPTIONS 響應),promise 必定是 resolved 的狀態。

因此要怎麼判斷一個 fetch 請求是否是成功呢?你得用 response.ok 這個字段:

fetch('xx.png')  
.then((response) => { if (response.ok) { console.log('ok'); } else { console.log('error'); } }) .catch(() => { console.log('error'); });

再執行一次,終於看到了正確的日誌:

Stream API,比你想象的複雜

當你的服務端返回的數據是 JSON 格式時,你確定但願 fetch 返回給你的是一個普通 JavaScript 對象,然而你拿到的是一個 Response 對象,而真正的請求結果 —— 即 response.body —— 則是一個 ReadableStream 。

fetch('/api/user.json?id=2')   // 服務端返回 {"name": "test", "age": 1} 字符串 .then((response) => { // 這裏拿到的 response 並非一個 {name: 'test', age: 1} 對象 return response.json(); // 將 response.body 經過 JSON.parse 轉換爲 JS 對象 }) .then(data => { console.log(data); // {name: 'test', age: 1} });

你可能以爲,這些寫在規範裏的技術細節使用 fetch 的人無需關心,然而在實際使用過程當中你會遇到各類各樣的問題迫使你不得不瞭解這些細節。

首先須要認可,fetch 將 response.body 設計成 ReadableStream 實際上是很是有前瞻性的,這種設計讓你在請求大致積文件時變得很是有用。然而,在咱們的平常使用中,仍是短小的 JSON 片斷更加常見。而爲了兼容不常見的設計,咱們不得很少一次 response.json() 的調用。

不只是調用變得麻煩,若是你的服務端採用了嚴格的 REST 風格, 對於某些特殊狀況並無返回 JSON 字符串,而是用了 HTTP 狀態碼(如: 204 No Content ),那麼在調用 response.json() 時則會拋出異常。

此外, Response 還限制了響應內容的重複讀取和轉換 ,例如以下代碼:

var prevFetch = window.fetch; window.fetch = function() { prevFetch.apply(this, arguments) .then(response => { return new Promise((resolve, reject) => { response.json().then(data => { if (data.hasError === true) { tracker.log('API Error'); } resolve(response); }); }); }); } fetch('/api/user.json?id=1') .then(response => { return response.json(); // 先將結果轉換爲 JSON 對象 }) .then(data => { console.log(data); });

是對 fetch 作了一個簡單的 AOP,試圖攔截全部的請求結果,並當返回的 JSON 對象中 hasError 字段若是爲 true 的話,打點記錄出錯的接口。

然而這樣的代碼會致使以下錯誤:

Uncaught TypeError: Already read

調試一番後,你會發現是由於咱們在切面中已經調用了 response.json() ,這個時候重複調用該方法時就會報錯。(實際上,再次調用其它任何轉換方法,如 .text() 也會報錯)

所以,想要在 fetch 上實現 AOP 仍需另闢蹊徑。

其它問題

1. fetch 不支持同步請求

你們都知道同步請求阻塞頁面交互,但事實上仍有很多項目在使用同步請求,多是歷史架構等等緣由。若是你切換了 fetch 則沒法實現這一點。

2. fetch 不支持取消一個請求

使用 XMLHttpRequest 你能夠用 xhr.abort() 方法取消一個請求(雖然這個方法也不是那麼靠譜,同時是否真的「取消」還依賴於服務端的實現),可是使用 fetch 就無能爲力了,至少目前是這樣的。

3. fetch 沒法查看請求的進度

使用 XMLHttpRequest 你能夠經過 xhr.onprogress 回調來動態更新請求的進度,而這一點目前 fetch 尚未原生支持。

小結

仍是要再次明確,fetch API 的出現絕對是推進了前端在請求發送功能方面的進步。

然而,也須要意識到, fetch 是一個至關底層的 API,在實際項目使用中,須要作各類各樣的封裝和異常處理,而並不是開箱即用 ,更作不到直接替換 $.ajax 或其餘請求庫。

參考資料

  1. fetch spec https://fetch.spec.whatwg.org/#body
  2. fetch 實現 https://github.com/github/fetch
  3. 什麼是 Already Read 報錯 http://stackoverflow.com/questions/34786358/what-does-this-error-mean-uncaught-typeerror-already-read
  4. 使用 fetch 處理 HTTP 請求失敗 https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
  5. https://jakearchibald.com/2015/thats-so-fetch/
相關文章
相關標籤/搜索