上文链接:也许你对 Fetch 了解得不是那么多(上)

编者按:除创宇前端与作者博客外,本文还在语雀发布。

编者还要按:作者也在掘金哦,欢迎关注:@GoDotDotDot


Fetch 与 XHR 比较

Fetch 相对 XHR 来说具有简洁、易用、声明式、天生基于 Promise 等特点。XHR 使用方式复杂,接口繁多,最重要的一点个人觉得是它的回调设计,对于实现 try...catch 比较繁琐。

但是 Fetch 也有它的不足,相对于 XHR 来说,目前它具有以下劣势:

  • 不能取消(虽然 AbortController 能实现,但是目前兼容性基本不能使用,可以使用 polyfill
  • 不能获取进度
  • 不能设置超时(可以通过简单的封装来模拟实现)
  • 兼容性目前比较差(可以使用 polyfill 间接使用 XHR 来优雅降级,这里推荐使用 isomorphic-fetch

在了解 Fetch 和 XHR 的一些不同后,还是需要根据自身的业务需求来选择合适的技术,因为技术没有永远的好坏,只有合不合适。

下面章节我们将介绍如何“优雅”的使用 Fetch 以及如何尽量避免掉劣势。

如何使用Fetch

前面了解了这么多基础知识,现在终于到了介绍如何使用 Fetch 了。老规矩,我们先来看下规范定义的接口。

partial interface mixin WindowOrWorkerGlobalScope {
[NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init);
};
复制代码

规范中定义的接口我们可以对应着 MDN 进行查看,你可以点击这里更直观的看看它的用法。

从规范中我们可以看到 fetch 属于 WindowOrWorkerGlobalScope 的一部分,暴露在 WindowWorkerGlobalScope 对象上。所以在浏览器中,你可以直接调用 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 实例对象,下面我们也给出一个示例。

代码位置:github.com/GoDotDotDot…

  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 的情况下,部分浏览器可能会报如下错误:

  • Chrome: "Failed to execute 'fetch' on 'Window': member signal is not of type AbortSignal."
  • Firefox: "'signal' member of RequestInit does not implement interface AbortSignal."

如果出现以上问题,我们也无能为力,可能原因是浏览器内部做了严格验证,对比发现我们提供的 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 规范

参考:

  1. MDN Fetch
  2. Fetch 规范
  3. 示例代码

文 / GoDotDotDot

Less is More.

编 / 荧声

作者其他文章:

优秀前端必知的话题:我们应该做些力所能及的优化

本文由创宇前端作者授权发布,版权属于作者,创宇前端出品。 欢迎注明出处转载本文。文章链接:blog.godotdotdot.com/2018/12/28/…

想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,我们会尽可能回复。

感谢您的阅读。

新年快乐 :)

也许你对 Fetch 了解得不是那么多(下)的更多相关文章

  1. git fetch批处理,遍历一个文件夹下的所有子目录,执行git fetch --all

    echo off for /d %%i in (*) do ( echo %%i cd %%i git fetch --all cd .. ) 判断子目录是否有.git文件夹 echo off for ...

  2. 使用 Fetch

    原文链接:https://css-tricks.com/using-fetch/. 本文介绍了Fetch基本使用方法及zlFetch库的使用 无论用JavaScript发送或获取信息,我们都会用到Aj ...

  3. 使用 Fetch完成AJAX请求

    使用 Fetch完成AJAX请求 写在前面 无论用JavaScript发送或获取信息,我们都会用到Ajax.Ajax不需要刷新页面就能发送和获取信息,能使网页实现异步更新. 几年前,初始化Ajax一般 ...

  4. 在 JS 中使用 fetch 更加高效地进行网络请求

    在前端快速发展地过程中,为了契合更好的设计模式,产生了 fetch 框架,此文将简要介绍下 fetch 的基本使用. 我的源博客地址:http://blog.parryqiu.com/2016/03/ ...

  5. Kafka Fetch Session剖析

    1.概述 最近有同学留言在使用Kafka的过程中遇到一些问题,比如在拉取的Topic中的数据时会抛出一些异常,今天笔者就为大家来分享一下Kafka的Fetch流程. 2.内容 2.1 背景 首先,我们 ...

  6. 【夯实Mysql基础】MySQL性能优化的21个最佳实践 和 mysql使用索引

    本文地址 分享提纲: 1.为查询缓存优化你的查询 2. EXPLAIN 你的 SELECT 查询 3. 当只要一行数据时使用 LIMIT 1 4. 为搜索字段建索引 5. 在Join表的时候使用相当类 ...

  7. MySQL性能优化

    当今数据库的操作越来越成为整个应用的性能瓶颈,特别是Web应用更加明显.当我们设计数据库和对数据库操作时,都要考虑到性能. 1.优化查询语句,方便查询缓存 大多数MySQL服务器都开启了查询缓存,这是 ...

  8. 【转】MySQL性能优化的最佳21条经验

    文章转自: http://blog.csdn.net/waferleo/article/details/7179009 今天,数据库的操作越来越成为整个应用的性能瓶颈了,这点对于Web应用尤其明显.关 ...

  9. MYSQL性能优化的最佳20+条经验

    MYSQL性能优化的最佳20+条经验 2009年11月27日 陈皓 评论 148 条评论  131,702 人阅读 今天,数据库的操作越来越成为整个应用的性能瓶颈了,这点对于Web应用尤其明显.关于数 ...

随机推荐

  1. 三通道低功耗ASK低频唤醒接收器PAN3501完全替代AS3933/GC3933

    低频唤醒接收器PAN3501软硬件兼容AS3933/GC3933,且新增了寄存器功能,可直接替换,供应稳定,高性价比. 产品介绍:    PAN3501是一款最多三个通道接收的低功耗ASK接收机,可用 ...

  2. problems

    exceptionUnable to connect to userservice.shanmaohuwai.com:80 . Error #0: stream_socket_client(): un ...

  3. Python操作rabbitmq系列(二):多个接收端消费消息

    今天,我们要逐步开始讨论rabbitmq稍微高级点的耍法了.了解这一步,对我们设计高并发的系统非常有用.当然,还可以使用kafka.不过还是算了,有几个硬性条件不支持,还是用rabbitmq吧. 循环 ...

  4. 对称加密算法在C#中的踩坑日常

    前言 有幸接触了一下传说中的对称加密算法3DES 感觉这些加密算法与我的工作是想去甚远的,一般没什么机会接触这些东西 今次了解了一下3DES这个对称算法 原理算不上明白,算是踩了C#中的一些坑吧 C# ...

  5. Centos7 编译安装 Libmcrypt 库

    0x00 先下载 libmcrypt 库源码 libmcrypt-2.5.8.tar.gz 或者去这里 libmcrypt 下载你需要的版本. 0x01 将下载的源码解压到文件夹 tar -zxvf ...

  6. 如何改变Xcode字体大小?

    运行Xcode后依次点击左上角Xcode/Preferences/Fonts & Colors里就可以调整,在右边随便点中一个字体就可以调整这个字体的大小和颜色了,按command+a可以将所 ...

  7. Linux常用命令01(文件和目录)

    目标 查看目录内容 ls 切换目录 cd 创建和删除文件 touch rm mkdir 拷贝和移动文件 cp mv 查看文件内容 cat more grep 其他 echo 重定向 > 和 &g ...

  8. 详解 NIO流

    在观看本篇博文前,建议先观看本人博文 -- <详解 IO流> NIO流: 首先,本人来介绍下什么是NIO流: 概述: Java NIO ( New IO )是从 Java 1.4 版本开始 ...

  9. python3_learn 实现文件夹内批量对图片重命名

    初衷 练习Python,提高动手能力. 珍藏的壁纸文件夹名命有点乱. 可以学习下一些基础的库 开始(.jpg,无筛选) First 首先找到OS库,寻找可以遍历文件名的.找到了OS.walk() os ...

  10. XML外部实体注入[转载]

    前言 对于xxe,深入的太少,一般做题也是复制payload再修改,没有了解过内部的结构规范等.这里转载了一篇先知社区的文章,排版了一下适合博客样式.文章总结的很好,结合了很多篇的博客文章,看完也是对 ...