异步请求方法总结

AJAX

(一)AJAX 概述

AJAX(Asynchronous JavaScript And XML,异步的 JavaScript 和 XML),是一种实现无页面刷新获取服务器数据的混合技术。它能够使浏览器在不刷新页面的情况下获取服务器响应,这将大大提升互联网用户的使用体验,同时,由于 AJAX 请求获取的是数据而不是 HTML 文档,因此它也节省了网络带宽,让互联网用户的网络冲浪体验变得更加顺畅。

1. XML 和 JSONP

XML(Extensible Markup Language,可拓展标记语言)是一种类似 HTML,用来描述数据是什么,并承载数据的标记语言。

而 JSON 仅仅是一种数据格式,在 JSON 发明之前,人们大量使用 XML 作为数据传输的载体,也正因如此,AJAX 技术的最后一个字母为“X”。而如今情况则发生了些变化,JSON 这种类似于字符串对象的轻量级的数据格式越来越受到开发者青睐,几乎变成了 AJAX 技术的标准数据格式。需要注意的是,JSON 并不是 XML 的替代品,两者各自有其适应的场景。

2. 无页面刷新

互联网最主要的功能在于资源交换,虽然在互联网中资源交换的主体都是计算机,但为了方便交流,我们通常将获取资源的一方称为客户端(主要的工具是浏览器),而将派发资源的一方称为服务端。

在 AJAX 技术出现之前,如果浏览器需要从服务器请求资源,其交互模式为“客户端发出请求 -> 服务端接收请求并返回相应 HTML 文档 -> 页面刷新,客户端加载新的 HTML 文档”。这种交互模式简洁明了,但是随着时代的进步,越来越多商业化网站的出现,使互联网不再局限于满足人们资源交换的需求,人们开始期待能够在互联网中获得更好的使用体验,而这种用户每次点击都会刷新页面的交互方式显然不够妥当。当用户点击页面中的某个按钮向服务器发送请求时,页面本质上可能只是一些数据发生了变化,而此时服务器却要将重绘的整个页面再返回给浏览器加载,这显然有悖于程序员的“DRY”原则,而且明明只是一些数据的变化却迫使服务器要返回整个 HTML 文档,这本身也会给网络带宽带来不必要的开销。

而 AJAX 能够在页面数据变动时,只向服务器请求新的数据,并且在阻止页面刷新的情况下,动态替换页面中展示的数据。AJAX 技术的问世,不仅通过阻止浏览器接受响应时刷新页面提升了互联网用户的使用体验,还使开发者能够以更加微观的视角重新思考互联网应用的构建,在数据层面而非资源层面以更高的自由度构建网站和 Web 应用。

3. 混合技术

AJAX 技术并不只是操作XMLHttpRequest对象发起异步请求,而是为了实现无页面刷新的资源获取的一系列技术的统称,这些技术包括了:

  • JavaScript:用来在获取数据后,通过操作 DOM 或其他方式达成目标;
  • 客户端(即浏览器)提供的实现异步服务器通信的XMLHttpRequest对象;
  • 服务器端允许浏览器向其发起 AJAX 请求的相关设置;

明白 AJAX 并不只是操作XMLHttpRequest对象,对于初学者而言是十分必要的。

(二)原生 AJAX 使用

1. XMLHttpRequest 对象

XMLHttpRequest对象是浏览器提供的一个 API,用来向服务器发送请求并解析服务器响应,整个过程中浏览器页面不会被刷新。XMLHttpRequest只是一个 JavaScript 对象,或者说是一个构造函数,它是由客户端 (即浏览器) 提供的(而不是 JavaScript 原生的)。它有属性和方法,需要通过new关键字进行实例化。

XMLHttpRequest对象是不断被扩展的。随着 XML 对象被广泛的接收,W3C 也开始着手制定相应的标准来规范其行为。目前,XMLHttpRequest有两个级别:1 级提供了 XML 对象的实现细节,2 级进一步发展了 XML 对象,额外添加了一些方法,属性和数据类型。但是并非所有浏览器都实现了 XML 对象 2 级的内容。

从一个 XML 对象的实例来剖析XMLHttpRequest实例的属性和方法。

1
const xhr = new XMLHttpRequest()

该实例的方法有:

  • .open():准备启动一个 AJAX 请求;
  • .setRequestHeader():设置请求头部信息;
  • .send():发送 AJAX 请求;
  • .getResponseHeader(): 获得响应头部信息;
  • .getAllResponseHeader():获得一个包含所有头部信息的长字符串;
  • .abort():取消异步请求;

