JS高程:读书摘要(十七)JSON & Ajax

一、JSON

JSON是一种数据格式,与XML相比,JSON 是在JavaScript中读写结构化数据的更好的方式。因为可以把JSON直接传给eval(),而且不必创建DOM对象。

  • JSON 对象没有声明变量(JSON 中没有变量的概念)
  • 没有末尾的分号,除最后一项外,每项末尾都需要有逗号
  • 对象的属性必须加双引
  • 属性的值可以是简单值,也可以是复杂类型值。
  • 能使用下标、方括号、点的方式直接访问 JSON对象中的属性值
1.1 JSON 解析与序列化
  • JSON.stringify()

把一个JavaScript对象序列化为一个JSON 字符串然后返回,默认情况下,JSON.stringify()输出的JSON 字符串不包含任何空格字符或缩进。

在序列化JavaScript 对象时,所有函数及原型成员都会被有意忽略,不体现在结果中。此外,值为undefined 的任何属性也都会被跳过。结果中最终都是值为有效JSON数据类型的实例属性。

还可以接收另外两个参数,第一个参数是个过滤器,可以是一个数组,也可以是一个函数;第二个参数是一个选项,表示是否在JSON字符串中保留缩进。

var book = {
    "title": "Professional JavaScript",
    "authors": [
        "Nicholas C. Zakas"
    ],
    "edition": 3,
    "year": 2011
};
var jsonText = JSON.stringify(book, ["title", "edition"]);
// 在返回的结果字符串中,就只会包含这两个属性:"title", "edition"

第二个参数是函数,行为会稍有不同。


var jsonText = JSON.stringify(book, function(key, value){
    switch(key){
        case "authors":
            return value.join(",")
        case "year":
            return 5000;
        case "edition":
            return undefined; // 通过返回undefined 删除该属性。
        default:
            return value;
    }
});

// {"title":"Professional JavaScript","authors":"Nicholas C. Zakas","year":5000}

第三个参数用于控制结果中的缩进和空白符。如果这个参数是一个数值,那它表示的是每个级别缩进的空格数。只要传入有效的控制缩进的参数值,结果字符串就会包含换行符。(只缩进而不换行意义不大。)最大缩进空格数为10,所有大于10 的值都会自动转换为10。在使用字符串的情况下,可以将缩进字符设置为制表符,或者两个短划线之类的任意字符

var jsonText = JSON.stringify(book, null, " - -");
{
--"title": "Professional JavaScript",
--"authors": [
----"Nicholas C. Zakas"
--],
--"edition": 3,
--"year": 2011
}
// 缩进字符串最长不能超过10 个字符长。如果字符串长度超过了10 个,结果中将只出现前10 个字符。
  • toJSON()

有时候,JSON.stringify()还是不能满足对某些对象进行自定义序列化的需求。在这些情况下,可以给对象定义toJSON()方法,在JSON.stringify()时返回其自定义数据格式。

序列化JSON对象的顺序如下:

(1) 如果存在toJSON()方法而且能通过它取得有效的值,则调用该方法。否则,返回对象本身。
(2) 如果提供了第二个参数,应用这个函数过滤器。传入函数过滤器的值是第(1)步返回的值。
(3) 对第(2)步返回的每个值进行相应的序列化。
(4) 如果提供了第三个参数,执行相应的格式化。

  • JSON.parse()

JSON字符串直接传递给JSON.parse()就可以得到相应的JavaScript 值,JSON.parse()方法也可以接收另一个参数,该参数是一个函数,将在每个键值对上调用。这个函数接收两个参数,一个键和一个值,而且都需要返回一个值。

var book = {releaseDate: new Date(2011, 11, 1)};
var jsonText = JSON.stringify(book);
var bookCopy = JSON.parse(jsonText, function(key, value){
    if (key == "releaseDate"){
        return new Date(value);
    } else {
        return value;
    }
})
alert(bookCopy.releaseDate.getFullYear()); 
// 最后解析出来的还是一个Date对象

二、Ajax

Ajax 技术的核心是XMLHttpRequest 对象(简称XHR),这是由微软首先引入的一个特性,其他浏览器提供商后来都提供了相同的实现。XHR 为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器取得更多信息,意味着用户单击后,可以不必刷新页面也能取得新据。也就是说,可以使用XHR 对象取得新数据,然后再通过DOM 将新数据插入到页面中。

使用

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

var xhr = new XMLHttpRequest();
xhr.open("get", "example.php", false);
xhr.send(null);
  • URL相对于执行代码的当前页面(当然也可以使用绝对路径);
  • 调用open()方法并不会真正发送请求,而只是启动一个请求以备发送。要发送特定的请求,必须调用send()方法,这里的send()方法接收一个参数,即要作为请求主体发送的数据。如果不需要通过请求主体发送数据,则必须传入null,因为这个参数对有些浏览器来说是必需的。调用send()之后,请求就会被分派到服务器。
  • 第三个参数false代表请求是同步的,JavaScript代码会等到服务器响应之后再继续执行,默认为true

