从 XHR 到 Fetch


曾经有一阵子,JavaScript 社区中流行着「你不需要 jQuery」的口号,社区里头嚷嚷着 Fecth API 将会取代这一切。

从今日的角度来看,XMLHttpRequest确实有许多设计不足之处,首先,一个XMLHttpRequest实例肩负着太多任务,包含了事件的注册、请求标头的设置、连线的开启、数据的传送、请求体的设置、响应状态的判断、响应内容的获取等,完全不符合关切点分离(Separation of Concerns)的原则,而且设定与调用顺序混乱,像是经常地,开发者会搞不清楚,到底是要调用open前还是之后设定请求标头。

就算是 2011 年标准化后的 XMLHttpRequest Level 1 也没有改变XMLHttpRequest的设计,没有适当地做职责分离也就算了,虽然增加了几个可注册的事件,然而依旧是采基本事件模型,而不是类似 DOM Level 2 事件模型那样,可以注册多个事件。

过去有不少程序库试着封装XMLHttpRequest来解决问题,例如,jQuery 的$.get$.post$.ajax$.ajax可使用选项对象来做更多细部设定(在jQuery 3,$.get$.post也可接受选项对象了),透过$.ajaxSetup等函数可设定默认值,这些设计非但隐藏了XMLHttpRequest的设定细节,也将一些职责从XMLHttpRequest中分离出来。

由于 Ajax 的处理天生就是非同步,这与开发者习惯的同步代码编写方式不同,而在非同步下顺序也变得重要时,回调地狱就会是个大问题,jQuery 3 中$.ajax可返回Promise对象,提供了 Ajax 请求时更一致的模式,可以采用像是同步的代码来编写非同步应用。

从设计的角度来看,Fetch API 就像是集合了过去 Ajax 使用上一些好实践的集合体,实现了职责分离,创建时可使用选项对象来进行相关设定,实际上,你也可以独立地创建HeadersRequestResponse实例。

例如,fetch除了可接受初始化对象设定之外:

fetch('POST-1.php', {
        method : 'POST',
        headers : {
            'Content-Type' : 'application/x-www-form-urlencoded'
        },
        body : reqString
    })

也可以接受Request实例:

let request = new Request('POST-1.php', {
        method : 'POST',
        headers : new Headers({
            'Content-Type' : 'application/x-www-form-urlencoded'
        }),
        body : reqString
    });

    fetch(request);

大部份的情况下,你不需要接触HeadersRequestResponse等实例,使用选项对象,通常足以应付,然而,如果需要明确的语义,或者是想重用某个设定,甚至是符合某个接口实现,那么HeadersRequestResponse等实例就会是需要的。

fetch的返回值是Promise,表面上看来,Fetch 很像在XMLHttpRequest上封装了一层Promise,这也是它为什么经常被拿来与$.ajax对比的原因之一,因为模式乍看之下十分类似,不过严格来说,$.ajax做了比较高阶的封装。

举例来说,$.ajaxdata选项指定对象时,会自动进行序列化与请求参数编码处理,然而使用fetchbody选项时,如〈简介 Fetch API〉中看到的范例,必须自行创建、编码请求参数,这是因为在Fetch 的规范前言中就清楚指出,Fetch 的定位本来就是低阶封装。

(Fetch 另一个与XMLHttpRequest不同的地方是Streams的支持,按照规范,响应对象的body特性会是个ReadableStream,行为上与Streams规范中的ReadableStream相同,在服务器的响应过程中,可以透过ReadableStream持续读取浏览器已接收之内容,虽然过去也可以使用XMLHttpRequestresponseText自行处理判断、读取想要的数据区域,然而,前者是直接处理串流数据,后者是对整个已获取之响应进行处理,本质上并不相同。)

正因为 Fetch 是基于Promise,而Promise主要只有三个状态,只能透过resolvereject从未定(pending)转移至满足(fulfilled)或背弃(rejected)状态,Promise实例本身也只有thencatch两个方法来处理对应的状态,在不施加额外设计上,自然也就无法提供逾时、进度处理等功能。

(若了解到某个 Fetch 的限制是来自于Promise的限制,就可以试着从设计上,依个别需求来来实现特定的方案。)

在浏览器支持上,对于不支持 Fetch 的浏览器,可以使用Fetch Polyfill,修补是基于XMLHttpRequest,仿造了 Fetch API 接口,不过正因为基于XMLHttpRequest,在某些方面功能会受限;在不支持Promise的浏览器上,除了 Fetch 修补之外,还要加上Promise修补(更旧的浏览器,像是 IE8/9,还要加上 ES5 修补等)。


展开阅读全文