该实例的属性有:

  • .responseText:包含响应主体返回文本;
  • .responseXML:如果响应的内容类型时text/xmlapplication/xml,该属性将保存包含着相应数据的 XML DOM 文档;
  • .status:响应的 HTTP 状态;
  • .statusText:HTTP 状态的说明;
  • .readyState:表示请求/响应过程的当前活动阶段

浏览器还为该对象提供了一个onreadystatechange监听事件,每当 XML 实例的readyState属性变化时,就会触发该事件。

至此,关于 XMLHttpRequest 实例对象的属性方法就全部罗列完毕了,接下来,我们将更进一步的探究如何使用这些方法,属性完成发送 AJAX 请求的流程。

2. 准备 AJAX 请求

.open()方法接收三个参数:请求方式请求 URL 地址是否为异步请求的布尔值

1
2
// 该段代码会启动一个针对“example.php”的 GET 同步请求。
xhr.open("get", "example.php", false)

① GET 与 POST

  • GET 请求

GET 请求用于获取数据,有时需要获取的数据得通过查询参数进行定位,在这种情况下应将查询参数追加到 URL 的末尾,令服务器解析。查询参数是指一个由?号起始,由&符号分割的包含相应键值对的字符串,用来告知浏览器所要查询的特定资源。查询字符串中每个参数的名和值都必须使用 encodeURIComponent() 进行编码(这是因为 URL 中有些字符会引起歧义,例如“&”)。

1
const query = "example.php?name=tom&age=24" // "?name=tom&age=24"即是一个查询参数
  • POST 请求

POST 请求用于向服务器发送应该被保存的数据,因此 POST 请求比 GET 请求多一份需要被保存的数据。需要发送的数据会作为.send()方法的参数最终被发往服务器,该数据可以是任意大小,任意类型。

这里需要注意以下两点:

  • .send()方法的参数是不可为空的,对于无需发送任何数据的 GET 请求,也需要在调用.send()方法时,向其传入null值;
  • 服务器对待表单提交以及发送 POST 请求并不一视同仁,这意味着服务器需要有相应的代码专门处理 POST 请求发送来的原始数据。

但好在我们可以通过 POST 请求模拟表单提交,只需要简单两步:

  1. 设置请求头参数:Content-Type: application/x-www-form-urlencoded(表单提交时的内容类型);
  2. 将表单数据序列化为查询字符串形式,传入.send()方法;

② 请求 URL 地址

这里需要注意若使用相对路径,请求 URL 是相对于执行代码的当前页面

③ 同步请求与异步请求

AJAX 并非总是异步的,AJAX 是避免页面在获取数据后刷新的一种技术,至于等待服务器响应的方式是同步还是异步,需要开发人员结合业务需求进行配置(虽然通常是异步的)。

3. 设置请求头

每个 HTTP 请求和响应都会带有一些与数据,收发者网络环境与状态相关的头部信息。XMLHttpRequest 对象提供的.setRequestHeader()方法为开发者提供了一个操作这两种头部信息的方法,并允许开发者自定义请求头的头部信息。

默认情况下,当发送 AJAX 请求时,会附带以下头部信息:

  • Accept:浏览器能够处理的内容类型;
  • Accept-Charset: 浏览器能够显示的字符集;
  • Accept-Encoding:浏览器能够处理的压缩编码;
  • Accept-Language:浏览器当前设置的语言;
  • Connection:浏览器与服务器之间连接的类型;
  • Cookie:当前页面设置的任何 Cookie;
  • Host:发出请求的页面所在的域;
  • Referer:发出请求的页面 URI;
  • User-Agent:浏览器的用户代理字符串;

部分浏览器不允许使用.setRequestHeader()方法重写默认请求头信息,因此自定义请求头信息是更加安全的方法:

1
2
// 自定义请求头
xhr.setRequestHeader("myHeader", "MyValue")

4. 发送请求

我们已经通过.open()方法确定了请求方式,等待响应的方式和请求地址,还通过.setRequestHeader()自定义了响应头,接下来需要使用.send()方法发送 AJAX 请求。

1
2
3
4
5
// AJAX 发送 get 请求
const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", false)
xhr.setRequestHeader("myHeader", "goodHeader")
xhr.send(null)
1
2
3
4
5
// AJAX 发送 post 请求
const xhr = new XMLHttpRequest()
xhr.open("post", "example.php", false)
xhr.setRequestHeader("myHeader", "bestHeader")
xhr.send(some_data)

5. 处理响应

