js高程读书笔记 第21章 Ajax与Comet

[TOC]

本章内容

  • 使用XMLHttpRequest对象
  • 使用XMLHttpRequest事件
  • 跨域Ajax通信的限制

XMLHttpReques对象

支持IE7以前的版本,创建XMLHttpRequest对象

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
33
34
35
36
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;

for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
//skip
}
}
}

return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}

var xhr = createXHR();
xhr.open("get", "example.txt", false);
xhr.send(null);

if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.statusText);
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}

至此,可以用以下代码创建XHR对象了

1
var xhr = createXHR();

XHR的用法

使用XHR对象时,要调用的第一个方法是open(),它接受3个参数:要发送的请求的类型、请求的URL和表示是否异步发送请求的布尔值。

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

  • URL相对于执行代码的当前页面;
  • 调用open()方法并不会真正发送请求

要发送特定的请求,必须调用send()方法。

请求是同步的,JavaScript代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充xhr对象的属性:

  • responseText: 作为响应主体被返回的文本。
  • responseXML: 如果响应的内容类型是”text/xml”或”application/xml”,这个属性中将保存包含着响应数据的XML DOM文档。
  • status: 响应的HTTP状态。
  • statusText: HTTP状态的说明。

状态码为304表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本。

所以应该向下列代码检查状态码:

1
2
3
4
5
6
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.statusText);
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}

如果要发送异步请求,可以检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段:

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

