XMLHttpRequest对象的简单使用

XMLHttpRequest对象的简单使用

XMLHttpRequest

XMLHttpRequest是一个浏览器对象,通过这个对象,可以实现和服务器进行数据上的交互。

Wiki上这么定义XMLHttpRequest

XMLHTTP是一组API函数集,可被JavaScriptJScriptVBScript以及其它web浏览器内嵌的脚本语言调用,通过HTTP在浏览器和web服务器之间收发XML或其它数据。**XMLHTTP最大的好处在于可以动态地更新网页,它无需重新从服务器读取整个网页,也不需要安装额外的插件**。该技术被许多网站使用,以实现快速响应的动态网页应用。

这也就是我们常说的通过AJAX技术来实现网页的局部更新。

Wiki上这么定义AJAX

AJAX即“Asynchronous JavaScript and XML”(异步的JavaScriptXML技术),指的是一套综合了多项技术的浏览器端网页开发技术。

传统的web应用允许用户端填写表单(form),当提交表单时就向网页服务器发送一个请求。服务器接收并处理传来的表单,然后送回一个新的网页,但这个做法浪费了许多带宽,因为在前后两个页面中的大部分HTML代码往往是相同的。由于每次应用的沟通都需要向服务器发送请求,应用的回应时间依赖于服务器的回应时间。这导致了用户界面的回应比本机应用慢得多。

与此不同,AJAX应用可以仅向服务器发送并取回必须的数据,并在客户端采用JavaScript处理来自服务器的回应。因为在服务器和浏览器之间交换的数据大量减少,服务器回应更快了。同时,很多的处理工作可以在发出请求的客户端机器上完成,因此web服务器的负荷也减少了。

也就是说,我们通过XMLHttpRequest对象和服务器进行交互,格式为XML,而现在大部分使用的是JSON格式的对象,Wiki上称此为AJAJ

类似于DHTMLLAMPAJAX不是指一种单一的技术,而是有机地利用了一系列相关的技术。虽然其名称包含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技术的网址时,会有下面的提示

bilibili的flash播放器

似乎扯远了,XMLHttpRequest可能我们在做项目的时候没见过(至少我做的两个都基本不需要跟他打交道,取而代之的是封装它的Axios)。

但是做项目大部分都使用过Axios这个库,这个库在浏览器端上的底层实现就是依赖了XMLHttpRequest

Axios在浏览器端底层的依赖

那么如何原生的使用使用这个对象呢?

首先,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-RouterKoa的路由中间件,可以很容易地进行api的编写)。

Koa-StaticKoa的静态文件映射中间件,这里主要映射下测试用的html文件)。

1
2
3
4
|-- server
|-- html // 这里存放Html文件
|-- index.html
|-- index.js // server的入口文件

index.js来编写我们的这个http服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Koa = require("koa");
const KoaStatic = require("koa-static");
const KoaRouter = require("koa-router");

const app = new Koa();
const router = new KoaRouter();
const path = require("path");

const home = KoaStatic(path.join(__dirname));

// 配置当前目录为静态目录,
app.use(home);

// 配置路由
app.use(router.routes()).use(router.allowedMethods());

const port = 3030;

app.listen(port, (err) => {
if (err) throw err;
console.log(`服务器已经运行,端口号为:${port}`);
});

使用node之后,如果启动成功,则会出现我们写在listen函数的回调。

ok,我们来写一个简单的index.html页面,放到html文件夹里面。

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>AJAX</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>

如果没有意外,就可以出现我们的初始的页面了。

ok,接下来我们写一个简单的接口,返回一个对象。

我们稍微改下项目的目录:

1
2
3
4
5
6
|-- server
|-- html // 这里存放Html文件
|-- index.html
|-- router // 放置接口的文件夹
|-- index.js
|-- index.js // server的入口文件

编写router文件下面的index.js

1
2
3
4
5
6
7
8
const KoaRouter = require("koa-router");
const router = new KoaRouter();

router.get("/hello", async (ctx, next) => {
ctx.body = "hello world!";
});

export default router;

在把根下面的index.js文件稍微更改下。

1
2
3
4
5
6
7
8
9
10
11
12
const Koa = require("koa");
const KoaStatic = require("koa-static");

// - const KoaRouter = require("koa-router");

const app = new Koa();

// - const router = new KoaRouter();
// +
const { router } = require("./router/index.js");

// ...

