异步请求之 Fetch
当我们谈到异步操作请求数据时,一般会提及 AJAX(Asynchronous Javascript And XML)。事实上,这种局部刷新交互式的开发技术,最早是由Adaptive Path公司的Jesse James Garrett在2005年2月提出。
在过去的十几年里,异步请求数据都是围绕 XMLHttpRequest 这个核心对象而展开的。我们要么自己封装一个原生的AJAX库,要么使用 jQuery 或者 Zepto 这种工具库里封装好的 $.ajax
方法。
针对多个回调的情况,为了优雅的处理异步操作,我们还得用 promise 来包装下 ajax 方法。
而现在我们开发的项目,大多数基于 React 或者 Vue 这种框架。所以,为一个ajax方法而去引入一个工具库,这种做法显然不合适。另外,用 Promise 去包装原生的ajax,再去处理兼容的做法,也有点过时了。
除了 AJAX,到现在,终于有了异步请求数据的替代方案,那就是W3C所推出的标准API-Fetch,这个API是挂载于 BOM 的 window 下。在写本篇文章时,chrome和firefox的最新版本都支持它。
一、一个简单的 Fetch
先来一个最简单的 fetch 请求,使用的接口数据来源于 JSONPlaceholder,它是一个在线模拟和仿照API的站点,我之前在 使用 JSON Server 构建数据接口 一文中有介绍。
首先,我们打开 JSONPlaceholder,开启控制台,输入以下内容:
fetch('http://jsonplaceholder.typicode.com/') |
返回的是:

可以看到,fetch 执行后返回的是一个 Response 对象,并且采用的是 Promise 的链式写法。当对请求后的内容执行 res.text()
后,便可以得到网页的源码。
这样,就完成一个简单的 fetch 请求。当你将控制台切换到 Network
选项,选中 XHR
,你会发现,请求的类型不再是 xhr
,而变成了 fetch
。

二、Fetch 用法
通过上面我们可以知道,要发起一个 fetch 请求,只需要:
fetch('https://jsonplaceholder.typicode.com/posts/1') |
在这里,我们请求jsonplaceholder上第一篇内容的数据(json格式),执行fetch请求后,它返回结果为:
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined} |
说明fetch执行后,返回的是一个 Promise 对象,这也是为什么我们在文章开头的例子中,可以在后面接 then
方法的原因。
接着,我们进一步来获取对应的请求内容:
fetch('https://jsonplaceholder.typicode.com/posts/1') |
代码中,then
函数中的参数(res)为一个 Response
对象,我们将它打印出来,会发现它包含了一系列属性:

其中包含请求地址(url)、ok(请求是否成功)、状态码(status)、状态描述(statusText)、body、header、type 之类的,当然,我们最关心的是 ok
, status
属性。当 ok
的值为 true 并且 status
的值是 200 ~ 299 时,表示fetch请求成功。
但是,我们在这个对象中并没有得到我们请求的业务数据。因此,我们进一步处理:
fetch('https://jsonplaceholder.typicode.com/posts/1') |
因为 res 是一个 Response
对象,在这里,我们在代码中使用了它隐式原型(__proto__
)上的方法 json()
,该方法将请求的内容转换为 json 格式,并且该方法仍然返回一个 Promise 对象。然后我们在第二个 then
函数里面获取返回的内容。即:

