也许你对 Fetch 了解得不是那么多(上)

8,103 阅读6分钟

编者按:除创宇前端与作者博客外,本文还在语雀发布。

编者还要按:作者也在掘金哦,欢迎关注:@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)。欢迎留言讨论,我们会尽可能回复。

感谢您的阅读。

新年快乐 :)