JavaScript 跨域技术

在CORS出现之前,要实现跨域Ajax通信颇费一些周折。开发人员想出了一些办法,利用DOM中能够执行跨域请求的功能,在不依赖XHR对象的情况下也能发送某种请求。虽然CORS技术已经无处不在,但开发人员自己发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务器端代码。

图像Ping

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

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

1
2
3
4
5
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "url";

JSONP

JSONP是JSON with padding(填充式JSON或参数JSON)的简写,是应用JSON的一种新方法,在后来的Web服务中非常流行。JSONP看起来与JSON差不多,只不过是被包含在函数调用中的JSON,就像下面这样。

callback({"name":"张飒"});

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

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

这个URL是在请求一个JSONP地理定位服务。通过查询字符串来指定JSONP服务的回调参数是很常见的。JSONP是通过动态<script>元素来使用的,使用时可以为src属性指定一个跨域RUL。这里的<script>元素与<img>元素类似,都有能力不受限制地从其他域加载资源。因为JSONP是有效的JavaScript代码,所以在请求完成后,即在JSONP响应加载到页面中以后,就会立即执行。

1
2
3
4
5
6
7
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);

优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过,JSONP也有两点不足。

首先,JSONP是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃JSONP调用之外,没有办法追究。因此在使用不是你自己运维的Web服务时,一定得保证它安全可靠。

其次,要确定JSONP请求是否失败并不容易。虽然HTML5给<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。但就算这样也不能尽如人意,毕竟不是每个用户上网的速度和宽带都一样。

Comet

Comet是Alex Russell发明的一个词,指的是一种更高级的Ajax技术(经常也有人称为”服务器推送”)。Ajax是一种从页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。

轮询

轮询(Polling)是一种CPU决策如何提供周边设备服务的方式,又称“程控输入输出”(Programmed I/O)。轮询法的概念是:由CPU定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。

有两种实现Comet方式:

  1. 长轮询和流。长轮询是传统轮询(也成为短轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?php
    $i = 0;
    while(true){
    // 输出一些数据,然后立即刷新输出缓存
    echo "number is $i";
    flush();

    // 等几秒钟
    sleep(10);

    $i++;
    }
    php>

随着不断从服务器接收数据,readyState的值会周期性的变为3.当readyState值变为3时,responseText属性中就会保存接收到的所有数据。此时,就需要比较此前接收到的数据,决定从什么位置开始取得最新的数据。使用XHR对象实现HTTP流的典型代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
recelived += result.length;

// 调用progress回调函数
progress(result);
} else if (xhr.readyState) {
finished(xhr, responseText)
}

};
xhr.send(null);
return xhr;
}

var client = createStreamingClient("streaming.php", function(data){
alert("Received:" + data);
},function(data){
alert("Done!");
})

这个createStreamingClient()函数接收三个参数:

  1. 要连接的URL;
  2. 在接收到数据时调用的函数以及关闭连接时调用的函数。
  3. 有时候,当连接关闭时,很可能还需要重新建立,所以关注链接什么时候关闭还是有必须要的。

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

服务器发送事件

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

HTTP流。它在页面整个生命周期内只使用一个HTTP连接,具体使用方法即页面向浏览器发送一个请求,而服务器保持tcp连接打开,然后不断向浏览器发送数据。

  1. SSE API

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

var source = new EventSource(“myevents.php”);

注意,传入的URL必须与创建对象的页面同源(相同的URL模式、域及端口)。EventSource的实例有一个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2表示关闭了连接。

另外,还有一下三个事件。

  • open: 在建立连接时触发。
  • message: 在从服务器接收到新事件时触发。
  • error: 在无法简历连接时触发。

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

1
2
3
4
source.onmessage = function(event){
var data = event.data;
// 处理数据
};

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

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

source.close();

  1. 事件流

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

1
2
3
4
5
data: foo
data: bar

data: foo
data: bar

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

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

1
2
data: foo
id: 1

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

Web Sockets

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

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

使用自定义协议而非HTTP协议的好处是,能够在客户端和服务端之间发送非常少量的数据,而不必担心HTTP那样字节集的开销。由于传递的数据包很小,因此WebSockets非常适合移动应用。毕竟对移动应用而言,带宽和网络延迟都是关键问题。使用自定义协议的缺点在于,制定协议的时间比制定JavaScriptAPI的时间还要常。Web Sockets曾几度搁浅,就因为不断有人发现这个新协议存在一致性和安全性问题。

  1. Web Sockets API

要创建WebSocket,先实例一个WebSocket对象并传入要连接得URL;

var socket = new WebSocket("URL");

WebSocket.OPENING(0): 正在建立连接。
WebSocket.OPEN(1): 已经建立连接。
WebSocket.CLOSING(2): 正在关闭连接。
WebSocket.CLOSE(3): 已经关闭连接。

关闭WebSocket连接,可以在任何时候调用close()方法: socket.close();

  1. 发送和接收数据
1
2
3
4
5
6
7
var message = {
time: new Date(),
text: "Hello world!",
clientId: "54564646"
};

socket.send(JSON.stringify(message));

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

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

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

  1. 其他事件

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var socket = new WebSocket("ws://www.example.com/server.php");

socket.onopen = function () {
alert("Connection established.");
};

socket.onerror = function () {
alert("Connection error.");
};

socket.onclose = function (event) {
alert("Connection closed.");
console.log("Was clean ? " + event.wasClean + "Code = " + event.code + " Reason= " + event.reason);
};

SSE 与 Web Sockets

面对某个具体的用力,在考虑是使用SSE还是使用WebSockets时,可以考虑如下几个因素。首先,你是否有自由度简历和维护WebSockets服务器?其次,到底需不需要双向通信。

安全

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

建议采用上面两种办法。

  • 要求发送POST而不是GET请求
  • 检查来源URL以确定是否可信
  • 基于cookie信息进行验证