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

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

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

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

前言

本篇主要講述 Fetch 的一些基本知識點以及我們在生產開發中怎麼去使用。為了能夠更好的瞭解 Fetch,我們希望你對以下知識點有所瞭解,如果有相關的開發經驗,那是最好不過的了。

本文中對有些關鍵詞提供了相應的連結,如果你對該關鍵詞不夠了解或想要了解更多,你可以通過點選它充實自己。文中有些知識點在 MDN Fetch 上已經寫的很詳細,因此有略過,希望同學們在閱讀本文章時能夠同時對照閱讀。

本文行文思路首先從

規範入手,目的是讓大家瞭解的更透徹,達到知其然知其所以然。

為了更好的掌握 Fetch,文章中還提供了一些示例程式碼供大家學習使用。在使用該示例程式碼前,我們希望你對 node.js 有一些瞭解,如果沒有的話,你可以根據示例中的友情提示完成你的這次學習體驗。

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

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

希望你通過讀完本篇文章後,對 Fetch 有一個基本的瞭解。

Fetch 簡介

Fetch 是一種新的用於獲取資源的技術,它被用來代替我們已經吐槽了很久的技術(

XHR)。

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

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

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

Fetch 基本概念

Fetch

中有四個基本概念,他們分別是 HeadersRequestResponseBody。為了更好的理解 Fetch,我們需要對這些概念做一個簡單的瞭解。

在一個完整的 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 方法新增。

示例

示例程式碼地址:github.com/GoDotDotDot…

開啟瀏覽器輸入: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 函式來幫助我們轉換成字串。

下面將給出一段示例程式碼。

示例

示例程式碼地址:github.com/GoDotDotDot…

開啟瀏覽器輸入: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 實現的)對服務端返回的資料進行處理。

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

示例

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

  // 客戶端
  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 返回的資料)

編者注:本文未完待續。


文 / GoDotDotDot

Less is More.

編 / 熒聲

作者其他文章:

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

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

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

感謝您的閱讀。

新年快樂 :)