javascript跨域传输数据总结

这是在面试迅雷的时候,迅雷面试官给我的一个问题,我没有回答上来,回来之后,就对这个问题进行了梳理,现在把它们总结一下。

1. 跨域的js文件

这是js里面最基本的一种跨域形式,我们通过<script src>引用非当前域名下的另外一个脚本文件。那么怎么进行跨域数据的传递呢?其实很简单,当浏览器在载入该js的时候,会阻塞整个页面的执行,载入js文件完成之后,会立即执行文件内的程序,因此,我们可以这样去做:

<script>var data;</script>
<script src="http://anotherdomain.com/user_info.json?access_token=xxx" id="json"></script>
<script>
window.onload = function() {
    if(typeof data == 'object') {
        alert(data.name);
    }
}
</script>

而我们在上面的#json的src文件正好给data赋值,在另外一个域名下,根据我们传入的一些值,返回不同的结果:

data = {name: 'kiddy',age: 18};

这样,相当于通过引入一个js给data进行赋值。而每次希望改变data的值的时候,通过脚本动态加载一个新的脚本就可以了:

var script = document.getElementById('script');
script.src = 'xxxxxx.json?acess_token=xxxx';

但是这种形式有一个缺点,就是在服务端文件内,会多出data = 这一段对于的信息,和我们平时直接输出的json数据不同,视觉感会差很多。有没有一种方法可以实现输出json,同时有完成相同的操作呢?

2. jsonp

相信很多人都听过这个大名鼎鼎的技术,JSONP实际上已经成为一种协议,主要用于XmlHttpRequest跨域请求。实际上,我们用jsonp以解决上面的那个问题。首先,我们来看下跨域js文件的另一种操作形式:

<script>
function alertName(data) {
    alert(data.name);
}
</script>
<script src="http://anotherdomain.com/xxx.josnp?callback=alertName"></script>

这是我们在原本文件中写到的,而我们在远方的josnp目标页面这样输出:

alertName({name: "Kafi", age: 15});

也就是说在当前页面定义了一个函数,然后在远端的js文件中执行这个函数,而函数本身是有一个data参数的,在远端就直接通过后台程序,把要传递的数据作为参数传递给要执行的函数。这就是jsonp的原型。但是,这样还是很麻烦,比起之前输出data = {}的形式反而更复杂了,而且还必须通过一个 callback来让远端接收指定的函数名。所以,这中形式不是我们最终的jsonp,而jsonp的合理操作是:

jsonp(url,function(data){
    // 利用data进行数据处理
});

其中,url就是远端返回数据的地址,它可以完全按照我们的想法,返回一个纯json数据,而这个数据,将被作为参数传递给jsonp的第二个参数函数,也就是data。当然,这是一种模型,我们并没有实现它,但它的原理,就是我们上面所述的,通过回调函数执行已经输出的数据。

不过需要注意的是,由于跨域操作,一般jsonp都是异步执行的,也就是说, 上面function函数体的程序,肯定要等到url返回数据之后才会执行,这就会导致在jsonp后面的javascript程序会先于这个function执行。

3. HTML5的window.postMessage

在javascript的跨域中,除了上面两种数据请求的情况,还有一种就是页面之间的通信问题。“请求”和“通信”是两种不同的行为,请求是当前页面主动去请求另外一个页面的数据,而通信往往是指当前页面和其他页面可以实现数据同步,也就是当自己的数据变化时,别的页面可以感知到,或者别的页面数据变化时,自己可以做出相应的动作。

HTML5的window.postMessage就是一种通信手段,它可以实现不同页面之间的信息通知,甚至是跨域的,比如我们看下面的代码:

window.onmessage = function(e){
    e = e || event;
    var data = e.data;
    var origin = e.origin; 
    var source = e.source;
    data = JSON.parse(data);
    alert(data.name);
}

这是我们希望当前页面在接收到postMessage通知的时候,所做出的响应。那么另外一个页面如何进行通知呢?

otherWindow.postMessage(message, targetOrigin);

这个otherWindow是一个比较麻烦的事情,我们下面再讲。先看postMessage方法的两个参数,第一个就是消息体,是字符串,第二个是允许哪些域名下的window对象接收postMessage的内容,它包含协议和域名,也就是说你想让那个页面接收消息,就要在这里填写这个页面所在的域名,例如“http://127.0.0.1”或者“https://www.yourdomain.com”,如果不限制域名,就直接用*代替,比如:

