头图

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest

「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1 月。

在上一篇文章中我向您介绍了,AJAX 技术是什么以及它的意义。在本篇文章中我们将更进一步,深入探究如何使用 AJAX 技术。让我们先搞清楚数据获取的来源,然后开始再深入了解使用 XMLHttpRequest API 获取数据的技术细节

1. 获取数据

我们已经知道 AJAX 技术被用来在项目中以阻止页面刷新的方式获取数据,那么数据从哪里来呢?我们又该如何获取数据?答案是我们通常使用 API 与各种服务器交互。

「API」是「Application Programming Interface」的缩写,形式为 URL,通过特定的 HTTP 方法和约定好的参数,我们可以获取期望的数据。。

还记得我们提到过使用 AJAX 技术需要服务端进行相应的设置吗?我们之后会再来谈这一点。

2. AJAX 技术的核心:XMLHttpRequest 对象

让我们先把服务端所需的设置抛在一边,聚焦 AJAX 技术的核心环节:XMLHttpRequest 对象。

要注意,在 2023 年,我们可以认为 XMLHttpRequest 对象已经被更先进的 Fetch API 所替代了,但是在本篇文章中,它依然是我们谈论的主角。因为它暴露了 HTTP 交互的更多细节,我认为开发者依然可以从该对象的设计中获益。

XMLHttpRequest 对象是由浏览器提供的 API,用来向服务器发送请求并解析响应。正如我们多次提到过的:在整个过程中,浏览器页面不会被刷新。让我们先宏观上对该对象有一个清晰的认知:

  1. XMLHttpRequest 只是一个 JavaScript 对象。或者说,是一个 构造函数。要明白它一点也不神秘,它的唯一特殊之处只在于它是由浏览器提供的,而并非被 JavaScirpt 语言所拥有;
  2. XMLHttpRequest 对象在不断被扩展的。随着 XML 对象被社会广泛的接受,W3C 也开始着手制定相应的标准来规范其行为。目前,XMLHttpRequest 有两个级别:第 1 级提供了 XMLHttpRequest 对象的实现细节,第 2 级则进一步发展了该对象,额外添加了一些方法和属性。

现在,让我们开始剖析 XMLHttpRequest 实例对象的属性和方法。首先,我们需要先创建一个 XMLHttpRequest 对象的实例:

const xhr = new XMLHttpRequest()

该实例继承了如下属性和方法:

2.1 继承的属性

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

2.2 继承的方法

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

除了上述属性方法外,浏览器还为该对象提供了一个名为 onreadystatechange 的监听事件,每当 XMLHttpRequest 实例的 readyState 属性变化时,就会触发该事件的发生。

至此,关于 XMLHttpRequest 实例对象的属性方法就全部介绍完了,接下来,我将向您介绍我们在开发时是如何利用这些属性和方法来发送 AJAX 请求的。

3. 准备 AJAX 请求

要想与服务器交互,我们首先需要明确以下问题:

  • 希望如何处理数据(获取,修改还是删除?),不同目标对应着不同的请求方法,例如 GETPOST
  • 希望处理什么数据? 在 RESTful 规范下,数据以 URL 形式声明它所在的位置;
  • 以何种方式等待响应?开发者有「同步」和「异步」两种选择;

而 XMLHttpRequest 实例的 .open() 方法的作用就是用来回答以上三个问题。.open() 方法依次接收三个参数:

  1. 请求方式
  2. 请求URL地址
  3. 是否为异步请求的布尔值

下面是调用 .open() 方法的一个例子,该代码会针对 example.php 发送一个 GET 请求,并且需要同步获得服务器响应:

xhr.open("get", "example.php", false)

正如在开始做饭前,我们需要提前准备好材料和工具,.open() 方法也同样出色地完成了发送 AJAX 请求前的准备工作。

让我们再深入聊聊一些准备工作的细节:

3.1 GET 请求与 POST 请求

3.1.1 GET 请求

