前端重新学习(9)javascipt之ajax深度概念

2005 年,Jesse James Garrett 发表了一篇在线文章,题为“Ajax: A new Approach to Web Applications”(http://www.adaptivepath.com/ideas/essays/archives/000385.php)。 他在这篇文章 里介绍了一种技术,用他的话说,就叫 Ajax,是对 Asynchronous JavaScript + XML的简写。

Ajax的入门

Ajax的组成

Ajax 是无需刷新页面就能够从服务器取得数据的一种方法。关于 Ajax,可以从以下几方面来总结 一下。

  • 负责 Ajax运作的核心对象是 XMLHttpRequest(XHR)对象。
  • XHR对象由微软早在 IE5中引入,用于通过 JavaScript从服务器取得 XML数据。
  • 在此之后,Firefox、Safari、Chrome和 Opera都实现了相同的特性,使 XHR成为了 Web 的一个 事实标准。
  • 虽然实现之间存在差异,但XHR对象的基本用法在不同浏览器间还是相对规范的,因此可以放 心地用在 Web 开发当中。

同源策略是对 XHR 的一个主要约束,它为通信设置了“相同的域、相同的端口、相同的协议”这一 限制。试图访问上述限制之外的资源,都会引发安全错误,除非采用被认可的跨域解决方案。这个解决 方案叫做 CORS(Cross-Origin Resource Sharing,跨源资源共享),IE8 通过 XDomainRequest 对象支持 CORS,其他浏览器通过 XHR对象原生支持 CORS。图像 Ping和 JSONP是另外两种跨域通信的技术, 但不如 CORS稳妥。

Comet 是对 Ajax 的进一步扩展,让服务器几乎能够实时地向客户端推送数据。实现 Comet 的手段 主要有两个:长轮询和 HTTP流。所有浏览器都支持长轮询,而只有部分浏览器原生支持 HTTP流。SSE (Server-Sent Events,服务器发送事件)是一种实现 Comet 交互的浏览器 API,既支持长轮询,也支持 HTTP流。

Web Sockets是一种与服务器进行全双工、双向通信的信道。与其他方案不同,Web Sockets不使用 HTTP 协议,而使用一种自定义的协议。这种协议专门为快速传输小数据设计。虽然要求使用不同的 Web 服务器,但却具有速度上的优势。 各方面对 Ajax和 Comet的鼓吹吸引了越来越多的开发人员学习 JavaScript,人们对 Web 开发的关注 也再度升温。与 Ajax有关的概念都还相对比较新,这些概念会随着时间推移继续发展。**要想了解 有关 Ajax的更多信息,请读者参考《Ajax高级程序设计(第 2版)》。 **

Ajax入门

Ajax 能够向服务器请求额外的数据而无须卸载页面,会带来更好的用户体验。Garrett 还解释了怎样使用这 一技术改变自从Web 诞生以来就一直沿用的“单击,等待”的交互模式。

Ajax技术的核心是 XMLHttpRequest 对象 (简称 XHR),这是由微软首先引入的一个特性,其他 浏览器提供商后来都提供了相同的实现。在 XHR出现之前,Ajax式的通信必须借助一些 hack手段来实 现,大多数是使用隐藏的框架或内嵌框架。XHR 为向服务器发送请求和解析服务器响应提供了流畅的 接口。能够以异步方式从服务器取得更多信息,意味着用户单击后,可以不必刷新页面也能取得新数据。 也就是说,可以使用 XHR 对象取得新数据,然后再通过 DOM 将新数据插入到页面中。另外,虽然名 字中包含 XML的成分,但 Ajax通信与数据格式无关;这种技术就是无须刷新页面即可从服务器取得数 据,但不一定是 XML数据。

实际上,Garrett 提到的这种技术已经存在很长时间了。在 Garrett 撰写那篇文章之前,人们通常将 这种技术叫做远程脚本(remote scripting),而且早在 1998年就有人采用不同的手段实现了这种浏览器 与服务器的通信。再往前推,JavaScript需要通过 Java applet或 Flash电影等中间层向服务器发送请求。 而 XHR则将浏览器原生的通信能力提供给了开发人员,简化了实现同样操作的任务。

在重命名为 Ajax 之后,大约是 2005 年底 2006 年初,这种浏览器与服务器的通信技术可谓红极一 时。人们对 JavaScript和 Web 的全新认识,催生了很多使用原有特性的新技术和新模式。就目前来说, 熟练使用 XHR对象已经成为所有 Web 开发人员必须掌握的一种技能。

XMLHttpRequest 对象

IE5是第一款引入 XHR对象的浏览器。在 IE5中,XHR对象是通过 MSXML 库中的一个 ActiveX 对象实现的。因此,在 IE 中可能会遇到三种不同版本的 XHR 对象,即 MSXML2.XMLHttp、 MSXML2.XMLHttp.3.0 和 MXSML2.XMLHttp.6.0。要使用 MSXML 库中的 XHR 对象,需要创建 XML文档时一样,编写一个函数,例如:

//适用于 IE7之前的版本
function createXHR(){     
    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){    
        //跳过            
            }       
        }    
    } 
 
    return new ActiveXObject(arguments.callee.activeXString);
} 
 

这个函数会尽力根据 IE中可用的 MSXML库的情况创建新版本的 XHR对象。

IE7+、Firefox、Opera、Chrome和 Safari都支持原生的 XHR对象,在这些浏览器中创建 XHR对象 要像下面这样使用 XMLHttpRequest 构造函数。


 var xhr = new XMLHttpRequest(); 
 

假如你只想支持 IE7 及更高版本,那么大可丢掉前面定义的那个函数,而只用原生的 XHR 实现。 但是,如果你必须还要支持 IE 的早期版本,那么则可以在这个 createXHR()函数中加入对原生 XHR 对象的支持。

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){        
                    //跳过               
               }           
           }      
        } 
 
       return new ActiveXObject(arguments.callee.activeXString); 
       
       } else {   
            throw new Error("No XHR object available.");     
       } 
    
} 

这个函数中新增的代码首先检测原生 XHR 对象是否存在,如果存在则返回它的新实例。如果原生 对象不存在,则检测 ActiveX对象。如果这两种对象都不存在,就抛出一个错误。然后,就可以使用下 面的代码在所有浏览器中创建 XHR对象了。


 var xhr = createXHR(); 

由于其他浏览器中对 XHR的实现与 IE早的实现是兼容的,因此就可以在所有浏览器中都以相同 方式使用上面创建的 xhr 对象。

XHR的用法

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

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

这行代码会启动一个针对 example.php 的 GET 请求。有关这行代码,需要说明两点:一是 URL 相对于执行代码的当前页面(当然也可以使用绝对路径);二是调用 open()方法并不会真正发送请求, 而只是启动一个请求以备发送。

只能向同一个域中使用相同端口和协议的 URL发送请求。如果 URL与启动请求 的页面有任何差别,都会引发安全错误。

要发送特定的请求,必须像下面这样调用 send()方法:

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

这里的 send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送 数据,则必须传入 null,因为这个参数对有些浏览器来说是必需的。调用 send()之后,请求就会被分 派到服务器。

由于这次请求是同步的,JavaScript 代码会等到服务器响应之后再继续执行。在收到响应后,响应 的数据会自动填充 XHR对象的属性,相关的属性简介如下。

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

在接收到响应后,第一步是检查 status 属性,以确定响应已经成功返回。一般来说,可以将 HTTP 状态代码为 200作为成功的标志。此时,responseText 属性的内容已经就绪,而且在内容类型正确的 情况下,responseXML 也应该能够访问了。此外,状态代码为 304 表示请求的资源并没有被修改,可 以直接使用浏览器中缓存的版本;当然,也意味着响应是有效的。为确保接收到适当的响应,应该像下 面这样检查上述这两种状态代码:

xhr.open("get", "example.txt", false); 
xhr.send(null); 
 
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){  
    alert(xhr.responseText); 
    
} else {  
    alert("Request was unsuccessful: " + xhr.status);
} 
 

根据返回的状态代码,这个例子可能会显示由服务器返回的内容,也可能会显示一条错误消息。我 们建议读者要通过检测 status 来决定下一步的操作,不要依赖 statusText,因为后者在跨浏览器使 用时不太可靠。另外,无论内容类型是什么,响应主体的内容都会保存到 responseText 属性中;而 对于非 XML数据而言,responseXML 属性的值将为 null。

有的浏览器会错误地报告 204状态代码。IE中 XHR的 ActiveX版本会将 204设 置为 1223,
而 IE中原生的 XHR则会将 204规范化为 200。Opera会在取得 204时报 告 status 的值为 0。 

