一、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
状态。 -
statusText
:HTTP
状态的说明。
但多数情况下,我们还是要发送异步请求,才能让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]));
- 超时设定
IE8
为XHR
对象添加了一个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
类型,即使数据是XML
,responseXML
属性中仍然是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
:在通信完成或者触发error
、abort
或load
事件后触发。
每个请求都从触发loadstart
事件开始,接下来是一或多个progress
事件,然后触发error
、abort
或load
事件中的一个,最后以触发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);
跨域
CORS
(Cross-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
信息。
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
请求。
使用也与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
对象可以访问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
:(可选)自定义的头部信息,多个头部以逗号分隔。
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
请求。
带凭据的请求
默认情况下,跨源请求不提供凭据(cookie
、HTTP
认证及客户端SSL
证明等)。通过将withCredentials
属性设置为true
,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会用下面的HTTP
头部来响应。
Access-Control-Allow-Credentials: true
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头部,那么responseText
中将是空字符串,status
的值为0
,而且会调用onerror()
事件处理程序。另外,服务器还可以在Preflight
响应中发送这个HTTP
头部,表示允许源发送带凭据的请求。
其他跨域技术
1、使用<img>
标签
一个网页可以从任何网页中加载图像,不用担心跨域不跨域。图像Ping
是与服务器进行简单、单向的跨域通信的一种方式。
图像Ping
有两个主要的缺点,一是只能发送GET
请求,二是无法访问服务器的响应文本。因此,图像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";
// 请求从设置src 属性那一刻开始
2、JSONP
JSONP
是JSON 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+
、Chrome
和iOS 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
的情况下,组合XHR
和SSE
也是能实现双向通信的。
安全
为确保通过XHR 访问的URL 安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。
有下列几种方式可供选择。
- 要求以
SSL
连接来访问可以通过XHR
请求的资源。 - 要求每一次请求都要附带经过相应算法计算得到的验证码。
请注意,下列措施对防范CSRF
攻击不起作用。
- 要求发送
POST
而不是GET
请求——很容易改变。 - 检查来源
URL
以确定是否可信——来源记录很容易伪造。 - 基于
cookie
信息进行验证——同样很容易伪造。