GET 请求用于获取数据,有时我们需要获取的数据需要通过「查询参数」进行定位,在这种情况下,我们会将查询参数追加到 URL 的末尾,供服务器解析。

查询参数是指一个由 ? 号起始,由 & 符号分割的包含键值对的字符串。用来告知服务器所要查询的特定资源。

const query = "example.php?name=tom&age=24"

🚨 注意:请务必对查询字符串中使用 encodeURIComponent() 方法进行编码,因为 URL 中有些字符会引起歧义,例如“&”。

3.1.2 POST 请求

POST 请求用于向服务器发送待保存的数据,因此 POST 请求天然比 GET 请求多一份待保存的数据。你可能注意到,之前提到的 .open() 方法似乎没有预留相应的参数,那么数据应该放在何处呢?。

答案是需要发送的数据会作为 .send() 方法的参数最终被发往服务器,浏览器并不会限制数据的大小和类型(敏感的开发者应该嗅到了一些危险的味道)。

这里需要注意以下两点:

  1. .send() 方法在调用时,参数不可以是空值。这意味着,即使是无需发送任何数据的 GET 请求,也需要在调用 .send() 方法时,显式的传入 null 值;
  2. 我们应该知道两种向服务器发送数据的方式:「表单提交」以及「发送POST请求」。要注意服务器对待这两种数据发送方式并非一视同仁,服务器需要有相应的代码专门处理 POST 请求发送来的原始数据。

通过在发送 POST 请求时模拟表单提交,我们可以降低服务端的编码成本:

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

3.2 请求 URL 地址

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

3.3 同步请求与异步请求

人们通常认为 AJAX 请求是异步的(即请求发出,等待响应的时间里,程序会正常执行),但实际上并非如此,正如我们多次提到的 AJAX 是避免页面在获取数据后刷新的一种技术,至于等待服务器响应的方式是同步还是异步,需要开发人员结合业务需求进行配置。

你可能会好奇,什么时候我们需要使用同步的 AJAX?我的答案是「任何时候都不需要」!如果您想要进一步了解这个问题,可以查看 Stack Overflow 上的相关讨论

4. 设置请求头

每个 HTTP 请求和响应都包含着例如网络环境,用户状态等的头部信息。XMLHttpRequest 对象提供的.setRequestHeader() 方法为开发者提供了操作 HTTP 请求中头部信息的方法。

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

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

🚨 注意!部分浏览器不允许使用 .setRequestHeader() 方法重写默认的请求头信息,因此如果想要在请求头中携带数据,自定义请求头信息将是更加安全的方法:

xhr.setRequestHeader("customHead", "customValue")

5. 发送请求

到此为止,我们已经完成了发送请求的所有准备:我们使用 .open() 方法指定了请求方式,请求地址和等待响应的方式,甚至还通过 .setRequestHeader() 自定义了响应头,接下来就到了最激动人心的时刻:使用 .send() 方法,发送一个 AJAX 请求!

const xhr = new XMLHttpRequest()
xhr.open("get", "example.php", false)
xhr.setRequestHeader("myHeader", "goodHeader")
xhr.send(null)

是不是非常简单?让我们尝试发送一个 POST:

const xhr = new XMLHttpRequest()
xhr.open("post", "example.php", false)
xhr.setRequestHeader("myHeader", "bestHeader")
xhr.send(some_data)

完美!不过您是否还觉得差了点什么?没错,目前为止我们只是发出了请求,还没有处理服务单的响应,这是我们接下来要做的事。

6. 处理响应

让我们直接看看如何处理一个同步的 GET 请求响应:

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 值,因此,我们永远都不可能获取到所请求的资源。

该如何解决呢?答案是在 xhr 实例上添加名为 onreadystatechange 的事件处理程序(当然你也可以直接使用 DOM2 级规范规定的 .addEventListener() 方法,但是注意,IE8 是不支持该方法的,不过谁又会在今天在乎 IE8 呢?)。