像前面这样发送同步请求当然没有问题,但多数情况下,我们还是要发送异步请求,才能让 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){  
            alert(xhr.responseText);         
            
        } else {          
            alert("Request was unsuccessful: " + xhr.status);     
        }    
    } 
    
}; 
xhr.open("get", "example.txt", true);
xhr.send(null); 

以上代码利用 DOM 0级方法为 XHR 对象添加了事件处理程序,原因是并非所有浏览器都支持 DOM 2 级方法。与其他事件处理程序不同,这里没有向 onreadystatechange 事件处理程序中传递 event 对象; 必须通过 XHR 对象本身来确定下一步该怎么做。

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

另外,在接收到响应之前还可以调用 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){    
            alert(xhr.responseText);        
        } else {           
            alert("Request was unsuccessful: " + xhr.status);      
        }    
    } 
}; 
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue"); 
xhr.send(null); 

服务器在接收到这种自定义的头部信息之后,可以执行相应的后续操作。我们建议读者使用自定义 的头部字段名称,不要使用浏览器正常发送的字段名称,否则有可能会影响服务器的响应。有的浏览器 允许开发人员重写默认的头部信息,但有的浏览器则不允许这样做。

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

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; 
    
} 
 

这个 addURLParam()函数接受三个参数:要添加参数的 URL、参数的名称和参数的值。这个函数 首先检查 URL是否包含问号(以确定是否已经有参数存在)。如果没有,就添加一个问号;否则,就添 加一个和号。然后,将参数名称和值进行编码,再添加到 URL的末尾。后返回添加参数之后的 URL。

下面是使用这个函数来构建请求 URL的示例。

var url = "example.php"; 

 
//添加参数
url = addURLParam(url, "name", "Nicholas"); 
url = addURLParam(url, "book", "Professional JavaScript"); 
 
//初始化请求 
xhr.open("get", url, false); 

在这里使用 addURLParam()函数可以确保查询字符串的格式良好,并可靠地用于 XHR对象。

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-urlencoded,也就是表单 提交时的内容类型,其次是以适当的格式创建一个字符串。第 14章曾经讨论过,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){   
                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)); 
    
} 
 

这个函数可以将 ID为"user-info"的表单中的数据序列化之后发送给服务器。而下面的示例 PHP 文件 postexample.php 就可以通过$_POST 取得提交的数据了:

<?php     
    header("Content-Type: text/plain");     
    echo <<<EOF 
Name: {$_POST[‘user-name’]}
Email: {$_POST[‘user-email’]} EOF;

---postexample.php 
?> 
 

如果不设置 Content-Type 头部信息,那么发送给服务器的数据就不会出现在$_POST 超级全局变 量中。这时候,要访问同样的数据,就必须借助$HTTP_RAW_POST_DATA。

与 GET 请求相比,POST 请求消耗的资源会更多一些。从性能角度来看,以发送 相同的数据计,GET 请求的速度最多可达到 POST 请求的两倍。

XMLHttpRequest 2级

鉴于 XHR 已经得到广泛接受,成为了事实标准,W3C 也着手制定相应的标准以规范其行为。 XMLHttpRequest 1级只是把已有的 XHR对象的实现细节描述了出来。而 XMLHttpRequest 2级则进一步 发展了 XHR。并非所有浏览器都完整地实现了 XMLHttpRequest 2级规范,但所有浏览器都实现了它规 定的部分内容。

FormData

现代 Web 应用中频繁使用的一项功能就是表单数据的序列化,XMLHttpRequest 2 级为此定义了 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){  
            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)); 

使用 FormData 的方便之处体现在不必明确地在 XHR 对象上设置请求头部。XHR对象能够识别传 入的数据类型是 FormData 的实例,并配置适当的头部信息。

支持 FormData 的浏览器有 Firefox 4+、Safari 5+、Chrome和 Android 3+版 WebKit。

超时设定

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

var xhr = createXHR();
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){          
             //假设由 ontimeout 事件处理程序处理    
        }     
        
    } 
    
}; 
 xhr.open("get", "timeout.php", true);
 xhr.timeout = 1000; 
 //将超时设置为 1 秒钟(仅适用于 IE8+) 
 xhr.ontimeout = function(){     
    alert("Request did not return in a second."); 
 }; 
 xhr.send(null); 
 

这个例子示范了如何使用 timeout 属性。将这个属性设置为 1000毫秒,意味着如果请求在 1秒钟 内还没有返回,就会自动终止。请求终止时,会调用 ontimeout 事件处理程序。但此时 readyState 可能已经改变为 4了,这意味着会调用 onreadystatechange 事件处理程序。可是,如果在超时终止 请求之后再访问 status 属性,就会导致错误。为避免浏览器报告错误,可以将检查 status 属性的语句封装在一个 try-catch 语句当中。

