fetch API 简介

2015-05-08 W.Y. 更多博文 » 博客 » GitHub »

fetch

原文链接 https://bubkoo.github.io/2015/05/08/introduction-to-fetch/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


十多年来,我们一直使用 XMLHttpRequest(XHR)来发送异步请求,XHR 很实用,但并不是一个设计优良的 API,在设计上并不符合职责分离原则,输入、输出以及状态都杂糅在同一对象中,并用事件机制来跟踪状态变化。并且,基于事件的模型与最近流行的 Promise 和 generator 异步编程模型不太友好。

Fetch API 旨在修正上述缺陷,它提供了与 HTTP 语义相同的 JS 语法,简单来说,它引入了 fetch() 这个实用的方法来获取网络资源。

Fetch 规范中对 API 进行了定义,它结合 ServiceWorkers,尝试做到如下优化:

  1. 改善离线体验
  2. 保持可扩展性

写这篇文章时,Fetch API 已被 Firefox 39(Nightly)以及 Chrome 42(dev)支持。在 github 上有相应的 polyfill

<!--more-->

特征检查

可以通过检查 HeadersRequestResponsefetch 在 window 或 worker 作用域中是否存在,来检查是否支持 Fetch API。

简单示例

Fetch API 中最常用的是 fetch() 方法,该方法最简单的形式是,接受一个 URL 参数并返回以一个 promise 对象:

fetch("/data.json").then(function(res) {
  // res instanceof Response == true.
  if (res.ok) {
    res.json().then(function(data) {
      console.log(data.entries);
    });
  } else {
    console.log("Looks like the response wasn't perfect, got status", res.status);
  }
}, function(e) {
  console.log("Fetch failed!", e);
});

如果是提交一个POST请求,代码如下:

fetch("http://www.example.org/submit.php", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  body: "firstName=Nikhil&favColor=blue&password=easytoguess"
}).then(function(res) {
  if (res.ok) {
    alert("Perfect! Your settings are saved.");
  } else if (res.status == 401) {
    alert("Oops! You are not authorized.");
  }
}, function(e) {
  alert("Error submitting form!");
});

fetch() 方法的参数和 Request() 构造函数的参数完全一致,所以你可以传任意复杂的参数来实现更强大的 fetch(),下面将详细介绍。

Headers

Fetch 引入了 3 个接口,分别是 HeadersRequestResponse。他们直接对应于的 HTTP 中相应的概念,但是基于隐私和安全考虑,也有些区别,例如支持 CORS 规则以及保证 cookies 不能被第三方获取。

Headers 接口是一个简单的键值对:

