XMLHttpRequest对象的简单使用
XMLHttpRequest对象的简单使用
XMLHttpRequest
XMLHttpRequest是一个浏览器对象,通过这个对象,可以实现和服务器进行数据上的交互。
Wiki上这么定义XMLHttpRequest。
XMLHTTP是一组API函数集,可被JavaScript、JScript、VBScript以及其它web浏览器内嵌的脚本语言调用,通过HTTP在浏览器和web服务器之间收发XML或其它数据。**XMLHTTP最大的好处在于可以动态地更新网页,它无需重新从服务器读取整个网页,也不需要安装额外的插件**。该技术被许多网站使用,以实现快速响应的动态网页应用。
这也就是我们常说的通过AJAX技术来实现网页的局部更新。
Wiki上这么定义AJAX。
AJAX即“Asynchronous JavaScript and XML”(异步的JavaScript与XML技术),指的是一套综合了多项技术的浏览器端网页开发技术。
传统的
web应用允许用户端填写表单(form),当提交表单时就向网页服务器发送一个请求。服务器接收并处理传来的表单,然后送回一个新的网页,但这个做法浪费了许多带宽,因为在前后两个页面中的大部分HTML代码往往是相同的。由于每次应用的沟通都需要向服务器发送请求,应用的回应时间依赖于服务器的回应时间。这导致了用户界面的回应比本机应用慢得多。
与此不同,
AJAX应用可以仅向服务器发送并取回必须的数据,并在客户端采用JavaScript处理来自服务器的回应。因为在服务器和浏览器之间交换的数据大量减少,服务器回应更快了。同时,很多的处理工作可以在发出请求的客户端机器上完成,因此web服务器的负荷也减少了。
也就是说,我们通过XMLHttpRequest对象和服务器进行交互,格式为XML,而现在大部分使用的是JSON格式的对象,Wiki上称此为AJAJ
类似于
DHTML或LAMP,AJAX不是指一种单一的技术,而是有机地利用了一系列相关的技术。虽然其名称包含XML,但实际上数据格式可以由JSON代替,进一步减少数据量,形成所谓的AJAJ。而客户端与服务器也并不需要异步。一些基于AJAX的“派生/合成”式(derivative/composite)的技术也正在出现,如AFLAX。
Tips: 在最后我们看到了一个AFLAX这个比较陌生的名词,那么这个又是啥呢
AFLAX是'A JavaScript Library for Macromedia's Flash™ Platform'的略称。AFLAX是(AJAX-Javascript + Flash) - 基于AJAX的“派生/合成”式(derivative/composite)技术。正如略称字面的意思,AFLAX是融合 Ajax 和 Flash的开发技术。
感觉这个都没怎么听过,chrome计划在今年年末就停止对flash的支持了,现在的js能操作的东西越来越多,感觉flash也逐渐的退出了历史的舞台(个人觉得 😂)。
当我们打开一个使用flash技术的网址时,会有下面的提示

似乎扯远了,XMLHttpRequest可能我们在做项目的时候没见过(至少我做的两个都基本不需要跟他打交道,取而代之的是封装它的Axios)。
但是做项目大部分都使用过Axios这个库,这个库在浏览器端上的底层实现就是依赖了XMLHttpRequest。

那么如何原生的使用使用这个对象呢?
首先,XMLHttpRequest是一个构造器,需要先 new 出来一个对象。
1 | const xmlHttpRequest = new XMLHttpRequest(); |
可以在浏览器上看到它的全部的属性和方法。

其中前面on开头的很明显是一个监听事件的回调函数:
onabortonerroronloadonloadstartonloadendonprogressonreadystatechangeontimeout
其他的就是一些属性:
readyStateresponseresponseTextresponseTyperesponseURLresponseXMLstatusstatusTexttimeoutwithCredentials
其中有个比较特别的是upload对象,这是和上传有关的对象,现在先不管他。
伟人鲁迅曾经说过:“光说不做,那叫耍流氓”。
前置准备
so,我们要实际操作来验证这些到底是个啥东西,他们的执行顺序以及含义。
我们先搭个http服务器出来。
这里我使用的是Koa以及配套的Koa-Router(Koa的路由中间件,可以很容易地进行api的编写)。
和Koa-Static(Koa的静态文件映射中间件,这里主要映射下测试用的html文件)。
1 | |-- server |
在index.js来编写我们的这个http服务器。
1 | const Koa = require("koa"); |
使用node之后,如果启动成功,则会出现我们写在listen函数的回调。

ok,我们来写一个简单的index.html页面,放到html文件夹里面。
1 |
|
如果没有意外,就可以出现我们的初始的页面了。