在写作本书时,IE 8+仍然是唯一支持超时设定的浏览器。

overrideMimeType()方法

Firefox早引入了 overrideMimeType()方法,用于重写 XHR响应的 MIME 类型。这个方法后 来也被纳入了 XMLHttpRequest 2级规范。因为返回响应的 MIME类型决定了 XHR对象如何处理它,所 以提供一种方法能够重写服务器返回的 MIME类型是很有用的。

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

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



 

这个例子强迫 XHR对象将响应当作 XML而非纯文本来处理。调用 overrideMimeType()必须在 send()方法之前,才能保证重写响应的 MIME类型。

支持 overrideMimeType()方法的浏览器有 Firefox、Safari 4+、Opera 10.5和 Chrome。

进度事件

load事件

progress事件

Mozilla对 XHR的另一个革新是添加了 progress 事件,这个事件会在浏览器接收新数据期间周期 性地触发。而 onprogress 事件处理程序会接收到一个 event 对象,其 target 属性是 XHR 对象,但 包含着三个额外的属性:lengthComputable、position 和 totalSize。其中,lengthComputable 是一个表示进度信息是否可用的布尔值,position 表示已经接收的字节数,totalSize 表示根据 Content-Length 响应头部确定的预期字节数。有了这些信息,我们就可以为用户创建一个进度指示器 了。下面展示了为用户创建进度指示器的一个示例。

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.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); 

每次触发 progress 事件,都会以新的状态信息更新 HTML 元素的内容。如果响应头部中包含 Content-Length 字段,那么也可以利用此信息来计算从响应中已经接收到的数据的百分比。

跨源资源共享

通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR 对象只能访 问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨 域请求对开发某些浏览器应用程序也是至关重要的。

CORS(Cross-Origin Resource Sharing,跨源资源共享)是 W3C的一个工作草案,定义了在必须访 问跨源资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想,就是使用自定义的 HTTP头部 让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain。在 发送该请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端 口),以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的一个示例:

Origin: http://www.nczonline.net 

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

Access-Control-Allow-Origin: http://www.nczonline.net 

如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器 会处理请求。注意,请求和响应都不包含 cookie信息。

IE对CORS的实现

微软在 IE8中引入了 XDR(XDomainRequest)类型。这个对象与 XHR类似,但能实现安全可靠 的跨域通信。XDR对象的安全机制部分实现了 W3C的 CORS规范。以下是 XDR与 XHR的一些不同之 处。

  •  cookie不会随请求发送,也不会随响应返回。
  •  只能设置请求头部信息中的 Content-Type 字段。
  •  不能访问响应头部信息。
  •  只支持 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 属性中,如下所示。

var xdr = new XDomainRequest(); 
xdr.onload = function(){     
    alert(xdr.responseText); 
    
}; 

xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null); 

在接收到响应后,你只能访问响应的原始文本;没有办法确定响应的状态代码。而且,只要响应有 效就会触发 load 事件,如果失败(包括响应中缺少 Access-Control-Allow-Origin 头部)就会触 发 error 事件。遗憾的是,除了错误本身之外,没有其他信息可用,因此唯一能够确定的就只有请求 未成功了。要检测错误,可以像下面这样指定一个 onerror 事件处理程序。

var xdr = new XDomainRequest(); 
xdr.onload = function(){  
    alert(xdr.responseText); 
    
};
xdr.onerror = function(){  
    alert("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(){  
    alert(xdr.responseText); 
    
}; 
xdr.onerror = function(){   
    alert("An error occurred."); 
    
};
xdr.timeout = 1000; 
xdr.ontimeout = function(){    
    alert("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(){
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");

这个属性是通过 XDR对象影响头部信息的唯一方式。

其他浏览器对CORS的实现

Firefox 3.5+、Safari 4+、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){   
            alert(xhr.responseText);        
        } else {          
            alert("Request was unsuccessful: " + xhr.status);  
        }    
    }
}; 

xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null); 

与 IE中的 XDR对象不同,通过跨域 XHR对象可以访问 status 和 statusText 属性,而且还支 持同步请求。跨域 XHR对象也有一些限制,但为了安全这些限制是必需的。以下就是这些限制。

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

