深度介紹:也許你對 Fetch 瞭解得不是那麼多

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


前言

本篇主要講述 Fetch 的一些基本知識點以及咱們在生產開發中怎麼去使用。爲了可以更好的瞭解 Fetch,咱們但願你對如下知識點有所瞭解,若是有相關的開發經驗,那是最好不過的了。javascript

本文中對有些關鍵詞提供了相應的連接,若是你對該關鍵詞不夠了解或想要了解更多,你能夠經過點擊它充實本身。文中有些知識點在 MDN Fetch 上已經寫的很詳細,所以有略過,但願同窗們在閱讀本文章時可以同時對照閱讀。html

本文行文思路首先從規範入手,目的是讓你們瞭解的更透徹,達到知其然知其因此然。前端

爲了更好的掌握 Fetch,文章中還提供了一些示例代碼供你們學習使用。在使用該示例代碼前,咱們但願你對 node.js 有一些瞭解,若是沒有的話,你能夠根據示例中的友情提示完成你的此次學習體驗。java

讀完本篇文章後你將瞭解到如下內容:node

  • 什麼是 Fetch
  • Fetch 的一些基本概念
  • 如何使用 Fetch
  • Fetch 的一些不足以及咱們如何「優雅」的使用它

但願你經過讀完本篇文章後,對 Fetch 有一個基本的瞭解。c++

Fetch 簡介

Fetch 是一種新的用於獲取資源的技術,它被用來代替咱們已經吐槽了好久的技術(XHR)。git

Fetch 使用起來很簡單,它返回的是一個 Promise,即便你沒有 XHR 的開發經驗也能快速上手。說了那麼多,咱們仍是先睹爲快吧,讓咱們快快下面的示例代碼。github

fetch('https://github.com/frontend9/fe9-library', {
method: 'get'
}).then(function(response) {
}).catch(function(err) {
// Error
});

是否是簡單的不能再簡單了?好,既然咱們 Fetch 有了簡單的認識以後,那咱們再來了解下 Fetch 的基本概念。web

Fetch 基本概念

Fetch 中有四個基本概念,他們分別是 HeadersRequestResponseBody。爲了更好的理解 Fetch,咱們須要對這些概念作一個簡單的瞭解。json

在一個完整的 HTTP 請求中,其實就已經包含了這四個概念。請求中有請求頭和請求體,響應中有響應頭和響應體。因此咱們有必要了解這些概念。

Headers

爲了實現頭部的靈活性,可以對頭部進行修改是一個很是重要的能力。Headers 屬於 HTTP首部的一份子,它是一個抽象的接口,利用它能夠對 HTTP 的請求頭和響應頭作出添加、修改和刪除的操做。

下面咱們先看一下它具備哪些接口:

typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;

[Constructor(optional HeadersInit init),
 Exposed=(Window,Worker)]
interface Headers {
  void append(ByteString name, ByteString value);
  void delete(ByteString name);
  ByteString? get(ByteString name);
  boolean has(ByteString name);
  void set(ByteString name, ByteString value);
  iterable<ByteString, ByteString>;
};interface Headers {
  void append(ByteString name, ByteString value);
  void delete(ByteString name);
  ByteString? get(ByteString name);
  boolean has(ByteString name);
  void set(ByteString name, ByteString value);
  iterable<ByteString, ByteString>;
};
// 來自 https://fetch.spec.whatwg.org/#headers-class

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

這裏咱們對 Headers 的構造參數作個解釋。首先參數類型爲 HeadersInit,咱們再看下這個類型支持哪些類型的值。咱們從規範中能夠看到的定義是:

typedef (sequence<sequence<ByteString>> or record<ByteString, ByteString>) HeadersInit;

這裏咱們對應到 JavaScript 這門語言,意思就是說這個對象能夠是數組或者是鍵值對(即對象)。關於如何初始化這些參數,咱們能夠看下規範中定義的流程

To fill a Headers object (headers) with a given object (object), run these steps:

  1. If object is a sequence, then for each header in object:

    1. If header does not contain exactly two items, then throw a TypeError.
    2. Append header’s first item/header’s second item to headers.
  2. Otherwise, object is a record, then for each key → value in object, append key/value to headers.

這裏我須要對這個作個說明,後面對 fetch 的用法會涉及到一點以及咱們看 polyfill 都會有所幫助。

  • 第一種:即數組,當數據每項若是不包含兩項時,直接拋出錯誤。而後數組第一項是 header 名,第二項是值。,最後直接經過 append 方法添加。
  • 第二種:即鍵值對(這裏指對象),咱們經過循環直接取到鍵值對,而後經過 append 方法添加。