服务器响应的数据会自动填充XHR对象的属性,相关的属性简介如下。

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

但多数情况下,我们还是要发送异步请求,才能让JavaScript 继续执行而不必等待响应。此时,可以检测XHR 对象的readyState 属性,该属性表示请求/响应过程的当前活动阶段。

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

只要readyState 属性的值由一个值变成另一个值,都会触发一次readystatechange 事件。可以利用这个事件来检测每次状态变化后readyState的值。通常,我们只对readyState 值为4的阶段感兴趣,因为这时所有数据都已经就绪。不过,必须在调用open()之前指定onreadystatechange事件处理程序才能确保跨浏览器兼容性。

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", "example.txt", true);
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
  • User-Agent:浏览器的用户代理字符串。

setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。

要成功发送请求头部信息,必须在调用open()方法之后且调用send()方法之前调用setRequestHeader()

xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
  • 响应头信息

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

GET请求

GET 是最常见的请求类型,最常用于向服务器查询某些信息。必要时,可以将查询字符串参数追加到URL 的末尾,以便将信息发送给服务器。对XHR而言,位于传入open()方法的URL末尾的查询字符串必须使用encodeURIComponent()进行编码。

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, false);
POST请求

POST 请求,通常用于向服务器发送应该被保存的数据。POST 请求应该把数据作为请求的主体提交,而GET请求传统上不是这样。POST 请求的主体可以包含非常多的数据,而且格式不限。

我们可以使用XHR 来模仿表单提交:首先将Content-Type头部信息设置为application/x-www-form-urlencoded,也就是表单提交时的内容类型,其次是以适当的格式创建一个字符串。

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)); // serialize(form) 自定义方法 用来序列化表单

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

<?php
    header("Content-Type: text/plain");
    echo <<<EOF
Name: {$_POST[‘user-name’]}
Email: {$_POST[‘user-email’]}
EOF;
?>
XMLHttpRequest 2 级
  • FormData对象

FormData 为序列化表单以及创建与表单格式相同的数据(用于通过XHR 传输)提供了便利

var data = new FormData();
data.append("name", "Nicholas");

append()方法接收两个参数:键和值,分别对应表单字段的名字和字段中包含的值。可以像这样添加任意多个键值对。

而通过向FormData 构造函数中传入表单元素,也可以用表单元素的数据预先向其中填入键值对。

var data = new FormData(document.forms[0]);

// 也可以将它直接传给XHR 的send()方法,
xhr.open("post","postexample.php", true);
xhr.send(new FormData(document.forms[0]));
  • 超时设定

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

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){
            //假设由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);

超时响应导致请求终止时,会调用ontimeout 事件处理程序。但此时readyStat可能已经改变为4了,这意味着会调用onreadystatechange 事件处理程序。可是,如果在超时终止请求之后再访问status 属性,就会导致错误。为避免浏览器报告错误,可以将检查status属性的语句封装在一个try-catch 语句当中。

  • overrideMimeType()方法

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

调用overrideMimeType()必须在send()方法之前,才能保证重写响应的MIME类型。

xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
进度事件
  • loadstart:在接收到响应数据的第一个字节时触发。
  • progress:在接收响应期间持续不断地触发。
  • error:在请求发生错误时触发。
  • abort:在因为调用abort()方法而终止连接时触发。
  • load:在接收到完整的响应数据时触发。
  • loadend:在通信完成或者触发errorabortload事件后触发。

每个请求都从触发loadstart 事件开始,接下来是一或多个progress事件,然后触发errorabortload事件中的一个,最后以触发loadend事件结束。

  • load事件

用以替代readystatechange 事件,响应接收完毕后将触发load事件,因此也就没有必要去检查readyState 属性了。

onload 事件处理程序会接收到一个event对象,其target 属性就指向XHR 对象实例,因而可以访问到XHR对象的所有方法和属性。然而,并非所有浏览器都为这个事件实现了适当的事件对象。所以建议依然使用xhr.status而非e.target.status

只要浏览器接收到服务器的响应,不管其状态如何,都会触发load 事件。而这意味着你必须要检查status属性,才能确定数据是否真的已经可用了

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事件

这个事件会在浏览器接收新数据期间周期性地触发。而onprogress事件处理程序会接收到一个event 对象,其target属性是XHR 对象,但包含着三个额外的属性:

  • lengthComputable:表示进度信息是否可用的布尔值。
  • position :表示已经接收的字节数byte
  • totalSize:表示根据Content-Length响应头部确定的预期字节数byte

为确保正常执行,必须在调用open()方法之前添加onprogress事件处理程序。

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

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

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

Origin: http://www.nczonline.net

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

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

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

IECORS的实现

微软在IE8中引入了XDRXDomainRequest)类型。这个对象与XHR 类似,但能实现安全可靠的跨域通信。XDR 对象的安全机制部分实现了W3CCORS规范。