由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,好使用相对 URL,在访 问远程资源时再使用绝对 URL。这样做能消除歧义,避免出现限制访问头部或本地 cookie信息等问题。

Preflighted Reqeusts

CORS通过一种叫做 Preflighted Requests的透明服务器验证机制支持开发人员使用自定义的头部、 GET或 POST之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务 器发送一个 Preflight请求。这种请求使用 OPTIONS方法,发送下列头部。

  •  Origin:与简单的请求相同。
  •  Access-Control-Request-Method:请求自身使用的方法。
  •  Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。 以下是一个带有自定义头部 NCZ的使用 POST 方法发送的请求。

例如:

  • Access-Control-Allow-Origin: http://www.nczonline.net
  • Access-Control-Allow-Methods: POST, GET
  • Access-Control-Allow-Headers: NCZ
  • Access-Control-Max-Age: 1728000

Preflight 请求结束后,结果将按照响应中指定的时间缓存起来。而为此付出的代价只是第一次发送 这种请求时会多一次 HTTP请求。

支持 Preflight请求的浏览器包括 Firefox 3.5+、Safari 4+和 Chrome。IE 10及更早版本都不支持。

带凭据的请求

默认情况下,跨源请求不提供凭据(cookie、HTTP 认证及客户端 SSL 证明等)。通过将 withCredentials 属性设置为 true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请 求,会用下面的 HTTP头部来响应。

Access-Control-Allow-Credentials: true

如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给 JavaScript(于是,responseText 中将是空字符串,status 的值为 0,而且会调用 onerror()事件处 理程序)。另外,服务器还可以在 Preflight响应中发送这个 HTTP头部,表示允许源发送带凭据的请求。

支持 withCredentials 属性的浏览器有 Firefox 3.5+、Safari 4+和 Chrome。IE 10及更早版本都不 支持。

跨浏览器的CORS

即使浏览器对 CORS的支持程度并不都一样,但所有浏览器都支持简单的(非 Preflight和不带凭据 的)请求,因此有必要实现一个跨浏览器的方案。检测 XHR是否支持 CORS的简单方式,就是检查 是否存在 withCredentials 属性。再结合检测 XDomainRequest 对象是否存在,就可以兼顾所有浏 览器了。

function createCORSRequest(method, url){     
    var xhr = new XMLHttpRequest();  
    if ("withCredentials" in xhr){    
        xhr.open(method, url, true);    
    } else if (typeof XDomainRequest != "undefined"){    
        vxhr = new XDomainRequest();
        xhr.open(method, url);
        
    } else {       
         xhr = null;  
    }    
     return xhr;
} 

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 对象类似,都 提供了够用的接口,因此以上模式还是相当有用的。这两个对象共同的属性/方法如下。

  •  abort():用于停止正在进行的请求。
  •  onerror:用于替代 onreadystatechange 检测错误。
  •  onload:用于替代 onreadystatechange 检测成功。
  •  responseText:用于取得响应内容。
  •  send():用于发送请求。

以上成员都包含在 createCORSRequest()函数返回的对象中,在所有浏览器中都能正常使用。

其他跨域技术

在 CORS出现以前,要实现跨域 Ajax通信颇费一些周折。开发人员想出了一些办法,利用 DOM中 能够执行跨域请求的功能,在不依赖 XHR对象的情况下也能发送某种请求。虽然 CORS技术已经无处 不在,但开发人员自己发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务器端代码。

图像Ping

上述第一种跨域请求技术是使用标签。我们知道,一个网页可以从任何网页中加载图像,不 用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。正如之前所讨论过的,也可以动态地创 建图像,使用它们的 onload 和 onerror 事件处理程序来确定是否接收到了响应。

动态创建图像经常用于图像 Ping。图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。 请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204响应。通过 图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时 候接收到的。来看下面的例子。

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

具体实例如下

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>图像 Ping跨域请求</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <input id="btn" type="button" value=" 图像 Ping跨域请求">
    <div id="result"></div>
<script>
    var add = (function () {
        var counter = 0;
        return function () {
            return ++counter;
        }
    })();
    btn.onclick = function () {
        var sum = add();
        var img = result.getElementsByTagName('img')[0];
        if (!img) {
            var img = new Image();
        }
        img.height = "100";
        img.onload = img.onerror = function () {
            result.appendChild(img);
            var oSpan = document.getElementById('sum');
            if (!oSpan) {
                oSpan = document.createElement('span');
                oSpan.id = "sum";
            }
            oSpan.innerHTML = '发送请求的次数:' + sum;
            result.appendChild(oSpan);
        };
        if (sum % 2) {
            img.src = "http://7xpdkf.com1.z0.glb.clouddn.com/eg_bulboff.gif?sum=" + sum;
        } else {
            img.src = "http://7xpdkf.com1.z0.glb.clouddn.com/eg_bulbon.gif?sum=" + sum;
        }
    }
