談到 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 使用上一些好實踐的集合體,可獨立地建立
Headers
、Request
、Response
實例,實現了職責分離,建立時可使用選項物件來進行相關設定,而 Fetch 的工廠函式 fetch
也可接受選項物件,而傳回值是個 Promise
。表面上看來,Fetch 很像在
XMLHttpRequest
上封裝了一層 Promise
,
這也是它為什麼經常被拿來與 $.ajax
對比的原因之一,因為模式乍看之下十分類似,不過嚴格來說,$.ajax
做了比較高階的封裝,舉例來說,$.ajax
的 data
選項指定物件時,會自動進行序列化與請求參數編碼處理,然而使用 fetch
的 body
選項時,必須自行建立、編碼請求參數,這是因為在 Fetch 的規範前
言中就清楚指出,fetch
的定位本來就是低階封裝。Fetch 另一個與
XMLHttpRequest
不同的地方是 Streams
的支援,按照規範,回應物件的 body
特性會是個 ReadableStream
,行
為上與 Streams
規範中的 ReadableStream
相同,在伺服器的回應過程中,可以透過 ReadableStream
持續讀取瀏覽器已接收之內容,雖然過去也可以使用 XMLHttpRequest
的 responseText
自行處理判斷、讀取想要的資料區段,然而,前者是直接處理串流資料,後者是對整個已取得之回應進行處理,本質上並不相同。在使用 Fetch 的主要考量是瀏覽器的支援度,對於不支援 Fetch 的瀏覽器,可以使用 Fetch 修補(polyfill),修補是基於
XMLHttpRequest
,仿造了 Fetch API 介面,由於 XMLHttpRequest
本身並沒有 Streams
的功能,因此在這方面功能受限。- 回頭看看
Promise
由於 Fetch 基於
Promise
,在不支援 Promise
的瀏覽器上,除了
Fetch 修補之外,還要加上 Promise
修補(更舊的瀏覽器,像是 IE8/9,還要加上 ES5
修補等),而在介紹 Fetch 的文件中,都會談到的一些缺點,像是不支援逾時、進度處理等,其實並不是 Fetch 本身的缺失,主要是來自於
Promise
的限制。因為 Promise/A+ 的規範,主要只有三個狀態,只能透過
resolve
、reject
從未定(pending)轉移至滿足(fulfilled)或背棄(rejected)狀態,Promise實例本身也只有 then
、catch
兩個方法來處理對應的狀態,在不施加額外設計上,自然也就無法提供逾時、進度處理等功能。若瞭解到某個 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 提供了
async
、await
,無論是 Promise
本身或者是
Fetch,都可搭配 async
、await
使用(可參考先前專欄〈從產生器到 async
、await
〉),
這會是它們未來的優勢之一。- Fetch 解決了什麼?
每當有新的技術或概念出現,人們總愛嚷嚷著舊的東西將會死去,新的東西會取代一切,喜新厭舊吧!舊東西誕生在舊的時代,適時地解決了當時的問題,而後 從中累積了不少的使用經驗,因而誕生了新的技術、概念或規範,急著預言舊東西將會逝去,並不會讓開發者看起來更為耀眼,只會讓開發者看不清楚新東西的 本質罷了。
舉個舊東西好了,
$.ajax
可以就這麼與 fetch
來比較嗎?高階封裝與低階
API 可以直接比較嗎?也許某些程度上,可以用 Fetch 解決時,無需掛個 jQuery 程式庫會是件好事,然而,$.ajax
也許可以基於 Fetch 來重構,在 Fetch 做不到的部份,使用 XMLHttpRequest
來實現,XMLHttpRequest
畢竟也不是完全那麼不堪,或者乾脆寫個 $.fetch
之類的擴充來實現前述概念如何呢?Fetch 實際上代表的是 Ajax 從
XMLHttpRequest
、選項物件、回呼處理、Promise
,
甚至往後銜接至產生器、async
/await
的這條發展路線中,各種實作與經驗的累積與修正,瞭解這個過程中的累積與修正,才會知道 Fetch
解決了什麼,又有哪些沒解決的,別急著馬上判處某些舊東西死刑,不然,過陣子可能又會急著要處死 Fetch 了吧!