导读
对于前端来说,Axios应该不陌生,自从尤大推荐后,Axios几乎成了前端必备工具库,Axios的体积也与日俱增,当前最新版本已经达到了14k的size,这样的大小,在sdk中引用是不太合适的,而XMLHttpRequest又过于原始,还不支持promise,需要进一步封装。那么有没有简单便捷的ajax API呢?它就是Fetch。
Fetch 是 web异步通信的未来. 从chrome42, Firefox39, Opera29, EdgeHTML14(并非Edge版本)起, fetch就已经被支持了. 其中chrome42~45版本, fetch对中文支持有问题, 建议从chrome46起使用fetch. 传送门: fetch中文乱码 .
Fetch
先过一遍Fetch原生支持率.
以下是2020年7月4日更新的 Fetch 兼容性统计,除了 IE 系列,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、IE10、IE11基于ES5,但支持不完全. 因此IE8+浏览器, 建议依次装载上述垫片.
尝试一个fetch
先来看一个简单的fetch.
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 域名的网页控制台运行以下代码.
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方法的第二个参数对象中定义.
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. 如下:
fetch(url).then(function(response) {
console.log(response.headers.get('Content-Type'));
});
设置http请求头也一样简单.
var headers = new Headers();
headers.append("Content-Type", "text/html");
fetch(url,{
headers: headers
});
header的内容也是可以被检索的.
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方法的第二个参数对象中设置.
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”.
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请求(毫无违和感). 如下:
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 对象.
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的.
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随着回调一起同步执行了,这便不符合我们阻塞循环的初衷。如下:
const fn = async (item)=>{
await wait(1000); // 循环中的多个await同时执行,因此等待1s后将同时输出数组各个元素
console.log(item);
};
array.forEach(fn);
正确的写法如下:
(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中可以使用如下方式直接运行:
node --harmony-async-await test.js
babel在线编译并运行 Babel · The compiler for writing next generation JavaScript .
本地使用babel编译es6或更高版本es.
1) 安装.
由于Babel5默认自带各种转换插件, 不需要手动安装. 然而从Babel6开始, 插件需要手动下载, 因此以下安装babel后需要再顺便安装两个插件.
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配置文件.
{ "presets": [ "es2015", "stage-0" ], "plugins": [] }
3) 如果不配置.babelrc. 也可在命令行显式指定插件.
babel es6.js -o es5.js --presets es2015 stage-0 # 指定使用插件es2015和stage-0编译js
4) 编译.
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) 实时编译
babel es6.js -w -o es5.js # 实时watch es6.js的变化,一旦改变就重新编译 babel es6.js -watch -o es5.js # -w也可写全为--watch
6) 编译目录输出到其他目录
babel src -d build # 编译src目录下所有js,并输出到build目录 babel src --out-dir build # -d也可写全为--out-dir
7) 编译目录输出到单个文件
babel src -o es5.js # 编译src目录所有js,合并输出为es5.js
8) 想要直接运行es6.js, 可使用babel-node.
npm i babel-node -g # 全局安装babel-node babel-node es6.js # 直接运行js文件
9) 如需在代码中使用fetch, 且使用babel-node运行, 需引入
node-fetch
模块.npm i node-fetch --save-dev
然后在es6.js中require
node-fetch
模块.var fetch = require('node-fetch');
本地使用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
使用起来非常简单. 如下是安装:
npm install fetch-jsonp --save-dev
如下是使用:
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()也随之可用. 下面我们来看下实现.
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.
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() 实现一个.
下面是一个简易的版本.
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.
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.
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)
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.
本问就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.
本文作者: louis
本文链接: http://louiszhai.github.io/2016/10/19/fetch/
参考文章