处理一个同步的 GET 请求响应:

1
2
3
4
5
6
7
8
9
10
11
const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", false)
xhr.setRequestHeader("myHeader", "goodHeader")
xhr.send(null)
// 由于是同步的 AJAX 请求,因此只有当服务器响应后才会继续执行下面的代码
// 因此 xhr.status 的值一定不为默认值
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Request was unsuccessful: " + xhr.status)
}

xhr.status属性存储着响应的 HTTP 状态,判断请求是否成功。如果成功,就将读取 xhr.responseText属性中存储的返回值。但当请求为异步时,在xhr.send(null)语句被执行后,JavaScript 引擎会紧接着执行下面的判断语句,而由于尚未来得及响应,此时会得到一个默认的 xhr.status 值,而不能获取到请求的资源。

所以我们需要通过为 XMLHTTPRequest 实例添加onreadystatechange事件处理程序(也可以直接使用 DOM2 级规范规定的.addEventListener()方法,但 IE8 不支持该方法)。

xhr 实例的readystatechange事件会监听 xhr.readyState属性的变化,你可以将这个属性想象为一个计数器,随着 AJAX 流程的推进而不断累加,其可取的值如下:

  • 0:未初始化 – 尚未调用.open()方法;
  • 1:启动 – 已经调用.open()方法,但尚未调用.send()方法;
  • 2:发送 – 已经调用.send()方法,但尚未接收到响应;
  • 3:接收 – 已经接收到部分响应数据;
  • 4:完成 – 已经接收到全部响应数据,而且已经可以在客户端使用了;

有了这个时间处理程序对 AJAX 进程做监听,剩下的事就简单多了,一个异步的 GET 请求代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readystate == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Request was unsuccessful: " + xhr.status)
}
}
}
xhr.open("get", "example.php", true)
xhr.send(null)

注意:为了确保跨浏览器的兼容性,必须要在调用.open()方法之前指定事件处理程序,毕竟.open()方法的执行也包含在该事件处理程序的监听范围之内。


6. 取消异步请求

有时可能要在接收到响应前取消异步请求,这时需要调用.abort()方法。该方法会令 XHR 对象实例停止触发事件,并且不再允许访问任何和响应有关的对象属性。当终止 AJAX 请求后,你需要手动对 XHR 对象实例进行解绑以释放内存空间。

7. XMLHttpRequest 2 级规范

W3C 提出了 XMLHttpRequest 2 级规范,虽然并非所有浏览器都实现了该规范所规定的内容。

① FormData 类型

FormData 是 XMLHttpRequest 2 级提供的新数据类型(构造函数),它能让 POST 请求伪装成表单进行提交的过程更加轻松,因为 XHR 对象能够识别传入的数据类型是 FormData 的实例,并自动配置适当的头部信息。

FormData 的使用方式如下:

1
2
3
4
5
6
7
8
// 添加数据
let data1 = new FormData()
data1.append("name", "Tom")
xhr.send(data1)

// 提取表单数据
let data2 = new FormData(document.forms[0])
xhr.send(data2)

FormData 的另一个好处是相较于传统 AJAX 请求,它允许我们上传二进制数据(图片,视频,音频等),详情可查看链接

FormData 的浏览器兼容性:

  • 桌面端:IE 10+ 与其他浏览器均支持
  • 移动端:Android,Firefox Mobile,OperaMobile 均支持,其余浏览器未知

② 超时设定

为了避免发送 AJAX 请求后出现迟迟得不到服务器响应的情况,2 级规范提供了一个额外的属性和事件监听事件:

  • timeout属性:设置超时时间,单位为毫秒;
  • timeout事件:当响应时间超出实例对象 timeout 属性时被触发;

使用方式如下:

1
2
3
// 当响应时间超过 1 秒时,请求中止,弹出提示框
xhr.timeout = 1000
xhr.ontimeout = () => { alert("Request did not return in a second.") }

当请求终止时,会调用ontimeout事件处理程序,此时 xhr 的readyState属性的值可能已变为 4,这意味着会继续调用onreadystatechange事件处理程序,但是当超时中止请求后再访问 xhr 的status属性会使浏览器抛出一个错误,因此需要将检查status属性的语句放入try-catch语句中。虽然这带来了一些麻烦,但却对 XMLHttpRequest 对象有了更多的控制。

浏览器兼容性:

  • 桌面端:IE 10+ 与其他浏览器均支持
  • 移动端:IE Mobile 10+ 与其他浏览器均支持

③ overrideMimeType() 方法