ok,接下来我们写一个简单的接口,返回一个对象。
我们稍微改下项目的目录:
1 | |-- server |
编写router文件下面的index.js。
1 | const KoaRouter = require("koa-router"); |
在把根下面的index.js文件稍微更改下。
1 | const Koa = require("koa"); |
然后访问/hello,如果显示了hello world!那就证明接口可以调用了。

XMLHttpRequest 测试
开始在index.html里面写请求。
如何去发送一个请求呢,这就要使用open函数。
open
open函数有5个参数,但大部分情况下只会说到3个
url请求的目标地址;method请求的方法;async(可选)请求是否异步,默认为true// Tips:一般都不会去指定为false(同步),由于 js 为单线程的模型,线程的阻塞意味着将无法响应页面上的其他操作(比如dom事件,或者其他同步的操作,比如一个while循环);user(可选)用户名用于认证用途;password(可选)密码用于认证用途。
ok,那我们写出来:
1 | const xmlHttpRequest = new XMLHttpRequest(); |
发现没有发送请求,what?

没错,open函数只是初始化一个请求而已,此时还没有发送 http 请求。
为了发送 http 请求,需要在open之后调用send方法。
send
send方法有一个参数,该参数也就是我们希望附带在请求上的数据。
body请求的主体数据,在 MDN 上标注着可以使用的几种类型,Document(发送前被序列化)Blob,BufferSource,FormData,URLSearchParams,USVString。
我们发送的是get请求,一般不在主题上附带数据,直接指定为null即可。
1 | const xmlHttpRequest = new XMLHttpRequest(); |
刷新之后我们发现出现了发送的请求。

但是单单成功发送可不行,我们当然希望可以拿到发送回来的数据。
这时候,我们就需要监听readystatechange这个事件,给onreadystatechange写上回调。
onreadystatechange
1 | const xmlHttpRequest = new XMLHttpRequest(); |
刷新发现,怎么出现了三个输出,其中一个是空白行,两个相同的hello world!。

这是为啥呢?MDN上有解释
只要
readyState属性发生变化,就会调用相应的处理函数。这个回调函数会被用户线程所调用。XMLHttpRequest.onreadystatechange会在XMLHttpRequest的readyState属性发生改变时触发readystatechange事件的时候被调用。
那这个readyState又是什么东西呢?记得我们前面也有在XMLHttpRequest看到这个属性,MDN上给出了解释:
XMLHttpRequest.readyState属性返回一个XMLHttpRequest代理当前所处的状态。一个XHR代理总是处于下列状态中的一个。
也就是说应该调用五次这个回调函数才对,那么为什么只调用了3次呢?
我们可以把readyState打印出来看一下:

发现只出现了2 3 4,并没有 0 1,看看缺失的0的意思是:代理被创建,但尚未调用open()方法。
再看看我们的代码,我们把回调写在了open函数之后,自然就不会调用到了,我们需要把回调的注册提前。
1 | const xmlHttpRequest = new XMLHttpRequest(); |

发现还是少了0这个状态,到底是为啥呢?
原因是回调函数是在readyState改变后才进行回调的。
也就是从0变为1然后调用回调,所以回调函数中的范围只有1 - 4。
也就是 0 -> 1 -> callback(此时是1) -> 2 -> callback(此时是2) -> 3 -> callback(此时是3) -> 4 -> callback(此时是4)。
我们可以在注册回调之前打印readyState的值看看。
1 | const xmlHttpRequest = new XMLHttpRequest(); |

发现出现了0状态。
所以如果在回调内判断是否为0来执行逻辑的,那么永远都不会执行。
(PS:看了好多网上的文章,都没讲清楚,懵懵懂懂的 😂,果然还是要实践出真知)
我也在火狐上面测试了这段代码,发现和谷歌浏览器的行为一致。

实现者也相当的贴心,已经在XMLHttpRequest构造器上挂载了静态属性供我们使用。
1 | XMLHttpRequest.UNSENT; |

这样子就可以减少魔法值的使用了,好处就是代码的意思更加明朗,并且如果以后这些对应的数字更改的话,对代码完全没有影响。
清楚之后,我们就明白了只需要判断在DONE状态下就可以拿到传输完成的数据了。
1 | const xmlHttpRequest = new XMLHttpRequest(); |

很好,现在已经可以拿到数据了。
但是我一个不小心把/hello写错成/hella。

