Ajax与Comet

服务器有新消息主动推送给客户端浏览器

下述内存主要讲述了《JavaScript高级程序设计(第3版)》第21章关于“Ajax与Comet”。

Ajax(Asynchronous JavaScript + XML的简写)可以向服务器请求数据而无需卸载(刷新)页面,带来更好的用户体验。
Ajax技术的核心是XMLHttpRequest对象(简称XHR)。

一、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
/* 兼容IE早期版本 */
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){ // 适用于IE7之前的版本
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 { // XHR对象和ActiveX对象都不存在,则抛出错误
throw new Error("No XHR object available.");
}
}

1. XHR的用法

1
xhr.open("请求的类型get|post等", "请求的URL", "是否异步发送请求");

说明:
(1)URL相对于执行代码的当前页面(当然也可以使用绝对路径)
(2)open()方法并不会真正发送请求,而只是启动一个请求以备发送

1
xhr.send("请求主体发送的数据");

说明:
(1)如果不需要通过请求主体发送数据(比如get请求),则必须传入null,因为这个参数对有些浏览器来说是必需的
(2)调用send()之后,请求就会被分派到服务器

补充:xhr.open()方法为“false”,即同步请求,JavaScript代码会等到服务器响应后再继续执行;否则,继续执行后续代码。

在收到服务器响应后,相应的数据会自动填充XHR对象的属性。

  • responseText:作为响应主体被返回的文本
  • responseXML:如果响应的内容类型是”text/xml”或”application/xml”,这个属性中将保存包含着响应数据的XML DOM文档
  • status:响应的HTTP状态
  • statusText:HTTP状态的说明
    1
    2
    3
    4
    // 为确保接收到适当的响应 200:成功;304:资源未被修改
    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
    console.log(xhr.responseText);
    }

说明
(1)有的浏览器会错误的报告成功状态码为204
(2)无论内容类型是什么,响应主体的内容都会保存到responseText属性中;而对于XML数据而言,responseXML同时也将被赋值,否则其值为null

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

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

readyState属性的值发生变化,都会触发readystatechange事件。可以利用这个事件来检测每次状态变化后readyState的值。不过,必须在调用open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性。

var xhr = createXHR();        
xhr.onreadystatechange = function(event){
    // 不要使用this,作用域会产生问题,在部分浏览器中会执行失败
    if (xhr.readyState == 4){
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
            console.log(xhr.responseText);
        } else {
            console.log("Request was unsuccessful: " + xhr.status);
        }
    }
};
xhr.open("get", "example.txt", true);
xhr.send(null);

在接收到响应数据之前可以调用abort()方法来取消异步请求:

xhr.abort();
xhr =null; // 解除引用,释放内存

2. HTTP头部信息

setRequestHeader():设置自定义的请求头信息。必须在调用open()方法之后且调用send()方法之前调用。
getResponseHeader() getAllResponseHeaders():可以获取指定(全部)响应头信息。

var xhr = createXHR();        
xhr.onreadystatechange = function(){};
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);

3. GET请求

open()方法的URL尾部的查询字符串必须经过正确的编码

functionaddURLParam(url, name, value) {
    url += (url.indexOf("?") == -1 ? "?" : "&");
    url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
    return url;
}

var url = "http://test.com";
url = addURLParam(url, "uid" , 5);
url = addURLParam(url, "siteid", 123);  // "http://test.com?uid=5&siteid=123"
xhr.open("get", url, true);
xhr.send(null);

4. POST请求

POST请求将数据作为请求的主体

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
/* 序列化表单 */
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("&");
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 发送请求 */
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));
}

二、XMLHttpRequest 2级

XMLHttpRequest 1级只是把已有的XHR对象的实现细节描述了出来。而XMLHttpRequest 2级则进一步发展了XHR。并非所有浏览器都完整地实现了XMLHttpRequest 2级规范,但所有浏览器都实现了它规定的部分内容。

1. FormData

1
2
3
4
5
6
7
8
9
10
11
12
// 创建FormData对象
var data=new FormData();
data.append("name", "ligang");



// 用表单元素填充
xhr.open("post", "postexample.php", true);
var form = document.getElementById("user-info");
// 使用FormData的方便之处在于不必明确地在XHR对象上设置请求头。
// xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(new FormData(form));

