前端工程中发送 HTTP 请求历来都不是一件容易的事,前有骇人的 ActiveXObject
,后有 API 设计十分别扭的 XMLHttpRequest
,甚至这些原生 API 的用法至今还是不少大公司前端校招的考点之一。javascript
也正是如此,fetch 的出如今前端圈子里一石激起了千层浪,你们欢呼雀跃弹冠相庆巴不得立刻把项目中的 $.ajax
所有干掉。然而,在新鲜感事后, fetch 真的有你想象的那么美好吗?前端
若是你还不了解 fetch,能够参考个人同事 @camsong 在 2015 年写的文章 《传统 Ajax 已死,Fetch 永生》java
在开始「批斗」fetch以前,你们须要明确 fetch 的定位: fetch 是一个 low-level 的 API,它注定不会像你习惯的 $.ajax
或是 axios
等库帮你封装各类各样的功能或实现。 也正是由于这个定位,在学习或使用 fetch API 时,你会遇到很多的挫折。ios
(对于没有耐心看彻底文的同窗,请先记住本文的主旨不在于批评 fetch,事实上 fetch 的出现绝对是前端领域的进步体现。在了解主旨的前提下,关注 加黑 部分便可。)git
不少人看到 fetch 的第一眼确定会被它简洁的 API 吸引:github
fetch('http://abc.com/tiger.png');
原来须要 new XMLHttpRequest
等小十行代码才能实现的功能现在一行代码就能搞定,能不让人动心吗!ajax
可是当你真正在项目中使用时,少不了须要向服务端发送数据的过程,那么使用 fetch 发送一个对象到服务端须要几行代码呢?(出于兼容性考虑,大部分的项目在发送 POST 请求时都会使用 application/x-www-form-urlencoded
这种 Content-Type
)json
先来看看使用 jQuery 如何实现:axios
$.post('/api/add', {name: 'test'});
而后再看看 fetch 如何处理:api
fetch('/api/add', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: Object.keys({name: 'test'}).map((key) => { return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); }).join('&') });
等等, body
字段那一长串代码在干什么? 由于 fetch 是一个 low-level 的 API,因此你须要本身 encode HTTP 请求的 payload,还要本身指定 HTTP Header 中的 Content-Type
字段。
这样就结束了吗?若是你在本身的项目中这样发送 POST 请求,极可能会获得一个 401 Unauthorized
的结果(视你的服务端如何处理无权限的状况而定)。若是你在仔细看一遍文档,会发现 原来 fetch 在发送请求时默认不会带上 Cookie!
好,咱们让 fetch 带上 Cookie:
fetch('/api/add', { method: 'POST', credentials: 'include', ... });
这样,一个最基础的 POST 请求才算可以发出去。
同理,若是你须要 POST 一个 JSON 到服务端,你须要这样作:
fetch('/api/add', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json;charset=UTF-8' }, body: JSON.stringify({name: 'test'}) });
相比于 $.ajax
的封装,是否是复杂的不是一点半点呢?
按理说,fetch 是基于 Promise
的 API,每一个 fetch 请求会返回一个 Promise 对象,而 Promise 的异常处理且不管是否方便,起码你们是比较熟悉的了。然而 fetch 的异常处理,仍是有很多门道。
假如咱们用 fetch 请求一个不存在的资源:
fetch('xx.png')
.then(() => { console.log('ok'); }) .catch(() => { console.log('error'); });
按照咱们的惯例 console 应该要打印出 「error」才对,可事实又如何呢?有图有真相:
为何会打印出 「ok」呢?
按照 MDN 的 说法 ,fetch 只有在遇到网络错误的时候才会 reject 这个 promise,好比用户断网或请求地址的域名没法解析等。只要服务器可以返回 HTTP 响应(甚至只是 CORS preflight 的 OPTIONS 响应),promise 必定是 resolved 的状态。
因此要怎么判断一个 fetch 请求是否是成功呢?你得用 response.ok
这个字段:
fetch('xx.png')
.then((response) => { if (response.ok) { console.log('ok'); } else { console.log('error'); } }) .catch(() => { console.log('error'); });
再执行一次,终于看到了正确的日志:
当你的服务端返回的数据是 JSON 格式时,你确定但愿 fetch 返回给你的是一个普通 JavaScript 对象,然而你拿到的是一个 Response
对象,而真正的请求结果 —— 即 response.body
—— 则是一个 ReadableStream
。
fetch('/api/user.json?id=2') // 服务端返回 {"name": "test", "age": 1} 字符串 .then((response) => { // 这里拿到的 response 并非一个 {name: 'test', age: 1} 对象 return response.json(); // 将 response.body 经过 JSON.parse 转换为 JS 对象 }) .then(data => { console.log(data); // {name: 'test', age: 1} });
你可能以为,这些写在规范里的技术细节使用 fetch 的人无需关心,然而在实际使用过程当中你会遇到各类各样的问题迫使你不得不了解这些细节。
首先须要认可,fetch 将 response.body
设计成 ReadableStream 实际上是很是有前瞻性的,这种设计让你在请求大致积文件时变得很是有用。然而,在咱们的平常使用中,仍是短小的 JSON 片断更加常见。而为了兼容不常见的设计,咱们不得很少一次 response.json()
的调用。
不只是调用变得麻烦,若是你的服务端采用了严格的 REST 风格, 对于某些特殊状况并无返回 JSON 字符串,而是用了 HTTP 状态码(如: 204 No Content
),那么在调用 response.json()
时则会抛出异常。
此外, Response
还限制了响应内容的重复读取和转换 ,例如以下代码:
var prevFetch = window.fetch; window.fetch = function() { prevFetch.apply(this, arguments) .then(response => { return new Promise((resolve, reject) => { response.json().then(data => { if (data.hasError === true) { tracker.log('API Error'); } resolve(response); }); }); }); } fetch('/api/user.json?id=1') .then(response => { return response.json(); // 先将结果转换为 JSON 对象 }) .then(data => { console.log(data); });
是对 fetch 作了一个简单的 AOP,试图拦截全部的请求结果,并当返回的 JSON 对象中 hasError
字段若是为 true
的话,打点记录出错的接口。
然而这样的代码会致使以下错误:
Uncaught TypeError: Already read
调试一番后,你会发现是由于咱们在切面中已经调用了 response.json()
,这个时候重复调用该方法时就会报错。(实际上,再次调用其它任何转换方法,如 .text()
也会报错)
所以,想要在 fetch 上实现 AOP 仍需另辟蹊径。
1. fetch 不支持同步请求
你们都知道同步请求阻塞页面交互,但事实上仍有很多项目在使用同步请求,多是历史架构等等缘由。若是你切换了 fetch 则没法实现这一点。
2. fetch 不支持取消一个请求
使用 XMLHttpRequest 你能够用 xhr.abort()
方法取消一个请求(虽然这个方法也不是那么靠谱,同时是否真的「取消」还依赖于服务端的实现),可是使用 fetch 就无能为力了,至少目前是这样的。
3. fetch 没法查看请求的进度
使用 XMLHttpRequest 你能够经过 xhr.onprogress
回调来动态更新请求的进度,而这一点目前 fetch 尚未原生支持。
仍是要再次明确,fetch API 的出现绝对是推进了前端在请求发送功能方面的进步。
然而,也须要意识到, fetch 是一个至关底层的 API,在实际项目使用中,须要作各类各样的封装和异常处理,而并不是开箱即用 ,更作不到直接替换 $.ajax
或其余请求库。