上文连接:也许你对 Fetch 了解得不是那么多(上)javascript
编者按:除创宇前端与做者博客外,本文还在语雀发布。html
编者还要按:做者也在掘金哦,欢迎关注:@GoDotDotDot前端
Fetch 相对 XHR 来讲具备简洁、易用、声明式、天生基于 Promise 等特色。XHR 使用方式复杂,接口繁多,最重要的一点我的以为是它的回调设计,对于实现 try...catch
比较繁琐。java
可是 Fetch 也有它的不足,相对于 XHR 来讲,目前它具备如下劣势:c++
在了解 Fetch 和 XHR 的一些不一样后,仍是须要根据自身的业务需求来选择合适的技术,由于技术没有永远的好坏,只有合不合适。git
下面章节咱们将介绍如何“优雅”的使用 Fetch 以及如何尽可能避免掉劣势。github
前面了解了这么多基础知识,如今终于到了介绍如何使用 Fetch 了。老规矩,咱们先来看下规范定义的接口。web
partial interface mixin WindowOrWorkerGlobalScope {
[NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);
};
复制代码
规范中定义的接口咱们能够对应着 MDN 进行查看,你能够点击这里更直观的看看它的用法。json
从规范中咱们能够看到 fetch 属于 WindowOrWorkerGlobalScope 的一部分,暴露在 Window 或 WorkerGlobalScope 对象上。因此在浏览器中,你能够直接调用 fetch。后端
规范中定义了 fetch 返回一个 Promise,它最多可接收两个参数( input 和 init )。为了可以对它的使用方法有个更全面的了解,下面来说一下这两个参数。
input 参数类型为 RequestInfo
,咱们能够回到前面的 Request
部分,来回顾一下它的定义。
typedef (Request or USVString) RequestInfo;
发现它是一个 Request
对象或者是一个字符串,所以你能够传 Request
实例或者资源地址字符串,这里通常咱们推荐使用字符串。
init 参数类型为 RequestInit
,咱们回顾前面 Requst
部分,它是一个字典类型。在 JavaScript
中你须要传递一个 Object
对象。
dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; USVString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; boolean keepalive; AbortSignal? signal; any window; // can only be set to null };
在本小节以前咱们都没有介绍 fetch 的使用方式,可是在其余章节中或多或少出现过它的容貌。如今,咱们终于能够在这里正式介绍它的使用方式了。
fetch 它返回一个 Promise,意味着咱们能够经过 then
来获取它的返回值,这样咱们能够链式调用。若是配合 async/await
使用,咱们的代码可读性会更高。下面咱们先经过一个简单的示例来熟悉下它的使用。
示例代码位置:github.com/GoDotDotDot…
// 客户端
const headers = new Headers({
'X-Token': 'fe9',
});
setTimeout(() => {
fetch('/data?name=fe', {
method: 'GET', // 默认为 GET,不写也能够
headers,
})
.then(response => response.json())
.then(resData => {
const { status, data } = resData;
if (!status) {
window.alert('发生了一个错误!');
return;
}
document.getElementById('fetch').innerHTML = data;
});
}, 1000);
复制代码
上面的示例中,咱们自定义了一个 headers
。为了演示方便,这里咱们设定了一个定时器。在请求成功时,服务器端会返回相应的数据,咱们经过 Response
实例的 json
方法来解析数据。细心的同窗会发现,这里 fetch 的第一个参数咱们采用的是字符串,在第二个参数咱们提供了一些 RequestInit
配置信息,这里咱们指定了请求方法(method)和自定义请求头(headers)。固然你也能够传递一个 Request
实例对象,下面咱们也给出一个示例。
const headers = new Headers({
'X-Token': 'fe9',
});
const request = new Request('/api/request', {
method: 'GET',
headers,
});
setTimeout(() => {
fetch(request)
.then(res => res.json())
.then(res => {
const { status, data } = res;
if (!status) {
alert('服务器处理失败');
return;
}
document.getElementById('fetch-req').innerHTML = data;
});
}, 1200);
复制代码
在浏览器中打开:http://127.0.0.1:4000/, 若是上面的示例运行成功,你将会看到以下界面:
好,在运行完示例后,相信你应该对如何使用 fetch 有个基本的掌握。在上一章节,咱们讲过 fetch 有必定的缺点,下面咱们针对部分缺点来尝试着处理下。
当网络出现异常,请求可能已经超时,为了使咱们的程序更健壮,提供一个较好的用户 体验,咱们须要提供一个超时机制。然而,fetch 并不支持,这在上一小节中咱们也聊到过。庆幸的是,咱们有 Promise ,这使得咱们有机可趁。咱们能够经过自定义封装来达到支持超时机制。下面咱们尝试封装下。
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
};
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const headers = { ...defaultOptions.headers, ...options.headers };
let abortId;
let timeout = false;
if (options.timeout) {
abortId = setTimeout(() => {
timeout = true;
reject(new Error('timeout!'));
}, options.timeout || 6000);
}
fetch(url, { ...defaultOptions, ...options, headers })
.then((res) => {
if (timeout) throw new Error('timeout!');
return res;
})
.then(checkStatus)
.then(parseJSON)
.then((res) => {
clearTimeout(abortId);
resolve(res);
})
.catch((e) => {
clearTimeout(abortId);
reject(e);
});
});
}
复制代码
上面的代码中,咱们须要注意下。就是咱们手动根据超时时间来 reject
并不会阻止后续的请求,因为咱们并无关闭掉这次链接,属因而伪取消。fetch 中若是后续接受到服务器的响应,依然会继续处理后续的处理。因此这里咱们在 fetch 的第一个 then
中进行了超时判断。
const controller = new AbortController();
const signal = controller.signal;
fetch('/data?name=fe', {
method: 'GET',
signal,
})
.then(response => response.json())
.then(resData => {
const { status, data } = resData;
if (!status) {
window.alert('发生了一个错误!');
return;
}
document.getElementById('fetch-str').innerHTML = data;
});
controller.abort();
复制代码
咱们回过头看下 fetch 的接口,发现有一个属性 signal
, 类型为AbortSignal,表示一个信号对象( signal object ),它容许你经过 AbortController 对象与DOM请求进行通讯并在须要时将其停止。你能够经过调用 AbortController.abort 方法完成取消操做。
当咱们须要取消时,fetch 会 reject 一个错误( AbortError DOMException ),中断你的后续处理逻辑。具体能够看规范中的解释。
因为目前 AbortController 兼容性极差,基本不能使用,可是社区有人帮咱们提供了 polyfill(这里我不提供连接,由于目前来讲还不适合生产使用,会出现下面所述的问题),咱们能够经过使用它来帮助咱们提早感觉新技术带来的快乐。可是你可能会在原生支持 Fetch
可是又不支持 AbortController 的状况下,部分浏览器可能会报以下错误:
若是出现以上问题,咱们也无能为力,可能缘由是浏览器内部作了严格验证,对比发现咱们提供的 signal
类型不对。
可是咱们能够经过手动 reject
的方式达到取消,可是这种属于伪取消,实际上链接并无关闭。咱们能够经过自定义配置,例如在 options
中增长配置,暴露出 reject
,这样咱们就能够在外面来取消掉。这里本人暂时不提供代码。有兴趣的同窗能够尝试一下,也能够在下面的评论区评论。
前面提到过的获取进度目前咱们还没法实现。
示例代码位置:github.com/GoDotDotDot…
下面咱们讲一讲如何作一个简单的拦截器,这里的拦截器指对响应作拦截。假设咱们须要对接口返回的状态码进行解析,例如 403 或者 401 须要跳转到登陆页面,200 正常放行,其余报错。因为 fetch 返回一个 Promise ,这就使得咱们能够在后续的 then
中作些简单的拦截。咱们看一下示例代码:
function parseJSON(response) {
const { status } = response;
if (status === 204 || status === 205) {
return null;
}
return response.json();
}
function checkStatus(response) {
const { status } = response;
if (status >= 200 && status < 300) {
return response;
}
// 权限不容许则跳转到登录页面
if (status === 403 || status === 401) {
window ? (window.location = '/login.html') : null;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
/** * @description 默认配置 * 设置请求头为json */
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
// credentials: 'include', // 跨域传递cookie
};
/** * Requests a URL, returning a promise * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * * @return {object} The response data */
function request(url, options = {}) {
return new Promise((resolve, reject) => {
const headers = { ...defaultOptions.headers, ...options.headers };
let abortId;
let timeout = false;
if (options.timeout) {
abortId = setTimeout(() => {
timeout = true;
reject(new Error('timeout!'));
}, options.timeout || 6000);
}
fetch(url, { ...defaultOptions, ...options, headers })
.then((res) => {
if (timeout) throw new Error('timeout!');
return res;
})
.then(checkStatus)
.then(parseJSON)
.then((res) => {
clearTimeout(abortId);
resolve(res);
})
.catch((e) => {
clearTimeout(abortId);
reject(e);
});
});
}
复制代码
从上面的 checkStatus
代码中咱们能够看到,咱们首先检查了状态码。当状态码为 403 或 401 时,咱们将页面跳转到了 login 登陆页面。细心的同窗还会发现,咱们多了一个处理方法就是 parseJSON
,这里因为咱们的后端统一返回 json 数据,为了方便,咱们就直接统一处理了 json 数据。
本系列文章总体阐述了 fetch 的基本概念、和 XHR 的差别、如何使用 fetch 以及咱们常见的解决方案。但愿同窗们在读完整篇文章可以对 fetch 的认识有所加深。
建议:在总体了解了 fetch 以后,但愿同窗们可以读一下 github polyfill 源码。在读代码的同时,能够同时参考 Fetch 规范。
参考:
文 / GoDotDotDot
Less is More.
编 / 荧声
做者其余文章:
本文由创宇前端做者受权发布,版权属于做者,创宇前端出品。 欢迎注明出处转载本文。文章连接:blog.godotdotdot.com/2018/12/28/…
想要订阅更多来自知道创宇开发一线的分享,请搜索关注咱们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,咱们会尽量回复。
感谢您的阅读。
新年快乐 :)