cy0707/Learn_JavaScript

Ajax(03)

Opened this issue · 0 comments

其他跨域技术

在 CORS 出现之前,要实现 Ajax 跨域通信是比较困难的。开发人员们想出了一些方法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。

图像Ping

我们知道一个网页可以从任何网页中加载图像。不用担心跨不跨域。可以动态地创建图像,使用他们的onload和onerror事件处理程序来确定是否接受到了响应。

动态创建图像经常用于图像Ping。图像Ping是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串的形式发送的、而响应可以是任意内容,但通常是像素图或者204响应。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,它能知道响应是什么时候接收到的。

  var img = new Image();
  img.onload = img.onerror = function() {
       alert("done!");
       var imgBox= document.getElementById("img-box");
      imgBox.appendChild(img);
   }
  img.src='http://www.xxx.com/test?name=nicko';
  • 这这里我们创建了一个Image的实例,然后在onload 和onerror 事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。

  • 请求从设置src属性你那一刻开始的。即图片的地址。

  • 图像Ping最常用于跟踪用户点击页面或动态的曝光次数。但是他有两个缺点

    • 只能发送GET请求
    • 无法访问服务器的响应文本。

图像Ping只能用于浏览器与服务器间的单向通信。

JSONP

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

callback({'name': 'mike'});

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

http://xx.net/json/?callback=handleResponse

上面这个例子,其中URL是在请求一个JSONP地理定位服务。通过查询字符串来指定JSONP服务的回调参数是很常见的,这里指定的回调函数的名字叫handleResponse()。

JSONP是通过动态的<script>元素来使用的,使用时可以为其src属性指定一个跨域的URL。这里的<script>元素和<img>元素类似,都有能力不受限制地从其他域加载资源。因为JSONP是有效的javascript代码,所以在请求完成后,即在JSONP响应加载到页面中以后,会立即执行。

  function handleResponse(response) {
      alert("you`re at IP address" + response.ip);
  }
var script = document.createElement('script');
script.src = 'http://xx.net/json/?callback=handleResponse';
docuemnt.body.insertBefore(script, document.body.firstChild);

这个例子通过查询地理定位服务来显示你的IP地址。

JSONP非常流行,在于其很容易。与图像Ping相比,他的优点在于
* 能够直接访问文本
* 支持浏览器与服务器的双向通信

但是他有他的缺点:
* JSONP是从其他域加载代码执行,如果其他域不安全的话,很可能夹带一些恶意代码。
* 确定请求是否失败并不容易,如果动态脚本插入有效,就执行调用;如果无效,就静默失败。失败是没有任何提示的。例如,不能从服务器捕捉到 404 错误,也不能取消或重新开始请求。不过,等待一段时间还没有响应的话,就不用理它了。(未来的 jQuery 版本可能有终止 JSONP 请求的特性)。

看一个例子,在A域名下,请求B域名下资源。

<script type="text/javascript">
   function addScriptTag(src){
       var script = document.createElement('script');
        script.setAttribute("type","text/javascript");
        script.src = src;
        document.body.appendChild(script);
   }
    
    window.onload = function(){
         //调用远程服务
         addScriptTag("http://localhost:20002/MyService.ashx?callback=person");         
    }
    //回调函数person
    function person(data) {
        alert(data.name + " is a " + data.sex);
     }
 </script> 

Comet

Ajax是一种页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。

Comet的实现方式:

长轮询:是传统轮询(也称短轮询)的一个翻版。短轮询---就是浏览器定时想服务器发送请求,看看有没有更新数据。而长轮询--就是页面发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随后又发送一个到服务器的新请求。这一过程在页面打开期间一直持续不断。

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

HTTP流:它在页面的整个生命周期内只使用一个HTTP连接,具体来说,就是浏览器想服务器发送一个请求,而服务器保持连接打开,然后周期性的向浏览器发送数据。

例如下面这段PHP脚本就是采用流实现的服务器中常见的形式。

<?php
   $i = 0;
   while(true) {
  //输出一些数据,然后理解刷新输出缓存
   echo 'Number is $i';
    flush();
 //等几秒中
   sleep(10)
   $i++
   }

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