示例

示例代碼地址:https://github.com/GoDotDotDo...

打開瀏覽器輸入:http://127.0.0.1:4000/headers

那麼咱們該如何使用它呢?首先咱們須要經過 new Headers() 來實例化一個 Headers 對象,該對象返回的是一個空的列表。在有了對象實例後,咱們就能夠經過接口來完成咱們想要的操做,咱們來一塊兒看看下面的示例:

function printHeaders(headers) {
    let str = '';
    for (let header of headers.entries()) {
      str += `
          <li>${header[0]}: ${header[1]}</li>
          `;
      console.log(header[0] + ': ' + header[1]);
    }
    return `<ul>
          ${str}
          </ul>`;
  }
  const headers = new Headers();
  // 咱們打印下看看是否返回的是一個空的列表
  const before = printHeaders(headers); // 發現這裏沒有任何輸出
  document.getElementById('headers-before').innerHTML = before;
  // 咱們添加一個請求頭
  headers.append('Content-Type', 'text/plain');
  headers.append('Content-Type', 'text/html');
  headers.set('Content-Type', ['a', 'b']);
  const headers2 = new Headers({
    'Content-Type': 'text/plain',
    'X-Token': 'abcdefg',
  });
  const after = printHeaders(headers); // 輸出:content-type:

若是你以爲每次都要 append 麻煩的話,你也能夠經過在構造函數中傳入指定的頭部,例如:

const headers2 = new Headers({
    'Content-Type': 'text/plain',
'X-Token': 'abcdefg'
});

printHeaders(headers2);
// 輸出:
// content-type: text/plain
// x-token: abcdefg

這裏我添加了一個自定義頭部 X-Token,這在實際開發中很常見也頗有實際意義。可是切記在 CORS 中須要知足相關規範,不然會產生跨域錯誤。

你能夠經過appenddeletesetgethas 方法修改請求頭。這裏對 setappend 方法作個特殊的說明:

set: 若是對一個已經存在的頭部進行操做的話,會將新值替換掉舊值,舊值將不會存在。若是頭部不存在則直接添加這個新的頭部。

append:若是已經存在該頭部,則直接將新值追加到後面,還會保留舊值。

爲了方便記憶,你只須要記住 set 會覆蓋,而 append 會追加。

Guard

Guard 是 Headers 的一個特性,他是一個守衛者。它影響着一些方法(像 appendsetdelete)是否能夠改變 header 頭。

它能夠有如下取值:immutablerequestrequest-no-corsresponsenone

這裏你無需關心它,只是爲你讓你瞭解有這樣個東西在影響着咱們設置一些 Headers。你也沒法去操做它,這是代理的事情。舉個簡單的例子,咱們沒法在 Response Headers 中插入一個 Set-Cookie

若是你想要了解更過的細節,具體的規範請參考 concept-headers-guardMDN Guard

注意

Body

Body 準確來講這裏只是 mixin,表明着請求體或響應體,具體由 ResponseRequest 來實現。

下面咱們來看看它具備哪些接口:

interface mixin Body {
  readonly attribute ReadableStream? body;
  readonly attribute boolean bodyUsed;
  [NewObject] Promise<ArrayBuffer> arrayBuffer();
  [NewObject] Promise<Blob> blob();
  [NewObject] Promise<FormData> formData();
  [NewObject] Promise<any> json();
  [NewObject] Promise<USVString> text();
};
// 來自 https://fetch.spec.whatwg.org/#body

規範中定義的接口咱們能夠對應着 MDN 進行查看,你能夠點擊這裏更直觀的看看它有哪些屬性和方法供咱們使用。

這裏須要注意看這些方法返回的都是 Promise,記住這在基於 fetch 進行接口請求中很重要。記住了這個,有利於咱們在後面的文章中理解 fetch 的用法。

示例

範例將在 Response 中體現。

Request

Request 表示一個請求類,須要經過實例化來生成一個請求對象。經過該對象能夠描述一個 HTTP 請求中的請求(通常含有請求頭和請求體)。既然是用來描述請求對象,那麼該請求對象應該具備修改請求頭(Headers)和請求體(Body)的方式。下面咱們先來看下規範中 Request 具備哪些接口:

typedef (Request or USVString) RequestInfo;

[Constructor(RequestInfo input, optional RequestInit init),
 Exposed=(Window,Worker)]
interface Request {
  readonly attribute ByteString method;
  readonly attribute USVString url;
  [SameObject] readonly attribute Headers headers;