响应返回的响应头里,描述了返回数据的 MIME 类型,浏览器通过识别该类型,告知 XMLHttpRequest 实例处理该数据的方式。如果我们想要以其它方式处理响应数据(例如将 XML 类型数据当做纯文本处理),可以使用.overrideMimeType()方法,该方法可以覆写响应头所描述数据的 MIME 类型。

1
2
3
4
const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", true)
xhr.overrideMimeType("text/xml") // 强迫浏览器将响应数据以指定类型方式解读
xhr.send(null)

浏览器兼容性:

  • 桌面端:IE 7+ 与其他浏览器均支持
  • 移动端:Firefox Mobile,Chrome for Android 均支持,其余浏览器未知

④ 进度事件

Progress Events 规范是 W3C 制定的一个工作草案。该规范定义了客户端与服务器通信相关的一系列事件,这些事件监听了通信进程中的各个关键节点,使我们能够以更细的颗粒度掌控数据传输过程中的细节。目前共有 6 个进度事件,他们会随数据传输进展被顺序触发(除了 error,abort 事件),让我们看看他们的定义和浏览器兼容情况:

loadstart:在接收到响应数据的第一个字节时触发;

  • 桌面端:除 Safari Mobile 未知外,其他浏览器均支持
  • 移动端:除 Safari Mobile 未知外,其他浏览器均支持

progress:在接收响应期间持续不断地触发;

  • 桌面端:IE10+ 与其他浏览器均支持
  • 移动端:均支持

