「震驚」你可能需要一個假的 Fetch API

z正小歪發表於2017-03-28

Fetch API 已經出現很久了,很多公司和個人都在鼓吹 Fetch 多麼牛逼,這點必須要同意。

Fetch 使用來替代老掉牙的 XMLHttpRequest,XMLHttpRequest 在設計上有著很多缺陷,比如呼叫方式混亂,不注重分離設計的原則等等,所以後來才會有了類似 JQuery Ajax 之類的庫出現。

首先先給出一個明確的觀點,我不否認 Fetch 相反我認為是很優秀的,但是 Fetch API 整體用起來還是有一些不爽的,雖然得益於 Promise 的助攻,但是更多的缺陷也來自 Promise,所以本文就針對基於標準 Promise 實現的 Fetch 吐槽一下用起來的不爽。

簡單回顧一下 Promise/A+ 規範

Promise 中文翻譯「承諾」,在非同步世界裡真的沒有什麼比承若更加重要了,因為真的不知道下一個出現的會是誰。

Promise 中值分成現在值和將來值兩個部分,將來值正是我們所關心的,所以給 Promise 下一個簡單定義就是:獲取意料當中值。

在 Promise 中分成三個狀態:

  • pending:初始狀態,未被 fulfilled 或者未被 rejected。
  • fulfilled:處理成功
  • rejected:處理失敗

是的就是隻有三種狀態(坑點就在這裡)

只提供了最簡單的 API:

在 Promise 原型鏈上有兩種方法:

所以 Promise 總的來說只有三種狀態,四個方法、兩個原型方法,多麼簡單。

「震驚」你可能需要一個假的 Fetch API

以上內容來自 MDN Promise

沒有 Timeout 機制的 Fetch

沒有這個功能確實很蛋疼,當遇到網路不順暢的時候,不能老是等待吧,這樣太噁心了。

這個槽點還是在 Promise 本身上,由於只有三種狀態,成功、掛起、失敗,並沒有取消啊,WTF???黑人問號??

怎麼辦?彈藥不夠敵人來造,最大的敵人就是 Promise 本身。Promise 中有一個方法叫做 race,該方法一組 Promise 中只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理。

So~,有了這種機制就可以造一個假的 Timeout 出來了。

function hackFetch(url, timeout=10, params={}) {
  // 用 Promise 包裝一個 timeout 的 reject
  var _abort = new Promise((resolve, reject) => {
    setTimeout(() => reject('abort promise'), timeout);
  })            
  var _fetch =  fetch(url, params);

  return Promise.race([_fetch, _abort])
}複製程式碼

實現的程式碼很簡單,兩個 Promise,一個是 timeout 、一個是 Fetch,對這樣就完成了。

然後再來一個工廠方法,多建立幾個,來嘗試一下。

// hackFetch 的工廠方法
function createHackFetch(url, timeout=10, params={}) {
    return () => {
      return hackFetch(url, timeout, params)
              .then(res => res.json())
              .then(json => textDOM.value += json.message + '\n')
              .catch(err => alert('fetch 超時'))
    }
}複製程式碼

實驗使用的是 Express,實現了 4 個介面,分別 0,5,10,15 秒返回資料。

完整例子可以轉到該專案的 Repo

「震驚」你可能需要一個假的 Fetch API

當我點選「測試15秒 timeout 的 fetch」過後的 10 秒,出現了 alert,中斷了這次 hackPromise,沒有在下面的 textarea 中新增獲取到字串。

事情並不會那麼美好,確認完成這個 alert 以後。觀察那個 fifteen-delay 的請求,它依然返回資料。

「震驚」你可能需要一個假的 Fetch API

此坑開始在於 Promise 本身沒有 cancel 機制。通過 hack 出來的帶有 Timeout 機制的 Fetch,只不過的騙過了自己,但是沒有騙過了瀏覽器。

