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版)》

原文地址:https://www.cnblogs.com/fqh123/p/10624832.html