然后访问/hello,如果显示了hello world!那就证明接口可以调用了。

XMLHttpRequest 测试

开始在index.html里面写请求。

如何去发送一个请求呢,这就要使用open函数。

open

open函数有5个参数,但大部分情况下只会说到3

  • url 请求的目标地址;
  • method 请求的方法;
  • async (可选)请求是否异步,默认为true // Tips:一般都不会去指定为false(同步),由于 js 为单线程的模型,线程的阻塞意味着将无法响应页面上的其他操作(比如dom事件,或者其他同步的操作,比如一个while循环);
  • user (可选)用户名用于认证用途;
  • password(可选)密码用于认证用途。

ok,那我们写出来:

1
2
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("get", "http://localhost:3030/hello");

发现没有发送请求,what?

没错,open函数只是初始化一个请求而已,此时还没有发送 http 请求。

为了发送 http 请求,需要在open之后调用send方法。

send

send方法有一个参数,该参数也就是我们希望附带在请求上的数据。

  • body 请求的主体数据,在 MDN 上标注着可以使用的几种类型,Document(发送前被序列化)BlobBufferSourceFormDataURLSearchParamsUSVString

我们发送的是get请求,一般不在主题上附带数据,直接指定为null即可。

1
2
3
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.send(null);

刷新之后我们发现出现了发送的请求。

但是单单成功发送可不行,我们当然希望可以拿到发送回来的数据。

这时候,我们就需要监听readystatechange这个事件,给onreadystatechange写上回调。

onreadystatechange

1
2
3
4
5
6
7
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.onreadystatechange = function (ev) {
const xhr = this;
console.log(xhr.response);
};
xmlHttpRequest.send(null);

刷新发现,怎么出现了三个输出,其中一个是空白行,两个相同的hello world!

这是为啥呢?MDN上有解释

只要readyState属性发生变化,就会调用相应的处理函数。这个回调函数会被用户线程所调用。XMLHttpRequest.onreadystatechange会在XMLHttpRequestreadyState属性发生改变时触发readystatechange事件的时候被调用。

那这个readyState又是什么东西呢?记得我们前面也有在XMLHttpRequest看到这个属性,MDN上给出了解释:

XMLHttpRequest.readyState属性返回一个XMLHttpRequest代理当前所处的状态。一个XHR代理总是处于下列状态中的一个。

也就是说应该调用五次这个回调函数才对,那么为什么只调用了3次呢?

我们可以把readyState打印出来看一下:

发现只出现了2 3 4,并没有 0 1,看看缺失的0的意思是:代理被创建,但尚未调用open()方法。

再看看我们的代码,我们把回调写在了open函数之后,自然就不会调用到了,我们需要把回调的注册提前。

1
2
3
4
5
6
7
8
const xmlHttpRequest = new XMLHttpRequest();
// 把注册回调提前
xmlHttpRequest.onreadystatechange = function (ev) {
const xhr = this;
console.log(xhr.readyState);
};
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.send(null);

发现还是少了0这个状态,到底是为啥呢?

原因是回调函数是在readyState改变后才进行回调的。

也就是从0变为1然后调用回调,所以回调函数中的范围只有1 - 4

也就是 0 -> 1 -> callback(此时是1) -> 2 -> callback(此时是2) -> 3 -> callback(此时是3) -> 4 -> callback(此时是4)

我们可以在注册回调之前打印readyState的值看看。

1
2
3
4
5
6
7
8
const xmlHttpRequest = new XMLHttpRequest();
console.log(xmlHttpRequest.readyState);
xmlHttpRequest.onreadystatechange = function (ev) {
const xhr = this;
console.log(xhr.readyState);
};
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.send(null);

发现出现了0状态。

所以如果在回调内判断是否为0来执行逻辑的,那么永远都不会执行。

(PS:看了好多网上的文章,都没讲清楚,懵懵懂懂的 😂,果然还是要实践出真知)

我也在火狐上面测试了这段代码,发现和谷歌浏览器的行为一致。

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

1
2
3
4
5
XMLHttpRequest.UNSENT;
XMLHttpRequest.OPENED;
XMLHttpRequest.HEADERS_RECEIVED;
XMLHttpRequest.LOADING;
XMLHttpRequest.DONE;

这样子就可以减少魔法值的使用了,好处就是代码的意思更加明朗,并且如果以后这些对应的数字更改的话,对代码完全没有影响。