完蛋,报错,也就是是说DONE状态只是标志了传输的完成而已,并不能保证传输正确。
在这个基础上,需要其他的状态来保证,这个就是状态码status。
关于状态码,可以查看:
(虽然英文文档看着痛苦,但还是要看啊 😭)
这里我们的重点不是状态码的类别,只需要简单地判断是否为200即可。
1 | const RequestStatus = { |

现在基本上就可以发送以及接受请求了,但是还有一些监听的钩子和一些属性没有说。
其他的监听函数
onabortonerroronloadonloadstartonloadendonprogressontimeout
不管三七二十一,简单地打印点东西,看看是什么东西
1 | const RequestStatus = { |

我们发现只打印了三个,其实从名字上,我们也能大致地推断出意思
onloadstart 在数据开始传输的回调onload 在数据传输过程中的回调onloadend 数据传输结束的回调
我们试着让连接出错,看看打印了什么回调

我们发现依然有onloadstart和onloadend,但是onload变成了onerror
除了这两个,还有一个onabort,onprogress和ontimeout
ontimeout
这个看名字其实很容易识别出来,就是连接超时了,就会调用这个回调函数。
那我们就把这个条件创造出来。
我们可以指定timeout属性来指定超时的时间,这个属性的值的单位是毫秒。
所以我们指定500ms之后提示超时。
1 | xmlHttpRequest.timeout = 500; // 这个语句要放在send之前 |
然后我们在服务端设置延迟2秒才进行数据的响应。
1 | router.get("/hello", async (ctx, next) => { |
然后我们一刷新网页,就可以发现回调函数被执行了。

onabort
abort在英文中是流产和中止的意思,也就是说当我们的请求发出去之后。
但是我们突然改变想法不想发这个请求了,我们就可以调用abort方法来停止这个请求。
这是onabort注册的回调函数就会执行。
为了创造这个条件,我们需要客户端去掉timeout超时的设置。
1 | // - xmlHttpRequest.timeout = 500; // 删除 |
然后我们在通过setTimeout延迟一秒来执行abort函数。
1 | // 延迟1秒执行,放在send方法之后 |
刷新之后就可以看到出现了onabort的回调地执行。

那么就剩下最后一个回调了。
onprogress和upload
这两个东西负责东西的下载和上传,其中onprogress负责下载,也不能说是下载,就是当我收到数据的时候,会周期性地执行这个回调。
MDN上对onprogress的解释如下
progress事件会在请求接收到数据的时候被周期性触发。
(PS:前面的代码没有写入onprogress函数)
这时我们写上onprogress回调,并且删除客户端setTimeout延迟和服务器的响应数据的延迟
客户端
1 | // 记得要写在send方法之前 |
服务器端
1 | router.get("/hello", async (ctx, next) => { |
刷新之后可以发现调用了onprogress

为了验证他是周期性地执行的,那么需要发送大一点的数据。
我们选择一张图片,先放到我们服务器上,建立一个images文件夹。
1 | |-- server |

选个漂亮的小姐姐也是个技术活(误 😂)。
更改服务端代码。
1 | router.get("/hello", async (ctx, next) => { |
更改客户端代码
1 | xmlHttpRequest.onreadystatechange = function (ev) { |
刷新之后就可以看到调用了多次的onprogress。

很多时候需要去查看当前的下载数据的进度,这时候就要通过回调的ev事件对象来获取。
我们可以打印出来看看是个什么东西。
1 | xmlHttpRequest.onprogress = function (ev) { |

可以看到里面有两个属性total和loaded,分别对应了全部数据的大小和已加载数据的大小。
那么我们就可以实现一个简单的下载进度条。
1 | <div class="line line-grey"> |
1 | .line { |
1 | xmlHttpRequest.onprogress = function (ev) { |
效果图:

可能有点看的不太清,可以自己搞搞,相信你也可以看到效果。
上传upload也是照葫芦画瓢,不过回调要绑定在upload对象的属性上。
进度条我们使用上面那个就行了。
要加一个input选择文件和一个button按钮,来控制上传流程。
1 | <input type="file" /> <button>点我上传</button> |
效果图:

然后编写js代码来控制控件(因为前面的代码写地有点杂了,就重新写)。
1 | const RequestStatus = { |
然后尝试着选择一张图片和点击上传按钮,就可以看到已经得到了图片的对象了。

接下来就是完成处理上传的逻辑了。
1 | document.getElementsByTagName("button")[0].onclick = function upload() { |
因为是要上传图片,所以使用POST方法,把图片封装在一个FormData对象里面然后发送。
然后服务器端使用koa-body来处理form表单的数据,这里就不贴出代码了。
可以看一下下面的效果图:

后记
还有一些方法和属性可能没讲到,可以在XMLHttpRequest的W3C标准学习。
