從 XHR 到 Fetch


曾經有一陣子,JavaScript 社群中流行著「你不需要 jQuery」的口號,社群裏頭嚷嚷著 Fecth API 將會取代這一切。

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

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

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

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

從設計的角度來看,Fetch API 就像是集合了過去 Ajax 使用上一些好實踐的集合體,實現了職責分離,建立時可使用選項物件來進行相關設定,實際上,你也可以獨立地建立 HeadersRequestResponse 實例。

例如,fetch 除了可接受初始物件設定之外:

    fetch('POST-1.php', {
        method : 'POST',
        headers : {
            'Content-Type' : 'application/x-www-form-urlencoded'
        },
        body : reqString
    })

也可以接受 Request 實例:

    let request = new Request('POST-1.php', {
        method : 'POST',
        headers : new Headers({
            'Content-Type' : 'application/x-www-form-urlencoded'
        }),
        body : reqString
    });

    fetch(request);

大部份的情況下,你不需要接觸 HeadersRequestResponse 等實例,使用選項物件,通常足以應付,然而,如果需要明確的語意,或者是想重用某個設定,甚至是符合某個介面實現,那麼 HeadersRequestResponse 等實例就會是需要的。

fetch 的傳回值是 Promise,表面上看來,Fetch 很像在 XMLHttpRequest 上封裝了一層 Promise,這也是它為什麼經常被拿來與 $.ajax 對比的原因之一,因為模式乍看之下十分類似,不過嚴格來說,$.ajax 做了比較高階的封裝。

舉例來說,$.ajaxdata 選項指定物件時,會自動進行序列化與請求參數編碼處理,然而使用 fetchbody 選項時,如〈簡介 Fetch API〉中看到的範例,必須自行建立、編碼請求參數,這是因為在 Fetch 的規範前言中就清楚指出,Fetch 的定位本來就是低階封裝。

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

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

(若瞭解到某個 Fetch 的限制是來自於 Promise 的限制,就可以試著從設計上,依個別需求來來實現特定的方案。)

在瀏覽器支援上,對於不支援 Fetch 的瀏覽器,可以使用 Fetch Polyfill,修補是基於 XMLHttpRequest,仿造了 Fetch API 介面,不過正因為基於 XMLHttpRequest,在某些方面功能會受限;在不支援 Promise 的瀏覽器上,除了 Fetch 修補之外,還要加上 Promise 修補(更舊的瀏覽器,像是 IE8/9,還要加上 ES5 修補等)。