清楚之后,我们就明白了只需要判断在DONE状态下就可以拿到传输完成的数据了。

1
2
3
4
5
6
7
8
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.onreadystatechange = function (ev) {
if (this.DONE === this.readyState) {
console.log(this.response);
}
};
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.send(null);

很好,现在已经可以拿到数据了。

但是我一个不小心把/hello写错成/hella

完蛋,报错,也就是是说DONE状态只是标志了传输的完成而已,并不能保证传输正确。

在这个基础上,需要其他的状态来保证,这个就是状态码status

关于状态码,可以查看:

(虽然英文文档看着痛苦,但还是要看啊 😭)

这里我们的重点不是状态码的类别,只需要简单地判断是否为200即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const RequestStatus = {
OK: 200,
};
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.onreadystatechange = function (ev) {
if (this.DONE === this.readyState) {
if (this.status === RequestStatus.OK) {
console.log(this.response);
} else {
console.log(
`Sorry啊,出现了一点小错误,错误状态码为:${this.status},原因为:${this.response}`
);
}
}
};
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.send(null);

现在基本上就可以发送以及接受请求了,但是还有一些监听的钩子和一些属性没有说。

其他的监听函数

  • onabort
  • onerror
  • onload
  • onloadstart
  • onloadend
  • onprogress
  • ontimeout

不管三七二十一,简单地打印点东西,看看是什么东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const RequestStatus = {
OK: 200,
};
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.onreadystatechange = function (ev) {
if (this.DONE === this.readyState) {
if (this.status === RequestStatus.OK) {
console.log(this.response);
} else {
console.log(
`Sorry啊,出现了一点小错误,错误状态码为:${this.status},原因为:${this.response}`
);
}
}
};
xmlHttpRequest.onload = function (ev) {
console.log("onload");
};
xmlHttpRequest.onloadstart = function (ev) {
console.log("onloadstart");
};
// ...其他回调的绑定
xmlHttpRequest.open("get", "http://localhost:3030/hello");
xmlHttpRequest.send(null);

我们发现只打印了三个,其实从名字上,我们也能大致地推断出意思

onloadstart 在数据开始传输的回调
onload 在数据传输过程中的回调
onloadend 数据传输结束的回调

我们试着让连接出错,看看打印了什么回调

我们发现依然有onloadstartonloadend,但是onload变成了onerror

除了这两个,还有一个onabortonprogressontimeout

ontimeout

这个看名字其实很容易识别出来,就是连接超时了,就会调用这个回调函数。

那我们就把这个条件创造出来。

我们可以指定timeout属性来指定超时的时间,这个属性的值的单位是毫秒。

所以我们指定500ms之后提示超时。

1
xmlHttpRequest.timeout = 500; // 这个语句要放在send之前

然后我们在服务端设置延迟2秒才进行数据的响应。

1
2
3
4
5
6
7
8
router.get("/hello", async (ctx, next) => {
ctx.body = await new Promise((resolve) => {
setTimeout(() => {
resolve("hello world!");
}, 2000);
});
await next();
});

然后我们一刷新网页,就可以发现回调函数被执行了。

onabort

abort在英文中是流产和中止的意思,也就是说当我们的请求发出去之后。

但是我们突然改变想法不想发这个请求了,我们就可以调用abort方法来停止这个请求。

这是onabort注册的回调函数就会执行。

为了创造这个条件,我们需要客户端去掉timeout超时的设置。

1
// - xmlHttpRequest.timeout = 500;  // 删除

然后我们在通过setTimeout延迟一秒来执行abort函数。

1
2
// 延迟1秒执行,放在send方法之后
setTimeout(() => xmlHttpRequest.abort(), 1000);

刷新之后就可以看到出现了onabort的回调地执行。

那么就剩下最后一个回调了。

onprogressupload

这两个东西负责东西的下载和上传,其中onprogress负责下载,也不能说是下载,就是当我收到数据的时候,会周期性地执行这个回调。

MDN上对onprogress的解释如下

progress事件会在请求接收到数据的时候被周期性触发。

(PS:前面的代码没有写入onprogress函数)

这时我们写上onprogress回调,并且删除客户端setTimeout延迟和服务器的响应数据的延迟

客户端

1
2
3
4
// 记得要写在send方法之前
xmlHttpRequest.onprogress = function (ev) {
console.log("onprogress");
};

服务器端

1
2
3
4
router.get("/hello", async (ctx, next) => {
ctx.body = "hello world!";
await next();
});

