ajax与comet
在XHR出现之前,Ajax的通信必须借助一些hack手段来实现,大多是使用隐藏的框架或内嵌框架。XHR为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器取得更多信息,意味着用户单击后,可以不必刷新页面也能取得新数据。虽然名字中包含XML成分,但Ajax通信与数据格式无关;这种技术就是无需刷新页面即可从服务器取得数据,但不一定是XML数据。人们也通常将这种技术成为远程脚本。
XMLHttpRequest对象
IE5是第一款引入XHR对象的浏览器。在IE5中,XHR对象是通过MSXML库中的一个ActiveX对象实现的。因此在IE中可能会遇到三种不同版本的XHR对象,即MSXML2.XMLHttp、MSXML2.XMLHttp.3.0和MSXMLHttp.6.0。要使用MSXML库中的XHR对象,需要像第18章讨论创建XML文档时一样,编写一个函数,例如:
// 适用于IE7之前版本 //此方法会返回一个XHR对象 function createXHR(){ if(typeof arguments.callee.activeXString!="string"){//检测函数体的自定义属性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]);//创建一个ActiveX类型的一个实例(需要版本号),若不支持这个版本会报错 arguments.callee.activeXString=versions[i];//为函数体自定义activeXString属性,并将版本号赋值给它 break; }catch(ex){ //跳过 } } } return new ActiveXObject(arguments.callee.activeXString); }
IE7+、firefox、opera、chrome、safari都支持XHR对象,在这些浏览器中创建XHR对象要像下面这样使用:
var xhr=new XMLHttpRequest();
然后我们就可以将IE7之前的和IE7之后的封装成一个跨浏览器创建XHR对象的方法,适合任何浏览器任何版本!!
function createXHR(){ if(typeof XMLHttpRequest!="undefined"){//检测是否支持XMLHttpRequest原生对象 return new XMLHttpRequest(); }else if(typeof ActiveXObject!="undefined"){ if(typeof arguments.callee.activeXString!="string"){//检测函数体的自定义属性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]);//创建一个ActiveX类型的一个实例(需要版本号),若不支持这个版本会报错 arguments.callee.activeXString=versions[i];//为函数体自定义activeXString属性,并将版本号赋值给它 break; }catch(ex){ //跳过 } } } return new ActiveXObject(arguments.callee.activeXString); }else{ throw new Error("No XHR object available."); } }
使用:
var xhr=createXHR();
XHR对象的用法
在使用XHR对象时,要调用的第一个方法是open(),它接收3个参数:要发送的请求的类型(get、post等等)、请求的URL和表示是否异步发送请求的布尔值。:
xhr.open("get","example.php",false);
这行代码会启动一个针对example.php的GET请求。有关这行代码,需要说明两点:一是URL相对于执行代码的当前页面(当然也可以用绝对路径);二是调用open()方法并不会真正发送请求,而只是启动一个请求以备发送。
注意:只能向同一个域中使用相同端口和协议的URL发送请求。如果URL与启动请求的页面有任何差别,都会引发安全错误。
要发送特定的请求,必须像下面这样调用send()方法:
xhr.send(null);
这里的send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入null,因为这个参数对有些浏览器来说是必要的。调用send()之后,请求就会被分派到服务器。
由于这次请求是同步的,javascript代码会等到服务器响应之后再继续执行。在收到响应之后,响应的数据就会自动填充XHR对象的属性,相关属性简介如下:
responseText:作为响应主体被返回的文本。
responseXML:如果响应的内容类型是“text/xml”或"application/xml",这个属性中将保存包含着响应数据的XML DOM文档。
status:响应的HTTP状态。
statusText:HTTP状态说明。
在接收到响应之后,第一步是检测status属性,以确定响应已经成功返回。一般来说,可以将HTTP状态码为200作为成功的标志。此时,reponseText属性的内容已经就绪,而且在内容类型正确的情况下,responseXML也应该能够访问了。此外,状态码为304表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本;当然,也意味着响应是有效的。为确保接收到适应的响应,应该像下面这样检查上述两种状态代码:
if((xhr.status>=200&&xhr.status<300)||xhr.status==304){ console.log(xhr.responseText); }else{ console.log("Request was unsuccessful:"+xhr.status); }
无论内容类型是什么,响应主体的内容都会保存到responseText属性中;而对于非XML数据而言,responseXML属性的值将为null。
像前面这样发送同步请求当然没有问题,但多数情况下,我们还是要发送异步请求,才能让javascript代码继续执行,而不必等待响应。此时可以检测XHR对象的readyState属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下:
0:未初始化。尚未调用open()方法。
1:启动。已经调用open()方法,但尚未调用send()方法。
2:发送。已经调用send()方法,但尚未接收到响应。
3:接收,已经接收到部分响应数据。
4:完成,已经接收到全部响应数据,而且已经可以在客户端使用了。
只要readyState属性值有一个值变成另一个值,都会触发一下readystatechange事件。可以利用这个事件来检测每次状态变化后readyState的值。通常我们只对readyState的值为4的阶段感兴趣,因为这时所有数据都已经就绪。不过,必须在地哦啊用open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性,例如:
var xhr=createXHR(); xhr.onreadystatechange=function(){ 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.php",false); xhr.send(null);
另外,在接收到响应之前还可以调用abort()方法来取消异步请求:
xhr.abort();
调用这个方法之后,XHR对象会停止触发事件,而且也不允许访问任何与响应有关的对象属性。在终止请求之后,还应该对XHR对象进行解引用操作。由于内存原因,不建议重用XHR对象。
HTTP头部信息
每个HTTP请求和响应都会带有相应的头部信息,其中有对开发人员有用的,有的也没什么用。XHR对象也提供了操作这两种头部(即请求头部和响应头部)信息的方法。
默认情况下,在发送XHR请求的同时,还会发送下列头部信息。
Accept:浏览器能够处理的内容类型。
Accept-Charset:浏览器能够显示的字符集。
Accept-Encoding:浏览器能够处理的压缩编码。
Accept-Language:浏览器当前设置的语言。
Connection:浏览器与服务器之间连接的类型。
Cookie:当前页面设置的任何Cookie。
Host:发送请求的页面所在的域。
Referer:发出请求的页面的URI。注意,HTTP规范将这个头部字段拼写错了,而为保证与规范一致,也只能将错就错了。(这个单词的正确拼法是referrer)。
User-Agent:浏览器的用户代理字符串。
虽然不同浏览器实际发送的头部信息会有所不同,但以上列出的基本上是所有浏览器都会发送的。使用setRequestHeader()方法可以设置自定义的请求头信息。这个方法接受两个参数:头部字段的名称和头部字段的值。要成功发送请求头部信息,必须在调用open()方法之后且调用send()方法之前调用setRequestHeader(),例如:
var xhr=createXHR(); xhr.onreadystatechange=function(){ 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.php",false); xhr.setRequestHeader("MyHeader","MyValue");//设置请求头 xhr.send(null);
服务器在接收到这种自定义的头部信息之后,可以执行响应的后续操作。我们建议使用自定义的头部字段名称,不要使用浏览器正常发送的字段名称,否则有可能会影响服务器的响应。有的浏览器允许开发人员重写默认的头部信息,有的浏览器则不允许这样做。
调用XHR对象的getRequestHeader()方法并传入头部字段名,可以取得相应的响应头部信息。而调用getAllRequestHeaders()方法,可以取得包含所有头部信息的字符串。例如:
var myHeader=xhr.getResponseHeader("myHeader");
var allHeaders=xhr.getAllResponseHeaders();
在服务器,也可以利用头部信息想浏览器发送额外的、结构化的数据。在没有自定义信息的情况下,getAllResponseHeaders()方法通常会返回如下所示的多行文本内容。
Date: Sun, 14 Nov 2004 18:04:03 GMT
Server:Apache/1.3.29 (unix)
Vary:Accept
X-Powered-By:PHP/4.3.8
Connection:close
Content-Type:text/html;charset=iso-8859-1
GET请求:
GET是最常见的请求类型,最常用于向服务器查询某些信息,必要时,可以将查询字符串参数追加到URL的末尾,以便将信息发送给服务器。对XHR而言,位于传入open()方法的URL末尾的查询字符串必须经过正确的编码才行。
使用GET请求经常会发生一个错误,就是查询字符串的格式有问题。查询字符串中每个参数的名称和值都必须使用encodeURIComponent()进行编码,然后才能放到URL的末尾;而且所有名-值对儿都必须有和号(&)分隔,例如:
xhr.open("get","example.php?name1=value1&name2=value2",true);
下面这个函数可以辅助向现有的URL的末尾添加查询字符串参数:
function addURLParam(url,name,value){
url += (url.indexOf("?") == -1 ? "?":"&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
下面是使用这个函数来构建请求URL的示例:
var url = example.php;
//添加参数
url = addURLParam(url,"name","Nicholas");
url = addURLParam(url,"book","Professional JavaScript");
//初始化请求
xhr.open("get",url,false);
POST请求
使用频率仅次于GET的是POST请求,通常用于向服务器发送应该被保存的数据。POST请求应该把数据作为请求的主体提交,而GET请求传统上不是这样。POST请求的主体可以包含非常多的数据,而且格式不限。在open()方法第一个参数的位置传入“post”,就可以初始化一个POST请求,如:
xhr.open("post",example.php,true);
发送POST请求的第二步就是向send()方法中传入某些数据。由于XHR最初的设计主要是为了处理XML,因此可以在此传入XML DOM文档,传入的文档经序列化之后将作为请求主体被提交到服务器,当然也可以在此传入任何想发送到服务器的字符串。
默认情况下,服务器对POST请求和提交web表单的请求并不会一视同仁。因此,服务器端必须有程序来读取发送过来的原始数据,并从中解析出有用的部分。不过,我们可以使用XHR来模仿表单提交:首先将Content-Type头部信息设置为application/x-www-form-urlencode,也就是表单提交时的内容类型,其次是以适当的格式创建一个字符串。之前提到过,POST数据格式与查询字符串格式相同。如果需要将页面中表单的数据进行序列化,然后再通过XHR发送到服务器,那么久可以使用之前提到过的serialize()函数来创建这个字符串:
function submitData(){ var xhr = createXHR(); xhr.onreadystatechange=function(){ 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("post","postexample.php",true); xhr.setRequestHeader("Content-Type","application/x-www-form-urlencode"); var form = document.getElementById("user-info"); xhr.send(serialize(form));//序列化页面汇总表单的数据
这个函数可以将ID为“user-info”的表单中的数据序列化之后发送给服务器。而下面的PHP文件postexample.php就可以通过$_POST取得提交的数据了:
<?php header("Content-Type: text/plain"); echo <<<EOF NAME: {$_POST[ 'user-name' ]} Email: {$_POST[ 'user-email' ]} EOF; ?>
如果不设置Content-Type头部信息,那么发送给服务器的数据就不会出现在$_POST超级全局变量中。这时候,要访问同样的数据,就必须借助$HTTP_RAW_POST_DATA.
与GET请求比,POST请求消耗的资源会更多一些。从性能角度来看,以发送相同的数据计,GET请求的速度最多可达到POST请求的两倍。
XMLHttpRequest2级
并非所有浏览器都完整实现了XMLHttpRequest2级规范,但所有浏览器都实现了它规定的部分内容。
FormData
XMLHttpRequest2级定义了FormData类型,FormData为序列化表单以及创建表单格式相同的数据(用于XHR传输)提供了便利。下面创建了一个FormData对象,冰箱其中添加了一些数据。
var data=new FormData();
data.append("name","Nicholas");
这个append()方法接收两个参数:键和值。分别对应表单字段的名字和字段中包含的值。可以像这样添加任意多个简直对儿。而通过向FormData构造函数里面传入表单元素,也可以用表单元素的数据预先向其中填入键值对儿:
var data=new FormData(document.forms[0]);
创建了FormData的实例后,可以将它直接传给XHR的send()方法,例如:
var xhr=createXHR(); xhr.onreadystatechange=function(){ 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("post","postexample.php",false); var form =document.getElementById("user-info"); xhr.send(new Data(form));
使用FormData的方便之处体现在不必明确的在XHR对象上设置请求头部。XHR对象能够识别传入的数据类型是FormData的实例,并配置适当的头部信息。
支持FormData的浏览器有firefox4+、safari5+、chrome和android3+版webkit。
超时设定
只有IE8+支持超时设定,XHR的timeout属性可以设定超时时间(ms)。请求超过这个设定时间后就会触发XHR的timeout事件,如果在超时终止事件之后再访问status属性,就会导致错误,为了避免报错,可以将检测status属性的语句放在一个try-catch块儿中。例如:
var xhr=createXHR(); xhr.onreadystatechange=function(){ if(xhr.readyState==4){ try{ if((xhr.status>=200&&xhr.status<300)||xhr.status==304){ console.log(xhr.responseText); }else{ console.log("Request was unsuccessful:"+xhr.status); } }catch(ex){ //请求超时后的处理 } } } xhr.open("post","postexample.php",false); xhr.timeout=1000;//将超时设置为1秒钟(仅适用于IE8+) xhr.ontimeout=function(){ console.log("Request did not return in a second"); }; xhr.send(new Data(null));
overrideMimeType()方法
此方法用于重写XHR响应的MIME类型。
因为返回响应的MIME类型决定了XHR对象如何处理它,所以提供一种方法能够重写服务器返回的MIME类型是很有用的。
比如服务器返回的MIME类型是text/plain,但数据中实际包含的是XML。根据MIME类型,即使数据是XML,responseXML属性中仍然是null;通过overrideMimeType()方法,可以保证把响应当做XML而非纯文本来处理。
var xhr=createXHR();
xhr.open("get","example.php",true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
上面的例子强迫XHR对象将响应当做XML而非纯文本来处理。调用overrideMimeType()必须在send()方法之前,才能保证重写响应的MIME类型。
支持overrideMimeType()方法的浏览器有:firefox、safari4+、opera10.5、和chrome。
进度事件
progress Events规范是W3C的一个工作草案,定义了与客户端服务器通信有关事件。这些事件最早其实只针对XHR操作,但目前也被其它API借鉴。有以下6个进度事件:
loadstart:在接收到响应数据的第一个字节时触发。
progress:在接收响应期间持续不断的触发。
error:在请求发生错误时触发;
abort:在因为调用abort()方法而终止连接时触发。
load:在接收到完整的响应数据时触发。
loadend:在通信完成或者触发error、abort、或load事件后触发。(无浏览器支持)
支持前5个事件的浏览器有:firefox3.5+、safari4+、chrome、ios版safari和Android版webkit。opera11+、IE8+只支持load事件。目前还没有浏览器支持loadend事件
load事件:
火狐为了简化异步交互模型,它使用load事件代替readystatechange事件,因此没必要检测readyState属性了。而onload事件处理程序会接收到一个event对象,其target属性就指向XHR实例,因而可以访问到XHR对象的所有方法和属性。然而并非所有浏览器都为这个事件实现了适当的事件对象。结果,开发人员还是要像下面这样被迫使用XHR对象:
var xhr=createXHR(); xhr.onload=function(){//在接收完整的响应数据时触发 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","events.php",true); xhr.send(null);
只要浏览器接收到服务器的响应,不管其状态如何,都会触发load事件。而这意味着你必须要检测status属性,才能确定数据是否真的已经可用了。firefox、opera、chrome和safari都支持load事件。
progress事件
mozilla对XHR的另一个革新是添加了progress事件,这个事件会在浏览器接收新数据期间周期性的触发。而onprogress事件处理程序会接收一个event对象,其target属性是XHR对象,但包含着额外的三个属性:lengthComputable、position和totalSize。其中,lengthComputable是一个表示进度信息是否可用的布尔值,position表示已经接收的字节数,totalSize表示根据Content-Length响应头部确定的预期字节数。有了这些信息,我们就可以为用户创建一个进度指示器了。例如:
var xhr=createXHR(); xhr.onload=function(){//在接收完整的响应数据时触发 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","events.php",true); xhr.send(null);
为确保正常执行,必须在调用open()方法之前添加onpress事件处理程序。
跨域源资源共享CORS(Cross-Origin Resource Sharing)
通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含它的页面位于同一个域中的资源。但是我们大多必须用到跨域。
CORS是W3C的一个工作草案,定义了在必须访问跨域资源时,浏览器与服务器如何沟通。CORS背后的基本思想是:使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或相响应是应该成功还是应该失败。
比如一个简单的使用GET或POST发送的请求,它没有自定义的头部,而主题内容是text/plain。在发送该请求时,需要给他附加一个额外的Origin头部,其中包含请求页面的源信息(协议、域名、和端口号),以便服务器根据这个头部信息来决定是否给予响应。下面是Origin头部信息的一个事例:
Origin:http://www.nczonline.net
前面讲过,我们可以使用XHR对象的setRequestHeader(“key”,“value”)方法设置自定义头部信息,不过要放在open()之后,send()之前(但是IE之外的浏览器跨域时又禁止设置自定义头部信息,继续往下看)。
如果浏览器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以会发“*”)。例如
Access-Control-Allow-Origin:http://www.nczonline.net
如果没有这个头部,或者有这个头部但愿信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含cookie信息。
IE对CORS的实现
微软在IE8中引入了XDR(XDomainRequest)类型。这个对象与XHR对象类似,但能实现安全可靠的跨域通信。XDR对象的安全机制部分实现了W3C的CORS规范。以下是XDR与XHR的一些不同之处。
1、cookie不会随请求发送,也不会随响应返回。
2、只能设置请求头部信息中的Content-Type字段。
3、不能访问响应头部信息。
4、只支持GET和POST请求
这些变化使CSRF(Cross-Site Request Forgery,跨站点请求伪造)和XSS(Cross-Site Scripting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)来决定是否设置Access-Control-Allow-Origin头部。作为请求的一部分,Origin头部的值表示请求的来源域,以便远程资源明确的识别XDR请求。
XDR对象的使用方法与XHR对象非常相似。也是创建一个XDomainRequest的实例,调用open()方法,再调用send()方法。但与XHR对象的open()方法不同,XDR的open()方法只接受两个参数:请求的类型和URL。
所有XDR请求都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发load事件,响应的数据也会保存在responseText属性中,例如:
ar xdr=new XDomainRequest();
xdr.onload=function(){
console.log(xdr.responseText);
};
xdr.open("get","http://www.somewhere-else.com/page/");
xdr.send(null);
在接收到响应后,你只能访问响应的原始文本,没有办法确认响应的状态码。而且,只要响应有效就会触发load事件,如果失败(包括响应中缺少Access-Control-Allow-Origin头部)就会触发error事件。遗憾的是,除了错误本身之外,没有其他信息可用,因此唯一能够确定的就只有请求未成功了。要检测错误,可以像下面这样指定一个error事件处理程序。
var xdr=new XDomainRequest();
xdr.onload=function(){
console.log(xdr.responseText);
};
xdr.onerror=function(){
console.log("An error occurred.");
}
xdr.open("get","http://www.somewhere-else.com/page/");
xdr.send(null);
鉴于导致XDR请求失败的因素很多,因此建议你不要忘记通过onerror事件处理程序来捕获该事件;否则,即使请求失败也不会有任何提示。
在请求返回前调用abort()方法可以终止请求:
xdr.abort();//终止请求
与XHR一样XDR也支持timeout属性以及ontimeout事件处理程序。下面是一个例子。
var xdr=new XDomainRequest();
xdr.onload=function(){
console.log(xdr.responseText);
};
xdr.onerror=function(){
console.log("An error occurred.");
};
xdr.timeout=1000;//设置超时时间
xdr.ontimeout=function(){
console.log("Request took too long.");
};
xdr.open("get","http://www.somewhere-else.com/page/");
xdr.send(null);
这个例子会在运行1秒后超时,并随即调用ontimeout事件处理程序。
为了支持POST请求,XDR对象提供了contentType属性,用来表示发送数据的格式,如下面的例子所示。
var xdr=new XDomainRequest(); xdr.onload=function(){ console.log(xdr.responseText); }; xdr.onerror=function(){ console.log("An error occurred."); }; xdr.open("post","http://www.somewhere-else.com/page/"); xdr.contentType="application/x-www-form-urlencode";//设置发送数据的格式 xdr.send("name1=value1&name2=value2");//发送数据
其它浏览器对CORS的实现(不用设置其它直接可以跨域!!)
firefox3.5+、safari4+、chrome、ios版safari和android平台中的webkit都通过XMLHttpRequest对象实现了对CORS的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可,例如:
var xhr = createXHR(); xhr.onreadystatechange=function(){ 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","http://www.somewhere-else.com/page/",true); xhr.send(null);
与IE的XDR对象不同,通过跨域XHR对象可以访问status和statusText属性,而且还同步请求。跨域XHR对象也有一些限制,但为了安全这些限制是必须的。一下就是这些限制。
1、不能使用setRequestHeader()设置自定义头部。
2、不能发送和接收cookie
3、调用getAllResponseHeaders()方法总是会返回空字符串。
由于无论同源请求还是跨域请求都使用相同的接口,因此对于本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL。这样做能消除歧义,避免出现限制访问头部或本地cookie信息等问题。
Preflighted Requests
CORS通过一种Preflighted Requests的透明服务器验证机制支持开发人员使用自定义的头部、GET或POST之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求。这种请求使用OPTIONS方法,发送下列头部。
1、Origin:与简单的请求相同
2、Access-Control-Request-Method:请求自身使用的方法
3、Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。
以下是一个带有自定义头部NCZ的使用POST方法发送的请求。
Origin:http://www.nczonline.net
Access-Control-Request-Method:POST
Access-Control-Request-Headers:NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。
1、Access-Control-Allow-Origin:与简单的请求相同
2、Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔
3、Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔
4、Access-Control-Max-Age:应该将这个Preflight请求缓存多长时间(以秒表示)。
例如:
Access-Control-Allow-Origin:http://www.nczonline.net
Access-Control-Allow-Methods:POST,GET
Access-Control-Allow-Headers:NCZ
Access-Control-Max-Age:172800
Preflight请求结束后,结果将按照响应中指定的时间缓存起来。而为此付出的代价只是第一次发送这种请求时会多一次HTTP请求。
支持Preflight请求的浏览器包括firefox3.5+、safari4+、和chrome。IE10及更早版本都不支持。
带凭据的请求
默认情况下,跨域请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会用下面的HTTP头部来响应。
Access-Control-Allow-Credentials:true
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给javascript(于是,responseText中将是空字符串,status的值为0,而且会调用onerror()事件处理程序)。另外,服务器还可以在Preflight响应中发送这个HTTP头部,表示允许源发送带凭据的请求。
支持withCredentials属性的浏览器有firefox3.5+、safari4+、chrome。IE10及更早版本都不支持。
跨浏览器的CORS
即使浏览器对CORS的支持程度并不一样,但所有浏览器都支持简单的(非Preflight和不带凭据的)请求,因此有必要实现一个跨浏览器的方案。检测XHR是否支持CORS的最简单方式,就是检测是否存在withCredentials属性。再结合检测XDomainRequest对象是否存在,就可以兼顾所有浏览器了。
function createCORSRequest(method,url){
var xhr=new XMLHttpRequest();
if("widthCredentials" in xhr){//判断是否支持最简单的CORS
xhr.open(method,url,true);
}else if(typeof XDomainRequest != "undefined"){//兼容IE
xhr = new XDomainRequest();
xhr.open(method,url);
}else{
xhr=null;
}
}
var request = createCORSRequest("get","http://www.somewhere-else.com/page/");
if(request){
request.onload=function(){
//对request.responseText进行处理
};
request.send();
}
firefox、safari和chrome中的XMLHttpRequest对象与IE中的XDomainRequest对象类似,都提供了够用的接口,因此以上模式还是非常有用的,这两个对象共同的方法如下。
1、abort():用于停止正在进行的请求
2、onerror:用于代替onreadystatechange检测错误
3、onload:用于代替onreadystatechange检测成功
4、responseText:用于取得响应内容
5、send():用于发送请求
以上成员都包含在createCORSRequest()函数返回的对象中,在所有浏览器中都能正常使用。
其它跨域技术
图像Ping
var img=new Image();
img.onload=img.onerror=function(){
console.log("do");
}
img.src="http://pic15.nipic.com/20110813/1993003_205156492136_2.jpg?name=haha";
这里创建了一个Image实例,然后onload和onerror事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置src属性那一刻开始,而这个例子在请求总发送一个name参数。图像Ping常用于用户点击页面或动态广告曝光次数。,图像Ping有两个缺点,一是只能发送GET请求,二是,无法访问服务器的响应文本。
JSONP
JSONP是通过动态创建<script>元素来使用的,之后为src属性指定一个跨域URL。因为JSONP是有效的javascript代码,所以在请求完成后,即在JSONP响应加载到页面中以后,就会立即执行。
function handleResponse(response){
console.log("IP adress"+response.ip+",city"+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);
这个例子通过查询地理定位服务来显示你的IP地址和位置信息。
JSONP也有两大缺点,1、不安全,请求过来的数据很可能有恶意代码,2、无法判断JSONP请求是否失败。
Comet
Comet是Alex Russell发明的一个词,指的是一种更高级的Ajax技术(经常也有人称为服务器推送)。Ajax是一种从页面向服务器请求数据的技术。Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时的被推送到页面,非常适合体育比赛分数和股票报价。
有两种实现Comet的方法:长轮询和流。长轮询是短轮询的一个翻版,短轮询即浏览器定时想服务器发送请求。
长轮询把短轮询颠倒了一下,页面发送一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完整数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这个过程在页面打开期间一直持续不断。
无论是长轮询还是短轮询,浏览器都要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用XHR对象和setTimeout()就能实现。而自己要做的就是决定什么时候发送请求。
第二种流行的Comet实现是HTTP流。流不同与上述两种轮询,因为他在页面的整个生命周期内只使用一个HTTP连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性的向浏览器发送数据。下面这种PHP脚本就是采用流实现的服务器中常见的形式:
<?php $i = 0; while(true){ //输出一些数据,然后立即刷新输出缓存 echo "Number is $i"; flush(); //等几秒中 sleep(10); $i++; } ?>
在firefox、safari、opera、chrome中,通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。在上述这些浏览器中,随着不断从服务器接受数据,readyState的值会周期性的变为3.当readyState值变为3时,responseText属性就会保存接受到的所有数据。此时就需要比较此前接收的数据,决定从什么位置开始取得最新数据。使用XHR对象实现HTTP流的典型代码如下所示:
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){
console.log("Received:"+data);
},function(data){
console.log("Done!");
})
这个createStreamingClient()函数接收三个参数:要连接的URL、在接收到数据时调用的函数以及关闭连接时调用的函数。有时候,当连接关闭时,很可能还需要重新建立,所以关注连接什么时候关闭还是有必要的。
只要readystatechange事件发生,而且readyState值为3,就对responseText进行分割以取得最新数据。这里的received变量用于记录已经处理了多少个字符串,每次readyState值为3时都递增。然后,通过progress回调函数来处理新传入的新数据。而当readyState值为4时,则执行finished回调函数,传入响应返回的全部内容。以上代码不支持IE。
服务器发送事件SSE(Server-Sent Events)
SSE是围绕只读Comet交互推出的API或者模式。SSE API 用于创建到服务器的单项连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是
text/event-stream,而且是浏览器中的javascript API能解析格式输出。SSE支持短轮询、长轮询、HTTP流,而且能在断开连接时自动确定何时重新连接。有了这么简单的API,再实现Comet就容易多了;
支持SSE的浏览器有:firefox6+、safari5+、opera11+、chrome、ios4+版Safari
1、SSE API
SSE的javascriptAPI与其他传递消息的javascript API 很相似。要预订新的事件流,首先要创建一个新的EventSource对象,并传进一个入口点:
var source = new EventSource("myevents.php");
注意,传入的URL必须与创建对象的页面同源(相同URL模式、域及端口)。EventSource的实例有一个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2,表示关闭了连接。
另外还有3个事件:
1、open:在建立连接时触发。
2、message:在从服务器接受到新事件时触发。
3、error:在无法建立连接时触发。
就一般而言,onmessage事件处理程序也没什么特别的。
source.onmessage=function(event){
var data=event.data;
//处理数据
}
服务器发回的数据以字符串形式保存在eventdata中。
默认情况下,EventSource对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味着SSE适合长轮询和HTTP流。如果想强制立即断开连接并且不再重新连接,可以调用close()方法。
2.事件流
所谓的服务器事件会通过衣蛾持久的HTTP响应发送,这个响应的MIME类型为text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都会前缀data:,例如:
data:foo
data:bar
data:foo
data:bar
对以上响应而言,事件流中的第一个message事件返回的event.data值为“foo”,第二个message事件返回的event.data值为“bar”,第三个message事件返回的event.data值为“foo/bar”(注意中间的换行符)。对于多个包含data:的数据行后面又空行时,才会触发message事件,因此在服务器上生成事件流时不能忘了多添加这一行。
通过id:前缀可以给特定的事件指定一个关联的ID,这个ID行位于data:行前面或后面皆可:
data:foo
id:1
设置ID后,EventSource对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个包含名为Last-Event-ID的特殊HTTP头部请求,以便服务器知道下一次该触发哪个事件。在多次连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。
Web Sockets
Web Sockets的目标是在一个单独的持久连接上提供全双工、双向通信。在javascript中国创建Web Sockets之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Sockets协议。也就是说,使用标准的HTTP服务器无法实现Web Sockets,值有支持这种协议的专门服务器才能正常工作。
由于Web Sockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://,而是wss://。在使用Web Sockets URL时,必须带着这个模式,因为将来还有可能支持其他模式。
目前支持Web Sockets的浏览器有firefox6+、safari5+、chrome、和ios4+版safari。
1.Web Sockets API
要创建一个Web Socket,先实例一个WebSocket对象并传入要连接的URL:
var socket=new WebSocket("ws://www.example.com/server.php");
注意,必须给Web Sockets构造函数传入绝对URL。同源策略对Web Sockets不使用,因此可以通过它打开到任意站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器。(通过握手信息就可以知道请求来自何方)。
实例化了WebSocket对象后,浏览器会马上尝试创建连接。与XHR类似,WebSocket也有一个表示当前状态的readyState属性。不过这个属性值与XHR并不相同,而是如下所示。
WebSocket.OPENING(0):正在建立连接
WebSocket.OPEN(1):已经建立连接
WebSocket.CLOSING(2):正在关闭连接
WebSocket.CLOSE(3):已经关闭连接
WebSocket没有readystatechange事件;不过它有其它事件,对应着不同的状态。readyState的值永远从0开始。
要关闭WebSocket连接,可以在任何时候调用close()方法。
socket.close();
调用了close()之后,readyState值立即变为2(正在关闭),而在关闭连接后就会变成3.
2.发送和接收数据
WebSocket打开之后,就可以通过连接发送和接收数据。要向服务器发送数据,使用send()方法并传入任意字符串,例如:
var socket=new WebSocket("ws://www.example.com/server.php");
socket.send("hellow world!");
因为WebSockets只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前,必须进行序列化。下面的例子展示了先将数据序列化为一个JSON字符串,然后再发送到服务器:
var message={
time:new Date(),
text:"hellow world",
clientId:"asdfp8734rew"
}
socket.send(JSON.stringify(message));
接下来,服务器要读取其中的数据,就要解析接收到的JSON字符串。
当服务器向客户端发来消息时,Web Socket对象就会触发message事件。这个message事件与其他传递消息的协议类似,也是把返回的数据保存在event.data属性中。
socket.onmessage=function(event){
var data = event.data;
//处理数据
}
与通过send()发送到服务器的数据一样,event.data中返回的也是字符串。如果你想得到其他的数据格式的数据,必须手工解析这些数据。
3.其它事件
Web Socket对象还有其它三个事件,在连接生命周期的不同阶段触发:
1、open:在成功建立连接时触发。
2、error:在发生错误时触发,连接不能持续。
3、close:在连接关闭时触发。
WebSocket对象不止吃DOM2级事件侦听器,因此必须使用DOM0级语法分别定义每个事件处理程序。
var socket=new WebSocket("ws://www.example.com/server.php");
socket.onopen=function(){
console.log("Connection established.");
}
socket.onerror=function(){
console.log("Connection error");
}
socket.onclose=function(){
console.log("Connection closed");
}
在这三个事件中,只有close事件的event对象有额外的信息。这个事件的事件对象有三个额外的属性:wasClean、code、reason。其中,wasClean是一个布尔值,表示连接是否已明确的关闭;code是服务器返回数值状态码;而reason是一个字符串,包含服务器发回的消息。可以把这些信息显示给用户,也可以记录到日志中以便将来分析。
socket.onclose=function(event){
console.log("was clean?"+event.wasclean+" code="+event.code+"Reason="+event.reason);
}
SSE与Web Sockets
面对某些具体用例,在考虑是使用SSE还是使用Web Sockets时,可以考虑如下几个因素,首先,你是否有自由度自由建立和维护Web Sockets服务器?因为Web Socket协议不同于HTTP,所以现有服务器不能用于Web Socket通信。SSE倒是通过常规HTTP通信,因此现有服务器就可以满足需求。
第二个要考虑的问题是到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩),那么SSE比较容易实现。如果用例必须双向通信(如聊天室),那么Web Sockets显然更好。别忘了。在不能选择Web Sockets的情况下,组合XHR和SSE也是能实现双向通信的。
要想了解Ajax更多信息,可以参考《Ajax 高级程序设计(第2版)》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通