error:在请求发生错误时触发;

  • 桌面端:所有浏览器均支持(信息来源
  • 移动端:除 IE Mobile 不支持外,其他浏览器均支持(信息来源

abort:再因为调用 abort() 方法时触发;

  • 桌面端:未知
  • 移动端:未知

load:在接收到完整的响应数据时触发;

  • 桌面端:IE7+ 与其他浏览器均支持
  • 移动端:Chrome for Android,Edge,Firefox Mobile 支持,其余浏览器未知

loadend:在通信完成或者触发 error,abort 或 load 事件后触发;

  • 桌面端:所有浏览器不支持
  • 移动端:所有浏览器不支持

这里着重讲解以下两个事件:

① load 事件

该事件帮助我们节省了readstatechange事件,我们不必在 XHR 对象实例上绑定该事件监听函数以追踪实例上readState属性的变化,而是可以直接使用以下代码:

1
2
3
4
5
6
7
8
9
10
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if ((xhr.status >= 200 && xhr.status <300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Something wrong!")
}
}
xhr.open("get", "example.php", true)
xhr.send(null)

② progress 事件

该事件可以实现加载进度条效果。因为onprogress事件处理程序会接收到一个event对象,其target属性为 XHR 对象实例,但却额外包含着三个属性:

  • lengthComputable:表示进度信息是否可用的布尔值;
  • position:表示目前接收的字节数;
  • totalSize:表示根据 Content-Length 响应头部确定的预期字节数;

显然加载进度条所需的一切资源都准备就绪,只需写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const xhr = new XMLHttpRequest()
xhr.onload = () => {
if ((xhr.status >= 200 && xhr.status <300) || xhr.status == 304) {
alert(xhr.responseText)
} else {
alert("Something wrong!")
}
}
// 加载进度条
xhr.onprogress = function(event) {
const divStatus = document.getElementById("status")
if (event.lengthComputable) {
divStatus.innerHTML = `Received ${event.postion} of ${event.totalSize} bytes`
}
}
xhr.open("get", "example.php", true)
xhr.send(null)

注意要在.open()方法前调用onprogress事件处理程序。

(三)jQuery AJAX 使用

jQuery Ajax 是对原生 XHR 的封装,除此以外还增添了对 JSONP 的支持。通过 jQuery AJAX 方法,能够使用 HTTP Get 和 HTTP Post 从远程服务器上请求文本、HTML、XML 或 JSON,同时还能把这些外部数据直接载入网页的被选元素中。

如果没有 jQuery,AJAX 编程还是有些难度的。编写常规的 AJAX 代码并不容易,因为不同的浏览器对 AJAX 的实现并不相同。这意味着必须编写额外的代码对浏览器进行测试。借助 jQuery,只需要一行简单的代码就可以实现 AJAX 功能。

1
2
3
4
5
6
7
8
$.ajax({
type: 'POST',
url: url,
data: data,
dataType: dataType,
success: function () {},
error: function () {}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<!-- 实例演示 -->
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script type="text/javascript" src="../jquery/jquery-3.5.1.js"></script>
<script type="text/javascript">
$(document).ready( () => {
$("#b01").click( () => {
htmlobj = $.ajax({ url: "../code/02 CSS/index1-intro.html", async: false });
$("#myDiv").html(htmlobj.responseText);
});
});
</script>
</head>
<body>
<div id="myDiv"><h2>通过 AJAX 改变文本</h2></div>
<button id="b01" type="button">改变内容</button>
</body>
</html>

然而 jQuery Ajax 美中不足的是:

  • 本身是针对 MVC 的编程,不符合现在前端 MVVM 的浪潮
  • 基于原生的 XHR 开发,XHR 本身的架构不清晰,已经有了 fetch 的替代方案
  • JQuery 整个项目太大,单纯使用 ajax 却要引入整个 JQuery 非常不合理(采取个性化打包的方案又不能享受 CDN 服务)

尽管 JQuery 对前端的开发工作曾有着(现在也仍然有着)深远的影响,但随着 VUE,REACT 新一代框架的兴起,以及 ES 规范的完善,更多 API 的更新,JQuery 这种大而全的 JS 库,未来的路会越走越窄。

1. jQuery 加载

jQuery load() 方法从服务器加载数据,并把返回的数据放入被选元素中。

1
$(selector).load(URL,data,callback);
  • 必需的 URL 参数规定要加载的 URL。
  • 可选的 data 参数规定与请求一同发送的查询字符串键/值对集合。
  • 可选的 callback 参数是 load() 方法完成后所执行的回调函数。

① 把文件 “demo_test.txt” 的内容加载到指定的

元素中:

1
$("#div1").load("demo_test.txt");

② 把 “demo_test.txt” 文件中 id=”p1” 的元素的内容,加载到指定的

元素中:

1
$("#div1").load("demo_test.txt #p1");

③ 可选的 callback 参数规定当 load() 方法完成后所要允许的回调函数。回调函数可以设置不同的参数:

  • responseTxt:调用成功时的结果内容
  • statusTXT :调用的状态
  • xhr:XMLHttpRequest 对象

下面的例子会在 load() 方法完成后显示一个提示框。如果 load() 方法已成功,则显示“外部内容加载成功!”,而如果失败,则显示错误消息:

1
2
3
4
5
6
7
8
$("button").click(function(){
$("#div1").load("demo_test.txt",(responseTxt,statusTxt,xhr) => {
if(statusTxt=="success")
alert("外部内容加载成功!");
if(statusTxt=="error")
alert("Error: "+xhr.status+": "+xhr.statusText);
});
});

2. jQuery GET/POST

jQuery get() 和 post() 方法用于通过 HTTP GET 或 POST 请求从服务器请求数据。

  • GET - 从指定的资源请求数据
  • POST - 向指定的资源提交要处理的数据

GET 基本上用于从服务器获得(取回)数据。注释:GET 方法可能返回缓存数据。POST 也可用于从服务器获取数据。不过,POST 方法不会缓存数据,并且常用于连同请求一起发送数据。

jQuery $.get() 方法

$.get() 方法通过 HTTP GET 请求从服务器上请求数据。

1
$.get(URL,callback);

必需的 URL 参数规定您希望请求的 URL。可选的 callback 参数是请求成功后回调的函数。

下面的例子使用 $.get() 方法从服务器上的一个文件中取回数据:

1
2
3
4
5
$("button").click(function(){
$.get("demo_test.asp",function(data,status){
alert("Data: " + data + "\nStatus: " + status);
});
});

$.get() 的第一个参数是我们希望请求的 URL(”demo_test.asp”)。第二个参数是回调函数。第一个回调参数存有被请求页面的内容,第二个回调参数存有请求的状态。

jQuery $.post() 方法

$.post() 方法通过 HTTP POST 请求从服务器上请求数据。

1
$.post(URL,data,callback);

必需的 URL 参数规定您希望请求的 URL。可选的 data 参数规定连同请求发送的数据。可选的 callback 参数是回调函数。

下面的例子使用 $.post() 连同请求一起发送数据:

1
2
3
4
5
6
7
8
9
10
$("button").click(function(){
$.post("demo_test_post.asp",
{
name:"Donald Duck",
city:"Duckburg"
},
(data,status) => {
alert("Data: " + data + "\nStatus: " + status);
});
});

Fetch

(一)Fetch 实现

fetch 号称是 ajax 的替代品,它的 API 是基于 Promise 设计的,旧版本的浏览器不支持 Promise,需要使用 polyfill es6-promise

1
2
3
4
5
6
7
8
9
10
function fetch(url) {
return new Promise(function (resolve, reject) {
ajax(url, function(res) {
resolve(res);
}, function(err) {
console.log(err);
reject(err);
})
})
}

(二)Fetch 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 原生 XHR
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText) //从服务器获取数据
}
}
xhr.send()

// fetch
fetch(url)
.then(response => {
if (response.ok) {
return response.json(); //服务器返回的是 json 格式
}
})
.then(data => console.log(data))
.catch(err => console.log(err))

由于 Fetch 底层是用 Promise 实现,可以直接用 async 来优化上面的代码,减少回调,使其更加语义化、容易理解。async/await 是 ES7 的 API,目前还在试验阶段,还需要使用 babel 进行转译成 ES5 代码。

1
2
3
4
5
async function test() {
let response = await fetch(url);
let data = await response.json();
console.log(data)
}

(三)Fetch 使用

两种调用方式:

1
2
fetch(url, options)
fetch(req, options)

推荐使用第一种,一眼就可以看到 url,更加直观。

options 是一个对象,可设置以下字段:

  1. method:请求方法,默认 GET;
  2. headers:请头信息,可以是简单的对象,也可以是 Headers 的实例;
  3. body:发送数据。BlodbufferSourceFormDataURLSearchParamsUSVstring(GET、HEAD 没有 body);
  4. mode:请求模式。
    • cors:跨域请求;
    • no-cors:只允许使用GETHEADPOST
    • same-origin:同源请求;
    • navigate:支持页面导航。
  5. credentials:是否发送 cookies
    • omit:不发送,默认;
    • same-origin:同源发送;
    • include:发送。
  6. cache:缓存策略:
    • default:请求之前检查缓存;
    • no-cache:有缓存,发送一个查询请求,缓存失效,再发送正常请求;
    • no-store:不检查缓存,直接请求;
    • reload:忽略缓存,拿到响应后,更新缓存;
    • force-cache:强制读取缓存,缓存过期,再发送正常请求;
    • only-if-cached:读取缓存,过期就报网络错误。mode 设置为 same-origin 时有效。
  7. redirect:重定向时的处理方法:
    • follow:跟随;
    • error:报错;
    • manual:用户手动跟随。
  8. integrity:包含一个验证资源完整性的字符串。

fetch 是比较底层的 API,很多情况下都需要我们再次封装。比如:

1
2
3
4
5
6
7
8
9
10
// jquery ajax
$.post(url, {name: 'test'})

// fetch
fetch(url, {
method: 'POST',
body: Object.keys({name: 'test'}).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&')
})

由于 fetch 是比较底层的 API,所以需要我们手动将参数拼接成’name=test’的格式,而 jquery ajax 已经封装好了。所以 fetch 并不是开箱即用的。

Headers

Headers 用于构造请求头信息,构造函数接收一个对象,对象的key-value就是请求头的信息。

1
2
3
4
5
6
7
8
9
10
11
12
let headers = new Headers(
{
'content-type':'text/plain',
'content-length':data.toString().length
}
);
headers.append('X-Custom-header','AnotherValue'); //追加
headers.has('content-type'); //true 查询
headers.get('content-type'); //'text/plain' 获取
// headers.getAll('content-type'); //['text/plain'] getAll 被移除了
headers.delete('content-type'); //删除
headers.set('content-type','json'); //重写

Request

请求对象。可以新建一个,也可以从已有的对象中继承。

1
2
3
4
let Url = '/users';
let req = new Request(Url, {method:'GET', headers})
// 扩展 request
let postReq= new Requset(req, {method:'POST'})

Response

Response 实例是 fertch 处理完 promise 之后的返回的。也可以手动创建,在servoceWorkers 中才真实有用。

1
let res = new Response(body, init)

body 可以是BlobBufferSourceFormDataURLSearchParamsUSVString 这些值。

init 是一个对象,可包含以下字段:

  • status:响应状态码;
  • statusText:状态文本;
  • headers:头部信息,普通对象或 Headers 的实例。

response 的实例还有一些可读属性:

  • ok:请求是否成功,状态码为 2xx 都为 true
  • status:状态码;
  • statusText:状态文本;
  • bodyUsed:响应数据是否被用过;
  • headers:头部信息;
  • url:响应地址;
  • type:响应类型:
    • basic:同源;
    • cors:跨域;
    • error:出错;
    • opaque:Request mode 设置为 no-cors 的响应。

当发起一个 Fetch 请求时,返回的 response 响应会自带一个 response.type 属性(basic、cors、opaque)。response.type 属性说明了异步资源的来源和其相应的处理方式。

当我们发起一个同源请求时,response.type 为 basic,我们可以从 response 读取全部信息。

当访问一个非同源域名,并且有返回相应的 CORs 响应头时,那么该请求类型是 cors。在 cors 响应里无法访问Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma

当对一个不同源的域名发起请求时,如果返回的响应头部没有 CORS 信息,那么这个 response 对应的类型就是 opaque 类型。一个 opaque 响应是无法读取返回的数据、状态,甚至无法确定这个请求是否成功。

我们可以自定义 Fetch 请求的模式,要求返回对应类型的响应,有以下几种响应:

  1. same-origin 只返回同源请求,其他类型会被 reject
  2. cors 接收同源、非同源请求,返回有 CORs 头部的响应
  3. cors-with-forced-preflight 在发出请求前会先做一次安全性检查
  4. no-cors 用来发起没有 CORS 头部并且非同源请求,并且会返回 opaque 响应。但是目前这种类型只能在 Service Worker 里使用,在 window.fetch 里不能用

response 有一些方法来 reslove 响应信息。

  • json,解析响应信息为对象,resolve promise;
  • text
  • blob
  • formData
  • arrayBuffer

(四)Fetch 缺点

  • 当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject,即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve(但是会将 resolve 的返回值的 ok 属性设置为 false),仅当网络故障时或请求被阻止时才会标记为 reject;
  • 默认情况下,fetch 不会从服务端发送或接收任何 cookies,如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项);
  • POST 的数据需要转为 JSON;
  • 不能设置超时和取消请求,可以通过Promise.race,模拟两者;
  • 文件上传和下载获取不到进度。response.body 是可读流,具有getReader,可根据这个来获取下载进度;
  • 不能直接获取到响应数据,需要调用响应方法,resolve 一下;

