前端工程中发送 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. Python学习笔记5-元组Tuple

    tuple和list非常类似,但是tuple一旦初始化就不能修改,它也没有append(),insert()这样的方法.其他获取元素的方法和list是一样的 元组是用圆括号括起来的,其中的元素之间用逗 ...

  2. 第六篇:二维数组的传输 (host <-> device)

    前言 本文的目的很明确:介绍如何将二维数组传递进显存,以及如何将二维数组从显存传递回主机端. 实现步骤 1. 在显存中为二维数组开辟空间 2. 获取该二维数组在显存中的 pitch 值 (cudaMa ...

  3. 【黑金原创教程】 FPGA那些事儿《概念篇》

    简介一本讲述非软硬片上系统的书,另外还是低级建模的使用手册. 目录[黑金原创教程] FPGA那些事儿<概念篇>:File01 - 结构的玩笑[黑金原创教程] FPGA那些事儿<概念篇 ...

  4. 转载 hibernate一级缓存和二级缓存的区别

    文章来源:http://blog.csdn.net/defonds/article/details/2308972     hibernate一级缓存和二级缓存的区别 缓存是介于应用程序和物理数据源之 ...

  5. 160323、理解Java虚拟机体系结构

    今天看到一篇文章,觉得写得不错,特拿来跟大家分享一下 1 概述 众所周知,Java支持平台无关性.安全性和网络移动性.而Java平台由Java虚拟机和Java核心类所构成,它为纯Java程序提供了统一 ...

  6. Python--进阶处理9

    # =========================第九章:元编程============================= # ----------------在函数上添加包装器--------- ...

  7. KVM虚拟机添加硬盘

    1,创建硬盘 qemu-img create -f raw /opt/GlusterFS1_data.img 30G 硬盘名称为GlusterFS1_data.img 大小为30G 2,编辑虚拟机配置 ...

  8. oracle导入sql文件

    oracle导入sql文件: 1.进入到sql文件目录下,登录需要导入文件的用户 打开cmd,输入以下命令,进入oracle, sqlplus username/password username:需 ...

  9. 第九课——MySQL优化之索引和执行计划

    一.创建索引需要关注什么? 1.关注基数列唯一键的数量: 比如性别,该列只有男女之分,所以性别列基数是2: 2.关注选择性列唯一键与行数的比值,这个比值范围在0~1之前,值越小越好: 其实,选择性列唯 ...

  10. 使用ganymed工具调用ssh2

    需要引入ganymed-ssh2-build210.jar包. 其实很简单.所以直接贴代码,代码说话. package com.eshore.framework.util; import java.i ...