HTML5 postMessage 跨域通信 跨窗口消息传递解决方案
HTML5 postMessage 跨域通信
之前简单讲解了利用script标签(jsonp)以及iframe标签(window.name、location.hash)来跨域交换数据,今天我们来学习一下HTML5的api,利用postMessage来跨域交换数据。和前面一些方式交换数据方式不同的是,利用postMessage不能和服务端交换数据,只能在两个窗口(iframe)之间交换数据,废话不多说,我们直接进入实战。
实战postMessage
overview
上文中说,postMessage是用于两个窗口(iframe)之间交换数据的,如果我们同时打开着百度和谷歌两个页面,是不是说这两者之间就可以通信了?No,no,no,事实并非如此,就算百度和谷歌俩页面有通信的意愿也不行。两个窗口能通信的前提是,一个窗口以iframe的形式存在于另一个窗口,或者一个窗口是从另一个窗口通过window.open()或者超链接的形式打开的(同样可以用window.opener获取源窗口);换句话说,你要交换数据,必须能获取目标窗口(target window)的引用,不然两个窗口之间毫无联系,想通信也无能为力。
既然是H5家族的,我们也得观望下它被广大浏览器的接受程度,可以看到接受程度还是相当高的:
而postMessage的使用方式也相当简单:
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow是对接收方窗口的引用,一般可以是以下几种方式:
window.frames[0].postMessage
document.getElementsByTagName('iframe')[0].contentWindow
window.opener.postMessage
event.source.postMessage
window.open 返回的引用
...
而message顾名思义就是发送的数据内容,支持字符串、数字、json等几乎所有形式的数据(详见The structured clone algorithm)
targetOrigin是接收方的URI(协议+主机+端口),也可以是url形式,但之后的内容(形如xx.html)会自动忽略;用通配符*可以指定所有域,但是切记不要用(for security)。
transfer可省略,没看懂是啥意思...以后有需要的时候再研究
而接受方窗口一般监听message事件,详见下面的例子。
window <-> iframe
假设index页面有个iframe(不同源),我们要给iframe发送数据,而iframe得到数据后也发送数据给top window,表示“我"得到数据了。直接看源码(思考如何发送and如何接收):
<!-- http://localhost:81/fish/index.html --> <script type="text/javascript"> // 页面加载完后才能获取dom节点(iframe) window.onload = function(){ // 向目标源发送数据 document.getElementsByTagName('iframe')[0].contentWindow.postMessage({"age":10}, 'http://localhost:8080'); }; // 监听有没有数据发送过来 window.addEventListener('message', function(e) { console.log(e); }); </script> <iframe src="http://localhost:8080/index.html"></iframe>
<!-- http://localhost:8080/index.html --> <script type="text/javascript"> // 监听有没有数据发送过来 window.addEventListener('message', function(e){ // 判断数据发送方是否是可靠的地址 if(e.origin !== 'http://localhost:81') return; // 打印数据格式 console.log(e); // 回发数据 e.source.postMessage('hello world', e.origin); }, false); </script>
我们截图看看打印的东西究竟长什么样(index页面传给iframe的数据):
红框标出的是三个最重要的属性,data顾名思义就是传输的数据了;origin就是发送消息窗口的源(URI 协议+主机+端口);而source就能引用发送消息的窗口对象(可以用它来引用发送窗口进行消息回传)。
在消息接收端监听可以监听message事件(代码如上),当然如果要兼容坑爹的ie肯定要用attachEvent。这里不推荐使用window.onmessage,兼容性不大好(比如不能兼容低版本ff)。
window <-> window
说完了跟同一页面中的iframe的数据交换,再来说说两个窗口之间的数据交换。我们知道用window.open()可以打开一个新的窗口,而如果两个窗口同源,则两个窗口的通信将会非常简单,我们可以通过window.opener.functionName在新窗口中调用原来窗口的方法(和变量)。但是如果两个窗口不同源,这样的操作将会变得很艰难,幸运的是H5给我们提供了postMessage,使得window.opener.postMessage()不会报错!demo很简单:
<!-- http://localhost:81/fish/index.html --> <script type="text/javascript"> // 打开一个新的窗口 var popup = window.open('http://localhost:8080/index.html'); /// When the popup has fully loaded, if not blocked by a popup blocker: setTimeout(function() { // 当前窗口向目标源传数据 popup.postMessage({"age":10}, 'http://localhost:8080'); }, 1000); </script>
<!-- http://localhost:8080/index.html --> <script type="text/javascript"> // 设置监听,如果有数据传过来,则打印 window.addEventListener('message', function(e) { console.log(e); // console.log(e.source === window.opener); // true }); </script>
这里要设置一个定时器的原因是向目标窗口发送数据必须等目标窗口完全加载完,也就是说要在目标窗口中先设置好“监听器”,发送窗口发的数据才能被监听到,所以给了个定时器delay,而因为加载时间的不确定所以定时器的delay值也不能确定;另外一个可行的办法是当目标页面加载完后,发个消息个源页面(postMessage),而源页面收到消息,再用postMessage发送消息给目标页面。
安全顾虑
提到跨域交换数据,条件反射都会问一句,安全吗?对于postMessage,答案是肯定的。
postMessage采用的是“双向安全机制”。发送方发送数据的时候,会确认接受方的源(所以最好不要用*),而接受方监听到message事件后,也可以用event.origin判断是否来自于正确可靠的发送方。
html5 postMessage解决跨域、跨窗口消息传递
平时做web开发的时候关于消息传递,除了客户端与服务器传值还有几个经常会遇到的问题
1.页面和其打开的新窗口的数据传递
2.多窗口之间消息传递
3.页面与嵌套的iframe消息传递
4.上面三个问题的跨域数据传递
postMessage()
这些问题都有一些解决办法,但html5引入的message的API可以更方便、有效、安全的解决这些难题。postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
postMessage(data,origin)方法接受两个参数
1.data:要传递的数据,html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果。
2.origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然如果愿意也可以建参数设置为"*",这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
http://test.com/index.html
<div style="width:200px; float:left; margin-right:200px;border:solid 1px #333;"> <div id="color">Frame Color</div> </div> <div> <iframe id="child" src="http://lsLib.com/lsLib.html"></iframe> </div>
我们可以在http://test.com/index.html通过postMessage()方法向跨域的iframe页面http://lsLib.com/lsLib.html传递消息
window.onload=function(){ window.frames[0].postMessage('getcolor','http://lslib.com'); }
接收消息
test.com上面的页面向lslib.com发送了消息,那么在lslib.com页面上如何接收消息呢,监听window的message事件就可以
http://lslib.com/lslib.html
window.addEventListener('message',function(e){ if(e.source!=window.parent) return; var color=container.style.backgroundColor; window.parent.postMessage(color,'*'); },false);
这样我们就可以接收任何窗口传递来的消息了,为了安全起见,我们利用这时候的MessageEvent对象判断了一下消息源,MessageEvent是一个这样的东东
有几个重要属性
data:顾名思义,是传递来的message
source:发送消息的窗口对象
origin:发送消息窗口的源(协议+主机+端口号)
这样就可以接收跨域的消息了,我们还可以发送消息回去,方法类似
简单的demo
在这个例子中,左边的div会根据右边iframe内div颜色变化而变化
<!DOCTYPE html> <html> <head> <title>Post Message</title> </head> <body> <div style="width:200px; float:left; margin-right:200px;border:solid 1px #333;"> <div id="color">Frame Color</div> </div> <div> <iframe id="child" src="http://lsLib.com/lsLib.html"></iframe> </div> <script type="text/javascript"> window.onload=function(){ window.frames[0].postMessage('getcolor','http://lslib.com'); } window.addEventListener('message',function(e){ var color=e.data; document.getElementById('color').style.backgroundColor=color; },false); </script> </body> </html>
<!doctype html> <html> <head> <style type="text/css"> html,body{ height:100%; margin:0px; } </style> </head> <body style="height:100%;"> <div id="container" onclick="changeColor();" style="widht:100%; height:100%; background-color:rgb(204, 102, 0);"> click to change color </div> <script type="text/javascript"> var container=document.getElementById('container'); window.addEventListener('message',function(e){ if(e.source!=window.parent) return; var color=container.style.backgroundColor; window.parent.postMessage(color,'*'); },false); function changeColor () { var color=container.style.backgroundColor; if(color=='rgb(204, 102, 0)'){ color='rgb(204, 204, 0)'; }else{ color='rgb(204,102,0)'; } container.style.backgroundColor=color; window.parent.postMessage(color,'*'); } </script> </body> </html>
在例子中页面加载的时候主页面向iframe发送’getColor‘ 请求(参数没实际用处)
window.onload=function(){ window.frames[0].postMessage('getcolor','http://lslib.com'); }
iframe接收消息,并把当前颜色发送给主页面呢
window.addEventListener('message',function(e){ if(e.source!=window.parent) return; var color=container.style.backgroundColor; window.parent.postMessage(color,'*'); },false);
主页面接收消息,更改自己div颜色
window.addEventListener('message',function(e){ var color=e.data; document.getElementById('color').style.backgroundColor=color; },false);
当点击iframe事触发其变色方法,把最新颜色发送给主页面
function changeColor () { var color=container.style.backgroundColor; if(color=='rgb(204, 102, 0)'){ color='rgb(204, 204, 0)'; }else{ color='rgb(204,102,0)'; } container.style.backgroundColor=color; window.parent.postMessage(color,'*'); }
主页面还是利用刚才监听message事件的程序处理自身变色
window.addEventListener('message',function(e){ var color=e.data; document.getElementById('color').style.backgroundColor=color; },false);
最后
很简单的用法却解决了大问题,据说Facebook已经在使用了,而且这也是html5另一个API——web workers传递消息的方法,那么它的浏览器兼容性怎么样呢?所谓浏览器兼容性几乎变成了IE几开始支持的问题了。。。不过好消息是跟localStorage一样,IE8+都支持了,只不过有些浏览器的低版本(比如FireFox4.0)并不支持window.onmessage=function(){}这种写法,所以我么最好使用事件绑定的写法,为了兼容IE,也要判断是否支持addEventListener。