這種方法是很危險的行為。輕的來看結果是顯示和實際情況不一致罷了,但是嚴重的來看,本不應該出現東西卻出現了,確確實實是一個漏洞。

此法有解嗎?目前來看前端無解,後端可以通過設定連線的 Timeout 時間來解決這個問題,Nginx 可以通過設定 send_timeout 來規定 Timeout 時間。

介面返回錯誤碼,不拋異常的 Fetch

在 MDN 的 Using Fetch 中有那麼一段話:

The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure or if anything prevented the request from completing.

翻譯過來就是:

fetch() 返回的 Promise 將不會拒絕HTTP錯誤狀態, 即使響應是一個 HTTP 404 或 500。相反,它會正常解決 (其中ok狀態設定為false), 只有在網路故障時或者請求被阻止時,它才會拒絕。

這個其實這個相比上一個來說並不是什麼嚴重的坑,只不過在開發上變的更加繁瑣一些,這個恰恰又和 Fetch 的理念相悖。

app.get('/api/error-five-delay', function(req, res) {
    res.type('json');
    res.status(500)
    setTimeout(() => {
        res.send(JSON.stringify({
            message: 'there is a error response'
        }));
    }, 5000)
});複製程式碼

新增一個 5 秒後返回 500 錯誤的介面,使用一個建立正常 Fetch 的工廠方法,繫結到 button 上。

function createFetch(url, params={}) {
    return () => {
      return fetch(url, params)
              .then(res => res.json())
              .then(json => textDOM.value += json.message + '\n')
              .catch(err => alert('請求失敗'))
    }
}

// 繫結事件
document.getElementById('error-five-fetch').onclick = createFetch('/api/error-five-delay');複製程式碼

5 秒之後,message 資訊如願的被新增到了 textarea 上。此時瀏覽器做到了它職責在控制檯中給出了錯誤,但是 Promise 忽略了它。

「震驚」你可能需要一個假的 Fetch API

「震驚」你可能需要一個假的 Fetch API

所以如 MDN 中所言,我們必須手動的檢查 response 中 ok 屬性是否為 false ,好了要在造一個假的 Fetch 了。

function xfetch(url, params) {
    return fetch(url, params)
            // 處理錯誤時候的 json
            .then(res => res.json().then(json => res.ok ? json : Promise.reject(json)))
            .then(json => textDOM.value += json.message + '\n')
            .catch(err => alert(err.message))
}

function createXfetch(url, params={}) {
    return () => xfetch(url, params)
}

document.getElementById('error-handling-five-fetch').onclick = createXfetch('/api/error-five-delay');複製程式碼

網路上有很多這樣處理髮生錯誤時候的 json,各種各樣的方法都有,其中一樣的就是必須先把 json 從 Promise 從解析出來,然後再來處理 response.ok 的狀態。

「震驚」你可能需要一個假的 Fetch API

完整例子可以轉到該專案的 Repo

其實這麼做面對 json 資料時候沒有壓力,但是對於需要解析多種資料時候還需要更多的引數和封裝,比如資料來源是 xml 或者 plain。

好嘛,又違背了 Promise 的設計原則。

總結一下

文中沒有實現一個 timeout 和 錯誤 json 處理例子,其實把 timeout 版中替換成 xfetch 就好了。

自從 Promise 的出現,在編寫非同步任務上有了很大的改進,Fetch 也孕育而生,在使用 Fetch 帶來的簡單、高效的同時也要主要它的坑點所在。本文只是總結了很小的一部分,在 Promise 還有無數的坑等著別去跳。

async / await 肯定是下一個方向,在還沒完善之前,為了新老語法過渡使用 Promise 無疑是非常聰明的選擇。可以給老程式碼以介面的方式打上一個 polyfill,同時新語法相容 Promise 。這樣完美的避開了像 Python 青黃不接的尷尬局面,Python 要加油了。

我是一個 Python 工程師,Python 大發好啊,Python 大發好啊,Python 大發好啊。

相關文章