</script>
</body>
</html>

这里创建了一个 Image 的实例,然后将 onload 和 onerror 事件处理程序指定为同一个函数。**这 样无论是什么响应,只要请求完成,就能得到通知。**请求从设置 src 属性那一刻开始,而这个例子在请 求中发送了一个 name 参数。

图像 Ping常用于跟踪用户点击页面或动态广告曝光次数。图像 Ping有两个主要的缺点,一是只 能发送 GET 请求,二是无法访问服务器的响应文本。 因此,图像 Ping只能用于浏览器与服务器间的单 向通信

JSONP

JSONP是 JSON with padding(填充式 JSON或参数式 JSON) 的简写,是应用 JSON的一种新方法, 在后来的 Web 服务中非常流行。JSONP看起来与 JSON差不多,只不过是被包含在函数调用中的 JSON, 就像下面这样。


 callback({ "name": "Nicholas" }); 

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


 http://freegeoip.net/json/?callback=handleResponse

这个 URL是在请求一个 JSONP地理定位服务。通过查询字符串来指定 JSONP服务的回调参数是很 常见的,就像上面的 URL所示,这里指定的回调函数的名字叫 handleResponse()。

JSONP 是通过动态<script>元素来使用的,使用时可以为 src 属性指定一个跨域 URL。

这里的<script>元素与元素类似,都有能力不受限制地从其他域 加载资源。因为 JSONP是有效的 JavaScript代码,所以在请求完成后,即在 JSONP响应加载到页面中 以后,就会立即执行。来看一个例子。

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); 

这个例子通过查询地理定位服务来显示你的 IP地址和位置信息。

JSONP 之所以在开发人员中极为流行,主要原因是它非常简单易用。与图像 Ping 相比,它的优点 在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信 。不过,JSONP也有两点不足。

首先,JSONP是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代 码,而此时除了完全放弃 JSONP调用之外,没有办法追究。因此在使用不是你自己运维的 Web 服务时, 一定得保证它安全可靠。

其次,要确定 JSONP请求是否失败并不容易。虽然 HTML5给<script>元素新增了一个 onerror 事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就算这样也不能尽如人意,毕竟不是每个用户上网的速度和带宽都一样。

Comet

Comet是 Alex Russell发明的一个词儿,指的是一种更高级的 Ajax技术(经常也有人称为“服务器 推送”)。Ajax 是一种从页面向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技 术。Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。

有两种实现 Comet的方式:长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览 器定时向服务器发送请求,看有没有更新的数据。下图展示的是短轮询的时间线。

image

长轮询把短轮询颠倒了一下。页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到 有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页 面打开期间一直持续不断。图 展示了长轮询的时间线。

image

无论是短轮询还是长轮询,浏览器都要在接收数据之前,先发起对服务器的连接。两者大的区别 在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响 应。轮询的优势是所有浏览器都支持,因为使用 XHR对象和 setTimeout()就能实现。而你要做的就 是决定什么时候发送请求。

第二种流行的 Comet实现是 HTTP流。流不同于上述两种轮询,因为它在页面的整个生命周期内只 使用一个 HTTP连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周 期性地向浏览器发送数据。比如,下面这段 PHP脚本就是采用流实现的服务器中常见的形式。

<?php     
    $i = 0;     
    while(true){ 
     //输出一些数据,然后立即刷新输出缓存
     echo "Number is $i";        
     flush(); 
 
        //等几秒钟         
        sleep(10); 
 
        $i++;     
        
    } 
 

所有服务器端语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户 端)的功能。而这正是实现 HTTP流的关键所在。

在 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){                 
        alert("Received: " + data);              
}, function(data){                
        alert("Done!");             
}); 
 

这个 createStreamingClient()函数接收三个参数:要连接的 URL、在接收到数据时调用的函 数以及关闭连接时调用的函数。

