导读
Fetch 是 web异步通信的未来. 从chrome42, Firefox39, Opera29, EdgeHTML14(并非Edge版本)起, fetch就已经被支持了. 其中chrome42~45版本, fetch对中文支持有问题, 建议从chrome46起使用fetch. 传送门: fetch中文乱码 .
Fetch
先过一遍Fetch原生支持率.
可见要想在IE8/9/10/11中使用fetch还是有些犯难的,毕竟它连 Promise 都不支持, 更别说fetch了. 别急, 这里有polyfill(垫片).
- es5 的 polyfill —
es5-shim, es5-sham
. - Promise 的 polyfill —
es6-promise
. - fetch 的 polyfill —
fetch-ie8
.
由于IE8基于ES3, IE9支持大部分ES5, IE11支持少量ES5, 其中只有IE10对ES5支持比较完整. 因此IE8+浏览器, 建议依次装载上述垫片.
尝试一个fetch
先来看一个简单的fetch.
1 2 3 4 5 6 7 8 9 |
var word = '123', url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3'; fetch(url,{mode: "no-cors"}).then(function(response) { return response; }).then(function(data) { console.log(data); }).catch(function(e) { console.log("Oops, error"); }); |
fetch执行后返回一个 Promise
对象, 执行成功后, 成功打印出 Response
对象.
该fetch可以在任何域名的网站直接运行, 且能正常返回百度搜索的建议词条. 以下是常规输入时的是界面截图.
以下是刚才fetch到的部分数据. 其中key name 为”s”的字段的value就是以上的建议词条.(由于有高亮词条”12306”, 最后一条数据”12366”被顶下去了, 故上面截图上看不到)
看完栗子过后, 就要动真格了. 下面就来扒下 Fetch.
Promise特性
fetch方法返回一个Promise对象, 根据 Promise Api
的特性, fetch可以方便地使用then方法将各个处理逻辑串起来, 使用 Promise.resolve() 或 Promise.reject() 方法将分别返会肯定结果的Promise或否定结果的Promise, 从而调用下一个then 或者 catch. 一但then中的语句出现错误, 也将跳到catch中.
Promise若有疑问, 请阅读 Promises .
① 我们不妨在 https://sp0.baidu.com 域名的网页控制台运行以下代码.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var word = '123', url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3'; fetch(url).then(function(response){ console.log('第一次进入then...'); if(response.status>=200 && response.status<300){ console.log('Content-Type: ' + response.headers.get('Content-Type')); console.log('Date: ' + response.headers.get('Date')); console.log('status: ' + response.status); console.log('statusText: ' + response.statusText); console.log('type: ' + response.type); console.log('url: ' + response.url); return Promise.resolve(response); }else{ return Promise.reject(new Error(response.statusText)); } }).then(function(data){ console.log('第二次进入then...'); console.log(data); }).catch(function(e){ console.log('抛出的错误如下:'); console.log(e); }); |
运行截图如下:
② 我们不妨在非 https://sp0.baidu.com 域名的网页控制台再次运行以上代码.(别忘了给fetch的第二参数传递{mode: “no-cors”})
运行截图如下:
由于第一次进入then分支后, 返回了否定结果的 Promise.reject 对象. 因此代码进入到catch分支, 抛出了错误. 此时, 上述 response.type
为 opaque
.
response type
一个fetch请求的响应类型(response.type)为如下三种之一:
- basic
- cors
- opaque
如上情景①, 同域下, 响应类型为 “basic”.
如上情景②中, 跨域下, 服务器没有返回CORS响应头, 响应类型为 “opaque”. 此时我们几乎不能查看任何有价值的信息, 比如不能查看response, status, url等等等等.
同样是跨域下, 如果服务器返回了CORS响应头, 那么响应类型将为 “cors”. 此时响应头中除 Cache-Control
, Content-Language
, Content-Type
, Expores
, Last-Modified
和 Progma
之外的字段都不可见.
注意: 无论是同域还是跨域, 以上 fetch 请求都到达了服务器.
mode
fetch可以设置不同的模式使得请求有效. 模式可在fetch方法的第二个参数对象中定义.
1 |
fetch(url, {mode: 'cors'}); |
可定义的模式如下:
- same-origin: 表示同域下可请求成功; 反之, 浏览器将拒绝发送本次fetch, 同时抛出错误 “TypeError: Failed to fetch(…)”.
- cors: 表示同域和带有CORS响应头的跨域下可请求成功. 其他请求将被拒绝.
- cors-with-forced-preflight: 表示在发出请求前, 将执行preflight检查.
- no-cors: 常用于跨域请求不带CORS响应头场景, 此时响应类型为 “opaque”.
除此之外, 还有两种不太常用的mode类型, 分别是 navigate
, websocket
, 它们是 HTML标准 中特殊的值, 这里不做详细介绍.
header
fetch获取http响应头非常easy. 如下:
1 2 3 |
fetch(url).then(function(response) { console.log(response.headers.get('Content-Type')); }); |
设置http请求头也一样简单.
1 2 3 4 5 |
var headers = new Headers(); headers.append("Content-Type", "text/html"); fetch(url,{ headers: headers }); |
header的内容也是可以被检索的.
1 2 3 4 5 |
var header = new Headers({ "Content-Type": "text/plain" }); console.log(header.has("Content-Type")); //true console.log(header.has("Content-Length")); //false |
post
在fetch中发送post请求, 同样可以在fetch方法的第二个参数对象中设置.
1 2 3 4 5 6 7 8 9 10 |
var headers = new Headers(); headers.append("Content-Type", "application/json;charset=UTF-8"); fetch(url, { method: 'post', headers: headers, body: JSON.stringify({ date: '2016-10-08', time: '15:16:00' }) }); |
credentials
跨域请求中需要带有cookie时, 可在fetch方法的第二个参数对象中添加credentials属性, 并将值设置为”include”.
1 2 3 |
fetch(url,{ credentials: 'include' }); |
除此之外, credentials 还可以取以下值:
- omit: 缺省值, 默认为该值.
- same-origin: 同源, 表示同域请求才发送cookie.
catch
同 XMLHttpRequest 一样, 无论服务器返回什么样的状态码(chrome中除407之外的其他状态码), 它们都不会进入到错误捕获里. 也就是说, 此时, XMLHttpRequest 实例不会触发 onerror
事件回调, fetch 不会触发 reject. 通常只在网络出现问题时或者ERR_CONNECTION_RESET时, 它们才会进入到相应的错误捕获里. (其中, 请求返回状态码为407时, chrome浏览器会触发onerror或者reject掉fetch.)
cache
cache表示如何处理缓存, 遵守http规范, 拥有如下几种值:
- default: 表示fetch请求之前将检查下http的缓存.
- no-store: 表示fetch请求将完全忽略http缓存的存在. 这意味着请求之前将不再检查下http的缓存, 拿到响应后, 它也不会更新http缓存.
- no-cache: 如果存在缓存, 那么fetch将发送一个条件查询request和一个正常的request, 拿到响应后, 它会更新http缓存.
- reload: 表示fetch请求之前将忽略http缓存的存在, 但是请求拿到响应后, 它将主动更新http缓存.
- force-cache: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 除非没有任何缓存, 那么它将发送一个正常的request.
- only-if-cached: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 如果没有缓存, 它将抛出网络错误(该设置只在mode为”same-origin”时有效).
如果fetch请求的header里包含 If-Modified-Since
, If-None-Match
, If-Unmodified-Since
, If-Match
, 或者 If-Range
之一, 且cache的值为 default
, 那么fetch将自动把 cache的值设置为 "no-store"
.
async/await
为什么是async/await
回调深渊一直是jser的一块心病, 虽然ES6提供了 Promise, 将嵌套平铺, 但使用起来依然不便.
要说ES6也提供了generator/yield, 它将一个函数执行暂停, 保存上下文, 再次调用时恢复当时的状态.(学习可参考 Generator 函数的含义与用法 – 阮一峰的网络日志) 无论如何, 总感觉别扭. 如下摘自推库的一张图.
我们不难看出其中的差距, callback简单粗暴, 层层回调, 回调越深入, 越不容易捋清楚逻辑. Promise 将异步操作规范化.使用then连接, 使用catch捕获错误, 堪称完美, 美中不足的是, then和catch中传递的依然是回调函数, 与心目中的同步代码不是一个套路.
为此, ES7 提供了更标准的解决方案 — async/await. async/await 几乎没有引入新的语法, 表面上看起来, 它就和alert一样易用, 虽然它尚处于ES7的草案中, 不过这并不影响我们提前使用它.
async/await语法
async 用于声明一个异步函数, 该函数需返回一个 Promise 对象. 而 await 通常后接一个 Promise对象, 需等待该 Promise 对象的 resolve() 方法执行并且返回值后才能继续执行. (如果await后接的是其他对象, 便会立即执行)
因此, async/await 天生可用于处理 fetch请求(毫无违和感). 如下:
1 2 3 4 5 6 7 8 9 10 |
var word = '123', url = 'https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd='+word+'&json=1&p=3'; (async ()=>{ try { let res = await fetch(url, {mode: 'no-cors'});//等待fetch被resolve()后才能继续执行 console.log(res); } catch(e) { console.log(e); } })(); |
自然, async/await 也可处理 Promise 对象.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let wait = function(ts){ return new Promise(function(resolve, reject){ setTimeout(resolve,ts,'Copy that!'); }); }; (async function(){ try { let res = await wait(1000);//① 等待1s后返回结果 console.log(res); res = await wait(1000);//② 重复执行一次 console.log(res); } catch(e) { console.log(e); } })(); //"Copy that!" |
可见使用await后, 可以直接得到返回值, 不必写 .then(callback)
, 也不必写 .catch(error)
了, 更可以使用 try catch
标准语法捕获错误.
由于await采用的是同步的写法, 看起来它就和alert函数一样, 可以自动阻塞上下文. 因此它可以重复执行多次, 就像上述代码②一样.
可以看到, await/async 同步阻塞式的写法解决了完全使用 Promise 的一大痛点——不同Promise之间共享数据问题. Promise 需要设置上层变量从而实现数据共享, 而 await/async 就不存在这样的问题, 只需要像写alert一样书写就可以了.
值得注意的是, await 只能用于 async 声明的函数上下文中. 如下 forEach 中, 是不能直接使用await的.
1 2 3 4 5 6 7 |
let array = [0,1,2,3,4,5]; (async ()=>{ array.forEach(function(item){ await wait(1000);//这是错误的写法, 因await不在async声明的函数上下文中 console.log(item); }); })(); |
如果是试图将async声明的函数作为回调传给forEach,该回调将同时触发多次,回调内部await依然有效,只是多次的await随着回调一起同步执行了,这便不符合我们阻塞循环的初衷。如下:
1 2 3 4 5 |
const fn = async (item)=>{ await wait(1000); // 循环中的多个await同时执行,因此等待1s后将同时输出数组各个元素 console.log(item); }; array.forEach(fn); |
正确的写法如下:
1 2 3 4 5 6 |
(async ()=>{ for(let i=0,len=array.length;i<len;i++){ await wait(1000); console.log(array[i]); } })(); |
如何试运行async/await
鉴于目前只有Edge支持 async/await, 我们可以使用以下方法之一运行我们的代码.
- 随着node7.0的发布, node中可以使用如下方式直接运行:
1node --harmony-async-await test.js - babel在线编译并运行 Babel · The compiler for writing next generation JavaScript .
- 本地使用babel编译es6或更高版本es.
1) 安装.
由于Babel5默认自带各种转换插件, 不需要手动安装. 然而从Babel6开始, 插件需要手动下载, 因此以下安装babel后需要再顺便安装两个插件.
1 2 3 |
npm i babel-cli -g # babel已更名为babel-cli npm install babel-preset-es2015 --save-dev npm install babel-preset-stage-0 --save-dev |
2) 书写.babelrc配置文件.
1 2 3 4 5 6 7 |
{ "presets": [ "es2015", "stage-0" ], "plugins": [] } |
3) 如果不配置.babelrc. 也可在命令行显式指定插件.
1 |
babel es6.js -o es5.js --presets es2015 stage-0 # 指定使用插件es2015和stage-0编译js |
4) 编译.
1 2 3 |
babel es6.js -o es5.js # 编译源文件es6.js,输出为es5.js,编译规则在上述.babelrc中指定 babel es6.js --out-file es5.js # 或者将-o写全为--out-file也行 bable es6.js # 如果不指定输出文件路径,babel会将编译生成的文本标准输出到控制台 |
5) 实时编译
1 2 |
babel es6.js -w -o es5.js # 实时watch es6.js的变化,一旦改变就重新编译 babel es6.js -watch -o es5.js # -w也可写全为--watch |
6) 编译目录输出到其他目录
1 2 |
babel src -d build # 编译src目录下所有js,并输出到build目录 babel src --out-dir build # -d也可写全为--out-dir |
7) 编译目录输出到单个文件
1 |
babel src -o es5.js # 编译src目录所有js,合并输出为es5.js |
8) 想要直接运行es6.js, 可使用babel-node.
1 2 |
npm i babel-node -g # 全局安装babel-node babel-node es6.js # 直接运行js文件 |
9) 如需在代码中使用fetch, 且使用babel-node运行, 需引入 node-fetch
模块.
1 |
npm i node-fetch --save-dev |
然后在es6.js中require node-fetch
模块.
1 |
var fetch = require('node-fetch'); |
4.本地使用traceur编译es6或更高版本es.请参考 在项目开发中优雅地使用ES6:Traceur & Babel .
如何弥补Fetch的不足
fetch基于Promise, Promise受限, fetch也难幸免. ES6的Promise基于 Promises/A+ 规范 (对规范感兴趣的同学可选读 剖析源码理解Promises/A规范 ), 它只提供极简的api, 没有 timeout 机制, 没有 progress 提示, 没有 deferred 处理 (这个可以被async/await替代).
fetch-jsonp
除此之外, fetch还不支持jsonp请求. 不过办法总比问题多, 万能的开源作者提供了 fetch-jsonp
库, 解决了这个问题.
fetch-jsonp
使用起来非常简单. 如下是安装:
1 |
npm install fetch-jsonp --save-dev |
如下是使用:
1 2 3 4 5 6 7 8 |
fetchJsonp(url, { timeout: 3000, jsonpCallback: 'callback' }).then(function(response) { console.log(response.json()); }).catch(function(e) { console.log(e) }); |
abort
由于Promise的限制, fetch 并不支持原生的abort机制, 但这并不妨碍我们使用 Promise.race() 实现一个.
Promise.race(iterable) 方法返回一个Promise对象, 只要 iterable 中任意一个Promise 被 resolve 或者 reject 后, 外部的Promise 就会以相同的值被 resolve 或者 reject.
支持性: 从 chrome33, Firefox29, Safari7.1, Opera20, EdgeHTML12(并非Edge版本) 起, Promise就被完整的支持. Promise.race()也随之可用. 下面我们来看下实现.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var _fetch = (function(fetch){ return function(url,options){ var abort = null; var abort_promise = new Promise((resolve, reject)=>{ abort = () => { reject('abort.'); console.info('abort done.'); }; }); var promise = Promise.race([ fetch(url,options), abort_promise ]); promise.abort = abort; return promise; }; })(fetch); |
然后, 使用如下方法测试新的fetch.
1 2 3 4 5 6 7 8 9 |
var p = _fetch('https://www.baidu.com',{mode:'no-cors'}); p.then(function(res) { console.log('response:', res); }, function(e) { console.log('error:', e); }); p.abort(); //"abort done." //"error: abort." |
以上, fetch请求后, 立即调用abort方法, 该promise被拒绝, 符合预期. 细心的同学可能已经注意到了, “p.abort();” 该语句我是单独写一行的, 没有链式写在then方法之后. 为什么这么干呢? 这是因为then方法调用后, 返回的是新的promise对象. 该对象不具有abort方法, 因此使用时要注意绕开这个坑.
timeout
同上, 由于Promise的限制, fetch 并不支持原生的timeout机制, 但这并不妨碍我们使用 Promise.race() 实现一个.
下面是一个简易的版本.
1 2 3 4 5 6 7 8 9 |
function timer(t){ return new Promise(resolve=>setTimeout(resolve, t)) .then(function(res) { console.log('timeout'); }); } var p = fetch('https://www.baidu.com',{mode:'no-cors'}); Promise.race([p, timer(1000)]); //"timeout" |
实际上, 无论超时时间设置为多长, 控制台都将输出log “timeout”. 这是因为, 即使fetch执行成功, 外部的promise执行完毕, 此时 setTimeout 所在的那个promise也不会reject.
下面我们来看一个类似xhr版本的timeout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var _fetch = (function(fetch){ return function(url,options){ var abort = null, timeout = 0; var abort_promise = new Promise((resolve, reject)=>{ abort = () => { reject('timeout.'); console.info('abort done.'); }; }); var promise = Promise.race([ fetch(url,options), abort_promise ]); promise.abort = abort; Object.defineProperty(promise, 'timeout',{ set: function(ts){ if((ts=+ts)){ timeout = ts; setTimeout(abort,ts); } }, get: function(){ return timeout; } }); return promise; }; })(fetch); |
然后, 使用如下方法测试新的fetch.
1 2 3 4 5 6 7 8 9 |
var p = _fetch('https://www.baidu.com',{mode:'no-cors'}); p.then(function(res) { console.log('response:', res); }, function(e) { console.log('error:', e); }); p.timeout = 1; //"abort done." //"error: timeout." |
progress
xhr的 onprogress 让我们可以掌控下载进度, fetch显然没有提供原生api 做类似的事情. 不过 Fetch中的Response.body
中实现了getReader()
方法用于读取原始字节流, 该字节流可以循环读取, 直到body下载完成. 因此我们完全可以模拟fetch的progress.
以下是 stackoverflow 上的一段代码, 用于模拟fetch的progress事件. 为了方便测试, 请求url已改为本地服务.(原文请戳 javascript – Progress indicators for fetch? – Stack Overflow)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function consume(reader) { var total = 0 return new Promise((resolve, reject) => { function pump() { reader.read().then(({done, value}) => { if (done) { resolve(); return; } total += value.byteLength; console.log(`received ${value.byteLength} bytes (${total} bytes in total)`); pump(); }).catch(reject) } pump(); }); } fetch('http://localhost:10101/notification/',{mode:'no-cors'}) .then(res => consume(res.body.getReader())) .then(() => console.log("consumed the entire body without keeping the whole thing in memory!")) .catch(e => console.log("something went wrong: " + e)); |
以下是日志截图:
刚好github上有个fetch progress的demo, 感兴趣的小伙伴请参看这里: Fetch Progress DEMO .
我们不妨来对比下, 使用xhr的onprogress事件回调, 输出如下:
我试着适当增加响应body的size, 发现xhr的onprogress事件回调依然只执行两次. 通过多次测试发现其执行频率比较低, 远不及fetch progress.
本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.
参考文章