Ajax 与 Comet

本章内容:使用 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 是一种与服务器进行双全工、双向通信的信道。使用自定义协议,专门为传输小数据设计,具有速度上是的优势。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容

  • 2002年,我进入了青岛的一所大学,开始了我的自考大学生涯。坏处是这里相对统招学校考试极其困难,好处是让我开始从学...
    庸亲王阅读 322评论 0 0
  • 原创作者/摄影:戴德文。版权所有,转载时请注明作者及文章出处。 如果说相片是定格的时光碎片,那么整理相片便像是一个...
    影像派阅读 702评论 0 5
  • 上完最后一节课,带着咕噜噜的肠子走出教室,走在前面的是两个女生,并排着走,从背后并不能认出是谁,因为我向来都是低着...
    臭小妈妈阅读 147评论 0 0