Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

传统 Ajax 已死,Fetch 永生 #2

Open
camsong opened this issue Sep 30, 2015 · 123 comments
Open

传统 Ajax 已死,Fetch 永生 #2

camsong opened this issue Sep 30, 2015 · 123 comments

Comments

@camsong
Copy link
Owner

camsong commented Sep 30, 2015

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 订阅

@Lucifier129
Copy link

不错的文章,赞

@youngdze
Copy link

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

@fanhc019
Copy link

fanhc019 commented Oct 1, 2015

火钳刘明

@1990lsf
Copy link

1990lsf commented Oct 1, 2015

不错的文章,赞

@humanhuang
Copy link

Fetch 永生

@ImHype
Copy link

ImHype commented Oct 10, 2015

学到了

@liyatang
Copy link

学习了

@ghost
Copy link

ghost commented Oct 12, 2015

确实有点标题

@chemzqm
Copy link

chemzqm commented Oct 14, 2015

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

@ss7247
Copy link

ss7247 commented Oct 26, 2015

围观

@xiongchen2012
Copy link

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

@Cydmi
Copy link

Cydmi commented Oct 27, 2015

科普下。说不定就用的上

@kevin-romens
Copy link

顶下

@shanelau
Copy link

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

@Shany100
Copy link

另一种选择,值得一看。

@kisnows
Copy link

kisnows commented Oct 28, 2015

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

@camsong
Copy link
Owner Author

camsong commented Oct 28, 2015

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

@VectorHo
Copy link

get it

@ralphite
Copy link

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

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

@camsong
Copy link
Owner Author

camsong commented Oct 31, 2015

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

@fifiteen82726
Copy link

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

@rehack
Copy link

rehack commented Nov 1, 2015

Copy that http://www.rehack.cn/

@Negaihoshi
Copy link

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

@gdzgshum
Copy link

gdzgshum commented Nov 3, 2015

在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)

@camsong
Copy link
Owner Author

camsong commented Nov 3, 2015

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

@zhangbg
Copy link

zhangbg commented Nov 26, 2015

mark!!!

@liyatang
Copy link

liyatang commented Dec 2, 2015

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

得到
image

有遇到么,怎么解决?

@camsong
Copy link
Owner Author

camsong commented Dec 3, 2015

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

@liyatang
Copy link

liyatang commented Dec 3, 2015

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

附上issue 链接

@camsong
Copy link
Owner Author

camsong commented Dec 3, 2015

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

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

@DevilRoshan
Copy link

fetch在google和Firefox发请求时,添加请求头后,Firefox无法正常返回数据。
let headers = new Headers();
headers.set('test','test');
然后将headers与其他参数一起发往服务端。
已知已经在服务端做好处理,且Google可以正常请求,而火狐不能。
这个问题是因为火狐不支持fetch带请求头,还是因为服务端处理不完善?求大佬解答一下。

@DevilRoshan
Copy link

自己已经解决,原因是因为服务端设置有问题
res.header("Access-Control-Allow-Credentials","true");
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "test");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
服务端必须设置可以接收options请求,且必须声明headers的字段,第一行的设置是不能少的,这是声明运行客户端携带证书式访问。

@watermountain
Copy link

onlooker

@vino24
Copy link

vino24 commented Aug 21, 2018

@liangklfangl response.json()返回值是 Promise

@mescalchuan
Copy link

fetch的出现的的确确解决了“回调地狱”,让代码更加易于维护。但目前不建议用在手机上,“超时重发3次后仍未响应则自动中断请求”是手机开发常见需求,reject中断的是promise的状态,请求其实还在继续发送,实际上这是promise的短板,我在将ajaxpromise封装时就遇到了该问题。期待fetch能有一个好的解决方案,让前端开发更加便利~ 持续关注~

@MontageD
Copy link

MontageD commented Sep 5, 2018

文章大部分提到的概念内容都可以从其他渠道去仔细研究和推敲,谢谢博主

@mirlww
Copy link

mirlww commented Oct 23, 2018

promise, 箭头函数, async/await和xhr不冲突吧, 所以fetch究竟比xhr好在哪里(⊙_⊙)?

@GoetheDady
Copy link

🐎

@cfangxx
Copy link

cfangxx commented May 10, 2020

请教作者:

  1. 既然fetch有那么多问题, 为什么它可以取代ajax,难道fetch的这些问题都不是问题吗?
  2. 难道我们用fetch 就是因为他可以开箱即用吗?ajax似乎有点啥大粗,但支持的特性多,而且我们用$.ajax 也挺好. 二者的本质区别是什么?fetch更高效吗?

@xiecz123
Copy link

xiecz123 commented Jul 1, 2020

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

response.json()这个方法是异步的吗,为什么要加await

@Lil-C0der
Copy link

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

response.json()这个方法是异步的吗,为什么要加await

json 方法返回的是一个 Promise
https://developer.mozilla.org/zh-CN/docs/Web/API/Body/json

@xiecz123
Copy link

xiecz123 commented Jul 2, 2020

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

response.json()这个方法是异步的吗,为什么要加await

json 方法返回的是一个 Promise
https://developer.mozilla.org/zh-CN/docs/Web/API/Body/json

谢谢大佬

@cosmosdawn
Copy link

“服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。”

这个是啥意思? 能具体说一下吗,意思是说 业务层的状态码不会导致 reject 只有http code 是非200 才会导致reject吗? 还是说其

他的情况? 网络错误导致请求不能完成,具体是哪种情况啊

@guohuihot
Copy link

guohuihot commented Nov 9, 2023

试用了下,fetch性能上貌似比xhr还差些

@meloseven
Copy link

meloseven commented Dec 8, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests