camsong / blog

✍️Front-end Development Thoughts

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

传统 Ajax 已死,Fetch 永生

camsong opened this issue · comments

image

原谅我做一次标题党,Ajax 不会死,传统 Ajax 指的是 XMLHttpRequest(XHR),未来现在已被 Fetch 替代。

最近把阿里一个千万级 PV 的数据产品全部由 jQuery 的 $.ajax 迁移到 Fetch,上线一个多月以来运行非常稳定。结果证明,对于 IE8+ 以上浏览器,在生产环境使用 Fetch 是可行的。

由于 Fetch API 是基于 Promise 设计,有必要先学习一下 Promise,推荐阅读 MDN Promise 教程。旧浏览器不支持 Promise,需要使用 polyfill es6-promise

本文不是 Fetch API 科普贴,其实是讲异步处理和 Promise 的。Fetch API 很简单,看文档很快就学会了。推荐 MDN Fetch 教程 和 万能的WHATWG Fetch 规范

Why Fetch

XMLHttpRequest 是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promise,generator/yield,async/await 友好。

Fetch 的出现就是为了解决 XHR 的问题,拿例子说明:

使用 XHR 发送一个 json 请求一般是这样:

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';

xhr.onload = function() {
  console.log(xhr.response);
};

xhr.onerror = function() {
  console.log("Oops, error");
};

xhr.send();

使用 Fetch 后,顿时看起来好一点

fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
  console.log(data);
}).catch(function(e) {
  console.log("Oops, error");
});

使用 ES6 的 箭头函数 后:

fetch(url).then(response => response.json())
  .then(data => console.log(data))
  .catch(e => console.log("Oops, error", e))

现在看起来好很多了,但这种 Promise 的写法还是有 Callback 的影子,而且 promise 使用 catch 方法来进行错误处理的方式有点奇怪。不用急,下面使用 async/await 来做最终优化:

注:async/await 是非常新的 API,属于 ES7,目前尚在 Stage 1(提议) 阶段,这是它的完整规范。使用 Babel 开启 runtime 模式后可以把 async/await 无痛编译成 ES5 代码。也可以直接使用 regenerator 来编译到 ES5。

try {
  let response = await fetch(url);
  let data = await response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);
}
// 注:这段代码如果想运行,外面需要包一个 async function

duang~~ 的一声,使用 await 后,写异步代码就像写同步代码一样爽await 后面可以跟 Promise 对象,表示等待 Promise resolve() 才会继续向下执行,如果 Promise 被 reject() 或抛出异常则会被外面的 try...catch 捕获。

Promise,generator/yield,await/async 都是现在和未来 JS 解决异步的标准做法,可以完美搭配使用。这也是使用标准 Promise 一大好处。最近也把项目中使用第三方 Promise 库的代码全部转成标准 Promise,为以后全面使用 async/await 做准备。

另外,Fetch 也很适合做现在流行的同构应用,有人基于 Fetch 的语法,在 Node 端基于 http 库实现了 node-fetch,又有人封装了用于同构应用的 isomorphic-fetch

注:同构(isomorphic/universal)就是使前后端运行同一套代码的意思,后端一般是指 NodeJS 环境。

总结一下,Fetch 优点主要有:

  1. 语法简洁,更加语义化
  2. 基于标准 Promise 实现,支持 async/await
  3. 同构方便,使用 isomorphic-fetch

Fetch 启用方法

下面是重点↓↓↓

先看一下 Fetch 原生支持率:
image

原生支持率并不高,幸运的是,引入下面这些 polyfill 后可以完美支持 IE8+ :

  1. 由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham
  2. 引入 Promise 的 polyfill: es6-promise
  3. 引入 fetch 探测库:fetch-detector
  4. 引入 fetch 的 polyfill: fetch-ie8
  5. 可选:如果你还使用了 jsonp,引入 fetch-jsonp
  6. 可选:开启 Babel 的 runtime 模式,现在就使用 async/await

Fetch polyfill 的基本原理是探测是否存在 window.fetch 方法,如果没有则用 XHR 实现。这也是 github/fetch 的做法,但是有些浏览器(Chrome 45)原生支持 Fetch,但响应中有中文时会乱码,老外又不太关心这种问题,所以我自己才封装了 fetch-detectorfetch-ie8 只在浏览器稳定支持 Fetch 情况下才使用原生 Fetch。这些库现在 每天有几千万个请求都在使用,绝对靠谱

终于,引用了这一堆 polyfill 后,可以愉快地使用 Fetch 了。但要小心,下面有坑:

Fetch 常见坑

  • Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'})
  • 服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

竟然没有提到 IE,这实在太不科学了,现在来详细说下 IE

IE 使用策略

所有版本的 IE 均不支持原生 Fetch,fetch-ie8 会自动使用 XHR 做 polyfill。但在跨域时有个问题需要处理。

IE8, 9 的 XHR 不支持 CORS 跨域,虽然提供 XDomainRequest,但这个东西就是玩具,不支持传 Cookie!如果接口需要权限验证,还是乖乖地使用 jsonp 吧,推荐使用 fetch-jsonp。如果有问题直接提 issue,我会第一时间解决。

Fetch 和标准 Promise 的不足

由于 Fetch 是典型的异步场景,所以大部分遇到的问题不是 Fetch 的,其实是 Promise 的。ES6 的 Promise 是基于 Promises/A+ 标准,为了保持 简单简洁 ,只提供极简的几个 API。如果你用过一些牛 X 的异步库,如 jQuery(不要笑) 、Q.js 或者 RSVP.js,可能会感觉 Promise 功能太少了。

没有 Deferred

Deferred 可以在创建 Promise 时可以减少一层嵌套,还有就是跨方法使用时很方便。
ECMAScript 11 年就有过 Deferred 提案,但后来没被接受。其实用 Promise 不到十行代码就能实现 Deferred:es6-deferred。现在有了 async/await,generator/yield 后,deferred 就没有使用价值了。

没有获取状态方法:isRejected,isResolved

标准 Promise 没有提供获取当前状态 rejected 或者 resolved 的方法。只允许外部传入成功或失败后的回调。我认为这其实是优点,这是一种声明式的接口,更简单。

缺少其它一些方法:always,progress,finally

always 可以通过在 then 和 catch 里重复调用方法实现。finally 也类似。progress 这种进度通知的功能还没有用过,暂不知道如何替代。

不能中断,没有 abort、terminate、onTimeout 或 cancel 方法

Fetch 和 Promise 一样,一旦发起,不能中断,也不会超时,只能等待被 resolve 或 reject。幸运的是,whatwg 目前正在尝试解决这个问题 whatwg/fetch#27

资料

最后

Fetch 替换 XHR 只是时间问题,现在看到国外很多新的库都默认使用了 Fetch。

最后再做一个大胆预测:由于 async/await 这类新异步语法的出现,第三方的 Promise 类库会逐渐被标准 Promise 替代,使用 polyfill 是现在比较明智的做法。

如果你觉得本文对你有帮助,请点击右上方的 Star 鼓励一下,或者点击 Watch 订阅

不错的文章,赞

谢谢科普!赞赞赞b( ̄▽ ̄)d

commented

火钳刘明

不错的文章,赞

commented

Fetch 永生

commented

学到了

学习了

确实有点标题

写的不错,还好我的前端组件跟请求都是分离的

围观

科普的不错。新的项目正在使用~

commented

科普下。说不定就用的上

顶下

滚粗,标题党。 ajax的promise 写法而已,没有本质差别。 IE11都还要 polyfill的支持, 产品环境用起来也太麻烦

另一种选择,值得一看。

polyfill 用的太多了,生产环境用起来压力太大

@kisnows 个数多而已,代码没多少,我们千万级 PV 产品一直运行稳定

get it

let data = response.json(); should be let data = await response.json();

btw, how do i log an issue for an issue? 😄

@ralphite wow, nice catch 🌟
Just add a comment here for issues, i will keep checking 😄

commented

但是看起來並沒有比較簡潔好寫啊?有辦法比較其中的效能嗎

@fifiteen82726 搭配 async/await 應該非常簡潔吧?

在IE里面 我导入了

<script src="./es5-shim.min.js"></script>
<script src="./es5-sham.min.js"></script>
<script src="./es6-promise.min.js"></script>
<script src="./fetch-detector.js"></script>
<script src="./fetch-ie8.js"></script>

然后在点击按钮 变成下载文件了 (文件里面是后端返回的JSON)

@gdzgshum fetch-ie8 和 fetch-detector 要以 CommonJS 方式引用,要使用 Webpack 来打包。如果想用 ES6 语法,就用 Babel 编译一下

mark!!!

