談到Ajax,一般會想到XMLHttpRequest,但使用上並不便利,就算是標準化後的XMLHttpRequest Level 1,而且,也只是功能上的加強,開發者通常會進一步地使用程式庫封裝,像是jQuery的$.get、$.post或$.ajax。曾經有一陣子流行「你不需要jQuery」,社群裏頭也有人嚷嚷著,認為Fetch 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的規範(https://goo.gl/nbJKxM))前言中就清楚指出,Fetch的定位本來就是低階封裝。

Fetch另一個與XMLHttpRequest不同的地方,是Streams的支援。按照規範,回應物件的body特性會是個ReadableStream,行為上,與Streams(https://goo.gl/zWjoDv)規範中的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了吧!

作者簡介


Advertisement

更多 iThome相關內容