fetch body里數據為ReadableStream 解決辦法


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

也正是如此,fetch 的出現在前端圈子里一石激起了千層浪,大家歡呼雀躍彈冠相慶恨不得馬上把項目中的 $.ajax 全部干掉。然而,在新鮮感過后, fetch 真的有你想象的那么美好嗎?

如果你還不了解 fetch,可以參考我的同事 @camsong 在 2015 年寫的文章 《傳統 Ajax 已死,Fetch 永生》

在開始「批斗」fetch之前,大家需要明確 fetch 的定位: fetch 是一個 low-level 的 API,它注定不會像你習慣的 $.ajax 或是 axios 等庫幫你封裝各種各樣的功能或實現。 也正是因為這個定位,在學習或使用 fetch API 時,你會遇到不少的挫折。

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

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

很多人看到 fetch 的第一眼肯定會被它簡潔的 API 吸引:

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

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

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

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

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

然后再看看 fetch 如何處理:

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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM