從XHR到Fetch


iThome 網站首載:從XHR到 Fetch

談到 Ajax,一般會想到 XMLHttpRequest,不過使用上不便,就算是標準化後的 XMLHttpRequest Level 1 也只是功能上的加強,開發者通常會進一步地使用程式庫封裝,像是 jQuery 的 $.get$.post$.ajax,曾經有一陣子流行「你不需要 jQuery」,社群裏頭嚷嚷著 Fecth API 將會取代這一切。

粗糙的 XMLHttpRequest

從今日的角度來看,XMLHttpRequest 確實有許多設計不足之處,首先,一個 XMLHttpRequest 實例肩負著太多任務,包含了事件的註冊、請求標頭的設置、連線的開啟、資料的傳送、請求本體的設置、回應狀態的判斷、回應內容的取得等,完全不符合關 切點分離(Separation of Concerns)的原則,而且設定與呼叫順序混亂,像是經常地,開發者會搞不清楚,到底是要呼叫 open 前還是之後設定請求標頭。

就算是 2011 年標準化後的 XMLHttpRequest Level 1 也沒有改變 XMLHttpRequest 的設計,沒有適當地做職責分離也就算了,雖然增加了幾個可註冊的事件,然而依舊是採基本事件模型,而不是類似 DOM Level 2 事件模型那樣,可以註冊多個事件。

過去有不少程式庫試著封裝 XMLHttpRequest 來解決問題,例如,jQuery的 $.get$.post$.ajax$.ajax 可使用選項物件(Option object)來做更多細部設定(在 jQuery 3,$.get$.post 也可接受選項物件了),透過 $.ajaxSetup 等函式可設定預設值,這些設計非但隱藏了 XMLHttpRequest 的設定細節,也將一些職責從 XMLHttpRequest 中分離出來。

由於 Ajax 的處理天生就是非同步,這與開發者習慣的同步程式碼撰寫方式不同,而在非同步下順序也變得重要時,回呼地獄就會是個大問題,為此 jQuery 提供了 Deferred,而後社群中又有了 Promise/A 與 Promise/A+ 規範,jQuery 3 實現了 Promise/A+,$.ajax 可傳回 Promise 物件,提供了 Ajax 請求時更一致的模式,可以採用像是同步的程式碼來撰寫非同步應用。

HTML5 的 Fetch API

在 2014 年 HTML5 正式標準後不久,在 Ajax 這塊出現了許多 Fetch API 的介紹,Fetch API 是 HTML5 的一部份,Google、Mozilla 在 2015 年於瀏覽器開始提供實作,一時之間許多暢談 Fetch API 取代 XMLHttpRequest 的文章出現,也有不少直挑 jQuery 的 $.ajax 作為取代對象。

從設計的角度來看,Fetch API 就像是集合了過去 Ajax 使用上一些好實踐的集合體,可獨立地建立 HeadersRequestResponse 實例,實現了職責分離,建立時可使用選項物件來進行相關設定,而 Fetch 的工廠函式 fetch 也可接受選項物件,而傳回值是個 Promise

表面上看來,Fetch 很像在 XMLHttpRequest 上封裝了一層 Promise, 這也是它為什麼經常被拿來與 $.ajax 對比的原因之一,因為模式乍看之下十分類似,不過嚴格來說,$.ajax 做了比較高階的封裝,舉例來說,$.ajaxdata 選項指定物件時,會自動進行序列化與請求參數編碼處理,然而使用 fetchbody 選項時,必須自行建立、編碼請求參數,這是因為在 Fetch 的規範前 言中就清楚指出,fetch 的定位本來就是低階封裝。

Fetch 另一個與 XMLHttpRequest 不同的地方是 Streams 的支援,按照規範,回應物件的 body 特性會是個 ReadableStream,行 為上與 Streams 規範中的 ReadableStream 相同,在伺服器的回應過程中,可以透過 ReadableStream 持續讀取瀏覽器已接收之內容,雖然過去也可以使用 XMLHttpRequestresponseText 自行處理判斷、讀取想要的資料區段,然而,前者是直接處理串流資料,後者是對整個已取得之回應進行處理,本質上並不相同。

在使用 Fetch 的主要考量是瀏覽器的支援度,對於不支援 Fetch 的瀏覽器,可以使用 Fetch 修補(polyfill),修補是基於 XMLHttpRequest,仿造了 Fetch API 介面,由於 XMLHttpRequest 本身並沒有 Streams 的功能,因此在這方面功能受限。

回頭看看 Promise 

由於 Fetch 基於 Promise,在不支援 Promise 的瀏覽器上,除了 Fetch 修補之外,還要加上 Promise 修補(更舊的瀏覽器,像是 IE8/9,還要加上 ES5 修補等),而在介紹 Fetch 的文件中,都會談到的一些缺點,像是不支援逾時、進度處理等,其實並不是 Fetch 本身的缺失,主要是來自於 Promise 的限制。

因為 Promise/A+ 的規範,主要只有三個狀態,只能透過 resolvereject 從未定(pending)轉移至滿足(fulfilled)或背棄(rejected)狀態,Promise實例本身也只有 thencatch 兩個方法來處理對應的狀態,在不施加額外設計上,自然也就無法提供逾時、進度處理等功能。

若瞭解到某個 Fetch 的限制是來自於 Promise 的限制,就可以試著從設計下手來解決需求,例如,可以透過 Promise.race 提供兩個 Promise,一個是 new Promise((resolve, reject) => setTimeout(() => reject('timeout'), timeout),一個來自 fetch,若前者先背棄了, 那麼 Promise.race 傳回的 Promise 就算是背棄了,從而實現後續的逾時處理。

不過,這樣的設計只個模擬,fetch 傳回的 Promise 依舊會執行直到進入背棄或滿足狀態,而不是真的逾時而被中斷,只是 Promise.race 傳回的 Promise 會忽略其狀態罷了,簡而言之,在非同步的處理模式上,Fetch 是基於 Promise,因而要能在非同步處理上活用 Fetch,就建立在對 Promise 能有多少認識。

更進一步地,由於 Fetch 是基於 Promise,若開發者熟悉 Promise, 應該也就知道可以透過 ECMAScript 6 的產生器語法,採用像是同步的流程來撰寫非同步應用,進一步地,ECMAScript 7 提供了 asyncawait,無論是 Promise 本身或者是 Fetch,都可搭配 asyncawait 使用(可參考先前專欄〈從產生器到 asyncawait〉), 這會是它們未來的優勢之一。

Fetch 解決了什麼?

每當有新的技術或概念出現,人們總愛嚷嚷著舊的東西將會死去,新的東西會取代一切,喜新厭舊吧!舊東西誕生在舊的時代,適時地解決了當時的問題,而後 從中累積了不少的使用經驗,因而誕生了新的技術、概念或規範,急著預言舊東西將會逝去,並不會讓開發者看起來更為耀眼,只會讓開發者看不清楚新東西的 本質罷了。

舉個舊東西好了,$.ajax 可以就這麼與 fetch 來比較嗎?高階封裝與低階 API 可以直接比較嗎?也許某些程度上,可以用 Fetch 解決時,無需掛個 jQuery 程式庫會是件好事,然而,$.ajax 也許可以基於 Fetch 來重構,在 Fetch 做不到的部份,使用 XMLHttpRequest 來實現,XMLHttpRequest 畢竟也不是完全那麼不堪,或者乾脆寫個 $.fetch 之類的擴充來實現前述概念如何呢?

Fetch 實際上代表的是 Ajax 從 XMLHttpRequest、選項物件、回呼處理、Promise, 甚至往後銜接至產生器、async/await 的這條發展路線中,各種實作與經驗的累積與修正,瞭解這個過程中的累積與修正,才會知道 Fetch 解決了什麼,又有哪些沒解決的,別急著馬上判處某些舊東西死刑,不然,過陣子可能又會急著要處死 Fetch 了吧!