readyState属性的值由一个值变成另一个值,都触发一次readystatechange事件。

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = createXHR();        
xhr.onreadystatechange = function(event){
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.txt", true);
xhr.send(null);

这个例子在onreadystatechange事件处理程序中使用了xhr对象,没有使用this对象,原因是onreadystatechange事件处理程序的作用域问题。如果使用this对象,在有的浏览器中会导致函数执行失败,或者导致错误发生。因此使用实际的XHR对象实例变量是较为可靠的一种方式。

HTTP头部信息

默认情况下,在发送XHR请求的同时,还会发送下列头部信息。

  • Accept: 浏览器能够处理的内容类型
  • Accept-Charset:浏览器能够显示的字符集。
  • Accept-Encoding:浏览器能够处理的压缩编码。
  • Accept-Language:浏览器当前设置的语言
  • Connection:浏览器与服务器之间连接的类型
  • Host:发出请求的页面所在的域。
  • Referer:发出请求的页面URI(正确拼法是Referrer)
  • User-Agent:浏览器的用户代理字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
var xhr = createXHR();        
xhr.onreadystatechange = function(){
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.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);

使用setRequestHeader()方法可以设置自定义的请求头部信息。

调用xhr对象的getResponseHeader()方法并传入头部字段名称,可以取得相应的相应头部信息。而调用getAllResponseHeaders()方法可以取得一个包含所有头部信息的长字符串。

1
2
var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllRespnseHeaders();

GET请求

1
xhr.open("get", "example.php?name1=value1&name2=value2", true);

这样经常会发生一个错误,就是查询字符串的格式有问题。

查询字符串中每个参数的名值对都必须使用encodeURIComponent()进行编码,然后才能放到URL末尾。

下面这个函数可以辅助向现有URL的末尾添加查询字符串参数。

1
2
3
4
5
function addURLParam(url, name, value){
url += (url.indexOf("?")) == -1? "?" : "&";
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}

POST请求

发送post请求,要讲content-type头部信息设置application/x-www-form-urlencoded,也就是表单提交时内容类型,其次是以适当创建一个字符串。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
function serialize(form){        
var parts = new Array();
var field = null;

for (var i=0, len=form.elements.length; i < len; i++){
field = form.elements[i];

switch(field.type){
case "select-one":
case "select-multiple":
for (var j=0, optLen = field.options.length; j < optLen; j++){
var option = field.options[j];
if (option.selected){
var optValue = "";
if (option.hasAttribute){
optValue = (option.hasAttribute("value") ?
option.value : option.text);
} else {
optValue = (option.attributes["value"].specified ?
option.value : option.text);
}
parts.push(encodeURIComponent(field.name) + "=" +
encodeURIComponent(optValue));
}
}
break;

case undefined: //fieldset
case "file": //file input
case "submit": //submit button
case "reset": //reset button
case "button": //custom button
break;

case "radio": //radio button
case "checkbox": //checkbox
if (!field.checked){
break;
}
/* falls through */

default:
parts.push(encodeURIComponent(field.name) + "=" +
encodeURIComponent(field.value));
}
}
return parts.join("&");
}

function submitData(){
var xhr = createXHR();
xhr.onreadystatechange = function(event){
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("post", "postexample.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var form = document.getElementById("user-info");
xhr.send(serialize(form));
}

文件postexample.php可以通过_$POST获取数据

1
2
3
4
5
6
7
<?php
header("Content-Type: text/plain");
echo <<<EOF
Name: {$_POST['user-name']}
Email: {$_POST['user-email']}
EOF;
?>

text/html与text/plain区别如下:

  1. text/html是html格式的正文,text/html的意思是将文件的content-type设置为text/html的形式
  2. text/plain是无格式正文,text/plain的意思是将文件设置为纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理。
  3. text/xml忽略xml头所指定编码格式而默认采用us-ascii编码
  4. application/xml会根据xml头指定的编码格式来编码

如果不设置Content-Type头部信息,那么发送给服务器的数据就不会出现在$_POST超级全局变量中。

XMLHttpRequest 2级

FromData

FromData为序列化表单以及创建与表单格式相同的数据提供便利。

下面代码创建了一个FromData对象,并向其中添加了一些数据。

1
2
var data = new FromData();
data.append("name", "Nicholas");

可以用FromData代替序列化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function submitData(){
var xhr = createXHR();
xhr.onreadystatechange = function(event){
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("post", "postexample.php", true);
var form = document.getElementById("user-info");
xhr.send(new FormData(form));
}

同时可以不必明确地在XHR对象上设置请求头部。

超时设定

IE8为XHR对象添加了一个timeout属性,表示请求在等待响应多少毫秒之后就终止。如果在规定的时间内浏览器还没有接收到响应,那么就会触发timeout事件,进而会调用ontimeout事件处理程序。这项功能已经被收入到XMLHttpRequest2规范中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var xhr = createXHR();        
xhr.onreadystatechange = function(event){
try {
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
} catch (ex){
//assume handled by ontimeout
}
};

xhr.open("get", "timeout.php", true);
xhr.timeout = 1000;
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);

overrideMimeType()方法

用于重写XHR响应的MIME类型,比如服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,即使数据是XML,responseXML属性中还是null。通过这个overrideMimeType()方法,可以保证把响应当做XML而非纯文本来处理。

1
2
3
4
var xhr = createXHR();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);

进度事件 Progress Events

  • loadstrart: 在接收到响应数据的第一个字节时触发。
  • progress: 在接收响应期间持续不断地触发。
  • error: 在请求发生错误时触发
  • abort: 在因为调用abort()方法而终止连接时触发。
  • load: 在接收到完整的响应数据时触发。
  • loadend: 在通信完成或者触发error、abort或load事件后触发。

load事件

用于替代readystatechange事件。onload事件处理程序会接收到一个event对象,其target属性就指向XHR对象实例,因而可以访问到XHR对象的所有方法和属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
window.onload = function(){
var xhr = createXHR();
xhr.onload = function(event){
if ((xhr.status >= 200 && xhr.status < 300) ||
xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.open("get", "altevents.php", true);

xhr.send(null);
};

并非所有的浏览器都为这个事件实现了适当的事件对象,所以还是采用XHR对象变量。

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
18
19
20
window.onload = function(){
var xhr = createXHR();
xhr.onload = function(event){
if ((xhr.status >= 200 && xhr.status < 300) ||
xhr.status == 304){
console.log(xhr.responseText);
} else {
console.log("Request was unsuccessful: " + xhr.status);
}
};
xhr.onprogress = function(event){
var divStatus = document.getElementById("status");
if (event.lengthComputable){
divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize + " bytes";
}
};
xhr.open("get", "altevents.php", true);

xhr.send(null);
};

跨资源共享 CORS

在发送请求时,需要给它附加一个额外的Origin头部,其中包含请求页面的源信息,以便服务器根据这个头部信息来决定是否给予响应。

如果服务器认为这个请求可以接受,就在Access-Control_Allow-Origin头部中发相同的源信息。

IE对CORS的实现

  • cookie不会随请求发送,也不会随响应返回
  • 只能设置请求头部信息中的Content-Type字段
  • 不能访问响应头部信息
  • 只支持GET和POST请求

所有的XDR(XDomainRequest)类型都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发load事件,响应的数据也会保存在responseText属性中。

1
2
3
4
5
6
7
8
9
10
11
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.onerror = function(){
alert("Error!");
};

//you'll need to replace this URL with something that works
xdr.open("get", "http://www.somewhere-else.com/xdr.php");
xdr.send(null);

其他浏览器对CORS的实现

只需要在open()方法中传入绝对URL即可。

限制:

  • 不能使用setRequestHeader()设置自定义头部
  • 不能发送和接受cookie
  • 调用getAllResponseHeaders()方法总会返回空字符串

Preflighted Reqeusts

CORS通过一种叫做PreFlighted Requests的透明服务器验证机制支持使用自定义头部、get和post之外的方法以及不同类型的主体内容。

带凭据的请求

默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。

服务器如果接受带凭据的请求,会响应

Access-Control-Allow-Credentials: true

如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给JavaScript。

跨浏览器的CORS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}

var request = createCORSRequest("get", "http://www.somewhere-else.com/xdr.php");
if (request){
request.onload = function(){
//do something with request.responseText
};
request.send();
}

其他跨域技术

图像ping

1
2
3
4
5
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";

图像ping最常用于跟踪用户点击页面或动态广告曝光次数。图像ping有两个主要的缺点:

  • 只能发送get请求
  • 无法访问服务器的响应文本。

因此,图像ping只能用于浏览器与服务器的单向通信。

JSONP (JSON with padding)

包含在函数调用中的JSON

像这样
callback({ "name": "Nicholas";});

JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名称一般是在请求中指定的。而数据就是传入回调函数中的JSON数据。

1
2
3
4
5
6
7
function handleResponse(response){
alert("You're at IP address " + response.ip + ", which is in " + response.city + ", " + response.region_name);
}

var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
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
function createStreamingClient(url, progress, finished){        

var xhr = new XMLHttpRequest(),
received = 0;

xhr.open("get", url, true);
xhr.onreadystatechange = function(){
var result;

if (xhr.readyState == 3){

//get only the new data and adjust counter
result = xhr.responseText.substring(received);
received += result.length;

//call the progress callback
progress(result);

} else if (xhr.readyState == 4){
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}

var client = createStreamingClient("streaming.php", function(data){
alert("Received: " + data);
}, function(data){
alert("Done!");
});

通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。在上述这些浏览器中,随着不断从服务器接收数据,readyState的值会周期性地变为3。当readyState值变为3时,responseText属性中就会保存接收到的所有数据。

createSteamingClient()函数接收三个参数:要连接的URL,在接收到数据时调用的函数以及关闭连接时调用的函数。
只要readystatechange事件发生,而且readyState值为3,就对responseText进行分割以取得最新数据。这里的received变量用于记录已经处理了多少个字符,每次readyState值为3时都递增。然后,通过progress回调函数来处理传入的新数据。而当readyState值为4时,则执行finished回调函数,传入响应返回的全部内容。

服务器发送事件

SSE(Server-Sent Events) SSE API用于创建到服务器的单向链接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是text/event-stream,而且是浏览器中javascript API能解析格式输出。

  1. SSE API
1
var source = new EventSource("myevents.php");

实例有个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2表示关闭了连接。

事件:

  • open: 建立连接时触发。
  • message: 从服务器接收到新事件时触发
  • error: 无法建立连接时触发。
1
2
3
4
source.onmessage = function(event) {
var data = event.data;
//处理数据
}

服务器返回的数据保存在event.data中。

默认情况下,EventSource对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味着SSE适合长轮询和HTTP流。

关闭连接使用source.close();

  1. 事件流

服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:

data:foo

data: bar

data: foo
data: bar

第一第二个事件流返回的event.data值分别为foo bar。
第三个返回foo/bar。:后面需要添加空格。

通过id: 前缀可以给特定的事件指定一个关联的ID。这个ID位于前后都可以

data: foo
id: 1

Web Sockets

全双工,双向通信。

URL模式由http://变为ws://,https://变为wss://

  1. Web Socket API

实例一个WebSocket对象并传入要连接的URL:

1
var socket = new WebSocket("ws://www.example.com/server.php");

readyState属性:

  • 0 正在建立连接
  • 1 已建立连接
  • 2 正在关闭连接
  • 3 已经关闭连接

要关闭连接,可以在任何时候调用close方法

  1. 发送和接受数据

使用send()方法并传入任意字符。

1
2
var socket = new WebSocket("ws://www.example.com/server.php");
socket.send("Hello World!");

Web Sockets 只能发送纯文本数据,所以对于复杂的结构需要进行序列化。

1
2
3
4
5
6
7
var message = {
time: new Date();
text: "Hello World";
clientId: "asdfp";
}

socket.send(JSON.stringify(message));

  1. 其他事件
  • open: 成功建立连接时触发。
  • error: 在发生错误时触发,连接不能持续。
  • close: 在连接关闭时触发。

这三个事件中,只有close事件的event对象有额外的信息。
wasClean、code和reason。
分别表示:布尔值(是否明确关闭连接)、服务器返回的数值状态码和服务器发回的消息。

安全

CSRF(Cross-Site Request Forgery)

  • 要求以SSL连接来访问可以通过XHR请求的资源
  • 要求每一次请求都要附带经过相应算法计算得到的验证码。

小结

Ajax是无需刷新页面就能够从服务器取得数据的一种方法。

  • 负责Ajax运作的核心对象是XMLHttpRequest(XHR)对象
  • XHR对象由微软最早在IE5中引入,用于通过JavaScript从服务器取得XML数据