// 伪代码,其中设置headers的时候在浏览器中看到是小写accept
fetch('url', {
    headers: {
        'Accept': 'application/json'
    }
})

得到
image

有遇到么,怎么解决?

@liyatang 测试了一下 Chrome 会把设置的 headers 全部改成小写,但 Firefox 下不会。可能是 Chrome 的 bug,你可以在这里提个 issue https://crbug.com/

@camsong 看官方的解释说正常,标准如此https://fetch.spec.whatwg.org/#terminology-headers

附上issue 链接

@liyatang 赞高效率,原来这并不是 bug,我也学习了,顺便再补充一下:

根据 HTTP 规范(RFC 7230,RFC 2616),HTTP header 的 name 是不区分大小写的。
而且根据规范,Fetch 和 XHR's setRequestHeader() 都应该把 header 的 name 转成小写,只是有些浏览器没有转而已。

A 页面设置SESSION
var url = 'http://pcapi.hileyou.com/a.php'
fetch(url).then(response => response.json())
.then(
data => data.status
? window.location.href = './#/user'
: this.showErrorModal(data.msg)
)
.catch(e => console.log("Oops, error", e))

B 页面读取
var url = 'http://pcapi.hileyou.com/b.php';
fetch(url).then(response => response.json())
.then(
data => this.setState({data: data.data})
)
.catch(e => console.log("Oops, login error", e))

为什么B页面读取不了session 但是在浏览器分别打开这两链接是可以的
怎么解决这个问题呢?

@gdzgshum 开启 cookie 试一下,参见:

Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'})

我传了 还是不行, 每次这个信息都不一样
Set-Cookie:PHPSESSID=sohts2vfrbsd1643jf856jhuql8tkhe7; path=/; HttpOnly

是不是浏览器认为这不是一个相同回话

不应该每次都 set PHPSESSID,这样会被认为是新的会话。你查一下 http request header 信息,与 xhr 比较一下,看是不是有些header 信息漏了

我用的webpack 生成的8080端口 请求的时候 session 跨域了,正在找处理方法。
谢谢您的解答

import 'fetch-ie8'
提示找不到module 'fetch-ie8',根据https://github.com/camsong/fetch-ie8/进去看到的npm的安装方法为
npm install fetch-polyfill --save

这个应该怎么处理呢

@babizhu npm install fetch-ie8 --save 已更新

反应好迅速,谢谢
import 'fetch-detector'

import 'fetch-ie8'
import Promise from 'es6-promise'

ie11反而提示找不到Promise了,请问这个应该怎么破

@babizhu 注意引用顺序,Promise 应该放最前面。如果你要兼容 IE8,还需要引入 es5-shim, es5-sham

赞!
另,Async Functions 现在已经上升到 stage 3 了。

https://github.com/tc39/ecma262

不错的文章~

感谢。如果要为 fetch 添加 progress 的监听,您觉得什么样的 API 是比较好的?

我想到有两种,第二种 compose 起来简单些。

promise.progressed(progressFn).then(resolveFn, rejectFn)
promise.then(resolveFn, rejectFn, progressFn)

学习了~

非常有用的文章,感谢

写的很详细

麻烦问下博主,fetch 的接口实际上还很原始,你们在使用的时候有没有对 fetch 进行一些自己的封装,例如便捷设置 content-type,并自动进行 body 的转换等

commented

清晰明了,有价值

@malcolmyu

同样的问题...

我在自己的个人项目里封装了fetch, copy了原本的fetch, 可以set预配的config, 以及一些post, postJSON, postForm等helper

不知道这样科学不

@Kaijun 我也是这样搞的,主要是 http 的各种 header 设置、参数传递和请求体的格式化用起来太不友好了

@malcolmyu @Kaijun 完全可行

commented

好文章,看这篇文章不是为了学习Fetch,而是想看看阿里是怎么写前端的

想问下有没有好的超时处理方案

@liyatang

export const setPromiseTimeout = function(promise, ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('request timeout');
    }, ms);
    promise.then(resolve, reject);
  });
};

@camsong thinks 。很不错

有便捷设置封装的比较好的开源组件吗
比如说timeout,header这些

不错,赞

不错

commented

good

很好的文章,知识点覆盖齐全。

在业务已经引入 jQuery 的情况下,列举的 fetch 的优点都没有什么吸引力。与其引入 es6-promise、fetch-polyfill,不如一个 jQuery 全包了。