  readonly attribute RequestDestination destination;
  readonly attribute USVString referrer;
  readonly attribute ReferrerPolicy referrerPolicy;
  readonly attribute RequestMode mode;
  readonly attribute RequestCredentials credentials;
  readonly attribute RequestCache cache;
  readonly attribute RequestRedirect redirect;
  readonly attribute DOMString integrity;
  readonly attribute boolean keepalive;
  readonly attribute boolean isReloadNavigation;
  readonly attribute boolean isHistoryNavigation;
  readonly attribute AbortSignal signal;

  [NewObject] Request clone();
};
Request includes Body;

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
};

enum RequestDestination { "", "audio", "audioworklet", "document", "embed", "font", "image", "manifest", "object", "paintworklet", "report", "script", "sharedworker", "style",  "track", "video", "worker", "xslt" };
enum RequestMode { "navigate", "same-origin", "no-cors", "cors" };
enum RequestCredentials { "omit", "same-origin", "include" };
enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" };
enum RequestRedirect { "follow", "error", "manual" };
// 來自 https://fetch.spec.whatwg.org/#request-class

規範中定義的接口咱們能夠對應着 MDN 進行查看,你能夠點擊這裏更直觀的看看它有哪些屬性和方法供咱們使用,這裏不作一一解釋。

注意這裏的屬性都是隻讀的,規範中咱們能夠看到構造函數的第一個參數爲 Request 對象或字符串,咱們通常採起字符串,即須要訪問的資源地址( HTTP 接口地址)。第二個參數接收一個 RequestInit 可選對象,而這個對象是一個字典。在 javascript 中,咱們能夠理解爲一個對象({})。RequestInit 裏面咱們能夠配置初始屬性,告訴 Request 咱們這個請求的一些配置信息。

這裏咱們須要對如下幾個屬性特別注意下。

mode 是一個 RequestMode 枚舉類型,可取的值有 navigate, same-origin, no-cors, cors。它表示的是一個請求時否使用 CORS,仍是使用嚴格同源模式。當處於跨域狀況下,你應當設置爲 cors。該值的默認值在使用 Request 初始化時,默認爲 cors。當使用標記啓動的嵌入式資源,例如 <link><script>標籤(未手動修改 crossorigin 屬性),默認爲 no-cors。詳細信息請參考 whatwg 規範或 MDN

