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
开头的很明显是一个监听事件的回调函数:
onabort
onerror
onload
onloadstart
onloadend
onprogress
onreadystatechange
ontimeout
其他的就是一些属性:
readyState
response
responseText
responseType
responseURL
responseXML
status
statusText
timeout
withCredentials
其中有个比较特别的是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 = { |
现在基本上就可以发送以及接受请求了,但是还有一些监听的钩子和一些属性没有说。
其他的监听函数
onabort
onerror
onload
onloadstart
onloadend
onprogress
ontimeout
不管三七二十一,简单地打印点东西,看看是什么东西
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
标准学习。