es5-sham 这个模块无法下载了咋办?

不错哦,赞👍

@camsong 跨域 是不是要设置 mode ?

fetch('xxxurl', {mode: 'cors'})

cors 和 no-cors有啥区别?

@codering 需要设置 mode。默认是 no-cors 禁止跨域请求。cors 为支持跨域请求

“”服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject“” 这好不科学啊。。错误处理怎么办,

@xiaoqi1102 自己处理返回的状态码就好了啊

@xwartz 然而catch 也捕获不到状态码耶 各位有没有一样的问题

@camsong 设置了modecors 和没设置一样, 同样会报跨域错误

 fetch(`http://xxxxxx.com`, {mode: 'cors'})
etch API cannot load http://xxxxxx.com. No 'Access-Control-Allow-Origin' header is present on 
the requested resource. Origin 'http://saas.800jit.com' is therefore not allowed access. 
If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

我按照错误提示设置mode为'no-cors', 可以响应,但是结果response如下
image

但是我用jquery请求,确能正常响应,

image

fetch该如何设置才好 ?

@xiaoqi1102 可以外面套一个promise,通过返回的状态码,在promise里面进行判断,我项目中就是若res.ok || res.status >=200 && res.status < 300 就reslove掉,否则就throw一个err,然后在catch中处理和reject掉,这样外面接受的就是一个promise,可以进行then和catch操作

@codering 这个报错和前端没有关系,服务器需要添加 CORS 支持的响应头,参考 http://enable-cors.org/server.html

@camsong 可是我就是想要jquery这样的处理的返回结果,不知道jquery这块怎么处理的,它并没有要求服务端做任何处理啊

@codering jQuery 有可能使用了 JSONP 的方式,否则也是需要后端添加支持跨域的 response header。再给你推荐个文章 http://www.ruanyifeng.com/blog/2016/04/cors.html

@camsong 谢谢。后来解决了,先前调试的时候估计代码没重新加载, 是能够得到类似jquery一样的响应结果的。

看着屌屌的

superagent

已经用上了,👍

学习了

commented

经实践,在锤子2中,的webview里面不支持fetch。。

服务器返回的是gbk我fetch完之后都变成菱形方块了……这个乱码问题咋解决?换用github的fetch-polyfill能解决这个问题么?

commented

好文,学习了,赞一下!

项目多为M端,苦于目前fetch对手机的兼容还不太好,不得不仅在PC尝试使用~

不错 很清晰

@ChenJiaH 可以加Polyfill,转成普通ajax。只是换个future-proof的语法

用JWT替代cookie进行接口认证的话,fetch在IE跨域方面还会有问题么?

写的很清晰,赞👍

wb-alibaba markalready,hope I will join it。

好文章, 总结得很到位

async/await对promise有强依赖,并非最佳的异步处理解决方案

Global Ajax Event Handlers 如何设置呢?

commented

写得好 点赞

其实说语法简洁啊,promise啊之类的,其实并不能说明比起jquery.ajax好在哪里,我觉得好处主要是fetch更加底层,支持的数据格式更多,给我们的api更丰富(response,request还有header对象之类的)。

赞一个

参考 axios #314 讨论。

  • 目前 Fetch 的 API 和主流浏览器实现都不支持 timeout 参数及主动 abort(),而且几年时间过去了也没有更多进展,在可预见的将来也未必有明确的结果。如果你的项目对此有需求,不可能无限期等待,只能寻求第三方的 fork 或自己 monkey-patching。
  • 通过 setTimeout()Promise.reject() 实现的 Fetch 超时控制,后果就是下载过程继续在后台进行,数据全部下载完毕后丢弃。这是对流量、带宽和服务器资源的无端浪费。
  • XMLHttpRequest 支持丰富的事件,包括上传和下载进度。在这方面 Fetch 无解。

用fetch能不能处理附件上传下载呢?

fetch似乎好像不能处理下载,我的下载请求发送成功,并且状态码200了,但是并没有把文件给我下载回来。

写的针不错

commented

对于 IE8+ 以上浏览器,在生产环境使用 Fetch 是可行的
原生支持率并不高,幸运的是,引入下面这些 polyfill 后可以完美支持 IE8+

文章前后有点矛盾吧

@sirzxj 上面是文章开头概括性的结论。下面是做法。IE8 原生不支持 Fetch,通过引用 polyfill 后可以完美支持 IE8。所以并不矛盾