在浏览器中,通过侦听readystatechange事件以及检测readyState的值是否为3,就可以利用XHR对象实现HTTP流。随着不断从服务器接受数据,readyState的值会周期性的变为3。当readyState的值为3时,responeseText属性中会保存接受到的数据,此时就需要比较接受到的数据,决定从什么位置开始取得最新的数据。

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回调函数
      progress(result);
      }else if(xhr.readyState == 4){
          finished(xhr.responseText);
      }
   };
xhr.send(null);
return xhr;
}

var client =  createStreamingClient('streaming.php', function(data){
   alert('received:'+ data);
}, function(data) {
     alert('done');
});

在这个createStreamingClient函数接受三个参数,要连接的URL、在接受到数据时调用的函数、以及关闭连接时调用的函数。有时候,当连接关闭时,很可能需要重新建立,所以关注连接什么时候关闭还是很有必要的。

只要readystatechange事件发生,而且readyState的值为3,就对responseText进行分割以取得最新数据,这里的received变量用于记录以及处理多少个字符,每次readyState的值为3时都以递增,然后,通过progress回调函数来处理传入的新数据,而的那个readyState的值为4时,则执行Finnish回调函数,传入响应返回的全部内容。

管理Comet的连接是很容易出错的,需要时间不断改进才能达到完美。

服务里发送事件

SSE(Server-Sent Events, 服务器发送事件)是围绕只读Comet交互推出的API或者模式,SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是
text/event-strem而且是在浏览器中JavaScript API能解析可是输出。SSE支持短轮询、长轮询、HTTP流、而且能在断开连接是自动确定何时重新连接。用这个的话,实现Comet就容易很多了。

  1. SSE API

SSE的JavaScript API与其他传递消息的JavaScript API很相似。要预定新的事件流,首先要创建一个显得EventSource对象,并传进一个入口点

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

注意,传入的URL必须与创建对象的页面同源。EvnetSource的实例有一个readyState属性,值为0表示正在连接服务器,值为1表示打开了连接,值为2表示关闭了连接。

还有3个事件。

  • open: 在建立连接时触发
  • message: 在从服务器接受到新事件时触发
  • error: 在无法创建连接时触发

就一般的用法而言,onmessage事件处理程序也没有什么特别的。

source.onmessage = function(event) {
   var data = event,data;
}

服务器发回的数据以字符串形式保存在event.data中。

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

source.close()

  1. 事件流

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

data: foo
data: bar
data: foo

对于以上响应而言,事件流中第一个message事件返回的event.data的值为foo,第二个message事件返回event.data值为bar...对于多个连续的以data:开头的数据行,将作为多段数据解析,每个值之前以一个换行符分隔。只有在包含data:的数据行后面有空行,才会触发message事件。因此在服务器上生产事件流是不能忘了多添加一行。

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

data: foo
id: 1

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

Web Sockets

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

由于 Web Sockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://而是ws://加密连接不再是https://而是wss://在使用Web Sockets URL时,必须带有这个模式,因为将来有可能支持其他模式。

使用自定义协议而非HTTP协议的好处:

  • 能够在客户端和服务器直接发送非常少量的数据,而不必担心HTTP那样字节级的开销。由于传递的数据包很小,因此Web Sockets非常适合移动应用。

使用自定义协议而非HTTP协议的缺点:

  • 制定协议的时间比制定JavaScript API的时间还要长。
  1. Web Socket API

要创建Web Socket,先实例一个Web Socket对象并传入要连接的URL

var socket = new WebSocket('ws://www.example.com/server.php');

注意,必须给WebSocket构造函数传递绝对的URL,同源策略对Web Socket不适用。因此可以通过它打开任何站点的连接。至于是否会在某个域中的页面通信,则完全取决于服务器。

