前端工程中发送 HTTP 请求从来都不是一件容易的事,前有骇人的 ActiveXObject ,后有 API 设计十分别扭的 XMLHttpRequest ,甚至这些原生 API 的用法至今仍是很多大公司前端校招的考点之一。

也正是如此,fetch 的出现在前端圈子里一石激起了千层浪,大家欢呼雀跃弹冠相庆恨不得马上把项目中的 $.ajax 全部干掉。然而,在新鲜感过后, fetch 真的有你想象的那么美好吗?

如果你还不了解 fetch,可以参考我的同事 @camsong 在 2015 年写的文章 《传统 Ajax 已死,Fetch 永生》

在开始「批斗」fetch之前,大家需要明确 fetch 的定位: fetch 是一个 low-level 的 API,它注定不会像你习惯的 $.ajax 或是 axios 等库帮你封装各种各样的功能或实现。 也正是因为这个定位,在学习或使用 fetch API 时,你会遇到不少的挫折。

(对于没有耐心看完全文的同学,请先记住本文的主旨不在于批评 fetch,事实上 fetch 的出现绝对是前端领域的进步体现。在了解主旨的前提下,关注 加黑 部分即可。)

发请求,比你想象的要复杂

很多人看到 fetch 的第一眼肯定会被它简洁的 API 吸引:

fetch('http://abc.com/tiger.png');

原来需要 new XMLHttpRequest 等小十行代码才能实现的功能如今一行代码就能搞定,能不让人动心吗!

但是当你真正在项目中使用时,少不了需要向服务端发送数据的过程,那么使用 fetch 发送一个对象到服务端需要几行代码呢?(出于兼容性考虑,大部分的项目在发送 POST 请求时都会使用 application/x-www-form-urlencoded 这种 Content-Type )

先来看看使用 jQuery 如何实现:

$.post('/api/add', {name: 'test'});

然后再看看 fetch 如何处理:

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');
});

再执行一次,终于看到了正确的日志:

Stream API,比你想象的复杂

当你的服务端返回的数据是 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 或其他请求库。

参考资料

  1. fetch spec https://fetch.spec.whatwg.org/#body
  2. fetch 实现 https://github.com/github/fetch
  3. 什么是 Already Read 报错 http://stackoverflow.com/questions/34786358/what-does-this-error-mean-uncaught-typeerror-already-read
  4. 使用 fetch 处理 HTTP 请求失败 https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
  5. https://jakearchibald.com/2015/thats-so-fetch/

fetch body里数据为ReadableStream 解决办法的更多相关文章

  1. C#使用ListView更新数据出现闪烁解决办法

    C#使用ListView更新数据出现闪烁解决办法 在使用vs自动控件ListView控件时候,更新里面的部分代码时候出现闪烁的情况 如图: 解决以后: 解决办法使用双缓冲:添加新类继承ListView ...

  2. [经验] 新版SkyIAR、Easy Image X在有些PE里不能运行的解决办法

    [经验] 新版SkyIAR.Easy Image X在有些PE里不能运行的解决办法 xxwl2008 发表于 2013-1-26 11:58:38 https://www.itsk.com/threa ...

  3. 360或者金山毒霸可能会导致HP网络打印机驱动安装失败“数据无效”的解决办法

    360或者金山毒霸可能会导致HP网络打印机驱动安装失败“数据无效”的解决办法     同事办公室的打印机是网线接口的那种网络打印机,不是直接连到电脑的那种,他电脑安装了360和金山毒霸,WIN10下安 ...

  4. ORACLE数据删除数据删除的解决办法

    今天主要以oracle数据库为例,介绍关于表中数据删除的解决办法.(不考虑全库备份和利用归档日志)删除表中数据有三种方法:·delete(删除一条记录)·drop或truncate删除表格中数据 1. ...

  5. hive数据倾斜的解决办法

    数据倾斜是进行大数据计算时常见的问题.主要分为map端倾斜和reduce端倾斜,map端倾斜主要是因为输入文件大小不均匀导致,reduce端主要是partition不均匀导致. 在hive中遇到数据倾 ...

  6. SQL Server跨库复制表数据错误的解决办法

    SQL Server跨库复制表数据的解决办法   跨库复制表数据,有很多种方法,最常见的是写程序来批量导入数据了,但是这种方法并不是最优方法,今天就用到了一个很犀利的方法,可以完美在 Sql Serv ...

  7. 使用AFNetworking请求新浪微博数据接口出错解决办法

    在使用AFNetworking请求新浪微博数据接口时会出这样的错误,如 这样的错误说明,AFNetworking无法处理这样的数据格式.所以,我们需要修改AFNetworking中的一些接收数据格式. ...

  8. Devexpres下LookUpEdit绑定数据后会默认弹出数据框的解决办法

    LookUpEdit绑定数据后会默认弹出数据框很不友好问题现象: 问题解决前的代码: lueManagement.Text = groupEntity.Name; 2 lueManagement.Ed ...

  9. VS中Dev控件在工具箱里的不见的解决办法

    出现问题:调整了VS中Dev控件后(以免生成程序每次都要在客户机上面注册dev),之前安装的DEV控件在vs工具箱中消失了,重装可以解决,但是太费时间了,检测dev自带的设置,找到了解决办法. 解决办 ...

随机推荐

  1. Docker(1)在CentOS上的安装与卸载

     一. Docker的安装 CentOS7 上安装: 1. 卸载旧版本 $ sudo yum remove docker \ docker-client \ docker-client-latest ...

  2. python--base64

    import base64import os # base64,参数为文件路径名def file_base64(filepath): if os.path.isfile(filepath): with ...

  3. CH5102 Mobile Service【线性dp】

    5102 Mobile Service 0x50「动态规划」例题 描述 一个公司有三个移动服务员,最初分别在位置1,2,3处.如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个 ...

  4. 在iOS模拟器上安装程式的ios-sim

    针对iOS装置进行开发时,绝大部分开发者采用的工具都是官方的Xcode.问题是负责图像设计和开发管理人员,却不一定熟悉Xcode的操作,这时ios-sim便是一个解决方案. 曾经从事iOS开发的朋友, ...

  5. 字符串与图片的Base64编码转换操作

    //图片 转为 base64编码的文本 private void button1_Click(object sender, EventArgs e) { OpenFileDialog dlg = ne ...

  6. 该死的Kafka,远程连接Kafka超时以及解决办法

    关于消息的发布与订阅,之前一直使用的是activeMQ基于JMS的消息队列进行操作的,最近听说有一个更高效的消息的发布与订阅技术,就是Kafka. 关于kafka的介绍,在这里就不做过多讲解了,因为我 ...

  7. LeetCode_链表操作1—Swap Nodes in Pairs

    Given a linked list, swap every two adjacent nodes and return its head. For example, Given 1->2-& ...

  8. 手游包压缩技术引领手游行业实现app页游化

    近些年,掌上游戏时代已经成为全民风尚,但身为游戏开发商考虑过手游安装包大小与用户转化率之间的关系吗? 随着手机游戏市场发展愈发壮大,行业发展愈加成熟,手游厂商愈来愈多,手游产业也进入了优胜劣汰的环节, ...

  9. 临时修改当前crontab编辑器

    EDITOR=viexport EDITOR然后crontab -e就不会有这个问题了

  10. python常见模块之序列化(json与pickle以及shelve)

    什么是序列化? 我们把对象(变量)从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flatte ...