以下是XDRXHR 的一些不同之处。

  • cookie 不会随请求发送,也不会随响应返回。
  • 只能设置请求头部信息中的Content-Type 字段。
  • 不能访问响应头部信息。
  • 只支持GETPOST 请求

这些变化使CSRFCross-Site Request Forgery,跨站点请求伪造)和XSSCross-Site Scripting,跨站点脚本)的问题得到了缓解。被请求的资源可以根据它认为合适的任意数据(用户代理、来源页面等)来决定是否设置Access-Control- Allow-Origin 头部。作为请求的一部分,Origin 头部的值表示请求的来源域,以便远程资源明确地识别XDR请求。

使用也与XHR对象非常相似,也是创建一个XDomainRequest的实例,调用open()方法,再调用send()方法。XDR 对象的open()方法只接收两个参数:请求的类型和URL。所有XDR请求都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发load事件,响应的数据也会保存在responseText属性中 。

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

如果失败(包括响应中缺少Access-Control-Allow-Origin 头部)就会触发error事件。遗憾的是,除了错误本身之外,没有其他信息可用,因此唯一能够确定的就只有请求未成功了。

也可以在请求返回前调用xdr.abort(); 来终止请求,也支持timeout属性以及ontimeout事件处理程序。

为支持POST请求,XDR 对象提供了contentType 属性,用来表示发送数据的格式。

xdr.open("post", "http://www.somewhere-else.com/page/");
xdr.contentType = "application/x-www-form-urlencoded";
xdr.send("name1=value1&name2=value2");
其他浏览器对CORS的实现

其他对象都通过XMLHttpRequest对象实现了对CORS的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的XHR对象并在open()方法中传入绝对URL即可。

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

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

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

Preflighted Reqeusts预检请求

CORS 通过一种叫做Preflighted Requests的透明服务器验证机制支持开发人员使用自定义的头部、使用GETPOST之外的方法,以及使用不同类型的主体内容。

在使用下列高级选项来发送请求时,就会向服务器发送一个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: myHeaders  

发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通。

  • Access-Control-Allow-Origin:与简单的请求相同。
  • Access-Control-Allow-Methods:允许的方法,多个方法以逗号分隔。
  • Access-Control-Allow-Headers:允许的头部,多个头部以逗号分隔。
  • 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: 1728000

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

带凭据的请求

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

Access-Control-Allow-Credentials: true

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

其他跨域技术

1、使用<img>标签

一个网页可以从任何网页中加载图像,不用担心跨域不跨域。图像Ping是与服务器进行简单单向的跨域通信的一种方式。

图像Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务器的响应文本。因此,图像Ping只能用于浏览器与服务器间的单向通信。

请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或204响应。通过图像Ping浏览器得不到任何具体的数据,但通过侦听loaderror事件,它能知道响应是什么时候接收到的。

var img = new Image();
img.onload = img.onerror = function(){
    alert("Done!");
};
// 为什么指定为同一个函数 因为只能知道响应是什么时候接收到的。
// 无论是什么响应,只要请求完成,就能得到通知
img.src = "http://www.example.com/test?name=Nicholas";
// 请求从设置src 属性那一刻开始

2、JSONP

JSONPJSON with padding(填充式JSON 或参数式JSON)的简写,是应用JSON 的一种新方法。JSONP 看起来与JSON 差不多,只不过是被包含在函数调用中的JSON。例如:callback({ "name": "Nicholas" });

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

JSONP 是通过动态<script>元素来使用的,使用时可以为src属性指定一个跨域URL。这里的<script>元素与<img>元素类似,都有能力不受限制地从其他域加载资源。

因为JSONP 是有效的JavaScript代码,所以在请求完成后,即在JSONP 响应加载到页面中以后,就会立即执行返回的JavaScript代码。(即调用定义在页面的回调函数,并将响应的数据当做参数传入)

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);
// 当script被添加进页面中之后 携带的js代码就会执行 触发回调

它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。’

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

其次,要确定JSONP 请求是否失败并不容易。虽然HTML5<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。

Comet: Ajax 的进一步扩展

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

有两种实现Comet 的方式:长轮询和流。

  • 长轮询

短轮询即浏览器定时向服务器发送请求,看有没有更新的数据。浏览器一请求,服务器就响应。

而长轮询则是页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。

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

  • HTTP

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

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

IE以外的浏览器中,通过侦听readystatechange事件及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。