实例化WebSocket对象后,浏览器就会马上尝试创建连接,与XHR类似,WebSocket也有一个表示当前状态的readyState属性,不过,这个属性与XHR并不相同。而是如下所示:

  • WebSocket.OPENING(0): 正在建立连接
  • WebSocket.OPEN(1): 已经建立连接
  • WebSocket.CLOSEING(1): 正在关闭连接
  • WebSocket.CLOSE(1): 已经关闭连接

WebSocket没有readystatechange事件,不过,它有其他事件,对应着不同的状态。redayState的值永远从0开始。

要关闭WebSocket连接,可以在任何时候调用close()方法。

socket.close()

调用close()之后,readyState的值立即变成2(正在关闭)。而在关闭连接后就会变成3

  1. 发送和接受数据

WebSocket打开之后,就可以通过连接发送和接受数据。要向服务器发送数据,使用send()方法并传入任意字符串。例如:

var socket = new WebSocket('ws://www.example.com/server.php'); 
socket.send('hello world');

因为Web Socket只能通过连接发送纯文本数据,所以对于复杂的数据结构,在通过连接发送之前,必须进行序列化,例如:

var message = {
   time: new Date(),
   text: 'hello world',
   clientId: 'asd0238'
};
socket.send(JSON.stringify(messsge));

接下来,服务器要读取其中的数据,就要解析接受到的JSON字符串。

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

socket.onmessage = function(event) {
   var data = event.data;
//处理数据
}

通过send()发送到服务器的数据一样,event.data中返回的数据也是字符串。如果你想得到其他格式的数据,必须手工解析这些数据。

  1. 其他事件

WebSocket对象还有其他三个事件,在连接生命周期的不同阶段触发。

  • open: 在成功建立连接时触发
  • error: 在发送错误时触发。连接不能持续
  • close: 在连接关闭时触发

WebSocket对象不支持DOM2级事件侦听器,因此必须使用DOM0D级语法分别定义每个事件处理程序。

var socket = new WebSocket('ws://www.example.com/server.php'); 
socket .onopen = function() {
    alert("Connect established");
}
socket.onerror = function() {
    alert("Connect error");
}
socket.onclose = function() {
   alert("Connect closed");
}

在这三个事件中,只有close事件的event对象有额外的信息,这个事件的事件对象有三个额外属性
wasClean code reason。其中,wasClean是一个布尔值,表示连接是否已经明确地关闭。code是服务器返回的数值状态码。 reason是一个字符串,包含服务器发回的消息。可以把信息给用户,也可以记录到日志中以便将来分析

socket.onclose = function(event) {
  console.log('was clean?'+ event.wasClean + 'code=' + event.code + 'reason=' + event.reason);
}

SSE与Web Sockets

用SSE还是Web Sockets呢?可以从下面几个方面考虑:

  • 你是否有自由度建立和维护Web Sockets的服务器?而SSE是通过常规的HTTP通信,现有的服务器就可以满足需求。

  • 到底需不需要双向通信。如果只需要读取服务器数据(如比赛成绩),那么SSE比较容易实现,如果必须双向通信(例如聊天室)那么Web Sockets显然更好。

  • 在不能选择Web Sockets的情况下,组合XHR和SSE也能实现双向通信。

安全

对于未被授权系统有权访问某个资源的情况,我们称之为CSRF(Cross-Site Request Forgery,跨站点请求伪造)。未被授权系统会伪装自己,让处理的请求服务器认为它是合法的。受到CSRF攻击的Ajax程序有大有小,攻击行为既有旨在揭示系统漏洞的恶作剧,也有恶意的数据窃取和数据销毁。

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

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

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

  • 要求发送POST而不是GET请求-----很容易改变
  • 检查来源URL以确定是否可信----来源记录很容易伪造
  • 基于cookie信息进行验证----同样很容易伪造

XHR对象也提供一些安全机制,虽然表面上可以保证安全,但实际上却相当不可靠。实际上,open方法还能再接受两个参数: 要随请求一起发送的用户名和密码。带有这两个参数的请求可以通过SSL发送给服务器上的页面。例如:

xhr.open('get', 'example.php', true, 'username', 'password'); //不要这样做

因为只要打开控制器就能发现纯文本的用户名和密码。