Ajax:Asynchronous JavaScript + XML的简写。
Ajax技术的核心是XMLHttpRequest对象(XHR),XHR为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器取得信息,再通过DOM将新数据插入到页面中。
XMLHttpRequest对象
IE7+ Firefox Opera Chrome Safari原生支持XHR对象。
var xhr = new XMLHttpRequest();
XHR的用法
在使用XHR对象时,要调用的第一个方法是open(),它接受3个参数:要发送的请求类型(get,post)、请求的URL、表示是否异步发送请求的布尔值。
这时就启动了一个请求以备发送。
xhr.open("get", "example.php", false);
只能向同一个域中使用相同端口和协议的URL发送请求,如果URL与启动请求的页面有任何差别,都会引发安全错误。
要发送请求就要调用send()方法:
xhr.send(null);
send方法接收的参数是要作为请求主体发送的数据,如果不需要数据就传null,因为有的浏览器必须要传这个数据。
如果请求是同步的,那么JS会在等到服务器响应之后再继续执行。
等到响应后,响应的数据会自动填充XHR对象的属性:
- responseText:响应主体的文本
- responseXML:这个属性中保存着包含响应数据的XML DOM文档
- status:响应的HTTP状态
- statusText:HTTP状态的说明
var xhr = new XMLHttpRequest();
xhr.open("get", "16_.html", false);
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
在发送异步请求时JS不会等待响应,此时的XHR会有readyState属性,表示请求响应过程的当前活动阶段:
- 0:未初始化,尚未调用open方法
- 1:启动,已经调用open未调用send
- 2:发送,已经调用send,未收到响应
- 3:接收,已经接收到部分响应数据
- 4:完成,已经接收到全部响应数据,且数据可用
readyState属性发生改变时会触发一次readystatechange事件,为确保浏览器兼容,需要在调用open前指定readystatechange事件处理程序。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
alert(xhr.readyState);
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", "16_.html", true);
xhr.send(null);
在接收到响应之前可以通过abort方法取消异步请求
xhr.abort();
在终止请求,应该解引用XHR对象,由于内存原因不建议重用XHR对象。
HTTP头部信息
每个HTTP请求和响应都会带有相应的头部信息,XHR对象也提供了操作这两种头部信息的方法。
默认情况下,在发送XHR请求的同时还会发送下列头部:
- Accept
- Accept-Charset
- Accept-Encoding
- Accept-Language
- Connection
- Cookie
- Host
- Referer
- User-Agent
使用setRequestHeader()方法可以设置自定义的请求头部。参数为头部字段的名称和头部字段的值。这个要在open和send之间调用才有效。
xhr.setRequestHeader("MyHeader", "MyValue");
在使用这个方法时,建议使用自定义的头部名,以免与浏览器发生冲突,有的浏览器可能不允许开发人员重写默认头部。
想要获得头部的值可以使用下面这两种方法。
var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllResponseHeaders();
GET请求
用于向服务器查询某些信息,将参数放到后面。使用addURLParam就是保证URI被正确编码,格式良好。
var xhr = new XMLHttpRequest();
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
var url = "example.php";
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
xhr.open("get", url, true);
xhr.send(null);
POST请求
用于向服务器发送应该保存的数据,POST请求应该会发送很多的数据到服务器。这时send的参数就是发送的数据了。一般是数据序列化后的字符串。
var xhr = new XMLHttpRequest();
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));
GET请求消耗的资源少,同等数据量是POST的两倍。
XMLHttpRequest 2级
FormData
Web应用中频繁使用的一项功能就是表单数据的序列化。为此,2级定义了FormData对象。
append方法可以向其添加数据,键值对形式
var data = new FormData();
data.append("name", "Nicholas");
也可以直接使用表单初始化FormData:
var data = new FormData(document.forms[0]);
xhr.send(data);
创建好的FormData的实例后直接传给XHR的send方法,这时XHR会自动帮你照顾好头部信息。
超时设定
2级中又规定了一个timeout属性,表示请求在等待响应多少毫秒之后就终止。设置之后,如果超时没有接收到响应,就回触发timeout事件,调用事件处理程序,这时xhr.readyState可能已经为4了,但是此时请求已经中止了,不能再访问xhr.status,所以如果使用timeout,onreadystatechange事件处理也要小心。
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
try {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
} catch (ex){
}
}
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // IE8+
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);
overrideMimeType()方法
重写XHR响应的MIME类型,因为响应的MIME类型决定了XHR对象如何处理它。如果服务器返回的是XML文件,MIME却是text/plain,那XHR对象就不能正确的设置responseXML。
要在send之前调用这个方法。
var xhr = createXHR(); xhr.open("get", "text.php", true); xhr.overrideMimeType("text/xml"); xhr.send(null);
进度事件
Progress Events定义了与客户端服务器通信有关的事件,这些事件最早其实只针对XHR,但目前也被其它API借鉴,有6个进度事件:
- loadstars:在接收到响应数据的第一个字节时触发
- progress:在接收响应期间不断触发
- error:在请求发生错误时触发
- abort:在因为调用abort()方法而终止连接时触发
- load:在接收到完整响应数据时触发
- loaded:在通信完成(触发error、abort、load事件后)时触发
支持前5个事件的有Firefox 3.5+、Safari 4+、Chrome、iOS 版Safari、Android 版 WebKit。
Opera 11+、IE8+只支持load事件。
load事件
只要浏览器接收到服务器的响应,不管其状态如何都会触发load事件,这就意味着必须检查XHR对象的status属性。对于这个事件有的浏览器实现了event对象,其target指向XHR对象,直接就可以使用所有对象和方法,可是有的浏览器并没有实现。这就是说还得使用原来的全局XHR变量对象。
var xhr = new XMLHttpRequest();
xhr.onload = function(){
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);
progress事件
这个事件会在浏览器接收数据的过程中持续的触发,其target属性为XHR对象再加额外3个属性:
- lengthComputable:表示进度信息是否可用
- position:表示已经接收到的字节数
- totalSize:根据Content-Length响应头部确定的预期字节数
这个可以用来作为进度指示器
var xhr = new XMLHttpRequest();
xhr.onprogress = function(event){
var divStatus = document.getElementById("highDiv");
if (event.lengthComputable) {
divStatus.innerHTML = "Received " + event.position + " of " +
event.totalSize + " bytes";
}
};
xhr.open("get", "img/paypal2.png", true);
xhr.send(null);
跨源资源共享
通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为,但实现合理的跨域请求也是一个很重要的需求。
CORS(Cross-Orign Resource Sharing)就是为了这样的需求而生的。
其背后的思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应应该成功还是失败。
比如一个简单的使用GET或POST发送的请求,添加一个自定义头部Origin来包含请求页面的源信息(协议,域名和端口),以便服务器根据这个头部信息来决定是否响应。如果服务器认为这个请求可以接受,那么就在Access-Control-Allow-Origin头部中发回相同的源或*,浏览器会检查这个头部,如果不符合则驳回请求。
这里的跨域请求和响应都不会包含cookie。
IE对CORS的实现
IE8中引入了XDR(XDomainRequest)类型,它与XHR相似,是用来实现安全可靠的跨域通信的。在安全机制方面,XDR实现了部分CORS规范:
- cookie不会随请求发送,也不会随响应返回
- 只能设置请求头部中的Content-Type字段
- 不能访问响应头部
- 只支持GET和POST
这些变化使得CSRF跨站点请求伪造和跨站点脚本的问题得到缓解,被请求的资源可以根据它认为合适的请求来决定是否设置Access-Control- Allow-Origin。作为请求的一部分Origin会自动设置为当前域。XDR只能发送异步请求。请求返回后会触发load事件。
var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.onerror = function(){
alert("An error occurred.");
};
xdr.open("post", "http://www.somewhere-else.com/page/");
xdr.contentType = "application/x-www-form-urlencoded";
xdr.send("name1=value1&name2=value2");
因为无法读取响应头部,也就无法获取状态码。只要响应有效就回触发load事件,失败就触发error事件。
abort方法可以终止请求
XDR也有timeout属性和ontimeout事件处理程序。
其它浏览器对CORS的实现
其它大多数浏览器并没有使用新类型的对象来实现CORS,而是直接使用XHR原生支持。
如果要访问其他域下的资源在XHR的open方法中传入绝对URL即可。
这里不仅可以访问到status和statusText属性,还可以发起同步请求。
不过限制还是有的:
- 不能发送接受cookie
- 不能使用setRequestHeader()设置自定义头部
- 调用getAllResponseHeaders()总会返回空字符串
var xhr = new XMLHttpRequest();
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", "http://www.somewhere-else.com/page/", true);
xhr.send(null);
Preflighted Reqeusts
CORS使用Preflighted Reqeusts的透明服务器验证机制支持开发人员使用自定义头部和GET或POST之外的方法以及不同类型的主体内容。
在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求。这种请求使用OPTIONS方法
- Origin:与简单请求相同
- Access-Control-Request-Method:请求自身使用的方法
- Access-Control-Request-Headers:自定义的头部
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求并发送响应头部与浏览器沟通:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Max-Age:这个Preflighted请求应该被缓存多久
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
IE10及之前的版本不支持
带凭据的请求
默认情况下,跨源请求不带凭据(cookie,HTTP认证,SSL证明等)通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会响应:Access-Control-Allow-Credentials: true。如果发送的是带凭据的请求,而服务器中的响应没有包含这个头部,那么JS获取不到这个响应中的信息。
跨浏览器的CORS
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.baidu.com/");
if (request){
request.onload = function(){
alert(request.responseText);
};
request.send();
}
其它跨域技术
在CORS出现之前,开发人员通过各种hack方式来实现跨域Ajax。
图像Ping
这个利用的是一个网页可以从任何地方加载图像,而不用担心跨域的问题。通过向一个URL请求图片的方式,可以向服务器单向使用GET传送参数,由于img元素的本身限制,并不能获取服务器发来的除图像以外的数据(响应文本)。只能通过监听load和error事件知道响应何时收到。
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";
响应从图片对象的src被设置时就开始了。
这个最大的用处就是跟踪用户点击或广告曝光次数,网页被打开了就使用img向纪录服务器发送个规定的参数,简单可靠。
不过只能使用GET方法和无法获得响应文本是其局限。
JSONP
JSON with padding参数式JSON
JSONP利用动态script元素从其他域中加载资源,将服务器传回的数据以JSON格式传入一个回调函数中,形式就像这样:
callback({ "name": "Nicholas" });
在实际使用时先创建一个用于处理数据的回调函数,创建一个script对象,将回调函数的函数名作为其src的URL的一个参数传递给服务器端,服务器端根据这个URL中的参数们取得响应的数据,并作为函数参数塞到回调函数中,返回一个像上面形式的对回调函数的调用。这是一个可执行的JS,被放在了一个动态创建的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);
这个就可以实现浏览器和服务器间的双向通信了,不过有两点问题:
- 必须确保你请求的Web服务是可靠的,否则就是你亲手给恶意代码提供了执行的机会
- 对于确定何时接到请求或请求何时失败并不好判断,script元素在HTML5规范中是有error事件的,不过没人实现,现在一个比较笨的解决办法是使用计时器手动检测请求超时
Comet
Comet是一种服务器像浏览器推送数据的技术。可以让信息近乎实时的被推送到页面上。有两种实现Comet的方式,长轮询和流。
长轮询
页面发起一个到服务器的请求,服务器连接一直打开,知道有数据可发送,浏览器接收完数据后关闭该连接,发起一个新的请求。
流
HTTP流的实现方式则不同,在页面的整个生命周期内只会有一个HTTP连接。浏览器向服务器发送一个请求,服务器保持连接打开,周期性的向浏览器发送数据。一般来说,都是把新的数据加到之前输出缓存的末尾再重新都发一遍。
通过监听readystatechange事件,当readyState为3时就代表数据都接收到了。随着不断从服务器端接收到数据,readyState会周期性的变为3。这时就可以读取responseText中的数据,并与上次接收到的做比对,获取新的数据。
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){
// 数 数
result = xhr.responseText.substring(received);
received += result.length;
// progress 数
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!"+ data);
});
Comet连接比较容易出错,不过大家都看好这项技术,为了简化这项技术的使用成本,提高可靠性,又有两个新的接口出现了。服务器发送事件(Server-Sent Events,SSE)和Web Socket。
服务器发送事件
Server-Sent Events,SSE,这是围绕只读Comet推出的API,用于创建到服务器的单向连接。服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是text/event-stream,并且是浏览器中的JS能解析的格式输出。SSE支持短轮询,长轮询,HTTP流。并且在断开连接时自动确定何时重连。
支持SSE的浏览器有:Firefox 6+ Safari 5+ Opera 11+ Chrome iOS 4+版 Safari。
SSE API
首先新建一个EventSource对象,参数为入口点URL,这个URL要与创建对象的页面同源(相同URL模式,域,及端口)。EventSource对象的readyState属性:
- 0:正在连接服务器
- 1:打开了连接
- 2:关闭了连接。
三个事件:
- open建立连接时
- message收到新服务器事件时
- error连接错误时
在连接断开时会自己重新连接。这就非常适合长轮询,HTTP流。
想要手动断开,有close方法。
var source = new EventSource("myevents.php");
source.onmessage = function(event){
var data = event.data;
};
source.close();
事件流
服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-stream,格式为纯文本。最简单的情况是每个数据项都有前缀data:
data: foo
data: bar
data: foo
data: bar
对应的message事件有3个,event.data的值分别为foo,bar,foo/bar。在服务器端要注意,只有data:后面有空行时才回触发message事件。
data: foo
id: 1
id: 2
data: foo
data: bar
id可以将事件与一个ID绑定,id的位置可以在data的前后。如果连接断开,会向服务器发送Last-Event-ID头部,以便服务器知道下次该发什么。
Web Socket
其目标是在一个单独的持久连接上提供一个全双工的双向通信。在Web Socket建立以后,会有一个HTTP请求发送到服务器,在取得响应后,连接会开始使用Web Socket协议。其URL模式为ws:// , wss://。
使用Web Socket意味着更小的开销,但是同时这个协议的问题还在不断的被发现,现在支持这个协议的有Firefox 6+ Safari 5+ Chrome iOS 4+版 Safari。
Web Sockets API
Web Socket的构造函数只接受绝对URL。并不要求同源,因为服务器可以判断并决定连接可以获取什么样的资源。
实例化后,连接马上会创建。readyState:
- WebSocket.OPENING (0)
- WebSocket.OPEN (1)
- WebSocket.CLOSING (2)
- WebSocket.CLOSE (3)
关闭时使用close()
发送和接收
发送只能是纯文本数据,所以复杂的数据结构要序列化。
var socket = new WebSocket("ws://www.example.com/server.php");
var message = {
time: new Date(),
text: "Hello world!",
clientId: "asdfp8734rew"
};
socket.send(JSON.stringify(message));
接受时会触发message事件
socket.onmessage = function(event){
var data = event.data;
};
其他事件
open,error,close
安全
跨站请求伪造是Ajax遇到的最大问题,如果你可以通过Ajax的URL获取,直接输URL也可以。
所以我们要确认请求发送者是否有访问资源的权限。有下列方式可以选择:
- SSL保护
- 每次请求附带算法计算的验证码
下面的方法没用,很容易伪造:
- 检查URL是否可信
- 基于cookie验证
XHR对象本身提供了一些安全机制,open 方法其实可以传用户名和密码。可是在JS中保存这些,用调试器一下就看到明文了。