2. 超时设定

IE8为XHR对象添加了一个timeout属性,表示请求在等待响应多少毫秒后就终止。

1
2
3
4
5
6
xhr.open("get", "timeout.php", true);
xhr.timeout = 60 * 1000;
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);

对于其他浏览器的兼容做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xhr.open("get", "timeout.php", true);
xhr.onreadystatechange = function(event){if (xhr.readyState == 4){
// 清除定时器
clearTimeout(timeout);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
console.log(xhr.responseText);
} else {
console.log("Request was unsuccessful: " + xhr.status);
}
}
};
// 设置超时时间 1分钟
var timeout = setTimeout(function() {
xmlHttpRequest.abort();
xmlHttpRequest = null;
}, 60 * 1000);
xmlHttpRequest.send(null);

3. overrideMimeType()方法

重写XHR响应的MIME类型,必须在send()方法之前

如果,服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,responseXML属性中仍然是null。此时,通过overrideMimeType()方法,可以保证把响应当作XML而非纯文本来处理(即,responseXML中被赋值)。

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

三、进度事件

6个进度事件:

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

1. load事件

可以代替readystatechagne事件。其处理程序会接收到一个event对象,其target属性指向XHR对象实例,因而可以访问到XHR对象的所有方法和属性。然而,并非所有浏览器都实现了事件对象。

2. progress事件

其处理程序会接收一个event对象,其target属性指向XHR对象实例,但包含着三个额外的属性

  • lengthComputable:是一个表示进度信息是否可用的布尔值
  • position:表示已经接收的字节数
  • totalSize:根据content-length响应头确定的预期字节数
    注意其必须在调用open()方法之前添加
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var xhr = createXHR();        
    xhr.onload = function(event){
    // event.target存在兼容性问题,所以只能使用xhr
    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(Cross-Origin Resource Sharing)背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

在发送请求时,给其附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。

Origin: http://www.test.com

如果服务认为这个请求可以接受,在Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以回发”*”)。

Access-Control-Allow-Origin: http://www.test.com

注意:请求和响应都不包含cookie信息。

1. IE中实现CORS:XDR(XDomainRequest),所有的XDR请求都是异步的,不能创建同步请求。其使用方法类似于XHR。

2. 其他浏览器对CORS的实现:通过XMLHttpRequest对象实现对CORS的原生支持。只需给open()方法传入绝对地址。支持同步请求。

跨域XHR对象的安全限制:

(1)不能使用setRequestHeader()设置自定义头部。

(2)不能发送和接收cookie。

(3)调用getAllResponseHeaders()方法总会返回空字符串。

建议:访问本地资源,最好使用相对URL;访问远程资源,使用绝对URL。

3. 跨浏览器的CORS

functioncreateCORSRequest(method, url){var xhr = new XMLHttpRequest();
    if ("withCredentials"in xhr){    // 检测XHR是否支持CORS的简单方式,就是检测是否存在withCredentials属性
        xhr.open(method, url, true);
    } elseif (typeof XDomainRequest != "undefined"){    // IE XDR
        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();
}

五、其他跨域技术

利用DOM中能够执行跨域请求的功能,在不依赖XHR对象的情况下也能发送某种请求,其不需要修改服务器端代码。

1. 图像Ping

<img>标签,可以从任何网页中加载图像,无需关注是否跨域。这也是广告跟踪浏览量的主要方式。

图像Ping是与服务器进行简单、单向的跨域通信的一种方式。浏览器得不到任何具体的数据。但通过监听load和error事件,可以知道响应是什么时间接收到的。

var img = new Image();
img.onload = img.error = function() {
    console.log("Done!");
};
img.src = "http://www.test.com/getImage?id=1";

缺点:

(1)只能发送Get请求

(2)无法访问服务器的响应文本

2. JSONP(JSON with padding)

两部分组成:回调函数和数据。

回调函数是当响应到来时应该在页面调用的函数。回到函数的名字一般是在请求中指定的。而数据是传入回调函数中的JSON数据。

JSONP是通过动态<script>元素来使用的

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)JSONP是从其他域中加载代码执行,其安全性无法确保。

(2)不能很容易的确定JSONP请求是否失败。

3. Comet