Axios

axios 是 Vue 官方推荐使用的,它也是对原生 XHR 的封装。它有以下几大特性:

  • 可以在 node.js 中使用
  • 提供了并发请求的接口
  • 支持 Promise API

(一)Axios 使用

1. 发送请求

1
2
3
4
5
6
7
8
axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
})
.then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});

axios 的用法与 jQuery 的 ajax 方法非常类似,两者都返回一个 Promise 对象(在这里也可以使用成功回调函数,但更推荐使用 Promiseawait),然后再进行后续操作。

2. 添加拦截器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加一个请求拦截器。注意,这里面有两个函数:分别是成功和失败时的回调函数,这样设计的原因会在之后介绍
axios.interceptors.request.use(function(config) {
// 发起请求前执行一些处理任务
return config; // 返回配置信息
}, function (error) {
// 请求错误时的处理
return Promise.reject(error);
});

// 添加一个响应拦截器
axios.interceptors.response.use(function(response) {
// 处理响应数据
return response; // 返回响应数据
}, function (error) {
// 响应出错后所做的处理工作
return Promise.reject(error);
});

发送请求之前,我们可以对请求的配置参数(config)做处理;在请求得到响应之后,我们可以对返回数据做处理。当请求或响应失败时,我们还能指定对应的错误处理函数。

