1. 程式人生 > >也許你對 Fetch 瞭解得不是那麼多(下)

也許你對 Fetch 瞭解得不是那麼多(下)

上文連結:也許你對 Fetch 瞭解得不是那麼多(上)

編者按:除創宇前端與作者部落格外,本文還在語雀釋出。

編者還要按:作者也在掘金哦,歡迎關注:@GoDotDotDot


Fetch 與 XHR 比較

Fetch 相對 XHR 來說具有簡潔、易用、宣告式、天生基於 Promise 等特點。XHR 使用方式複雜,介面繁多,最重要的一點個人覺得是它的回撥設計,對於實現 try...catch 比較繁瑣。

但是 Fetch 也有它的不足,相對於 XHR 來說,目前它具有以下劣勢:

  • 不能取消(雖然 AbortController 能實現,但是目前相容性基本不能使用,可以使用
    polyfill
  • 不能獲取進度
  • 不能設定超時(可以通過簡單的封裝來模擬實現)
  • 相容性目前比較差(可以使用 polyfill 間接使用 XHR 來優雅降級,這裡推薦使用 isomorphic-fetch

在瞭解 Fetch 和 XHR 的一些不同後,還是需要根據自身的業務需求來選擇合適的技術,因為技術沒有永遠的好壞,只有合不合適。

下面章節我們將介紹如何“優雅”的使用 Fetch 以及如何儘量避免掉劣勢。

如何使用Fetch

前面瞭解了這麼多基礎知識,現在終於到了介紹如何使用 Fetch 了。老規矩,我們先來看下規範定義的介面。

partial interface mixin WindowOrWorkerGlobalScope {
  [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);
};
複製程式碼

規範中定義的介面我們可以對應著 MDN 進行檢視,你可以點選這裡更直觀的看看它的用法。

從規範中我們可以看到 fetch 屬於 WindowOrWorkerGlobalScope 的一部分,暴露在 WindowWorkerGlobalScope 物件上。所以在瀏覽器中,你可以直接呼叫 fetch。

規範中定義了 fetch 返回一個 Promise,它最多可接收兩個引數( input 和 init )。為了能夠對它的使用方法有個更全面的瞭解,下面來講一下這兩個引數。

  • input 引數型別為 RequestInfo,我們可以回到前面的 Request 部分,來回顧一下它的定義。

    typedef (Request or USVString) RequestInfo;

    發現它是一個 Request 物件或者是一個字串,因此你可以傳 Request 例項或者資源地址字串,這裡一般我們推薦使用字串。

  • init 引數型別為 RequestInit,我們回顧前面 Requst 部分,它是一個字典型別。在 JavaScript 中你需要傳遞一個 Object 物件。

    dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null };

在本小節之前我們都沒有介紹 fetch 的使用方式,但是在其他章節中或多或少出現過它的容貌。現在,我們終於可以在這裡正式介紹它的使用方式了。

fetch 它返回一個 Promise,意味著我們可以通過 then 來獲取它的返回值,這樣我們可以鏈式呼叫。如果配合 async/await 使用,我們的程式碼可讀性會更高。下面我們先通過一個簡單的示例來熟悉下它的使用。

示例

示例程式碼位置:github.com/GoDotDotDot…

  // 客戶端
  const headers = new Headers({
    'X-Token': 'fe9',
  });

  setTimeout(() => {
    fetch('/data?name=fe', {
      method: 'GET', // 預設為 GET,不寫也可以
      headers,
    })
      .then(response => response.json())
      .then(resData => {
        const { status, data } = resData;
        if (!status) {
          window.alert('發生了一個錯誤!');
          return;
        }
        document.getElementById('fetch').innerHTML = data;
      });
  }, 1000);
複製程式碼

上面的示例中,我們自定義了一個 headers 。為了演示方便,這裡我們設定了一個定時器。在請求成功時,伺服器端會返回相應的資料,我們通過 Response 例項的 json 方法來解析資料。細心的同學會發現,這裡 fetch 的第一個引數我們採用的是字串,在第二個引數我們提供了一些 RequestInit 配置資訊,這裡我們指定了請求方法(method)和自定義請求頭(headers)。當然你也可以傳遞一個 Request 例項物件,下面我們也給出一個示例。

程式碼位置:github.com/GoDotDotDot…

  const headers = new Headers({
    'X-Token': 'fe9',
  });  
  const request = new Request('/api/request', {
    method: 'GET',
    headers,
  });

  setTimeout(() => {
    fetch(request)
      .then(res => res.json())
      .then(res => {
        const { status, data } = res;
        if (!status) {
          alert('伺服器處理失敗');
          return;
        }
        document.getElementById('fetch-req').innerHTML = data;
      });
  }, 1200);
複製程式碼

在瀏覽器中開啟:http://127.0.0.1:4000/, 如果上面的示例執行成功,你將會看到如下介面:

img

好,在執行完示例後,相信你應該對如何使用 fetch 有個基本的掌握。在上一章節,我們講過 fetch 有一定的缺點,下面我們針對部分缺點來嘗試著處理下。

解決超時

當網路出現異常,請求可能已經超時,為了使我們的程式更健壯,提供一個較好的使用者 體驗,我們需要提供一個超時機制。然而,fetch 並不支援,這在上一小節中我們也聊到過。慶幸的是,我們有 Promise ,這使得我們有機可趁。我們可以通過自定義封裝來達到支援超時機制。下面我們嘗試封裝下。

const defaultOptions = {
  headers: {
    'Content-Type': 'application/json',
  },
};
function request(url, options = {}) {
  return new Promise((resolve, reject) => {
    const headers = { ...defaultOptions.headers, ...options.headers };
    let abortId;
    let timeout = false;
    if (options.timeout) {
      abortId = setTimeout(() => {
        timeout = true;
        reject(new Error('timeout!'));
      }, options.timeout || 6000);
    }
    fetch(url, { ...defaultOptions, ...options, headers })
      .then((res) => {
        if (timeout) throw new Error('timeout!');
        return res;
      })
      .then(checkStatus)
      .then(parseJSON)
      .then((res) => {
        clearTimeout(abortId);
        resolve(res);
      })
      .catch((e) => {
        clearTimeout(abortId);
        reject(e);
      });
  });
}
複製程式碼

上面的程式碼中,我們需要注意下。就是我們手動根據超時時間來 reject 並不會阻止後續的請求,由於我們並沒有關閉掉此次連線,屬於是偽取消。fetch 中如果後續接受到伺服器的響應,依然會繼續處理後續的處理。所以這裡我們在 fetch 的第一個 then 中進行了超時判斷。

取消

  const controller = new AbortController();
  const signal = controller.signal;

  fetch('/data?name=fe', {
    method: 'GET',
    signal,
  })
    .then(response => response.json())
    .then(resData => {
      const { status, data } = resData;
      if (!status) {
        window.alert('發生了一個錯誤!');
        return;
      }
      document.getElementById('fetch-str').innerHTML = data;
    });
  controller.abort();
複製程式碼

我們回過頭看下 fetch 的介面,發現有一個屬性 signal, 型別為AbortSignal,表示一個訊號物件( signal object ),它允許你通過 AbortController 物件與DOM請求進行通訊並在需要時將其中止。你可以通過呼叫 AbortController.abort 方法完成取消操作。

當我們需要取消時,fetch 會 reject 一個錯誤( AbortError DOMException ),中斷你的後續處理邏輯。具體可以看規範中的解釋

由於目前 AbortController 相容性極差,基本不能使用,但是社群有人幫我們提供了 polyfill(這裡我不提供連結,因為目前來說還不適合生產使用,會出現下面所述的問題),我們可以通過使用它來幫助我們提前感受新技術帶來的快樂。但是你可能會在原生支援 Fetch 但是又不支援 AbortController 的情況下,部分瀏覽器可能會報如下錯誤:

  • Chrome: "Failed to execute 'fetch' on 'Window': member signal is not of type AbortSignal."
  • Firefox: "'signal' member of RequestInit does not implement interface AbortSignal."

如果出現以上問題,我們也無能為力,可能原因是瀏覽器內部做了嚴格驗證,對比發現我們提供的 signal 型別不對。

但是我們可以通過手動 reject 的方式達到取消,但是這種屬於偽取消,實際上連線並沒有關閉。我們可以通過自定義配置,例如在 options 中增加配置,暴露出 reject,這樣我們就可以在外面來取消掉。這裡本人暫時不提供程式碼。有興趣的同學可以嘗試一下,也可以在下面的評論區評論。

前面提到過的獲取進度目前我們還無法實現。

攔截器

示例程式碼位置:github.com/GoDotDotDot…

下面我們講一講如何做一個簡單的攔截器,這裡的攔截器指對響應做攔截。假設我們需要對介面返回的狀態碼進行解析,例如 403 或者 401 需要跳轉到登入頁面,200 正常放行,其他報錯。由於 fetch 返回一個 Promise ,這就使得我們可以在後續的 then 中做些簡單的攔截。我們看一下示例程式碼:

function parseJSON(response) {
  const { status } = response;
  if (status === 204 || status === 205) {
    return null;
  }

  return response.json();
}

function checkStatus(response) {
  const { status } = response;
  if (status >= 200 && status < 300) {
    return response;
  }
  // 許可權不允許則跳轉到登陸頁面
  if (status === 403 || status === 401) {
    window ? (window.location = '/login.html') : null;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}
/**
 * @description 預設配置
 * 設定請求頭為json
 */
const defaultOptions = {
  headers: {
    'Content-Type': 'application/json',
  },
  // credentials: 'include', // 跨域傳遞cookie
};

/**
 * Requests a URL, returning a promise
 *
 * @param  {string} url       The URL we want to request
 * @param  {object} [options] The options we want to pass to "fetch"
 *
 * @return {object}           The response data
 */
function request(url, options = {}) {
  return new Promise((resolve, reject) => {
    const headers = { ...defaultOptions.headers, ...options.headers };
    let abortId;
    let timeout = false;
    if (options.timeout) {
      abortId = setTimeout(() => {
        timeout = true;
        reject(new Error('timeout!'));
      }, options.timeout || 6000);
    }
    fetch(url, { ...defaultOptions, ...options, headers })
      .then((res) => {
        if (timeout) throw new Error('timeout!');
        return res;
      })
      .then(checkStatus)
      .then(parseJSON)
      .then((res) => {
        clearTimeout(abortId);
        resolve(res);
      })
      .catch((e) => {
        clearTimeout(abortId);
        reject(e);
      });
  });
}
複製程式碼

從上面的 checkStatus 程式碼中我們可以看到,我們首先檢查了狀態碼。當狀態碼為 403 或 401 時,我們將頁面跳轉到了 login 登入頁面。細心的同學還會發現,我們多了一個處理方法就是 parseJSON,這裡由於我們的後端統一返回 json 資料,為了方便,我們就直接統一處理了 json 資料。

總結

本系列文章整體闡述了 fetch 的基本概念、和 XHR 的差異、如何使用 fetch 以及我們常見的解決方案。希望同學們在讀完整篇文章能夠對 fetch 的認識有所加深。

建議:在整體瞭解了 fetch 之後,希望同學們能夠讀一下 github polyfill 原始碼。在讀程式碼的同時,可以同時參考 Fetch 規範

參考:

  1. MDN Fetch
  2. Fetch 規範
  3. 示例程式碼

文 / GoDotDotDot

Less is More.

編 / 熒聲

作者其他文章:

優秀前端必知的話題:我們應該做些力所能及的優化

本文由創宇前端作者授權釋出,版權屬於作者,創宇前端出品。 歡迎註明出處轉載本文。文章連結:blog.godotdotdot.com/2018/12/28/…

想要訂閱更多來自知道創宇開發一線的分享,請搜尋關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回覆。

感謝您的閱讀。

新年快樂 :)