有时候,当连接关闭时,很可能还需要重新建立,所以关注连接什么时候关闭还是有必要的。 只要 readystatechange 事件发生,而且 readyState 值为 3,就对 responseText 进行分割以 取得新数据。这里的 received 变量用于记录已经处理了多少个字符,每次 readyState 值为 3时都 递增。然后,通过 progress 回调函数来处理传入的新数据。而当 readyState 值为 4 时,则执行 finished 回调函数,传入响应返回的全部内容。

虽然这个例子比较简单,而且也能在大多数浏览器中正常运行(IE除外),但管理 Comet的连接是 很容易出错的,需要时间不断改进才能达到完美。浏览器社区认为 Comet 是未来 Web 的一个重要组成 部分,为了简化这一技术,又为 Comet创建了两个新的接口。

服务器发送事件

SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet交互推出的 API或者模式。SSE API 用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的 MIME 类型必须是 text/event-stream,而且是浏览器中的 JavaScript API 能解析格式输出。SSE 支持短轮 询、长轮询和 HTTP流,而且能在断开连接时自动确定何时重新连接。

有了这么简单实用的 API,再实 现 Comet就容易多了。 支持 SSE的浏览器有 Firefox 6+、Safari 5+、Opera 11+、Chrome和 iOS 4+版 Safari。

SSE API

SSE的 JavaScript API与其他传递消息的 JavaScript API很相似。要预订新的事件流,首先要创建一 个新的 EventSource 对象,并传进一个入口点:

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

注意,传入的 URL必须与创建对象的页面同源(相同的 URL模式、域及端口)。EventSource 的 实例有一个 readyState 属性,值为 0表示正连接到服务器,值为 1表示打开了连接,值为 2表示关闭 了连接。 另外,还有以下三个事件。

  • open:在建立连接时触发。
  • message:在从服务器接收到新事件时触发。
  • error:在无法建立连接时触发。

就一般的用法而言,onmessage 事件处理程序也没有什么特别的。

source.onmessage = function(event){ 
    var data = event.data;     //处理数据 
    
}; 
 

服务器发回的数据以字符串形式保存在 event.data 中。 默认情况下,EventSource 对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这 就意味着 SSE适合长轮询和 HTTP流。如果想强制立即断开连接并且不再重新连接,可以调用 close() 方法。

source.close(); 

事件流

所谓的服务器事件会通过一个持久的 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\nbar"(注意中间的换行符)。对于多个连续的以 data:开头的数据行,将作为多段数据解析, 每个值之间以一个换行符分隔。只有在包含 data:的数据行后面有空行时,才会触发 message 事件, 因此在服务器上生成事件流时不能忘了多添加这一行。 通过 id:前缀可以给特定的事件指定一个关联的 ID,这个 ID行位于 data:行前面或后面皆可:

data: foo
id: 1

设置了 ID后,EventSource 对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个 包含名为 Last-Event-ID 的特殊 HTTP头部的请求,以便服务器知道下一次该触发哪个事件。在多次 连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。

Web Sockets

要说令人津津乐道的新浏览器 API,就得数 Web Sockets了。Web Sockets的目标是在一个单独的 持久连接上提供全双工、双向通信。在 JavaScript中创建了 Web Socket之后,会有一个 HTTP请求发送 到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为Web Socket协议。也就是说,使用标准的 HTTP 服务器无法实现 Web Sockets,只有支持这种协议的专门服 务器才能正常工作。

由于 Web Sockets使用了自定义的协议,所以 URL模式也略有不同。未加密的连接不再是 http://, 而是 ws://;加密的连接也不是 https://,而是 wss://。在使用 Web Socket URL时,必须带着这个 模式,因为将来还有可能支持其他模式。

使用自定义协议而非 HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不 必担心 HTTP那样字节级的开销。由于传递的数据包很小,因此 Web Sockets非常适合移动应用。毕竟 对移动应用而言,带宽和网络延迟都是关键问题。使用自定义协议的缺点在于,制定协议的时间比制定 JavaScript API的时间还要长。Web Sockets曾几度搁浅,就因为不断有人发现这个新协议存在一致性和 安全性的问题。Firefox 4和 Opera 11都曾默认启用 Web Sockets,但在发布前夕又禁用了,因为又发现 了安全隐患。目前支持 Web Sockets的浏览器有 Firefox 6+、Safari 5+、Chrome和 iOS 4+版 Safari。

Web Sockets API

要创建 Web Socket,先实例一个 WebSocket 对象并传入要连接的 URL: var socket = new WebSocket("ws://www.example.com/server.php");

