Why's THE Design(为何这么设计) 是一系列关于计算机领域程序设计决策的文章(偏向于前端领域),在该系列会从不一样的角度讨论这种设计的优缺点、对具体实现形成的影响。由 Draveness 的《为何这么设计》 启发javascript
在阅读本文以前,须要你们先忘掉相似于 $.ajax()
和 axios
这类的库方法或库,回归到最原始的 XMLHttpRequest
,而后再去思考新设计的 fetch API
。所以阅读本文以前,你须要一些简单的前置知识:XMLHttpRequest
(后面以 XHR
简称) 和 fetch API
。html
首先,咱们给两个概念下一个定义。XHR
和 fetch API
都是浏览器给上层使用者暴露出来的 API(相似于操做系统暴露系统 API 给浏览器这类应用同样)。这两套暴露的 API 给上层使用者提供了部分操做 http 包的能力。换句话说,这二者都是创建在 http 协议上的,咱们能够将其当成具备部分功能的 http 客户端。前端
XHR
出现时间很早,最开始在 Microsoft Exchange Server 2000 的 Outlook Web Access 中引入,随后在 1999 年加入到 IE5 中,最后全部主流的浏览器都引入了该特性。也就是说,XHR
已经 21 岁高龄了(一种技术能存活如此之久,足够证实其经典)。而 XHR
变得人尽皆知则是因为 AJAX 架构的出现(在这里须要提到的是,Ajax 更多的被认为是一种 Web 应用架构,其最先出如今 Jesse James Carrett 于 2005年 发表的一篇 《Ajax: A New Approach to Web Applications》),各类著名应用都使用了 AJAX(好比 Google 的 Gmail)。无论是谁促进了谁,都足以证实 XHR
解决了当时很大的痛点问题,减小网络延迟或损耗,提升用户体验,并加强了 JavaScript 在浏览器上操做 HTTP 的能力。java
用技术术语来说,XHR
在当时很好的解决了客户端和服务器端的异步通讯。咱们想象下当时的情况,Web 体系增加很快,但web 应用的功能仍然是相对简单的,更可能是对信息的展现,所以 XHR
设计者无需考虑过多的架构设计(仔细思考一下,当时没有硬需求,并且过分设计一样是个问题)。ios
以现代软件工程的角度来看,XHR
的整个设计很是混乱,将 request、response 和事件监听混在一个对象里,而且须要经过实例化才可以发送请求(后面咱们经过实际代码来演示)。这带来的问题是在实际使用过程当中配置和调用方式没有组织和可维护性(注意,这在 web 应用比较简单的时候不构成问题)。用架构的术语来说,XHR 不符合关注点分离原则(SOC),SOC 原则指望在设计系统时候可以将系统元素分离开来,尽可能保持各个元素的职责单一(好比 TCP/IP 协议簇的分层、经典 MVC 架构都是 SOC 原则的经典体现)。git
咱们来看一下最原始的 XHR
的使用:程序员
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
console.log(xhr.response);
}
xhr.onerror = function() {
console.log('error');
}
xhr.send();
复制代码
从上面代码能够很明显的看出,http request 、http response 和事件监听都处于同一个 xhr
实例里面。整个代码组织缺乏语义化,而且可能陷入回调地狱的窘境。若是没有各类包装库的实现(这也一样是 fetch
API 出现后难以推广的缘由之一,由于库封装的很好),手写 xhr
绝对是个痛苦的事情。github
按照 jake archibald(Google chrome 成员,我的比较喜欢的一个技术专家 ) 的话来说:web
XHR was an ugly baby and time has not been kind to it. It's 16 now. In a few years it'll be old enough to drink, and it's enough of a pain in the arse when it's soberajax
从近二十年的历史来看,Web 绝对是世界的中心(你看 JavaScript 设计得这么潦草的语言仍然占据很重要地位就能够侧面理解这句话的含义了)。Web 的发展表明着用户群体增多,也就意味着各类需求的增多(好比由文本传递到各类多媒体信息传递),各类技术方案、标准不断被引入到 Web 体系中(好比著名的 WHATWG 组织和 ECMA International 组织),XHR
显然到了该改变的时候,而改变每每有两种大方向:
XHR
中有 XMLHttpRequest2
的方案发布(提供了操做二进制数据的能力)。到如今为止,XHR
standard 仍然有少许的更新。fetch API
)。好处在于没有历史的束缚,而问题在于如何让开发者社区来接受这一套新东西(尤为是在旧方案仍然知足大部分需求的时候)标准编著者在设计方案时除了须要考虑使用者的方便,还要着眼于将来趋势和是否与现有其余议案产生冲突。从各种相关产品(这里我将各类设计称为产品的目的是想从产品的角度思考问题)的时间线上来看:
$.ajax()
) 出如今 2011 年你会发现,这几个时间线是很接近的,若是你再联想到 JavaScript,当时 JavaScript 世界的最重大事件莫过于 ES2015 被一步步标准化(这也意味着 Promise 被正式引入标准,尽管 Promise
的理念早已经在程序世界家喻户晓),所以上面的几个产品不约而同地使用了 Promise
的方式来设计各自上层的 API(这也侧面说明了回调这种异步写法不太符合程序员线性顺序处理思惟)。
咱们来看看 fetch API
在设计时主要考虑点在哪里:
具体 fetch API
使用方式以下:
fetch(url)
.then((r) => r.json())
.then((data) => console.log(data))
.catch((e) => console.log('error'))
复制代码
代码组织比最开始的 XHR
更加清爽了不少,若是使用 async/await
语法则更加简洁。
self.addEventListener('fetch', function (event) {
if (event.request.url === new URL('/', location).href) {
event.respondWith(
new Response('<h1>Hello!</h1>', {
headers: {'Content-Type', 'text/html'}
})
)
}
})
复制代码
在上面的代码中, event.request
是一个 Request
。这意味着能够直接在客户端实现 response
,而不是让浏览器去请求网络,这样能够结合 cache
实现某些灵活功能,这是 XHR
不能实现的。
可是一项新技术方案的出现,必定会引发业界的讨论,甚至是争议。很明显,fetch API
尽管有很是先进的设计理念,但仍然带来了很多的争议(尤为是 fetch API
很难实现某些 XHR
的功能时),这类争议被我分为两类:
第一个误解是(来自于一个 JavaScript 社区成员),“做为平台方,不该该在 XHR
基础上添加 high level features,而是应该提供更加 low level 的 primitives”。
很明显,上面的话陷入了一个误区,“一个设计良好的、简洁的 API 就是 high level的”。若是仔细看过规范的,你会发现, XHR 目前是创建在 fetch 的基础上的,有规范为证(在 XHR 的 send()
上):
Fetch req. Handle the tasks queued on the networking task source per below.
也就是说,fetch
API 实际上更加低阶,也就会给上层开发者提供更加灵活的能力(指的普通前端开发者)。
第二个误解是(也是社区常常抱怨的),认为规范须要提供一个更加完善的东西,而不是一个半成品。
说实话我的认为这是属于开发者的“双标”。当咱们做为开发者时,咱们对于本身产品的要求是迭代式开发;而当咱们做为消费者使用第三方技术或标准时,咱们的反应是 “how dare you present me with such incomplete imperfection”,这不是妥妥的双标又是什么呢?迭代发布意味着规范方可以从实际使用中得到反馈进行改进,而且能够指导将来的设计(若是你有看过 CSS 特性发布历史的话你能够理解这句话的含义 - 具体指的 CSS 各类前缀的滥用,致使尾大不掉)。jake archibald 的图片很好的体现了这一点:
第三个误解是,认为 fetch API
对于某些 http 错误码不会 reject(好比 400
、500
等)。可是我支持规范方,由于 fetch API
做为一个更 low-level 的 API,无论是错误码仍是正确码都表示 http 客户端有接受到服务器的 response,而网络错误这类才真正表明着异常。
第一类争议在通过解释后每每会消失掉,而真正麻烦的是第二类 - 缺乏方便实现旧方案功能的特性(注意这里指的是方便实现,而不是各类 hack 实现),具体到 fetch API
有如下几个(不全,可是也能基本描述本文所想讲的东西了):
XMLHttpRequest
单独提供了 timeout
属性用来处理超时中断。XHR
提供了 onprogress
事件来帮助咱们来实现该功能,具体代码以下:const xhr = new XMLHttpRequest();
xhr.open('GET', '/foo');
xhr.addEventListener('progress', (event) => {
const { lengthComputable, loaded, total } = event;
if (lengthComputable) {
console.log(`Downloaded ${loaded} of ${total} (${(loaded / total * 100).toFixed(2)}%)`);
} else {
console.log(`Downloaded ${loaded}`);
}
});
xhr.send();
复制代码
首先看 request aborting 功能,在 XHR
中,中断一个请求能够直接调用 XHR
实例上的 abort()
方法,而且可使用事件监听(onabort
事件),监听请求中断,做出相应的响应。 对于超时中断,XHR
实例提供了 timeout
属性来帮助咱们方便的实现功能,同时 XHR
还提供了 ontimeout
事件(这里就不聊具体代码如何写了,Google 一下就行)
在最开始的 fetch API
并无提供上面的功能(这里你能够先思考一个问题,为何这么简单的 API 实现,在 fetch API
中就成了很是激烈的讨论?),具体的讨论总共经历了漫长的两年之久,堪称 fetch API
讨论最激烈的特性了,我先给出讨论的几个阶段(在这里我不过多描述争论的细节,只解释几个方案的 tradeoff,细节能够看我给出的连接):
fetch
。在最开始的讨论中,主要有两种方案:
cancelable promise
方案(由 jake archibald 提出),也就是特别封装一个 CancellablePromise
,具体使用以下:var p = fetch(url).then(r => r.json());
p.abort(); // cancels either the stream, or the request, or neither (depending on which is in progress)
var p2 = fetch(url).then(response => {
return new CancellablePromise(resolve => {
drainStream(
response.body.pipeThrough(new StreamingDOMDecoder())
).then(resolve);
}, { onCancel: _ => response.body.cancel() });
});
复制代码
首先这种方案的第一个须要考虑的问题是,该叫什么才能不产生歧义,abort
(和 XHR
保持一致)、terminate
仍是 cancel
。注意这也是很是重要的,由于一个新的 API 设计必定要考虑其语义性。
先把命名放一边,你会发现这种方案对于上层开发者是否是特别友好,也比较符合原有 XHR
的写法。
可是这种方案却受到另外一方的强烈反对,主要表明是 getify(《you don't know js》 做者,我的认为其水平很高,可是容易夹带私货)。他反对的点主要是更加深层次的设计,咱们来看下他主要的论点是什么:
cancelable promise
的设计背离了 promise
的设计(若是后面看不懂,这个论点看到这里就能够了),若是咱们的设计将 abort 特性独立在 promise 的创造上下文,那么意味着 promise
会丢失可信赖性,影响对于内部的 promise 观察(这种讨论在最开始 promise
引入 ES6 时已经进行了好久,你仔细思考下 promise 的 resolve 和 reject 是否是在初始化时就已经肯定了,这也是显示了一种不可变性)Abort != Promise Cancelation... Abort == async Cancel
。这句话的含义是 promise 的取消意味着 fetch
的终止,这是 back-pressure。这样是否是会给 async/await
的某些设计带来问题。规范在设计一个新的 API,也须要考虑是否会影响到的方案,由于各个方案参与者并不同(好比对于 streams API
是否有影响)到这里,不一样的讨论方都相应给出了各自设计的考虑点,可是却谁也说服不了谁,直到后来第一种方案出现可能的安全问题,才被直接否决。可是 abort
的需求仍然是存在的,尽管 controller + signal
的方案看起来没有那么的优雅,可是在第一种方案有重大问题时,其余方案的缺点就显得没那么重要了。最终第二种方案 controller + signal
被肯定下来(具体讨论细节看上面的连接 3)。
到这里,你是否是已经能体会到上面简单的特性引入到底是怎么涉及到深层次设计问题的。
正如我在 《为何 setTimeout 的最小延迟是 4ms》 中一直在讲的观点是,“对于同一个需求,不一样参与方的思考角度不同,会带来不一样的方案,也就会产生不少不一样的 tradeoff。而如何思考、权衡,则是架构师的必备技能”
尽管该方案的讨论时长过于的长,咱们最终仍是有了落地的方案(尽管不少人仍是不能接受,但有时候有了总比原地踏步要强些),具体代码以下所示:
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000);
fetch(url, { signal }).then(response => {
return response.text();
}).then(text => {
console.log(text);
});
复制代码
上面的代码就是结合 controller + setTimeout
实现了超时中断的功能。
可是,不幸的是 progress events 并无不少的进展。最开始提的几个方案都被否决了,而且到笔者写做时仍然处于停滞当中(2020 年 7 月)。我的认为主要缘由仍是规范方精力有限,而该特性又不是优先级特别高。
可是,做为上层应用者咱们仍然能够结合 streams API
和 service worker
来实现上述需求(提一句,streams API
是很是优秀的功能,给了前端开发者更多操做底层网络数据的能力)。在这里,就不具体阐述实现方案了,给出我我的认为实现很好的: fetch progress indicators
在正文部分,咱们详细描述了 XHR
的设计问题和 fetch API
的设计理念,而且分析了这样设计的优缺点。可是我仍然须要提到的是,这并非 fetch API
的所有,没法在一篇文章里面讲完全部的细节。
诚然,对于上层开发者来说,理解这些设计理念和背后的各类方案、tradeoff,很是有助于开阔咱们的视野,以及更加深刻理解设计背后所涉及到的 computer science 概念和优秀实践。可是,咱们仍然须要依托于现实世界的业务需求,毕竟需求每每是更加接近本身的。
换句话说,咱们须要考虑实际业务状况来考虑使用旧的 XHR
, 仍是新的 fetch API
(看起来是废话,可是是你须要的)。
我举几个你能够权衡的点:
fetch API
, 那么须要 polyfill(使用 XHR
polyfill)。streams API
和 service worker
。若是这两个特性对于你的应用特别重要,那么我推荐你使用 fetch API
。下面回答最后一个问题,除了兼容性,还有什么遏制了 fetch API
替代了 XHR
?
那是由于各类优秀的库(XHR
封装)基本可以知足上层应用者大部分的功能需求,而且也没有特别大的缺点,那么为何还要冒着风险使用新的 fetch API
呢?这是否是也体现了你已经默默作了技术选择的 tradeoff 呢?
尽管有种种的问题,可是 fetch API
的将来仍然是光明的,npm 的 polyfill 包下载量也能简单的说明问题:
whatwg-fetch
周下载量是 8,744,612
axios
周下载量是 10,041,206
二者的差距也并非太远,不是吗?
我是 BY,一个有趣的人,之后会带来更多的原创文章。
本文章遵循 MIT 协议,转载请联系做者。
有兴趣能够关注公众号(百学原理)或者 Star GitHub repo.