十多年来,我们一直使用 XMLHttpRequest(XHR)来发送异步请求,XHR 很实用,但并不是一个设计优良的 API,在设计上并不符合职责分离原则,输入、输出以及状态都杂糅在同一对象中,并用事件机制来跟踪状态变化。并且,基于事件的模型与最近流行的 Promise 和 generator 异步编程模型不太友好。
Fetch API 旨在修正上述缺陷,它提供了与 HTTP 语义相同的 JS 语法,简单来说,它引入了 fetch()
这个实用的方法来获取网络资源。
在 Fetch 规范中对 API 进行了定义,它结合 ServiceWorkers,尝试做到如下优化:
- 改善离线体验
- 保持可扩展性
写这篇文章时,Fetch API 已被 Firefox 39(Nightly)以及 Chrome 42(dev)支持。在 github 上有相应的 polyfill。
特征检查
可以通过检查 Headers
、Request
、Response
或 fetch
在 window 或 worker 作用域中是否存在,来检查是否支持 Fetch API。
简单示例
Fetch API 中最常用的是 fetch()
方法,该方法最简单的形式是,接受一个 URL 参数并返回以一个 promise 对象:
|
|
如果是提交一个POST请求,代码如下:
|
|
fetch()
方法的参数和 Request()
构造函数的参数完全一致,所以你可以传任意复杂的参数来实现更强大的 fetch()
,下面将详细介绍。
Headers
Fetch 引入了 3 个接口,分别是 Headers
,Request
和 Response
。他们直接对应于的 HTTP 中相应的概念,但是基于隐私和安全考虑,也有些区别,例如支持 CORS 规则以及保证 cookies 不能被第三方获取。
Headers 接口是一个简单的键值对:
|
|
也可以给构造函数传一个多维数组或 JS 字面量对象:
|
|
Headers 的内容可被检索:
|
|
一些操作只在 ServiceWorkers 中可用,但这些 API 使得操作 header 更为方便。
由于 header 可以在发送请求时被发送或在收到响应时被接收,并规定了那些参数可写,所以在 Headers
对象中有个一 guard
属性,来指定哪些参数可以被改变。
可能的值如下:
"none"
:默认值"request"
:Request.headers
对象只读"request-no-cors"
:在no-cors
模式下,Request.headers
对象只读"response"
:Response.headers
对象只读"immutable"
:通常在 ServiceWorkers 中使用,所有 Header 对象都为只读
在规范中对每个 guard
属性值有更详细的描述。例如,当 guard
为 request
时,你将不能添加或修改header 的 Content-Length
属性。
如果使用了一个不合法的 HTTP Header 名,那么 Headers 的方法通常都抛出 TypeError 异常。如果不小心写入了一个只读属性,也会抛出一个 TypeError 异常。除此以外,失败了将不抛出任何异常。例如:
|
|
Request
通过构造一个 Request
对象来获取网络资源,构造函数需要 URL
、method
和 headers
参数,同时也可以提供请求体(body)、请求模式(mode)、credentials
和 cache hints
等参数。
最简单的形式如下:
|
|
也可以将一个 Request
对象传给构造函数,这将返回该对象的一个副本(这与 clone()
方法不同,后面将介绍)。
|
|
同时,这种形式通常只在 ServiceWorkers 中使用。
除 URL
之外的参数只能通过第二个参数传递,该参数是一个键值对:
|
|
mode
参数用来决定是否允许跨域请求,以及哪些 response
属性可读。可选的 mode
值为 "same-origin"
、"no-cors"
(默认)以及 "cors"
。
same-origin
该模式很简单,如果一个请求是跨域的,那么将返回一个 error
,这样确保所有的请求遵守同源策略。
|
|
no-cors
该模式允许来自 CDN 的脚本、其他域的图片和其他一些跨域资源,但是首先有个前提条件,就是请求的 method 只能是HEAD
、GET
或 POST
。此外,如果 ServiceWorkers 拦截了这些请求,它不能随意添加或者修改除这些之外 Header 属性。第三,JS 不能访问 Response 对象中的任何属性,这确保了跨域时 ServiceWorkers 的安全和隐私信息泄漏问题。
cors
该模式通常用于跨域请求,用来从第三方提供的 API 获取数据。该模式遵守 CORS 协议,并只有有限的一些 Header 被暴露给 Response 对象,但是 body 是可读的。例如,获取一个 Flickr 最感兴趣的照片的清单:
|
|
你将无法从 Headers 中读取 Date
属性,因为 Flickr 在 Access-Control-Expose-Headers
中设置了不允许读取它。
|
|
另外,credentials
属性决定了是否可以跨域访问 cookie 。该属性与 XHR 的withCredentials
标志相同,但是只有三个值,分别是 omit
(默认)、same-origin
和 include
。
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-Cookie
和Set-Cookie2
之外的所有 Header 可用"cors"
:Response 从一个合法的跨域请求获得,某些 Header 和 body 可读"error"
:网络错误。Response 对象的status
属性为0
,headers
属性为空并且不可写。当 Response 对象从Response.error()
中得到时,就是这种类型"opaque"
:在"no-cors"
模式下请求了跨域资源。依靠服务端来做限制
当 type
属性值为 "error"
时会导致 fetch()
方法的 Promise 被 reject,reject 回调的参数为 TypeError 对象。
还有一些属性只在 ServerWorker 下有效。在 ServerWorker 下返回一个 Response 的正确方式为:
|
|
如你所见,Response 构造函数接收两个参数:返回的 body 和一个键值对对象,通过该对象来设置 status
、statusText
和 headers
属性。
静态方法 Response.error()
将返回一个错误响应,Response.redirect(url, status)
将返回一个跳转响应。
处理 body
在 Request 和 Response 对象中都可能有 body
属性,并且 body
可以是各种类型,比较复杂,所以前面我们故意先跳过它,在这里单独拿出来讲解。
body
可以是以下任何一种类型的实例:
- ArrayBuffer
- ArrayBufferView (Uint8Array and friends)
- Blob/File
- string
- URLSearchParams
- FormData —— 目前不被 Gecko 和 Blink 支持,Firefox 预计在版本 39 和 Fetch 的其他部分一起推出
此外,Request 和 Response 都为操作 body
提供了以下方法,这些方法都返回一个使用实际内容 resolve 的 Promise 对象。
- arrayBuffer()
- blob()
- json()
- text()
- formData()
所以,在处理非文本的数据方面,Fetch API 比 XHR 更为便利。
设置请求体:
|
|
Responses 构造函数的第一个参数是响应体:
|
|
Request 和 Response(扩展的 fetch()
方法)都能够自动识别自己的内容类型,Request 还可以自动设置 Content-Type
头,如果开发者没有设置它的话。
流和克隆
非常重要的一点是,Request 和 Response 的 body 只能被读取一次!它们有一个属性叫 bodyUsed
,读取一次之后设置为 true
,之后就不能再被读取了。
|
|
这样设计的目的是为了之后兼容基于流的 API,我们的目的是当数据到达时就进行相应的处理,这样就使得 JavaScript 可以处理大文件例如视频,并且可以支持实时压缩和编辑。
有时候,我们希望能多次访问 body,例如,你可能想使用即将支持的 Cache API 来缓存 Request 和 Response,以便于可以离线使用,Cache 要求 body 能被再次读取。
那么,如何让 body 能被多次读取呢?API 为这两个对象提供了一个 clone()
方法。调用这个方法可以得到一个克隆对象,对象中包含全新的 body。不过要记得,clone()
必须要在使用 body 之前调用,也就是先 clone()
再读使用。
|
|
未来的改进
为了支持流,Fetch 最终将提供可以中断执行和得到读取进度的 API。这些在 XHR 中有,但是想要实现基于 Promise 的 Fetch API 有些麻烦。
你可以加入 WHATWG 的邮件组参与 Fetch 和 ServiceWorker 的讨论,为改进 API 贡献自己的力量。
为了创造更好的互联网而努力!
感谢 Andrea Marchesini, Anne van Kesteren 和 Ben Kelly 感谢他们对规范和实现所做的努力。