var content = "Hello World";
var reqHeaders = new Headers();
reqHeaders.append("Content-Type", "text/plain"
reqHeaders.append("Content-Length", content.length.toString());
reqHeaders.append("X-Custom-Header", "ProcessThisImmediately");

也可以给构造函数传一个多维数组或 JS 字面量对象:

reqHeaders = new Headers({
  "Content-Type": "text/plain",
  "Content-Length": content.length.toString(),
  "X-Custom-Header": "ProcessThisImmediately",
});

Headers 的内容可被检索:

console.log(reqHeaders.has("Content-Type")); // true
console.log(reqHeaders.has("Set-Cookie")); // false
reqHeaders.set("Content-Type", "text/html");
reqHeaders.append("X-Custom-Header", "AnotherValue");

console.log(reqHeaders.get("Content-Length")); // 11
console.log(reqHeaders.getAll("X-Custom-Header")); // ["ProcessThisImmediately", "AnotherValue"]

reqHeaders.delete("X-Custom-Header");
console.log(reqHeaders.getAll("X-Custom-Header")); // []

一些操作只在 ServiceWorkers 中可用,但这些 API 使得操作 header 更为方便。

由于 header 可以在发送请求时被发送或在收到响应时被接收,并规定了那些参数可写,所以在 Headers 对象中有个一 guard 属性,来指定哪些参数可以被改变。

可能的值如下:

  • "none":默认值
  • "request"Request.headers 对象只读
  • "request-no-cors":在 no-cors 模式下,Request.headers 对象只读
  • "response"Response.headers 对象只读
  • "immutable":通常在 ServiceWorkers 中使用,所有 Header 对象都为只读

规范中对每个 guard 属性值有更详细的描述。例如,当 guardrequest 时,你将不能添加或修改header 的 Content-Length 属性。

如果使用了一个不合法的 HTTP Header 名,那么 Headers 的方法通常都抛出 TypeError 异常。如果不小心写入了一个只读属性,也会抛出一个 TypeError 异常。除此以外,失败了将不抛出任何异常。例如:

var res = Response.error();
try {
  res.headers.set("Origin", "http://mybank.com");
} catch(e) {
  console.log("Cannot pretend to be a bank!");
}

Request

通过构造一个 Request 对象来获取网络资源,构造函数需要 URLmethodheaders 参数,同时也可以提供请求体(body)、请求模式(mode)、credentialscache hints 等参数。

最简单的形式如下:

var req = new Request("/index.html");
console.log(req.method); // "GET"
console.log(req.url); // "http://example.com/index.html"

也可以将一个 Request 对象传给构造函数,这将返回该对象的一个副本(这与 clone() 方法不同,后面将介绍)。

var copy = new Request(req);
console.log(copy.method); // "GET"
console.log(copy.url); // "http://example.com/index.html"

同时,这种形式通常只在 ServiceWorkers 中使用。

URL 之外的参数只能通过第二个参数传递,该参数是一个键值对:

var uploadReq = new Request("/uploadImage", {
  method: "POST",
  headers: {
    "Content-Type": "image/png",
  },
  body: "image data"
});

mode 参数用来决定是否允许跨域请求,以及哪些 response 属性可读。可选的 mode 值为 "same-origin""no-cors"(默认)以及 "cors"

same-origin

该模式很简单,如果一个请求是跨域的,那么将返回一个 error,这样确保所有的请求遵守同源策略。

var arbitraryUrl = document.getElementById("url-input").value;
fetch(arbitraryUrl, { mode: "same-origin" }).then(function(res) {
  console.log("Response succeeded?", res.ok);
}, function(e) {
  console.log("Please enter a same-origin URL!");
});

no-cors

该模式允许来自 CDN 的脚本、其他域的图片和其他一些跨域资源,但是首先有个前提条件,就是请求的 method 只能是HEADGETPOST。此外,如果 ServiceWorkers 拦截了这些请求,它不能随意添加或者修改除这些之外 Header 属性。第三,JS 不能访问 Response 对象中的任何属性,这确保了跨域时 ServiceWorkers 的安全和隐私信息泄漏问题。

cors

该模式通常用于跨域请求,用来从第三方提供的 API 获取数据。该模式遵守 CORS 协议,并只有有限的一些 Header 被暴露给 Response 对象,但是 body 是可读的。例如,获取一个 Flickr 最感兴趣的照片的清单:

var u = new URLSearchParams();
u.append('method', 'flickr.interestingness.getList');
u.append('api_key', '<insert api key here>');
u.append('format', 'json');
u.append('nojsoncallback', '1');

var apiCall = fetch('https://api.flickr.com/services/rest?' + u);

apiCall.then(function(response) {
  return response.json().then(function(json) {
    // photo is a list of photos.
    return json.photos.photo;
  });
}).then(function(photos) {
  photos.forEach(function(photo) {
    console.log(photo.title);
  });
});

你将无法从 Headers 中读取 Date 属性,因为 Flickr 在 Access-Control-Expose-Headers 中设置了不允许读取它。

response.headers.get("Date"); // null

另外,credentials 属性决定了是否可以跨域访问 cookie 。该属性与 XHR 的 withCredentials 标志相同,但是只有三个值,分别是 omit(默认)、same-origininclude

Request 对象也提供了客户端缓存机制(caching hints)。这个属性还在安全复审阶段。Firefox 提供了这个属性,但目前还不起作用。

Request 对象还有两个与 ServiceWorks 拦截有关的只读属性。其中一个是referrer,表示该 Request 的来源,可能为空。另外一个是 context,是一个非常大的枚举集合,定义了获得的资源的种类,它可能是 image 当请求来自于 img 标签时,可能是 worker 如果是一个 Worker 脚本,等等。如果使用 fetch() 函数,这个值是 fetch

Response

Response 对象通常在 fetch() 的回调中获得,也可以通过 JS 构造,不过这通常只在 ServiceWorkers 中使用。

Response 对象中最常见的属性是 status(整数,默认值是 200)和statusText(默认值是 "OK")。还有一个 ok 属性,这是 status 值为 200~299 时的语法糖。

另外,还有一个 type 属性,它的值可能是 "basic""cors""default""error""opaque"

  • "basic":同域的响应,除 Set-CookieSet-Cookie2 之外的所有 Header 可用
  • "cors":Response 从一个合法的跨域请求获得,某些 Header 和 body 可读
  • "error":网络错误。Response 对象的 status 属性为 0headers 属性为空并且不可写。当 Response 对象从 Response.error() 中得到时,就是这种类型
  • "opaque":在 "no-cors" 模式下请求了跨域资源。依靠服务端来做限制

type 属性值为 "error" 时会导致 fetch() 方法的 Promise 被 reject,reject 回调的参数为 TypeError 对象。

还有一些属性只在 ServerWorker 下有效。在 ServerWorker 下返回一个 Response 的正确方式为:

addEventListener('fetch', function(event) {
  event.respondWith(new Response("Response body", {
    headers: { "Content-Type" : "text/plain" }
  });
});

如你所见,Response 构造函数接收两个参数:返回的 body 和一个键值对对象,通过该对象来设置 statusstatusTextheaders 属性。

静态方法 Response.error() 将返回一个错误响应,Response.redirect(url, status) 将返回一个跳转响应。

处理 body

在 Request 和 Response 对象中都可能有 body 属性,并且 body 可以是各种类型,比较复杂,所以前面我们故意先跳过它,在这里单独拿出来讲解。

body 可以是以下任何一种类型的实例:

此外,Request 和 Response 都为操作 body 提供了以下方法,这些方法都返回一个使用实际内容 resolve 的 Promise 对象。

  • arrayBuffer()
  • blob()
  • json()
  • text()
  • formData()

所以,在处理非文本的数据方面,Fetch API 比 XHR 更为便利。

设置请求体:

var form = new FormData(document.getElementById('login-form'));
fetch("/login", {
  method: "POST",
  body: form
})

Responses 构造函数的第一个参数是响应体:

var res = new Response(new File(["chunk", "chunk"], "archive.zip",
{type: "application/zip"})                      
);

Request 和 Response(扩展的 fetch() 方法)都能够自动识别自己的内容类型,Request 还可以自动设置 Content-Type 头,如果开发者没有设置它的话。

流和克隆

非常重要的一点是,Request 和 Response 的 body 只能被读取一次!它们有一个属性叫 bodyUsed,读取一次之后设置为 true,之后就不能再被读取了。

var res = new Response("one time use");
console.log(res.bodyUsed); // false
res.text().then(function(v) {
  console.log(res.bodyUsed); // true
});
console.log(res.bodyUsed); // true

res.text().catch(function(e) {
  console.log("Tried to read already consumed Response");
});

这样设计的目的是为了之后兼容基于流的 API,我们的目的是当数据到达时就进行相应的处理,这样就使得 JavaScript 可以处理大文件例如视频,并且可以支持实时压缩和编辑。

有时候,我们希望能多次访问 body,例如,你可能想使用即将支持的 Cache API 来缓存 Request 和 Response,以便于可以离线使用,Cache 要求 body 能被再次读取。

那么,如何让 body 能被多次读取呢?API 为这两个对象提供了一个 clone() 方法。调用这个方法可以得到一个克隆对象,对象中包含全新的 body。不过要记得,clone() 必须要在使用 body 之前调用,也就是先 clone() 再读使用。

addEventListener('fetch', function(evt) {
  var sheep = new Response("Dolly");
  console.log(sheep.bodyUsed); // false
  var clone = sheep.clone();
  console.log(clone.bodyUsed); // false

  clone.text();
  console.log(sheep.bodyUsed); // false
  console.log(clone.bodyUsed); // true

  evt.respondWith(cache.add(sheep.clone()).then(function(e) {
    return sheep;
  });
});

未来的改进

为了支持流,Fetch 最终将提供可以中断执行和得到读取进度的 API。这些在 XHR 中有,但是想要实现基于 Promise 的 Fetch API 有些麻烦。

你可以加入 WHATWG 的邮件组参与 FetchServiceWorker 的讨论,为改进 API 贡献自己的力量。

为了创造更好的互联网而努力!

感谢 Andrea Marchesini, Anne van Kesteren 和 Ben Kelly 感谢他们对规范和实现所做的努力。

```div j-quote 参考资源: