本章内容:使用 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
第一种跨域请求技术是 使用 标签。一个网页可以从任何网页中加载图像,不用担心跨域不跨域。也可以动态创建图像,使用它们的 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 是通过动态