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

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

編者按:除創宇前端與做者博客外,本文還在語雀發佈。html

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


Fetch 與 XHR 比較

Fetch 相對 XHR 來講具備簡潔、易用、聲明式、天生基於 Promise 等特色。XHR 使用方式複雜,接口繁多,最重要的一點我的以爲是它的回調設計,對於實現 try...catch 比較繁瑣。java

可是 Fetch 也有它的不足,相對於 XHR 來講,目前它具備如下劣勢:c++

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

在瞭解 Fetch 和 XHR 的一些不一樣後,仍是須要根據自身的業務需求來選擇合適的技術,由於技術沒有永遠的好壞,只有合不合適。git

下面章節咱們將介紹如何「優雅」的使用 Fetch 以及如何儘可能避免掉劣勢。github

如何使用Fetch

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

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

規範中定義的接口咱們能夠對應着 MDN 進行查看,你能夠點擊這裏更直觀的看看它的用法。json

從規範中咱們能夠看到 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)。歡迎留言討論,咱們會盡量回復。

感謝您的閱讀。

新年快樂 :)

相關文章
相關標籤/搜索