注意,必须给 WebSocket 构造函数传入绝对 URL。同源策略对 Web Sockets不适用,因此可以通 过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器。(通过握手信 息就可以知道请求来自何方。)

实例化了 WebSocket 对象后,浏览器就会马上尝试创建连接。与 XHR类似,WebSocket 也有一 个表示当前状态的 readyState 属性。不过,这个属性的值与 XHR并不相同,而是如下所示。

  • WebSocket.OPENING (0):正在建立连接。
  • WebSocket.OPEN (1):已经建立连接。
  • WebSocket.CLOSING (2):正在关闭连接。
  • WebSocket.CLOSE (3):已经关闭连接。

WebSocket 没有 readystatechange 事件;不过,它有其他事件,对应着不同的状态。readyState 的值永远从 0开始。

要关闭 Web Socket连接,可以在任何时候调用 close()方法。

socket.close(); 

调用了 close()之后,readyState 的值立即变为 2(正在关闭),而在关闭连接后就会变成 3。

发送和接收数据

Web Socket打开之后,就可以通过连接发送和接收数据。要向服务器发送数据,使用 send()方法 并传入任意字符串,例如:

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

因为 Web Sockets只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前, 必须进行序列化。下面的例子展示了先将数据序列化为一个 JSON字符串,然后再发送到服务器:

var message = {     
    time: new Date(),     
    text: "Hello world!",     
    clientId: "asdfp8734rew" 
    
}; 
socket.send(JSON.stringify(message)); 

接下来,服务器要读取其中的数据,就要解析接收到的 JSON字符串。 当服务器向客户端发来消息时,WebSocket 对象就会触发 message 事件。

这个 message 事件与 其他传递消息的协议类似,也是把返回的数据保存在 event.data 属性中。


socket.onmessage = function(event){   
    var data = event.data;  
 
    //处理数据 
    
}; 

与通过 send()发送到服务器的数据一样,event.data 中返回的数据也是字符串。如果你想得到 其他格式的数据,必须手工解析这些数据。

其他事件

WebSocket 对象还有其他三个事件,在连接生命周期的不同阶段触发。

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

WebSocket 对象不支持 DOM 2级事件侦听器,因此必须使用 DOM 0级语法分别定义每个事件处理程序。

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

 
socket.onopen = function(){      
    alert("Connection established."); 
    
}; 
 
socket.onerror = function(){  
    alert("Connection error."); 
    
}; 
 
socket.onclose = function(){    
    alert("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也是能实现双向通信的。

安全性

首先,可以通过 XHR访问的任何 URL也可以通过浏览器或服务器来访问。下面的 URL就是一个 例子。

/getuserinfo.php?id=23

如果是向这个 URL发送请求,可以想象结果会返回 ID为 23的用户的某些数据。谁也无法保证别 人不会将这个 URL的用户 ID修改为 24、56或其他值。因此,getuserinfo.php 文件必须知道请求者 是否真的有权限访问要请求的数据;否则,你的服务器就会门户大开,任何人的数据都可能被泄漏出去。

对于未被授权系统有权访问某个资源的情况,我们称之为 CSRF(Cross-Site Request Forgery,跨站 点请求伪造)==。==未被授权系统会伪装自己,让处理请求的服务器认为它是合法的。受到 CSRF攻击的 Ajax程序有大有小,攻击行为既有旨在揭示系统漏洞的恶作剧,也有恶意的数据窃取或数据销毁。

为确保通过 XHR访问的 URL安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。 有下列几种方式可供选择。

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

请注意,下列措施对防范 CSRF攻击不起作用。

  • 要求发送 POST 而不是 GET 请
  •  
  • 求——很容易改变。
  • 检查来源 URL以确定是否可信——来源记录很容易伪造。
  • 基于 cookie信息进行验证——同样很容易伪造。

XHR对象也提供了一些安全机制,虽然表面上看可以保证安全,但实际上却相当不可靠。实际上, 前面介绍的 open()方法还能再接收两个参数:要随请求一起发送的用户名和密码。带有这两个参数的 请求可以通过 SSL发送给服务器上的页面,如下面的例子所示。

xhr.open("get", "example.php", true, "username", "password"); //不要这样做!! 

即便可以考虑这种安全机制,但还是尽量不要这样做。把用户名和密码保存在 JavaScript代码中本身就是极为不安全的。任何人,只要他会使用 JavaScript调试器, 就可以通过查看相应的变量发现纯文本形式的用户名和密码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值