更高级的Ajax技术,服务器向页面推送数据。

Comet 本质上和 C/S 中的通信并不一样,它是通过长连接来模拟推送的。也就是说,在没有数据的时候,这个连接挂起,直到有数据来了(推送),服务器端返回响应,该连接结束,客户端的 JS 重新建立下一个等待连接。

这种方式并不像 C/S 通信建立长期使用的通道,只是长期“等待”而已,避免了在数据更新频率不大的情况下轮询的开销:试想如果五分钟才一次更新,那么轮询方式在此期间几秒钟就要发生一次请求&响应,而这些请求响应都是没有价值的,因为它们并没有传输有用数据。Comet 避免的是这方面的浪费,不再有空请求,因为挂起的连接直到数据更新了才结束。

性能方面,对于数据量不大但需要实时更新的应用来说,Comet 能更有效利用连接,同时因为没有轮询的心跳频率,Comet 会比轮询更加实时——因为只消耗响应的网络传输时间。

但是 Comet 本身并不能在数据传输方面提供比轮询更高的效率,仅仅避免了轮询的空请求浪费。所以 Comet 和 web socket 之类的通讯方式差距还是有的。

大型网站的应用方面,知乎、QQ 邮箱都有用到 Comet,还有新浪微博的私信(聊天)。使用 Comet 主要需要是服务器端的支持,因为使用长连接,所以要有一定负载量一般得使用异步的网络框架,Python 的 tornado、gevent 和 JavaScript 的 node.js 都是此列。

两种实现Comet的方式:长轮询和流。
Ajax与Comet-Comet长轮询

(1)长轮询:页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。【区别:短轮询,服务器立即发送响应,无论是否有效,而长轮询是等待发送响应。】

(2)HTTP流:生命周期内只使用一个HTTP连接。浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。

/**
 * progress:接收数据时调用的函数
 * finished:关闭连接时调用的函数
 */
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);
        } elseif (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!");});

服务器发送事件:SSE和事件流

4. Web Sockets

目标是在一个单独的持久连接上提供全双工、双向通信。

优点:能够在客户端和服务器之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。

缺点:制定协议的时间比制定JavaScript API的时间还要长。

1
2
3
4
5
6
7
8
// 必须给WebSocket构造函数传入绝对URL
var socket = new WebSocket("ws://www.example.com/server.php");
// 向服务器发送数据(只能发送纯文本,其他数据需要序列化)
socket.send("Hello");
// 接收服务器的响应数据
socket.onmessage = function(event) {
var data = event.data;
};

其他事件:

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

注意:WebSocket对象不支持DOM 2级事件侦听器,必须使用DOM 0级语法分别定义各个事件。

jQuery.comet.js

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
67
jQuery.comet = {

fetching: false,
settings: {},
url: '',
bound: {},
connect: function(url, options) {
jQuery.comet.settings = jQuery.extend({
timeout: 60000,
onError: null,
requestMethod: 'GET',
typeAttr: 'type',
dataAttr: 'data'
}, options);
jQuery.comet.url = url;
jQuery.comet.fetch();
},

fetch: function() {
if (jQuery.comet.fetching)
return;

jQuery.comet.fetching = true;
$.ajax({
type: jQuery.comet.settings.requestMethod,
url: jQuery.comet.url,

async: true,
cache: true,
timeout: jQuery.comet.settings.timeout,
ifModified: true,

success: function(data) {
jQuery.comet.fetching = false;
jQuery.comet.handle_update(data);
setTimeout(jQuery.comet.fetch, 1);
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
jQuery.comet.fetching = false;
if (textStatus == 'timeout') {
jQuery.comet.fetch()
} else {
if (jQuery.comet.settings.onError != null) {
jQuery.comet.settings.onError(XMLHttpRequest, textStatus, errorThrown);
}
setTimeout(jQuery.comet.fetch, 10000);
}

}
});
},

handle_update: function(update) {
type = null;
data = update;

if (update[jQuery.comet.settings.typeAttr]) {
type = update[jQuery.comet.settings.typeAttr];
}
if (update[jQuery.comet.settings.dataAttr]) {
data = update[jQuery.comet.settings.dataAttr];
}

jQuery(document).trigger(type + ".comet", [data, type]);
},

};