3. 撤销 HTTP 请求

在开发与搜索相关的模块时,我们经常要频繁地发送数据查询请求。一般来说,当发送下一个请求时,需要撤销上个请求。因此,能撤销相关请求的功能非常有用。axios 撤销请求的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 例子一
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('请求撤销了', thrown.message);
} else {
// 处理错误
}
});

// 例子二
axios.post('/user/12345', {
name: '新名字'
}, {
cancelToken: source.token
}).

// 撤销请求 (信息参数是可选的)
source.cancel('用户撤销了请求');

在 axios 中,使用基于 CancelToken 的撤销请求方案。然而该提案现已撤回,详情 点这里。具体的撤销请求的实现方法,将在后面的源代码分析的中解释。

(二)Axios 核心模块

下面将根据模块分析 axios 的设计和实现。

image-20210409195435367

HTTP 请求模块

请求模块的代码放在了 core/dispatchRequest.js 文件中,这里只展示了一些关键代码来简单说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);

// 其他源码

// 默认适配器是一个模块,可以根据当前环境选择使用 Node 或者 XHR 发送请求。
var adapter = config.adapter || defaults.adapter;

return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);

// 其他源码

return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

// 其他源码

return Promise.reject(reason);
});
};

上面的代码中,我们能够知道 dispatchRequest 方法是通过 config.adapter 获得发送请求模块的。可以通过传递符合规范的适配器函数来替代原来的模块(一般不会这样做,但它是一个松散耦合的扩展点)。

