跨域消息传递
一些浏览器窗口和标签之间都是完全相互独立的,在其中一个窗口或者标签中运行的代码在其他窗口或标签中完全无法识别。但是,在其他的一些场景下,当脚本显式打开一个新窗口或者在嵌套的窗体中运行的时候,多个窗口或者窗体之间是互相可识别的。如果它们包含的文档是来自同一台Web服务器,则再这些窗口和窗体中的脚本可以互相之间进行交互和操作对方的文档。
然而,有的时候,尽管脚本可以引用其他的Window对象,但是由于那个窗口中的内容是来自于不同的源,Web浏览器(遵循同源策略)不会允许访问其他窗口中的文档内容。大部分情况下,浏览器还不允许脚本读取其他窗口的属性或调用其他窗口方法。不过有个window方法,是允许来自非同源脚本调用的:
postMessage()
方法,该方法允许有限的通信——通过异步消息传递的方式一在来自不同源的脚本之间。这类通信机制是在HTML5标准中定义的,所有主流的浏览器(包括IE8和更新版本)都已经实现了该通信机制。这项技术称为“跨文档消息传递”,而由于该API是定义在Window对象上的,而不是文档对象上的,因此,它又称为“窗口间消息传递”或者“跨域消息传递”。
postMessage()
方法接受两个参数。其中第一个参数是要传递的消息。HTML5标准提到,该参数可以是任意基本类型值或者可以复制的对象,但是,有些当前浏览器(包括Firefox 4 beta版本)的实现只支持字符串,因此,如果想要作为消息传递对象或者数组,首先应当使用JSON.stringify()
方法对其序列化。
其中第二个参数是一个字符串,指定目标窗口的源。其中包括协议、主机名以及URL(可选的)端口部分(可以传递一个完整的URL,但是除了协议、主机名和端口号之外的任何信息都会忽略)。这是一个安全特性:由于恶意代码或普通用户都可以在窗口中浏览新的未知文档,因此
postMessage()
只会将消息传递给指定的窗口,而不会传递给包含非同源文档的窗口。当然,如果传递的消息不包含任何敏感信息的话,并且愿意将其传递给任何窗口,就可以直接将该参数设置成“*
”通配符即可。如果要指定和当前窗口同源的话,那么也可以简单地使用“/
”。
如果指定的源匹配的话,那么当调用
postMessage()
方法的时候,在目标窗口的Window对象上就会触发一个message事件。在目标窗口中的脚本则可以定义通知message事件的处理程序函数。调用该事件处理程序的时候会传递给它一个拥有如下属性的事件对象:
data
作为第一个参数传递给
postMessage()
方法的消息内容副本。
source
消息源自的Window对象。
origin
一个字符串,指定消息来源(URL形式)。
通常,
onmessage()
事件处理程序应当首先检测其中的origin属性,忽略来自未知源的消息。
当想要在Web页面中嵌入一个来自其他站点的模块或者“gadget”的时候,利用
postMessage()
和message事件实现的跨域消息传递是很有用的。当然,如果gadget本身就很简单并且又是自包含的,就可以直接简单地将它放在<iframe>
中实现隔离即可。然而,假设gadget本身比较复杂,它自身还定义了一些API,同时Web页面需要利用这些API和它进行交互。这个时候,用<iframe>
就不行了,而如果将它嵌入在<script>
元素中,它可以提供一个正常的JavaScript API,但是同时它也可以完全操控页面和页面内容了。目前在Web上通常不会这样去做(尤其是Web广告),哪怕信任第三方站点,这也不是个好的方案。
跨域消息传递提供了另外一种实现方案:首先gadget的开发者可以将gadget内容定义在一个HTML页面中,它负责监听message事件,并将它们分发给对应的JavaScript函数去处理。然后,嵌入gadget的Web页面就可以通过
postMessage()
方法传递消息来和gadget进行交互了。下面两例展示了如何使用该方案。第一个例子是一个简单的gadget,放置在<iframe>
中,它搜索Twitter并将匹配指定搜索项的tweet显示出来。要让它实现真正的搜索功能,包含的页面只需要简单地作为消息传递搜索项给它即可。
例:Twitter搜索gadget,由postMessage()来控制 <!DOCTYPE html> <!-- 这是一个Twitter搜索gadget。将它通过iframe的形式内嵌在任何Web页面中, 通过postMessage()方法将査询字符串传递给它来搜索tweet。由于它是内嵌在 <iframe>中而不是<script>中,因此它无法对内嵌它的页面造成破坏 --> <html> <head> <style> body { font: 9pt sans-serif; } </style> <!-- 使用jQuery的jQuery.getJSON()工具函数 --> <script src="http://code.jquery.com/jquery-1.4.4.min.js"/></script> <script> // 原本只要能够使用window.onmessage就可以了,但是考虑到早期的浏览器(比如:Firefox 3)不支持它,因此,采用如下兼容方式实现 if (window.addEventListener) window.addEventListener("message", handleMessage, false); else window.attachEventConmessage("onmessage",handleMessage); // For IE8 function handleMessage(e) { // 不在意消息来源:愿意接受任何来源的Twitter搜索请求 // 但是,希望消息源自内嵌gadget的窗口 if (e.source !== window.parent) return; var searchterm = e.data; // 获取捜索内容 // 使用jQuery Ajax工具函数以及Twitter的搜索API来查找匹配消息的tweet jQuery.getJSON("http://search.twitter.com/search.json?callback=?", { q: searchterm }, function(data) { // 使用请求结果调用 > var tweets = data.results; // 构造一个HTML文档来显示搜索结果 var escaped = searchterm.replace("<", "<"); var html = "<h2>" + escaped + "</h2>"; if (tweets.length == 0) { html += "No tweets found"; } else { html += "<dl>"; // 以<dl>列表形式呈现结果 for(var i = 0; i < tweets.length; i++) { var tweet = tweets[i]; var text = tweet.text; var from = tweet.from_user; var tweeturl = "http://twitter.com/#!/" + > from + "/status/" + tweet.id_str; html += "<dt><a target='_blank' href='" + > tweeturl + "'>" + tweet.from_user + "</a></dt><dd>" + tweet_text + "</dd>"; } html += "</dl>"; } // 设置<iframe>文档 document.body.innerHTML = html; }); } $(function() { // 通知内嵌gadget的页面 // 我们(gadget)已经准备就绪 // 容器在没有收到这条消息前,它不能发送任何消息 // 因为我们还没有准备好接收消息 // 通常,容器只需要等待onload事件的触发,以此来得知所有的<iframe>都已载入完毕 // 我们发送消息告诉容器已经准备就绪,甚至有可能在容器获得onload事件之前 // 我们并不知道容器的源,所以采用来让浏览器把消息发送给任何窗口 window.parent.postMessage("Twitter Search v0.1", "*"); }); </script> </head> <body> </body> </html>
下例是一个简单的JavaScript文件,可以将它引入到任何想要使用Twitter捜索gadget的Web页面中。它将gadget插入到文档中,然后为文档中所有的链接都添加一个事件处理程序,以便当鼠标指针划过一个链接的时候,就会调用
postMessage()
方法,让gadget去搜索链接上的URL指定的内容。这可以允许用户在发一条包含网站内容的tweet时,在未访问该站点前就能够先看到网站内容。例: 通过postMessage()来使用Twitter搜索gadget // 如下JS代码实现将Twitter搜索gadget添加到文档中 // 然后为文档中所有的链接都添加一个事件处理程序 // 实现当鼠标指针划过一个链接的时候,就会调用postMessage()方法 // 让gadget去捜索链接上的URL指定的内容。这可以允许用户要发一条包含网站内容的tweet时 // 在未访问该站点前就能够先看到网站内容 window.addEventListener("load", function() { // 在IE9以下的版本无效 var origin "http://davidflanagan.com"; // gadget源 var gadget "/demos/TwitterSearch.html"; // gadget路径 var iframe = document.createElement("iframe"); // 创建iframe iframe.src = origin gadget; // 设置它的URL iframe.width = "250"; // 250个像素宽 iframe.height = "100%"; // 整个文档高度 iframe.style.cssFloat = "right"; // 右浮动 // 将该iframe插入到文档的最开始 document.body.insertBefore(iframe, document.body.firstChild); // 査找所有的链接,并把它们绑定到gadget上 var links = document.getElementsByTagName("a"); for(var i = 0; i < links.length; i++) { // addEventListener在IE8及其早期版本无效 links[i].addEventListener("mouseover", function() { // 作为査询内容传递url // 只当iframe仍然显示来自davidflanagan.com文档的时候传递它 iframe.contentWindow.postMessage(this.href, origin); }, false); } }, false);