随着不断从服务器接收数据,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 回调函数 每次传入的result都是接受的最新的数据
            progress(result);
        } else if (xhr.readyState == 4){
            finished(xhr.responseText); // 关闭连接时调用的函数
        }
    };
    xhr.send(null);
    return xhr;
}

1、服务器发送事件 SSE(Server-Sent Events)

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

  • SSE API

首先要创建一个新的EventSource对象,并传进一个入口点:

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

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

另外,还有以下三个事件。

  • open:在建立连接时触发。
  • message:在从服务器接收到新事件时触发。
  • error:在无法建立连接时触发。
source.onmessage = function(event){
    var data = event.data;
    //处理数据 服务器发回的数据以字符串形式保存在event.data 中。
};

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

  • 事件流

所谓的服务器事件会通过一个持久的HTTP 响应发送,这个响应的MIME类型为text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:

data: foo //  onmessage 第一次的event.data就会接受到 foo

data: bar  //  onmessage 第一次的event.data就会接受到bar

data: foo
data: bar  //  onmessage 第一次的event.data就会接受到"foo\nbar" 注意中间的换行符

// 只有在包含data:的数据行后面有空行时,才会触发message 事件,
// 因此在服务器上生成事件流时不能忘了多添加这一行。

通过id:前缀可以给特定的事件指定一个关联的ID,这个ID行位于data:行前面或后面皆可。

data: foo
id: 1

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

2、Web Sockets

是一种在客户端与服务器之间保持TCP长连接的网络协议,这样它们就可以随时进行信息交换。WebSockets不使用HTTP 协议,而使用一种自定义的协议。这种协议专门为快速传输小数据设计。虽然要求使用不同的Web 服务器,但却具有速度上的优势。

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

未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://。在使用Web Socket URL 时,必须带着这个模式,因为将来还有可能支持其他模式。

使用自定义协议而非HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不必担心HTTP 那样字节级的开销。由于传递的数据包很小,因此Web Sockets非常适合移动应用。毕竟对移动应用而言,带宽和网络延迟都是关键问题。

使用自定义协议的缺点在于,制定协议的时间比制定JavaScript API的时间还要长。目前支持Web Sockets的浏览器有Firefox 6+Safari 5+ChromeiOS 4+Safari

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

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

WebSocket 也有一个表示当前状态的readyState 属性。

  • 0:正在建立连接。
  • 1:已经建立连接。
  • 2:正在关闭连接。
  • 3:已经关闭连接。

WebSocket 没有readystatechange 事件;不过,它有其他事件,对应着不同的状态。readyState的值永远从0 开始。要关闭Web Socket 连接,可以在任何时候调用close()方法:socket.close(); 调用了close()之后,readyState 的值立即变为2(正在关闭),而在关闭连接后就会变成3

  • send()发送数据
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));

因为Web Sockets只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前,必须进行序列化。JSON.stringify()

  • onmessage事件接受数据

服务器向客户端发来消息时,WebSocket 对象就会触发message 事件。这个message事件与其他传递消息的协议类似,也是把返回的数据保存在event.data 属性中。

socket.onmessage = function(event){
    var data = event.data;
    //处理数据
};
// event.data 中返回的数据也是字符串。
// 如果你想得到其他格式的数据,必须手工解析这些数据。
  • 其他事件
    • open:在成功建立连接时触发。
    • error:在发生错误时触发,连接不能持续。
    • close:在连接关闭时触发

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

只有close 事件的event对象有额外的信息。这个事件的事件对象有三个额外的属性:

  • wasClean:是一个布尔值,表示连接是否已经明确地关闭;
  • code:是服务器返回的数值状态码;
  • reason:是一个字符串,包含服务器发回的消息。

3、选择

在考虑是使用SSE 还是使用Web Sockets 时,可以考虑如下几个因素。

首先,你是否有自由度建立和维护Web Sockets 服务器?因为Web Socket 协议不同于HTTP,所以现有服务器不能用于Web Socket 通信。SSE 倒是通过常规HTTP 通信,因此现有服务器就可以满足需求。

第二个要考虑的问题是到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩),那么SSE 比较容易实现。如果用例必须双向通信(如聊天室),那么Web Sockets 显然更好。别忘了,在不能选择Web Sockets 的情况下,组合XHRSSE 也是能实现双向通信的。

安全

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

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

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

  • 要求发送POST 而不是GET 请求——很容易改变。
  • 检查来源URL以确定是否可信——来源记录很容易伪造。
  • 基于cookie信息进行验证——同样很容易伪造。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,271评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,533评论 3 405
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,580评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,203评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,204评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,664评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,014评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,991评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,536评论 1 326
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,558评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,678评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,267评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,997评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,429评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,580评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,259评论 3 382
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,744评论 2 366

推荐阅读更多精彩内容