credentials 是一個 RequestCredentials 枚舉類型,可取的值有 omit, same-origin, include。它表示的是請求是否在跨域狀況下發送 cookie。看到這,若是對 XHR 瞭解的同窗應該很熟悉。這和 XHR 中的 withCredentials 很類似。可是 credentials 有三個可選值,它的默認值爲 same-origin。當你須要跨域傳遞 cookie 憑證信息時,請設置它爲 include。注意這裏有一個細節,當設置爲 include 時,請確保 Response HeaderAccess-Control-Allow-Origin 不能爲 *,須要指定源(例如:http://127.0.0.1:4001),不然會你將會在控制檯看到以下錯誤信息。詳細信息請參考 whatwg 規範或 MDN

The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

你可使用文章中提供的代碼中啓動 cors 示例代碼,而後在瀏覽器中輸入 http://127.0.0.1:4001/request,若是不出意外的話,你能夠在控制檯中看到上面的錯誤提示。

body 是一個 BodyInit 類型。它可取的值有 Blob,BufferSource , FormData , URLSearchParams , ReadableStream , USVString。細心的同窗不知道有沒有發現,咱們常見的 json 對象卻不在其中。所以,咱們若是須要傳遞 json 的話,須要調用 JSON.stringify 函數來幫助咱們轉換成字符串。

下面將給出一段示例代碼。

示例

示例代碼地址:https://github.com/GoDotDotDo...

打開瀏覽器輸入:http://127.0.0.1:4000/request

// 客戶端
  const headers = new Headers({
    'X-Token': 'fe9',
  });
  const request = new Request('/api/request', {
    method: 'GET',
    headers,
  });
  console.log(request); // Request {method: "GET", url: "http://127.0.0.1:4000/api/request", headers: Headers, destination: "", referrer: "about:client", …}
  console.log(request.method); // GET
  console.log(request.mode); // cors
  console.log(request.credentials); // same-origin
  // 若是你想打印headers信息,能夠調用 printHeaders(request.headers)

這裏咱們先以 GET 簡單請求做爲示例,咱們傳遞了一個自定義的 Headers,指定了請求方法 methodGET(默認爲 GET)。在上面的接口規範中,咱們能夠經過 Request 對象拿到一些經常使用的屬性,好比 methodurlheadersbody 等等只讀屬性。

Response

Response 和 Request 相似,表示的是一次請求返回的響應數據。下面咱們先看下規範中定義了哪些接口。

[Constructor(optional BodyInit? body = null, optional ResponseInit init), Exposed=(Window,Worker)]
interface Response {
  [NewObject] static Response error();
  [NewObject] static Response redirect(USVString url, optional unsigned short status = 302);

  readonly attribute ResponseType type;

  readonly attribute USVString url;
  readonly attribute boolean redirected;
  readonly attribute unsigned short status;
  readonly attribute boolean ok;
  readonly attribute ByteString statusText;
  [SameObject] readonly attribute Headers headers;
  readonly attribute Promise<Headers> trailer;

  [NewObject] Response clone();
};
Response includes Body;

dictionary ResponseInit {
  unsigned short status = 200;
  ByteString statusText = "";
  HeadersInit headers;
};

enum ResponseType { "basic", "cors", "default", "error", "opaque", "opaqueredirect" };
// 來自 https://fetch.spec.whatwg.org/#response-class

規範中定義的接口咱們能夠對應着 MDN 進行查看,你能夠點擊這裏更直觀的看看它有哪些屬性和方法供咱們使用,這裏不作一一解釋。

其中 status, headers 屬性最爲經常使用。經過 status 狀態碼咱們能夠判斷出服務端請求處理的結果,像 200, 403 等等常見狀態碼。這裏舉個例子,當 status401 時,能夠在前端進行攔截跳轉到登陸頁面,這在現現在 SPA(單頁面應用程序)中尤其常見。咱們也能夠利用 headers 來獲取一些服務端返回給前端的信息,好比 token

仔細看上面的接口的同窗能夠發現 Response includes Body; 這樣的標識。在前面咱們說過 BodyRequestResponse 實現。因此 Body 具備的方法,在 Response 實例中均可以使用,而這也是很是重要的一部分,咱們經過 Body 提供的方法(這裏準確來講是由 Response 實現的)對服務端返回的數據進行處理。

下面咱們將經過一個示例來了解下簡單用法:

示例

示例代碼位置:https://github.com/GoDotDotDo...

// 客戶端
  const headers = new Headers({
    'X-Token': 'fe9-token-from-frontend',
  });
  const request = new Request('/api/response', {
    method: 'GET',
    headers,
  });

  // 這裏咱們先發起一個請求試一試
  fetch(request)
    .then(response => {
      const { status, headers } = response;
      document.getElementById('status').innerHTML = `${status}`;
      document.getElementById('headers').innerHTML = headersToString(headers);

      return response.json();
    })
    .then(resData => {
      const { status, data } = resData;
      if (!status) {
        window.alert('發生了一個錯誤!');
        return;
      }
      document.getElementById('fetch').innerHTML = data;
    });

這裏咱們先忽略 fetch 用法,後面的章節中會進行詳細介紹。咱們先關注第一個 then 方法回調裏面的東西。能夠看到返回了一個 response 對象,這個對象就是咱們的 Response 的實例。示例中拿了 statusheaders ,爲了方便,這裏我將其放到 html 中。再看看該回調中最後一行,咱們調用了一個 response.json() 方法(這裏後端返的數據是一個 JSON 對象,爲了方便直接調用 json()),該方法返回一個 Promise,咱們將處理結果返給最後一個 then 回調,這樣就能夠得到最終處理事後的數據。

打開瀏覽器,輸入 http://127.0.0.1:4000/response,若是你的示例代運行正常,你將會看到如下頁面:

img

(查看 Response 返回的數據)

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 使用,咱們的代碼可讀性會更高。下面咱們先經過一個簡單的示例來熟悉下它的使用。

示例

示例代碼位置:https://github.com/GoDotDotDo...

// 客戶端
  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 實例對象,下面咱們也給出一個示例。

代碼位置:https://github.com/GoDotDotDo...

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,這樣咱們就能夠在外面來取消掉。這裏本人暫時不提供代碼。有興趣的同窗能夠嘗試一下,也能夠在下面的評論區評論。

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

攔截器

示例代碼位置:https://github.com/GoDotDotDo...

下面咱們講一講如何作一個簡單的攔截器,這裏的攔截器指對響應作攔截。假設咱們須要對接口返回的狀態碼進行解析,例如 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.

編 / 熒聲

做者其餘文章:

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

本文由創宇前端做者受權發佈,版權屬於做者,創宇前端出品。
歡迎註明出處轉載本文。本文連接:https://blog.godotdotdot.com/...

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

歡迎點贊、收藏、留言評論、轉發分享和打賞支持咱們。打賞將被徹底轉交給文章做者。

感謝您的閱讀。

相關文章
相關標籤/搜索