xhr 实例的 readystatechange 事件会监听 xhr.readyState 属性的变化,这个属性的设计很有意思,它是一个从 0 开始的计数器,会随着请求的发展而不断迭代,它有如下值:

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

有了这个计数器状态用于记录 AJAX 请求进度后,剩下的事就简单多了,一个异步的 GET 请求的完整代码如下:

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() 方法的执行也包含在该事件处理程序的监听范围之内。

7. 取消异步请求

有时候,您可能需要在接收到响应之前取消请求的发送(例如您已经收到了响应,无需再次发送请求),这时候,您需要调用 .abort() 方法。

该方法会令 XHRHttpRequest 的实例对象停止接收 onreadystatechange 事件,并且不再允许开发者访问任何和响应有关的对象属性。既然没了监控器,我们也就再也无法获得响应。

🚨 注意:当终止 AJAX 请求后,您需要手动注销 xhr 实例以释放内存空间。


🎉 🎉 🎉 恭喜您!到这里您已经掌握了所有关于 AJAX 的基础知识,您知道了 AJAX 是什么,因何而存在,以及如何使用 JavaScript 发起一个 AJAX 请求并接收响应,您已然成为一名 AJAX 大师!祝贺您!太棒了!


















干得漂亮!尊敬的 AJAX 大师,您居然没有选择离开看到了这里,那么我将传授您最后一部分 AJAX 秘籍,帮助您成为一名 AJAX 忍者,这是您的坚持赢得的!

8. XMLHttpRequest 2 级

还记得我们一开始有提到,W3C 提出了 XMLHttpRequest 2 级规范吗?虽然并非所有的浏览器都实现了该规范所规定的所有内容,但一些内容还是被全部或大多数浏览器所实现。想成为 AJAX 忍者?往下看吧。

💡 友情提示:在这一部分,您将会看到很多有关浏览器兼容性的信息,希望您不要觉得枯燥,毕竟这可是忍者的修行,对吧?

8.1 FormData 类型

FormData 是 XMLHttpRequest 2 级为我们提供的新的数据结构,还记的我们之前是如何将一个 POST 请求伪装成一个表单提交的样子吗(答案是通过 HTTP 请求头)?FormData 将令这一过程变得更加轻松,因为 XHRHttpRequest 对象能够在识别传入的数据类型是 FormData 的实例时,自动配置适当的头部信息。

具体的使用方式如下:

// 添加数据
let data1 = new FormData()
data1.append("name", "Tom")
xhr.send(data1)

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

除此之外,FormData 的另一个好处是相较于传统的 AJAX 请求,它允许我们上传二进制数据(图片,视频,音频等),具体详情可查看该官方文档获取更多信息。

最后我们来看一看 FormData 的浏览器兼容性:

  • PC:IE 10+ 浏览器都支持;
  • 移动端:Android,Firefox Mobile,OperaMobile均支持,其余浏览器未知

8.2 超时设定

当我们发送一个 AJAX 请求,却迟迟得不到服务器响应,这种感觉是很糟糕的。为了缓解这种糟糕的感觉,XMLHttpRequest 2 级规范为我们提供了一个额外的属性和事件监听事件:

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

使用方式如下:

// 当响应时间超过 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对象有了更多的控制。

该属性的浏览器兼容性:

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

8.3 overrideMimeType() 方法

响应返回的响应头里,描述了返回数据的 MIME 类型,浏览器通过识别该类型,告知 XMLHttpRequest 实例处理该数据的方式。然而有时候(例如将 XML 类型数据当做纯文本处理),我们想要以我们想要的方式处理响应的数据,在XMLHttpRequest 2 级规范中,我们可以使用 .overrideMimeType() 方法实现这一点。

其写法如下:

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

该方法的浏览器兼容性:

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