otherWindow.postMessage('{name: "kiidy", age: 10}','*');

接下来我们讲otherWindow,它其实是指代某一个窗口。我们上面讲targetOrigin用*代替的话,就不限制域名,但是实际上,只有otherWindow可以接收到这个消息通知。也就是说,如果你的targetOrigin进行了限制,那么otherWindow必须在该域名下面,如果不在,那就接收不到消息,而如果使用*的话,无论otherWindow在哪个域名下,都可以接收消息。可是这里有一个BUG不知道你有没有发现,就是otherWindow的存在是一切的前提,如果没有找到otherWindow,一切都是百搭。所以,我们一般即使用*,也只能在所有浏览器打开的窗口中,找到一个otherWindow,不可能找到第二个同名的窗口。

浏览器一个标签就是一个窗口,这个窗口大部分情况下是匿名的,但是也有一些情况下不匿名,比如使用window.open打开的窗口window.open(pageURL,name,parameters),其中name就是这个窗口的名称,可以作为我们这里的otherWindow,还有一种情况就是iframe窗口对象,虽然它也是匿名的,但是我们却可以确定iframe从而确定该匿名窗口的所属,使用 document.getElementById('iframe_id').contentWindow来作为otherWindow。我们看下面这个例子:

//当前页面http://127.0.0.1/a.html
<iframe id="f" src="http://localhost/b.html"></iframe>
<script>
var f=document.getElementById("f");
f.onload=function(){
    f.contentWindow.postMessage("SB","http://localhost");
}
</script>

上面这段代码通过一个iframe确定了otherWindow,所以只要这个iframe没有被销毁,它加载完的时候,"SB"这个字符串就会被推送到iframe里面的窗口去。而在iframe里面的窗口,我们使用下面的js来接收:

<script>
onmessage=function(e){
    e=e||event;
    document.write("我是",e.data);
};
</script>

这样,iframe里面的内容就会发生变化。

但是这种方法和我们前面介绍的方法,存在这么几个不足之处:

  • 必须是不同的浏览器窗口之间通信,换一个浏览器,换一个设备,就没任何作用了
  • otherWindow难以灵活运用,无法实现不同的浏览器标签之间进行通信(这需要我们使用更高级的websocket技术)

所以,一般这种方法的时候,会用在一些比较高阶的场景,也就是和浏览器的worker搭配起来处理,其技术难度也相对高很多。

4. window.name

window.name是window对象的一个特殊方法,它可以实现在不同的窗口之间变化的时候,仍然保持同一个值,即使不同域名下的页面,仍然有效。

比如我们在a.html中写入:

window.name = '{name: "dotiy"}';

当页面跳转到b.html中我们可以使用下面的方法来获取:

var name  = window.name;

这是很有意思的一种数据传输方式,但是window.name的值可以随时被改,该了之后在下一个新打开的页面中就会使用新的值。

另外,它也可以在iframe的窗口对象中生效,但是仅针对该窗口,即iframe.contentWindow.name,如果是这样,就可以让iframe内的页面和宿主页面通信,在宿主页面使用:

var name = document.getElementById('iframe_id').contentWindow.name;

这样就取到了iframe中这个页面设置的window.name值,从而实现了跨域数据传输。

5. document.domain

其实这不算一种方法,而是javascript本身的一种机制。简单的说就是通过修改document.domain来使子域名下的页面,可以和父域名下的页面通信。注意,这里用的是“通信”,也就是说可以通过ajax的方法,从父域名的地址中得到数据。一般这种跨域用在iframe中:

document.domain = 'rootdomain.com';

这种方法的限制太多,主要包括:

  • 这一句代码必须在两个页面中都加以举出,即使该页面是在根域名下面。
  • 只能由子域名向父域名扩展,而不能在父域名的页面中要求document.domain为子域名
  • 只能解决iframe这类弱处理,而不能真正实现跨域的数据请求

最后,在我们使用ajax进行跨域请求的时候,还可以通过在服务端进行一些配置,可以让ajax在请求对应域名时,可以被正常接受。请看《服务端php解决jquery ajax跨域请求restful api问题及实践》《jquery ajax 请求中多出现一次OPTIONS请求及其解决办法》这两篇文章,可以帮你解决服务端是php的应用程序的配置方法。

2016-04-22 |