本章内容:使用 XMLHttpRequest对象、使用 XMLHttpRequest 事件、跨域 Ajax通信
2005年,Jesse James Garrett 发表了一篇在线文章,,题为“Ajax:A new Approach to Web Applications”。他在这篇文章中介绍了一种技术,用他的话来说,就叫 Ajax,是对 Asyncchronous JavaScript + XML 的缩写。这一技术能够像服务器请求额外的数据而无须卸载页面,会带来更好的用户体验。Garrett 还解释了怎样使用这一技术改变自从 Web诞生以来就一直沿用的“单机,等待”的交互模式。
Ajax技术的核心是 XMLHttpRequest 对象(检测 XHR),这是由微软首先引进的一个特性,其它浏览器提供商后来都提供了相同的实现。
一、XMLHttpRequest 对象
IE5 是第一款引入 XHR 对象的浏览器。XHR 对象通过 MSXML 库中的一个 ActiveX 对象实现。
因此,在 IE 中可能会遇到三种不同版本的 XHR 对象,即 MSXML2.XMLHttp、MSXML2.XMLHttp.3.0、MSXML2.XMLHttp.6.0。
创建适用于 IE7 之前的版本
function createXHR() {
if (typeof arguments.callee.activeXString != 'string') {
var version = ['MSXML2.XHLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp']
for (var i = 0, len = version.length; i< len; i++) {
try {
new ActiveXObject(version[i])
arguments.callee.activeXString = version[i]
break;
} catch(err) {
}
}
}
return new ActiveXObject(arguments.callee.activeXString)
}
这个函数会激励根据IE中可用的 MSXML 库的情况创建最新版本的 XHR 对象
IE7+、Firefox、Opera、Chrome、Safari 都支持原生的 XHR 对象,在这些浏览器中创建 XHR 对象要像下面这样使用 XMLHttpRequest 构造函数
var xhr = new XMLHttpRequest()
如果想要兼顾 IE的早起版本,那么则可以在这个 createXHR() 函数中加入对原生 XHR 对象的支持。
function createXHR() {
if (typeof XMLHttpRequest != 'undefined') {
return new XMLHttpRequest()
} else if (typeof ActiveXObject != 'undefined') {
if (typeof arguments.callee.activeXString != 'string') {
var version = ['MSXML2.XHLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp']
for (var i = 0, len = version.length; i< len; i++) {
try {
new ActiveXObject(version[i])
arguments.callee.activeXString = version[i]
break;
} catch(err) {
}
}
}
return new ActiveXObject(arguments.callee.activeXString)
} else {
throw new Error('No XHR object availbale.')
}
}
然后,就可以使用下面的代码在所有浏览器中创建 XHR 对象了
var xhr = createXHR()
1.1、XHR 的用法
在使用 XHR 对象时,要调用的第一个方法是 open(),他接受 3 个参数:
- 要发送的请求类型(get、post等)
- 请求的URL
- 表示是否异步发送请求的布尔值
下面是调用这个方法的实例:
xhr.open('get', 'example.php', false)
调用 open() 方法并不会真正发送请求,而只是启动一个请求以备发送
第二步是,调用 send() 方法,接收一个参数:
- 作为请求主题发送的数据。如果不需要通过请求主体发送数据,则必须传入 null,因为这个参数对有些浏览器来说是必须的。
调用 send() 之后,请求就会被分派到服务器。
在接收到响应后,响应的数据会自动填充 XHR 对象的属性,相关的属性简介如下:
- responseText:作为响应主题被返回的文本。
- responseXML:如果响应的内容类型是“text/xml” 或 “application/xml”,这个属性中将保存包含着响应数据的 XML DOM 文档。
- status:响应的Http状态。
- statusText:Http状态的说明。
在接收到响应后,第一步是检查 status 属性,已确定响应已经成功返回。一般来说,可以将 HTTP 状态码为 200 作为成功的标志。状态码 为 304 表示请求资源并没有被修改,可以直接使用浏览器缓存的版本;
为了确保接受都适当的响应,应该像下面这样检测上述的两种状态代码。
xhr.open('get', 'example.txt', false)
xhr.send(null)
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { // 成功状态
// todo
} else { // 失败状态
// todo
}
无论内容类型是什么,响应主题的内容都后悔保存到 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 = new createXHR()
xhr.onreadystatechange = function(event) {
if (xhr.readyState == 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { // 成功状态
// todo
} else { // 失败状态
// todo
}
}
}
xhr.open('get', 'example.txt', true)
xhr.send(null)
另外,在接收到响应之前还可以调用 abort() 方法来取消异步请求
xhr.abort()
调用这个方法后,XHR对象会停止触发事件,而且也不再允许访问任何域响应有关的对象属性。
1.2、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 = new createXHR()
xhr.onreadystatechange = function(event) {
// ...
}
xhr.open('get', 'example.php', true)
xhr.setRequestHeader('MyHeader', 'MyValue') // 设置自定义请求头信息
xhr.send(null)
建议使用 自定义的 头部字段名称,不要使用浏览器正常发送的字段名称,否则有 可能 会影响服务器的响应。有的浏览器允许开发人员重写默认的头部信息,但有的浏览器则不允许这样做。
相应的,使用 getResponseHeader() 方法并传入头部字段名称,可以去的相应的响应头部信息。而调用 getAllResponseHeaders() 方法则可以取得一个包含所有头部信息的长字符串。
var myHeader = xhr.getResponseHeader('myHeader')
var allHeaders = xhr.getAllResponseHeaders()
1.3、GET 请求
GET 是最常见的请求类型学,最常用于向服务器查询某些信息。可以将查询字符串参数最佳到 URL 的末尾,以便将信息发送给服务器。传入 open() 方法的 URL 末尾的查询字符串必须经过正确的编码才行。
建议对查询字符串中每个参数的名称和值都使用 encodeURIComponent() 进行编码
下面这个函数可以辅助向现有 URL 的末尾添加查询字符串参数:
function addURLParam(url, key, value) {
url += (url.indexOf('?') == -1 ? '?' : '&')
url += encodeURIComponent(key) + '=' + encodeURIComponent(value)
return url
}
下面是使用这个函数构建请求 URL 的示例:
var url = 'example.php'
// 添加参数
url = addURLParam(url, 'name', '纤风')
url = addURLParam(url, 'friend', '了凡')
// example.php?name=%E8%86%BE%E3%82%89%EF%BF%BD%EF%BF%BD&friend=%E7%AF%8B%EF%BF%BD%EF%BF%BD%EF%BF%BD
// 初始化请求
xhr.open('get', url, false)
// ....
1.4、POST 请求
使用评论仅次于 GET 的是 POST 请求,通常用于向服务器发送应该被保存的数据。POST 请求应该把数据作为请求的主体提交,POST请求的主体可以包含非常多的数据,而且格式不限。
第一步首先初始化一个 POST 请求。
xhr.open('post', 'example.php', true)
第二步是向 send() 方法中传入某些数据。
默认情况下,服务器对 POST 请求和 提交Web 表单的请求并不会一视同仁。因此,服务器端必须有程序来读取发送过来的原始数据,并从中解析出有用的部分。不过,我们可以使用 XHR 来模仿表单提交:
首先将 Content-type 头部信息设置为 application/x-www-form-urlencoded表单提交内容类型
其次是创建一个适当格式的字符串
xhr.onreadystatechange = function(event) {}
xhr.open('post', 'postexample.php', true)
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(serialize(document.forms[0])) // 序列化
关于更多的 请求类型风格规范,可以参考 restful接口风格
二、XMLHttpRequest 2级
鉴于 XHR 已经得到广泛接受,成为了事实标准,W3C 也着手制定相应的标准以规范其行为。XMLHttpRequest 1级 只是把已有的 XHR 对象的实现细节描述了出来。而XMLHttpRequest2 级则进一步发展了 XHR。
2.1、FormData
现代 Web 应用中频繁使用 的一项功能就是表单序列化,XMLHttpRequest 2级为此定义了 FormData 类型。FormData 为序列化表单以及创建于表单格式相同的数据提供了便利。
下面创建一个 FormData 对象,并向其中添加了一些数据。
var data = new FormData()
data.append('name', '风子')
append() 方法接受两个参数:
- 键——对应表单的名字
- 值——对应名字包含的值
通过向 FormData 构造函数中传入表单元素,也可以用表单元素的数据预先向其中填入键值对儿
var data = new FormData(document.forms[0])
// ...
xhr.send(data)
使用 FormData的方便之处体现在 不必明确地在 XHR 对象上设置请求头。XHR 对象能够识别传入的数据类型是 FormData 的实例,并配置适当的头部信息。
2.2、超时设定
IE8 为 XHR 对象添加了一个 timeout 属性,表示请求在等待响应多少毫秒之后就终止。如果在规定的时间内浏览器还没有接收到响应,那么就会触发 timeout 事件,进而会调用 ontimeout 事件处理程序。
var xhr = new createXHR()
xhr.onreadystatechcange = fucntion(event) {}
xhr.open('get', 'timeout.php', true)
xhr.timeout = 1000 // 设置超时时间,1s
xhr.ontimeout = function(event) { // 超时 事件监听
console.log('you are late')
}
xhr.send(null)
需要注意的是:超时的情况下,readyState 也可能为4。但这是很去访问 xhr.status 就会导致错误。为了避免这种错误,可以将检测 state属性的语句,包含在 try-catch 语句块中,
2.3、overrideMimeType() 方法
Firefox 最早引入了 overrideMimeType() 方法,用于重写 XHR 响应的 MIME 类型。这个方法后来也被纳入了 XMLHttpRequest 2级规范。因为返回响应的 MIME 类型决定了 XHR 对象如何处理它,所以提供一种方法能够重写服务器返回的MIME类型是很有用的。
xhr.overrideMimeType('text/xml')
xhr.send(null)
调用 overrideMimeType() 必须在 send() 方法之前,才能保证重写响应的 MIME 类型
三、进度事件
Progress Events规范,定义了与客户端服务器通信的有关事件。这些事件最早其实只针对 XHR 操作,但也被其它 API 借鉴,有以下6个进度事件。
- loadstart:在接受到响应数据的第一个字节时触发
- progress:在接收响应期间持续不断地触发
- error:在请求发生错误时触发。
- abort:在因为调用 abort() 方法而终止连接时 触发
- load:在接收到完整的响应数据时触发
- loadend:在通信完成或者触发 error、abort、load 事件后触发
这些事件大都很直观,但其中有两个事件有一些细节需要注意。
3.1、load 事件
Firefox 在实现XHR 对象的某个版本时,曾致力于简化异步交互模型。最终,Firefox 实现中引入了 load 事件,用以替代 readystatechange 事件。
onload 事件 处理程序会接收到一个 event 对象,其 target 属性,就指向XHR对象实例,因而可以访问到XHR对象的所有方法和属性。
然而,并非所有浏览器都为这个事件实现了适当的事件对象。结果,还是需要使用到xhr变量
var xhr = createXHR()
xhr.onload = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) { // 成功状态
// todo
} else { // 失败状态
// todo
}
}
// xhr.open()
// xhr.send()
3.2、progress 事件
Mozilla 对 XHR 的另一个 革新就是添加了 progress 事件,这个事件会在浏览器接受新数据期间周期性触发
onprogress 事件处理程序会接收到一个 event 对象,其 target 属性是 XHR 对象,但包含三个额外的属性:
- lengthComputable——是一个表示进度信息是否可用的布尔值
- position——表示已经接受的字节数
- totalSize——表示根据 Content-Length 响应头部确定的预期字节数
有了这些信息,我们就可以为用户创建一个进度指示器
var xhr = new createXHR()
xhr.onload = function(event) {}
xhr.onprogress = function(event) {
var divStatus = document.getElementById('status') // 用于显示进度的 DOM 元素
if (event.lengthComputale) {
divStatus.innerHTML = 'Received ' + event.postion + ' of ' + event.totalSize + ' bytes';
}
}
xhr.open('get', 'altevents.php', true)
xhr.send(null)
为了确保正常执行,必须在调用 open() 方法之前添加 onporgress 事件处理程序。
如果响应头部中包含 Content-Length 字段,那么也可以利用此信息来计算从响应中已经接收到的数据的百分比。
四、跨域源资源共享
通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含它的页面位于同一个域中的资源。这种安全策略可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。
CORS(Cross-Origin Resource Sharing,跨域源资源共享),定义了必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是否应该成功。
下面是 Origin 头部的一个示例:
Origin: http://www.nczoline.net
如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公公资源,可以回发“*”)。
例如:
Access-Control-Allow-Origin: http://www.nczonline.net
// Access-Control-Allow-Origin: *
如果没有这个头部,或者有这个头部但源信息不匹配,浏览器都会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含 cookie 信息。
4.1、IE 对 CORS 的实现
微软在 IE8 中引入了 XDR(XDomainRequest)类型。这个对象与XHR类似,但能实现安全可靠的跨域通信。XDR对象的安全机制部分实现了对 W3C 的 CORS 规范。以下是 XDR 与 XHR 的一些不同之处。
- cookie 不会随着请求发送,也不会随响应返回
- 只能设置请求头部信息中的 Content-Type 字段。
- 不能访问响应头信息
- 只能支持 GET 和 POST 请求
XDR 对象的使用方法和 XHR 对象非常相似。也是创建一个 XDomainRequest 的实例,调用 open() 方法,再调用 send() 方法。丹玉 XHR 对象的 open() 方法不同,XDR对象的 open() 方法只接受两个参数:
- 请求的类型
- URL
所有的 XDR 请求都是异步执行的,不能用它来创建同步请求。请求返回之后,会触发 load 事件,响应的数据也会保存在 responseText 属性中。
如下所示:
var xdr = new XDomainRequest()
xdr.onload = function() {
alert(xdr.respoonseText)
}
xdr.open('get', 'http://www.somewhere-else.com/pages/')
xdr.send(null)
只要响应有效就会触发 load 事件,如果失败(包括响应中缺少 Access-Control-Allow-Origin 头部)就会触发 error 事件。
要检测错误,可以像下面这样指定一个 onerror 事件处理程序
xdr.onerror = function() {
alert('GG')
}
与 XHR 一样,XDR 对象也支持 timeout 属性 以及 ontimeout 事件处理程序。
var xdr = new XDomainRequest()
xdr.onload = function() {
alert(xdr.responseText)
}
xdr.onerror = function() {
alert('An error occured')
}
xdr.timeout = 1000
xdr.ontimeout = function() {
alert('Requesy took too long')
}
xdr.open('get', 'http://www.somewhere-else.com/page')
xdr.send(null)
为支持 POST 请求,XDR对象提供了 contentType 属性,用来表示发送数据的格式
var xdr = new XDomainRequest()
xdr.onload = function() {
// todo
}
xdr.onerror = function() {
// todo
}
xdr.open('post', 'http://www.somewhere-else.com/page/')
xdr.contentType = 'application/x-www-form-urlencoded'
xdr.send('name1=value1&name2=value2')
4.2、其他浏览器对 CORS 的实现
Firefox3.5+、Safari4+、Chrome、iOS版 Safari 和 Android 平台中的 WebKit 都通过 XMLHttpRequest 对象实现了 对象 CORS 的原生支持。要请求位于另一个域中的资源,使用标准的 XHR 对象并在 open() 方法中传入绝对的 URL 即可。
例如:
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
// todo
}
xhr.open('get', 'http://www.xxx.com/xxx/', true)
xhr.send(null)
跨域的 XHR 对象也有一些限制,但为了安全这些限制时必须的。
- 不能使用 setRequestHeader() 设置自定义头部
- 不能发送和接收 cookie
- 调用 getAllResponseHeaders() 方法总会返回空字符串
4.3、Preflighted Reqeusts
CORS 通过 一种叫做 Preflighted Requests 的透明服务器验证机制支持开发人员使用 自定义的头部、GET 或 POST 之外的方法,以及不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个 Preflight请求。这种请求使用 OPTIONS 方法,发送下列头部
- Origin:与简单的请求相同
- Access-Control-Request-Method:请求自身使用的方法
- Access-Cotrol-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔
以下是一个带有自定义头部 NCZ 的使用 POST 方法发送的请求。
Origin:http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通:
- 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, PUT
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
Preflight 请求结束后,结果将按照响应中指定的事件缓存起来。而为此付出的代价只是第一次发送这种请求会多一次HTTP 请求。
4.4、带凭据的请求
默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端 SSL 证明等)。通过将 withCredentials 属性设置为 true,可以指定某个请求应该发送凭据。如果服务器接受带凭据的请求,会用下面的 HTTP 头部来响应。
Access-Control-Allow-Credentials: true
4.5、跨浏览器的 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') {
xhr = 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():用于发送请求
五、其他跨域技术
在 CORS 出现之前,要实现 跨域 Ajax 通信颇费一些周折。开发人员想出了一些办法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。虽然 CORS 技术已经无处不在,但开发人员自己发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务端代码。
5.1、图像 Ping
第一种跨域请求技术是 使用 <img>标签。一个网页可以从任何网页中加载图像,不用担心跨域不跨域。也可以动态创建图像,使用它们的 onload 和 onerror 事件处理程序来确定是否接收到响应。
动态创建图像经常用于图像 Ping。图像Ping 是与服务器 进行简单、单向的跨域通信的一种方式。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,他能知道响应是什么时候接受到的。
如下示例:
var img = new Image()
img.onload = img.error = function() {
console.log('Done')
}
img.src = 'http://www.example.com/test?name=Nicolas'
图像 Ping 最常用于 跟踪用户点击页面或动态广告曝光次数。图像 Ping 有两个主要的确定,一是只能发送 GET 请求,而是无法访问服务器的响应文本。因此,图像 Ping 只能用于浏览器与服务器的单向通行。
5.2、 JSONP
JSONP 是 JSON with padding(填充式 JSON 或 参数式JSON)的简写,JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON,就像下面这样。
callback({"name": "Nicholas"})
JSONP 由两部分组成:
- 回调函数——当响应到来时应该在页面中调用的函数,回调函数的名字一般是在强求中指定的。而数据就是传入回调函数中的 JSON 数据。
- 数据
下面是一个典型的JSONP请求。
http://freegeoip.net/json/?callback=handleResponse
这里指定的回掉函数的名字叫 handleResponse()
JSONP 是通过动态 <script> 元素 来使用的,使用时可以为 src 属性指定一个跨域的 URL。因为 JSONP 是有效的 Javascript 代码,所以在请求完成后,即在 JSONP 响应加载到页面中以后,就会立即执行。
如下示例:
var script = document.createElement('script')
script.src = 'http://freegeoip.net/json/?callback=handleResponse'
document.body.insertBefore(script, document.body.firstChild)
JSONP 之所以在开发人员中极为流行,主要原因是它非常简单易用。它的有点在于能够 访问响应文本,支持在浏览器与服务器之间双向通信。不过JSONP 也有两点不足:
- 首先,JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码。
- 其次,要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 <script> 元素新增了一个 onerror 事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用 计时器 检测指定时间内是否接收到了响应。但就这样也不能尽如人意,毕竟不是每个用户上网的速度和宽度都一样。
5.3、Comet
Comet 是 Alex Russell 发明的一个词,指的是一种更高级的 Ajax 技术(经曾也有人称为“服务器推送”)Ajax 是一种从页向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技术。Comet 能够让信息几乎实时地被推送到页面上,非常适合处理体育比赛的分数 和 股票报价。
有两种实现 Comet 的方式:长轮询和流。长轮询是传统轮询(也称为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看看有没有更新的数据。
下图展示的是短轮询的时间线:
长轮询把短轮询颠倒了以下。页面发起一个到服务器的请求,然后服务器一直保持链接打开,知道有数据可发送。发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求。这一过程在页面打开期间一直持续不断。
下图展示的是长轮询的时间线:
无论是短轮询还是长轮询,浏览器都要在接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用 XHR 对象和 setTimeout() 就能实现。而需要做的就是决定什么时候发送请求。
第二种流行的 Comet 实现就是 HTTP 流。流不同于上述两种轮询,因为它能在页面的整个生命周期内只使用一个 HTTP 连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后服务器周期性地向浏览器发送数据。
而浏览器通过侦听 readystatechange 事件及检测 readtState 的值是否为3,就可以利用 XHR 对象实现 HTTP 流。随着不断从服务器接收数据,readyState 的值会周期性地变为3.当 readyState 变成3时,responseText 属性中就会保存接收的 所有数据。此时,就需要比较此前接收到的数据,决定从什么位置开始取得最新的数据。
使用 XHR 对象实现HTTP流的 典型代码如下
function createSteamingClient(url, progress, finished) {
var xhr = new XMLHttpRequest()
var 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()
} else if(xhr.readyState == 4) {
finished(xhr.responseText)
}
}
xhr.send(null)
return xhr
}
var client = createSteamingClient('streaming.php', function(data) {
console.log('Received' + data)
}, function(data) {
console.log('Done')
})
5.4、服务器发送事件
SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet 交互推出的 API 或者模式。 SSE API用于创建到服务器的 单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的 MIME 类型必须是 text/event-stream,而且是浏览器中的 JavaScript API 能解析格式属性。
SSE 支持短轮询、长轮询 和 HTTP 流,而且能在断开连接时自动确定何时重新连接。
5.4.1、SSE API
SSE 的 JavaScript API 与其他传递消息的 JavaScript API 很相似。要预定新的事件流,首先要创建一个新的EventSource 对象,并传入一个入口点:
var source = new EventSource('myevents.php')
传入的 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()
5.4.2、事件流
所谓服务器时间会通过一个持久的 HTTP 响应发送,这个响应的 MIME 类型为 text/event-stream。 响应的格式是存文本,最简单的情况是每个数据都带有前缀 data:
例如:
data: foo
data: bar
data: foo
data: bar
只有在包含 data: 的数据航后面有空行时,才会触发 message 事件
通过 id: 前缀 可以给特定的事件流指定一个关联的ID,这个ID行位于 data: 行前面或后面皆可:
data: foo
id: 1
设置了 ID 后,EventSource 对象会跟踪上一次触发的事件。如果断开连接,会向服务器发送一个包含为 Last-Event-ID 的特殊 HTTP 头部的请求,以便服务器知道下一次该触发哪个事件。
5.5、Web Sockets
要说最令人津津乐道的新浏览器API,就得数 Web Sockets了。Web Sockets 的目标是一个单独的持久连接上提供全双工、双向通信。在JavaScript中创建了 Web Sockets 之后,会有一个 HTTP 请求发送到浏览器已发起连接。在取得服务器响应后,建立的连接会使用HTTP 升级从 HTTP 协议交换为 Web Sockets 协议。
由于 Web Sockets 使用了自定义的协议,所以 URL 模式也略有不同,未加密的连接不再是 http:// 而是 ws://;加密的连接也不再是https://,而是 wss://
- 使用自定义协议而非 HTTP 协议的优点是,能够在客户端和 服务器之间发送非常少量的数据,而不必担心 HTTP 那样字节级的开销。由于传递的数据包很小,因此 Web Sockets 非常适合移动应用。
- 使用自定义协议的缺点在于,指定协议的时间不制定 JavaScript API 的时间还要长。Web Sockets 曾几度搁浅,就因为不断有人发现这个新协议存在一致性和安全性的问题。
5.5.1 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开始。
要关闭 WebSockets 的连接,可以在任何时候调用 close() 方法。
socket.close()
5.5.2、发送和接收数据
Web Socket 打开之后,就可以通过连接发送和接收数据。要向服务器发送数据,使用 send() 方法,并传入任意字符串。
例如:
var socket = new WebSocket('ws://www.example.com/server.php')
socket.send('Hello World!')
WebSocket 只能通过连接发送纯文本数据,所以对于复杂的数据结构,再通过连接发送之前必须进行序列化
var message = {
time:new Date(),
text:"Hello world!",
cliendId: "asdfp56sd"
}
socket.send(JSON.stringify(message))
当服务器向客户的发来消息是,WebSocket 对象就会触发 message 事件。这个 message 事件与其他传递消息的协议类似,也是吧返回的数据保存在event.data 属性中。
socket.onmessage = function(event) {
var data = event.data
}
与通过 send() 发送到服务器的数据一样, event.data 中返回的数据也是字符串。如果你想得到其他格式的数据,必须手工解析这些数据。
5.5.3、其他事件
WebSocket 对象还有其他三个事件,在连接生命周期的不同阶段触发
- open:在成功建立连接时触发
- error:在发生错误时触发,连接不能持续。
- close:在连接关闭时触发
WebSocket 对象不支持 DOM 2级事件侦听器,因此必须使用 DOM 0级语法分别定义每个事件处理程序
var socket = new WebSocket('ws://example.com/server.php')
socket.onopen = function() {
console.log('Connection established')
}
socket.onerror = function() {
console.log('Connection error')
}
socket.onclose = function() {
console.log('Connection close')
}
其中 close 事件的 event对象有额外的信息,这个事件的事件对象有三个额外的属性:
- wasClean:布尔值,便是连接是否已经明确地关闭
- code:是服务器返回的数值状态码
- reason:是一个字符串,包含服务器返回的消息
5.6、SSE 与 Web Sockets
面对某个具体的用例。在考虑使用 SSE 还是 Web Sockets 时,可以考虑如下几个因数。
- 首先,你是否有自由度建立和维护 Web Sockets 服务器?因为 Web Socket 协议不同于 HTTP,所以现有服务器不能用于 Web Socket 通信。SSE倒是通过常规 HTTP 通信,因此现有服务器就可以满足需求。
- 其次要考虑的问题是到底需不需要双向通信。如果用例只需要读取服务器数据(如比赛成绩),那么 SSE 比较容易实现。如果用力必须双向通信(如聊天室),那么 Web Sockets 显然更好。在不能选择 Web Sockets的情况下,组合 XHR 和 SSE 也是能实现双向通信的。
六、安全
讨论 Ajax 和 Comet 安全的文字可谓是连篇累牍,但我们可以从普通意义上探讨一些基本问题。
首先,可以通过XHR 访问的任何 URL 也可以通过 浏览器或 服务器来访问。
下面的URL就是一个例子:
/getuserinfo.php?id = 23
无法保证别人不会将这个 URL 的用户 ID 改为24、25或其他值。因此,getuserinfo.php 必须知道请求者 是否真的有权限要请求的数据;否则,你的服务器就会门户大开,任何人的数据都可能被泄漏出去。
对于未被授权系统有权访问某个资源的情况,我们称之为 CSRF(Cross-Site Request Forgery,跨站点请求伪造)。未被授权系统会伪装自己,让处理请求的服务器认为它是合法的。
为确保通过 XHR 访问的 URL 安全,同喜的做法就是验证发送请求者是否有权限访问相应的资源。有下列几种方式可供选择:
- 要求以 SSL 连接来访问 通过 XHR 请求的资源
- 要求每一次请求都要附带经过相应算法计算得到的验证码。
下列措施 对 防范 CSRF 攻击不起作用
- 要求发送 POST 而不是 GET 请求——很容易改变
- 检测来源 URL 以确定是否可信——来源记录很容易伪造
- 基于 cookie 信息进行验证——同样很容易伪造。
XHR 对象也提供了一些安全机制,虽然表面上看可以保证安全,但实际上却相当不可靠。
七、 小结
Ajax 是无需刷新页面就能够从服务器取得数据的一种方法。
- 负责 Ajax 运作的核心对象是 XMLHttpRequest 对象
- 同源策略是对 XHR 的一个主要限制,解决方案为 CORS,IE8通过 XDomainRequest对象支持CORS,其他浏览器通过 XHR 对象原生支持 CORS。图像 Ping 和 JSONP 是另外两种跨域通信的技术,但不如 CORS 稳妥。
- Comet 是对 Ajax的进一步扩展,让服务器几乎能够实时的向客户端推送数据。实现 Comet 的手段主要有两个:长轮询、HTTP流。SSE是一种实现 Comet 交互的浏览器API,即支持 长轮询,也支持 HTTP 流。
- Web Sockets 是一种与服务器进行双全工、双向通信的信道。使用自定义协议,专门为传输小数据设计,具有速度上是的优势。