刷新之后可以发现调用了onprogress

为了验证他是周期性地执行的,那么需要发送大一点的数据。

我们选择一张图片,先放到我们服务器上,建立一个images文件夹。

1
2
3
4
5
6
|-- server
|-- html // 这里存放Html文件
|-- index.html
|-- images
|-- 1.jpg
|-- index.js // server的入口文件

选个漂亮的小姐姐也是个技术活(误 😂)。

更改服务端代码。

1
2
3
4
5
6
7
router.get("/hello", async (ctx, next) => {
// 同步读取一张图片得到一个buffer
ctx.response.body = fs.readFileSync("./images/1.jpg");
// 要设置类型头部为图片,不然客户端是乱码
ctx.response.set("content-type", "image/png");
await next();
});

更改客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
xmlHttpRequest.onreadystatechange = function (ev) {
if (this.DONE === this.readyState) {
if (this.status === RequestStatus.OK) {
// 如果直接输出的话是乱码,不方便查看
console.log("响应成功");
} else {
console.log(
`Sorry啊,出现了一点小错误,错误状态码为:${this.status},原因为:${this.response}`
);
}
}
};

刷新之后就可以看到调用了多次的onprogress

很多时候需要去查看当前的下载数据的进度,这时候就要通过回调的ev事件对象来获取。

我们可以打印出来看看是个什么东西。

1
2
3
xmlHttpRequest.onprogress = function (ev) {
console.log(ev);
};

可以看到里面有两个属性totalloaded,分别对应了全部数据的大小和已加载数据的大小。

那么我们就可以实现一个简单的下载进度条。

1
2
3
<div class="line line-grey">
<div style="width: 0" class="line line-blue"></div>
</div>
1
2
3
4
5
6
7
8
9
10
.line {
width: 100%;
height: 5px;
}
.line-grey {
background-color: #c1c1c1;
}
.line-blue {
background-color: #4b8cff;
}
1
2
3
4
5
6
xmlHttpRequest.onprogress = function (ev) {
const loaded = ev.loaded;
const total = ev.total;
// 设置样式 取五位小数,乘以100然后加上%
el.style.setProperty("width", (loaded / total).toFixed(5) * 100 + "%");
};

效果图:

可能有点看的不太清,可以自己搞搞,相信你也可以看到效果。

上传upload也是照葫芦画瓢,不过回调要绑定在upload对象的属性上。

进度条我们使用上面那个就行了。

要加一个input选择文件和一个button按钮,来控制上传流程。

1
<input type="file" /> <button>点我上传</button>

效果图:

然后编写js代码来控制控件(因为前面的代码写地有点杂了,就重新写)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const RequestStatus = {
OK: 200,
};
const el = document.querySelector(".line.line-blue");
let file;

document.getElementsByTagName("input")[0].onchange = function selectFile() {
file = this.files[0];
};

document.getElementsByTagName("button")[0].onclick = function upload() {
if (!file) {
return;
}
console.log(file);
// 处理上传
};

然后尝试着选择一张图片和点击上传按钮,就可以看到已经得到了图片的对象了。

接下来就是完成处理上传的逻辑了。

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
document.getElementsByTagName("button")[0].onclick = function upload() {
if (!file) {
return;
}
console.log(file);
// 处理上传
const xmlHttpRequest = new XMLHttpRequest();
xmlHttpRequest.onreadystatechange = function (ev) {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === RequestStatus.OK) {
console.log("上传成功");
} else {
console.log(
`Sorry啊,出现了一点小错误,错误状态码为:${this.status},原因为:${this.response}`
);
}
}
};
// 记得这里是绑定的upload对象上的onprogress
xmlHttpRequest.upload.onprogress = function (ev) {
const loaded = ev.loaded;
const total = ev.total;
el.style.setProperty("width", (loaded / total).toFixed(5) * 100 + "%");
};
const formData = new FormData();
formData.append("file", file);
xmlHttpRequest.open("POST", "http://locahost:3030/hello");
xmlHttpRequest.send(formData);
};

因为是要上传图片,所以使用POST方法,把图片封装在一个FormData对象里面然后发送。

然后服务器端使用koa-body来处理form表单的数据,这里就不贴出代码了。

可以看一下下面的效果图:

后记

还有一些方法和属性可能没讲到,可以在XMLHttpRequestW3C标准学习。

XMLHttpRequest Level 1