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进行优化(属于ES7)
try {
let response = await fetch(url);
let data = response.json();
console.log(data);
} catch (e) {
console.log('Oops, error', e);
}
// 注:这段代码如果想运行,外面需要包一个async function
使用await后,写异步代码就像写同步代码一样。await后面可以跟Promise对象,表示等待Promise的resolve()才继续向下执行,如果Promise被reject()或抛出异常则被外面的try...catch捕获
Fetch优点:
1. 语法简洁,更加语义化
2. 基于标准Promise实现,支持 async/await
3. 同构方便,使用 isomorphic-fetch
Fetch启用方法
- 由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham
- 引入 Promise 的 polyfill: es6-promise
- 引入 fetch 探测库:fetch-detector
- 引入 fetch 的 polyfill: fetch-ie8
- 可选:如果你还使用了 jsonp,引入 fetch-jsonp
- 可选:开启 Babel 的 runtime 模式,现在就使用 async/await
Fetch polyfill的基本原理是探测是否存在 window.fetch 方法,如果没有则用XHR实现。有些浏览器(Chrome 45)原生支持Fetch,但响应中有中文时会乱码。(可使用fetch-detector和fetch-ie8)
Fetch常见坑
- Fetch请求默认不带cookie的,需要设置
fetch(url, {credentials: 'include'})
- 服务器返回400、500错误码并不会reject,只有网络错误这些导致请求不能完成时,fetch才会被reject
- IE8、9的XHR不支持CORS跨域,虽然提供XDomainRequest,但这个东西不支持传cookie,所以必要时还是使用jsonp,推荐使用fetch-jsonp
fetch请求对某些错误http状态不会reject
因为fetch返回promise导致码,在某些错误的http状态下如400、500等不会reject,相反会被resolve;只有网络错误会导致请求不能完成时,fetch才会被reject;所以一般会对fetch请求做一层封装,如下:
function checkStatus (response) {
if ( response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error (response.statusText);
error.response = response;
throw error;
}
function parseJSON (response) {
return response.json();
}
export default function request (url, options) {
let opt = options || {};
return fetch (url, {credentials : 'include', ...opt})
.then (checkStatus)
.then (parseJSON)
.then (data => data)
.catch (err => err)
}
fetch不支持超时timeout处理
fetch不像大多数ajax库可以对请求设置超时timeout,所以在fetch标准添加超时feature之前,都需要polyfill该特性。
我们真正需要的是abort(), timeout可以通过timeout+abort方式实现,起到真正超时丢弃当前的请求。
实现fetch的timeout功能,思想就是新创建一个可以手动控制promise状态的实例,对于不同状态来对新创建的promise进行resolve或者reject,从而达到实现timeout功能。
方法一
var oldFetchfn = fetch; // 拦截原始的fetch方法
window.fetch = function (input, opts) {
return new Promise ( function (resolve, reject) {
var timeoutId = setTimeout (function () {
reject (new Error ('fetch timeout'))
}, opts.timeout);
oldFetchfn (input, opts).then(
res => {
clearTimeout (timeoutId);
resolve (res);
},
err => {
clearTimeout (timeoutId);
reject (err);
}
)
})
}
模拟XHR的abort功能:
var oldFetchfn = fetch;
window.fetch = function (input, opts) {
return new Promise ( function (resolve, reject) {
var abort_promise = function () {
reject (new Error ('fetch abort'))
};
var p = oldFetchfn (input, opts).then (resolve, reject);
p.abort = abort_promise;
return p;
})
}
方法二: 利用Promise.race方法
Promise.race方法接受一个promise实例数组参数,表示多个promise实例中任何一个最先改变状态,那么race方法返回的promise实例状态就跟着改变。
var oldFetchfn = fetch; // 拦截原始的fetch方法
window.fetch = function (input, opts) {
var fetchPromise = oldFetchfn (input, opts);
var timeoutPromise = new Promise ( function (resolve, reject) {
setTimeout ( () => {
reject (new Error ('fetch timeout'))
}, opts.timeout)
});
return Promise.race([fetchPromise, timeoutPromise])
}