如果你请求的是其他格式的数据,通过上面的截图,Response
对象还提供了其他的内容转换方法,这些方法都返回的是一个 Promise 对象,它们主要的有:
Body.text()
将请求后返回的内容解析为字符串格式
Body.json()
将请求后返回的内容解析为Json格式
Body.blob()
将请求后返回的内容解析为Blob格式
Body.arrayBuffer()
将请求后返回的内容解析为ArrayBuffer格式
Body.formData()
将请求后返回的内容解析为formData格式
更多关于Response
对象内容,可查看相关资料
2.1 捕获错误
我们在请求接口时,大部分请求都会成功返回。但是,也存在发送错误的情况,比如说网络错误,或者说将接口地址拼写错误,这个时候,我们可以像 Promise 一样,使用 catch
方法来捕获这个错误,用代码来说明下:
fetch('https://xx.typicode.com/posts/1') |
在上面代码中,将请求接口地址的主域名改为 xx
,这显然是一个不存在的接口地址,执行fetch后在 catch
函数中里将会捕获到一个请求失败的错误。
因此,为了保证代码的健壮性,最后在后面加上 then
函数。
在早期的Fetch版本中,如果请求的是一个不存在的接口,可能代码中的第一个 then
函数还是会执行的,然后才会执行 catch
函数,但这显然是不对的。针对这种情况,我们早期得在第一个 then
函数这样处理:
fetch('https://xx.typicode.com/posts/1') |
即通过判断 Response
对象的 ok
属性,如果该值为 false,则直接抛出错误,触发后面的 catch
函数。
2.2 get 请求
前面讲到的都是 get 请求,但是请求地址里都没涉及到参数。倘若,你的get请求里面带有参数,则只能在请求地址拼接,如:
fetch('https://jsonplaceholder.typicode.com/posts?_page=2&_limit=5') |
上面的代码表示请求第2页的5条数据。
2.3 post 请求
默认情况下,使用 fetch 发送 get 请求非常简单,你几乎不用作任何设置。但如果要进行其他方式的请求,则需要使用到 fetch 函数的第二个参数,它是一个对象,主要配置请求的相关选项。
比如发送一个 post 请求,代码如下:
fetch('http://jsonplaceholder.typicode.com/posts', { |
上面的代码,表示新增一篇文章。可以看到,第二参数中指定了请求类型为 post
,并且在 header 中设置了向服务器发送的内容编码类型(json),最后,在 body 中通过 JSON.stringify
函数设置传入的参数。
这里要注意,对于post请求,如果不在 headers 中设置 'Content-Type': 'application/json'
,可能会导致不能正常请求到数据。因为此时在 Request Headers
中,content-type
的值默认为 content-type:text/plain;charset=UTF-8
,但在 Response Headers
中则是 Content-Type:application/json; charset=utf-8
,前后数据类型不匹配!
最后,对于get请求,还要注意的是,get 请求地址的参数只能拼接URL后面,强制将请求参数放在fetch函数中第二个参数的body里,会导致报错。
2.4 自定义Header
传统的XMLHttpRequest有两个主要缺点:
- 对搜索引擎的支持比较弱
- 不支持浏览器的history功能,即网页不能前进或后退
其中第二个问题可以使用 pjax 来解决,它的原理主要是用到了 pushState API,并且在发送 ajax 前,定义一个专属头部,以便服务端识别:
xhr.setRequestHeader('X-PJAX', true) |
github 中也大量使用了 pjax,你可以打开 github,打开控制台的 Network 选项,就能看到:

说这么多,我只是想表示,fetch API 也提供了自定义 Header 接口,在这个接口上,我们可以对请求头和响应头执行各种操作,其中包含添加,删除、检索。其实,你可以把Headers对象看成是一组键值对的集合。
来看下它们的具体操作:
var myHeaders = new Headers(); |
在 fetch 请求中获取 Header:
fetch('http://jsonplaceholder.typicode.com/posts', { |
2.5 第二参数的其他属性
除了 method
、headers
、body
属性,fetch 的第二个参数还包含了其他属性,简述如下:
- method:发送请求的方法,比如 get、post、put、DELETE 等。get 请求可省略第二参数
- headers:请求的头信息
- body:请求的 body 信息
- mode:请求模式,值可以是
cors
(支持跨域)、no-cors
(跨域请求,不需要服务端支持cors) 或same-origin
(不允许跨域) - credentials:请求证书,值可以是
omit
(默认值,不发送cookie)、same-origin
(cookie只能同域发送,不可跨域)、include
(cookie同域,跨域都可发送) - cache:缓存设置,它的值有
default
(默认值,fetch请求前进行http缓存) 、no-store
(完全不http缓存) 、no-cache
(有缓存时,fetch将发送请求,并更新缓存) 、reload
(忽略之前的缓存,请求后更新缓存) 、force-cache
(严重依赖缓存,即使缓存过期,也读取该缓存) 或者only-if-cached
(严重依赖缓存,无缓存时将抛出错误) - redirect:重定向设置,它的值有
follow
(自动重定向),error
(产生重定向时将自动终止并且抛出一个错误), 或者manual
(手动处理重定向)
三、相关问题
作为下一代异步通信的规范,Fetch API 虽然提供了一些强大的功能以及异步写法,但是也存在不少问题,这也是它至今未大规模使用的原因。
3.1 兼容性
对于不支持 fetch 的浏览器或者服务器请求,我们可以通过以下代码做兼容:
if(self.fetch) { |
这里使用 self
主要考虑 Window 或 Worker 环境。如果你要针对不支持它的浏览器也使用 fetch,可使用它的语法糖 Fetch Polyfill
3.2 没有 timeout 特性
fetch 除了不能取消发送外,还有另外一点饱受诟病,那就是不支持 timeout。
用过 $.ajax
的人都知道,我们可以在其中的选项中设置 timeout
属性,它的值是一个时间(毫秒),它表示请求若超过该时间,我们便可以在 error
或者 complete
函数中判断状态值,粗略代码如下:
var getUserData = $.ajax({ |
这样,当发送ajax请求出现了问题,便能给用户良好的反馈。但很可惜,本文介绍的 fetch 却没有提供相关设置。
针对此情况,目前网上有两种替代方案,即 setTimeout
和 Promise.race
。
3.2.1 setTimeout
这种方式比较简单粗暴,主要是利用 Promise 内部状态发生改变后(fulfilled 或 rejected),就再也会发生变化了。即在一个 Promise 中设置一个超时 reject,以及将 resolve 传入 fetch 中。若它们两者之间只有其中一个执行了,便达到了我们的效果,代码如下:
function fetchTimeout1(promise, timeout) { |
3.2.2 Promise.race
我们知道,Promise.race 返回的结果取决于所监听的 Promise 列表中最先改变状态的(无论是 fulfilled 还是 rejected) 的那个 Promise,利用这点,再结合上面的 timeout,我们又可以这样处理:
function fetchTimeout2(promise, timeout) { |
其实,无论是 setTimeout
还是 Promise.race
,它们的解决方案的原理都大同小异。
3.3 不支持取消(abort)发送的请求
以前的 ajax 请求中,在某些情况下,我们可以通过 abort
方法来停止 ajax 请求,终止一切网络活动。
但 fetch 不同,一旦你发起了一个 fetch 请求,便不能停止。但是,你可以像上面处理 setTimeout 一样,来模拟一个 abort 特性。
除了上面的做法,如果你实在非常喜欢 fetch 的语法,又想在某些情况下,能够中断其请求,延续传统ajax里的一些好的特性。那么,我建议你可以考虑 axios。它是一个基于 Promise 并将 XMLHttpRequest(浏览器端)、HTTP(node服务端)结合封装起来的网络请求库,也就是说在客户端和服务端都能使用,另外,听说它的实现方式非常优雅。
3.4 fetch 对一些状态码不会 reject
如果你用过 XMLHttpRequest
,我们在发送请求后,首先会对这样判断状态码:
xhr.onreadystatechange = function() { |
由于封装良好的原因,$.ajax()
方法我们则无需单独判断状态码。
但是,fetch 比较特殊,由于它返回的是一个 Promise 对象,所以不管请求结果的状态码是 4XX 还是 5XX ,它都不会被 reject
。且只有网络错误时,才会被 reject
。
所以,必要的时候,我们可以在第一个 then
函数里面进行状态码处理,即如果服务端返回的状态码是非 200 的情况,可以考虑抛出错误。
或者,通过在Fetch请求后的then函数中判断 response.ok
来确定请求是否成功。