8.4 进度事件

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

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

    • PC:除 Safari Mobile 未知外,其他浏览器均支持;
    • 移动端:同 PC;
  • progress:在接收响应期间持续不断地触发;

    • PC:IE10+ 以上浏览器均支持;
    • 移动端:均支持 ;
  • error:在请求发生错误时触发;

    • PC:所有浏览器均支持(信息来源
    • 移动端:除 IE Mobile 不支持外,其他浏览器均支持(信息来源);
  • abort:再因为调用abort()方法时触发;

    • PC & 移动端:未知;
  • load:在接收到完整的响应数据时触发;

    • PC:IE7+ 以上浏览器均支持;
    • 移动端:Chrome for Android,Edge,Firefox Mobile支持,其余浏览器未知;
  • loadend:在通信完成或者触发 errorabortload 事件后触发;

    • PC & 移动端:均不支持;

这里我们将着重展开讲解以下两个事件:

8.4.1 load 事件

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

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)

8.4.2 progress 事件

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

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

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

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 事件处理程序。

9. 总结

恭喜您!您已经了解了关于 XHRHttpRequest 类的所有关键细节,我愿承您为 AJAX 忍者!不过在庆祝之前,让我们先回顾一下这漫长旅途中所学到的知识:

  1. 首先,我们学习了 XHRHttpRequest 1 级所规定的核心属性和方法,掌握了如何通过 XHRHttpRequest 实例发送 HTTP 请求并接收响应;
  2. 其次,通过学习 XHRHttpRequest 2 级的规定,我们掌握了在一般的 HTTP 请求处理外,可以执行的额外操作,例如获取进度,丢弃请求,设置超时时间和覆写响应数据类型等;

现如今,除非设计库或业务场景需要严苛的考虑浏览器兼容,否则我们并不需要使用 XHRHttpReuqest 对象(您会发现我在交替使用「类」,「对象」和「构造函数」的说法,这些叫法都没错!)。但我之所以选择保留并更新这篇文章的原因在于,我认为我们可以从 API 设计的角度上进行思考,并从 XHRHttpRequest 规范的制定和实现上获得灵感。

在下一篇文章中,我将向各位介绍浏览器提供的另一个发送 AJAX 请求的接口:Fetch API,通过与 XHRHttpRequest 的实现相对比,相信我们会对 HTTP 请求 API 设计有更深刻的理解。

再次感谢您阅读到这里,这的确很不容易,希望您花费的时间没有白费。


李世界
本专栏将以通俗易懂的方式,体系化地介绍 Web 开发中的各种实用技术和概念。

一位全栈 Web 应用开发者 & 交互设计师,曾服务于国内一线大厂,主导设计开发过多款效能类 Sass 产品。...

1.6k 声望
2.4k 粉丝
0 条评论
推荐阅读
再也不学AJAX了!(二)使用AJAX ② Fetch API
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第三篇,最近更新于 2023 年 1...

libinfs2阅读 501

封面图
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
过去 5 年,我前后在菜鸟网络和蚂蚁金服做开发工作,一方面支撑业务团队开发各类业务系统,另一方面在自己的技术团队做基础技术建设。期间借着 Node.js 的锋芒做了不少 Web 系统,有的至今生气蓬勃、有的早已夭折...

乌柏木140阅读 11.9k评论 10

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木60阅读 5.9k评论 16

从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木39阅读 7k评论 6

CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan41阅读 2.8k评论 14

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan34阅读 2.2k评论 2

封面图
【关于Javascript】--- 正则表达式篇
基础知识一、元字符 {代码...} 二、量词 {代码...} 三、集合 字符类 {代码...} 四、分支 {代码...} 五、边界 开始结束 {代码...} 六、修饰符 {代码...} 七、贪婪模式和非贪婪模式js默认贪婪模式即最大可能的匹配...

Jerry35阅读 2.9k

一位全栈 Web 应用开发者 & 交互设计师,曾服务于国内一线大厂,主导设计开发过多款效能类 Sass 产品。...

1.6k 声望
2.4k 粉丝
宣传栏