defaults.js 文件中,我们可以看到相关适配器的选择逻辑——根据当前容器的一些独特属性和构造函数,来确定使用哪个适配器。

1
2
3
4
5
6
7
8
9
10
11
12
function getDefaultAdapter() {
var adapter;
// 只有在 Node.js 中包含 process 类型对象时,才使用它的请求模块
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// Node.js 请求模块
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器请求模块
adapter = require('./adapters/xhr');
}
return adapter;
}

axios 中的 XHR 模块相对简单,它是对 XMLHTTPRequest 对象的封装,源码位于 adapters/xhr.js 文件中。

拦截器模块

现在让我们看看 axios 是如何处理请求和响应拦截器函数的。这就涉及到了 axios 中的统一接口 ——request 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Axios.prototype.request = function request(config) {

// 其他源码

var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

这个函数是 axios 发送请求的接口。因为函数实现代码相当长,这里简单讨论相关设计思想:

  1. chain 是一个执行队列。队列的初始值是一个携带配置(config)参数的 Promise 对象。
  2. 在执行队列中,初始函数 dispatchRequest 用来发送请求,为了与 dispatchRequest对应,我们添加了一个 undefined。添加 undefined 的原因是需要给 Promise 提供成功和失败的回调函数,从promise = promise.then(chain.shift(), chain.shift()); 就能看出来。因此,函数 dispatchRequestundefiend 可以看成是一对函数。
  3. 在执行队列 chain 中,发送请求的 dispatchReqeust 函数处于中间位置。它前面是请求拦截器,使用 unshift 方法插入;它后面是响应拦截器,使用 push 方法插入,在 dispatchRequest 之后。需要注意的是,这些函数都是成对的,也就是一次会插入两个。

撤销请求模块

与撤销请求相关的模块位于 Cancel/ 文件夹下,现在我们来看下相关核心代码。

首先,我们来看下基础 Cancel 类。它是一个用来记录撤销状态的类,具体代码如下:

1
2
3
4
5
6
7
8
9
function Cancel(message) {
this.message = message;
}

Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

使用 CancelToken 类时,需要向它传递一个 Promise 方法,用来实现 HTTP 请求的撤销,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}

var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});

var token = this;
executor(function cancel(message) {
if (token.reason) {
// 已经被撤销了
return;
}

token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}

CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};

adapters/xhr.js 文件中,撤销请求的地方是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (config.cancelToken) {
// 等待撤销
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}

request.abort();
reject(cancel);
// 重置请求
request = null;
});
}

通过上面的撤销 HTTP 请求的例子,让我们简要地讨论一下相关的实现逻辑:

  1. 在需要撤销的请求中,调用 CancelToken 类的 source 方法类进行初始化,会得到一个包含 CancelToken 类实例 A 和 cancel 方法的对象。
  2. 当 source 方法正在返回实例 A 的时候,一个处于 pending 状态的 promise 对象初始化完成。在将实例 A 传递给 axios 之后,promise 就可以作为撤销请求的触发器使用了。
  3. 当调用通过 source 方法返回的 cancel 方法后,实例 A 中 promise 状态从 pending 变成 fulfilled,然后立即触发 then 回调函数。于是 axios 的撤销方法——request.abort() 被触发了。

axios 这样设计的好处是什么?

发送请求函数的处理逻辑

如前几章所述,axios 不将用来发送请求的 dispatchRequest 函数看做一个特殊函数。实际上,dispatchRequest 会被放在队列的中间位置,以便保证队列处理的一致性和代码的可读性。

适配器的处理逻辑

在适配器的处理逻辑上,httpxhr 模块(一个是在 Node.js 中用来发送请求的,一个是在浏览器里用来发送请求的)并没有在 dispatchRequest 函数中使用,而是各自作为单独的模块,默认通过 defaults.js 文件中的配置方法引入的。因此,它不仅确保了两个模块之间的低耦合,而且还为将来的用户提供了定制请求发送模块的空间。

撤销 HTTP 请求的逻辑

在撤销 HTTP 请求的逻辑中,axios 设计使用 Promise 来作为触发器,将 resolve 函数暴露在外面,并在回调函数里使用。它不仅确保了内部逻辑的一致性,而且还确保了在需要撤销请求时,不需要直接更改相关类的样例数据,以避免在很大程度上入侵其他模块。