越来越多的网站需要相互协作。例如,在线房屋租赁网站需要谷歌地图的支持,以显示特定租赁房屋的位置。为了满足这样的需求,已经出现了各种各样的 mashup。mashup 是一种将不同供应商的数据或组件集成起来,使之更加有用或更用户化的 Web 应用程序。Mashup(或协作功能)被认为是 Web 2.0 的重要组成部分。
出人意料的是,将异步的 JavaScript、XML (Ajax) 和 mashup 结合起来并不容易。由于浏览器施加的安全限制,让页面上的不同小部件彼此之间相互通信比较麻烦。通常,您可以通过在服务器端设置一个代理来解决此问题,该方法是不可扩展的。本文中,将了解客户端上针对跨域通信和数据传输的其他一些解决方案。
同源策略 (SOP) 可防止从一个来源加载的脚本获取或操纵来自另一来源的文档的属性或方法。术语来源 是域名、应用程序协议和运行脚本文档的端口的结合。可能存在关于 SOP 概念的一些误解;SOP 指只能从站点 A、不能从站点 B 获取信息。您需要知道在 SOP 限制下可以做什么,不可以做什么。
例如,来自源点 A 的 Web 页面可以:
- 从源点 B 获取脚本、CSS 样式表或图像
- 包含一个指向源点 B 中页面的 iframe/frame。
- 使用 HTML 元素的
src属性(比如iframe或img)将信息发送到源点 B 。
来自源点 A 的 Web 页面不可以:
- 对源点 B 进行 Ajax 调用
- 读取或操纵 iframe/frame 中指向页面 B 的内容
为什么会这样做呢?主要是为了保护用户的重要信息。假设如下:如果某个用户与供应商交互,并且不希望提交到站点的任何信息被泄露到其他站点。这种约束限制了不同站点之间的协作,但可以保护用户免受潜在的恶意攻击。
解决这个问题有很多方法。例如,JSONP 利用了 Web 页面可以从任何源码中下载脚本的能力。但 JSONP 有两个主要的限制:它与 Ajax 调用一样没有错误处理机制,并且脚本标记请求要使用 Get method,其中对长度有所限制。(有关 JSONP 的更多信息,请参阅 参考资料 部分)。
以下各小节探讨了针对跨域通信和数据传输的客户端解决方案。每个解决方案都有其优点和缺点;应用程序场景在很大程度上会影响您的选择。
如果源点 A 和 B 共享相同的超级域,那么更改 document.domain 属性可以使两个文档轻松进行互访。在 HTML 规范中,document.domain 是只读属性;目前的大多数浏览器允许将它设置成超级域(而不是顶级域)。例如,一个 URL 为 www.myapp.com/index.html 的文档可以将其域名设置为 myapp.com,而另一个来自 sample.myapp.com/index2.html 的文档也可以将域名设置为 myapp.com。图 1 显示了document.domain 的作用方式。
图 1. document.domain
利用跨子域的解决方案,不同子域的源点可以在相同超级域下进行通信,这不是 SOP 限制。但是,严格地说,跨子域的解决方案最适合 intranet 应用程序。
一个 URL 由几部分组成,如下图 2 所示:
图 2. URL 组件
通常,URL 的任何更改都会导致加载新的页面。唯一的例外是 hash 值的变化。任何 URL 的 hash 更改都不会导致页面刷新。许多 Web 2.0 网站中都已经开始广泛使用 Hash,以标记部分刷新页面时的每一个步骤。在跨域通信中,hash 是一笔宝贵的资产。虽然在获取对方 hash 值方面有一些限制,但来自不同源点的文件可以设置对方的 URL,包括 hash 值。文档相互之间可以使用 hash 传送信息。图 3 显示了一个示例。
图 3. 使用 URL.hash(fragment id) 进行通信
在图 3 中,当 A 想要向 B 发送信息时,它可以修改 B 的 hash 值,如清单 1 所示:
清单 1. 通过 url.hash 发送信息
function sendMsg(originURL, msg){
var data = {from:originURL, msg:msg};
var src = originURL + “#” + dojo.toJson(data);
document.getElementById('domainB').src=src;
}
|
B 中的函数将轮询自身的 hash 值,找出 A 发送的是什么,然后它可以用同样的方式回复 A。如果 A 想接收这条消息,也需要轮询其本身的 hash 值。
清单 2. 监视 url.hash 并从中接收信息
window.oldHash="";
checkMessage = function(){
var newHash = window.location.hash;
if(newHash.length > 1){
newHash = newHash.substring(1,newHash.length);
if(newHash != oldHash){
oldHash = newHash;
var msgs = dojo.fromJson(newHash);
var origin = msgs.from;
var msg = msgs.msg;
sendMessage(origin, "Hello document A");
}
}
}
window.setInterval(checkMessage, 1000);
sendMessage = function(target, msg){
var hash = "msg="+ msg;
parent.location.href= target + “#” + hash;
}
|
像 JSONP 一样,这种方法也有一个长度限制,但它可以更好地处理错误。一些特殊字符,比如问号(?),是 URL 中的保留字符,应先对这些字符进行编码。
清单 3. 通过 url.hash 发送包含特殊符号的信息
function sendMsg(originURL, msg){
…
var src = originURL + “#” + encodeURI (dojo.toJson(data));
…
}
|
接收时,应先进行解码。
清单 4. Receiving message containing special characters
function checkMsg(){
…
var msgs = decodeURI(dojo.fromJson(newHash));
…
}
|
由于许多网站将 hash 用于其他用途,对于这些网站来说,要将 URL.hash(fragment id) 技术并入跨域通信和数据传输是比较复杂的。跨帧 (Cross-frame) 信息传递有点类似片段 id 信息传递。图 4 显示了跨段 (cross-fragment) 是如何起作用的。
图 4. 跨段技术
在上图中,当 A 想要与 iframe B 进行通信时,会先在其自身中创建一个 iframe。这个 iframe 指向与 B 位于同一域中的 “代理” C。代理 URL 中包含参数、数据以及 B 的框架标识符。
清单 5. 使用跨段技术发送消息
function sendMsg(msg){
var frame = document.createElement(“iframe”);
var baseProxy = “http://www.otherapp.com/proxy.html”;
var request = {frameName:’otherApp’,data:msg};
frame.src = baseProxy+”#”+encodeURI (dojo.toJson(request));
frame.style.display=”none”;
document.body.appendChild(frame);
}
|
当 C 加载信息时,它会从 A 中获取请求和数据并调用 B 中相应的方法。由于 B 和 C 在相同的域中,所以可以通过另一个窗口的处理程序直接调用对方的方法。A 可以成功地向 B 发送信息,B 也可以用同样的方式进行响应。
清单 6. 使用 cross-fragment 技术接收消息
window.onLoad = function(){
var hash = window.location.hash;
if(hash && hash.length>1){
var request = hash.substring(1,hash.length);
var obj = dojo.fromJson(decodeURI (request));
var data = obj.data;
//process data
parent.frames[obj.frameName].getData(…);// getData in a function defined in B
}
}
|
OpenAjax 提供了托管集线器模块,以支持基于 fragment id 和 cross-frame 技术的跨域通信和数据传输。托管集线器模块包括管理器端和客户端。托管集线器中包含一个消息集线器,用于存储消息。每一个想要与其他部件通信的小部件都需要在自身建立一个集线器客户端,并且还需建立一个相应的容器来连接它。该容器将与代表客户端的托管集线器进行交互。客户端可以利用订阅/发布机制发送和接收消息。OpenAjax 的基本工作流如图 5 所示。
图 5. OpenAjax 的主要工作流
Window.name 是跨域通信和数据传输的有点棘手的一个解决方案。通常,window.name 的使用方式如下。
- 使用
window.frames[windowName]获取子窗口。 - 将它设置成链接元素中的目标属性。
虽然 window.name 的显著特点是非常适合将它用作不同源点文档之间的 “桥”,但它不是一个常用属性。无论加载什么页面,window.name 的值都不会发生改变。在 SOP 限制下如何使用它?图 6 说明了 window.name 是如何协助跨域通信的。
图 6. window.name 和 cross-domain 通信
页面 A 想获取其他源点的资源或 Web 服务时,它可以在自身中添加一个针对外部资源或服务的新的 iframe B。服务器会使用 HTML 文件进行响应,因此要设置数据的window.name 属性。由于现在 A 和 B 不在相同的域中,所以 A 仍然不能获取 B 的名称属性。B 获取数据后,应该可以导航回到任何与 A 位于相同域中的页面,使 A 可以访问名称属性。在 A 获取数据后,可以随时销毁 B。
Dojo 支持基于 window.name 的跨域通信。唯一的 API 是 dojox.io.windowName.send(method, args),它类似于 dojo.xhrGet/dojo.xhrPost。该方法可以是 GET 或 POST,并且 args 与 dojo.xhr 中的类似。例如,您可以在客户端发送一个跨域请求,如清单 7 所示:
清单 7. 通过 window.name 发送消息
var args = {
url: "http://www.sample.com/testServlet?windowName=true",
load: function(data){
alert("You've got the data from server " + data);
},
error: function(error){
alert("Error occurred: " + error);
}
}
dojox.io.windowName.send("GET",args);
|
可以按照使用 dojo.xhr 的方式来使用 dojox.io.windowName。对于服务器端,如果您希望利用 windowname 传输来访问资源或服务,建议您检查一下请求中的 windowName 参数。如果请求中包含此参数,那么服务器会用一个 HTML 文档进行响应,此文档将 window.name 设置为需要交付给客户端的数据。清单 8 中的代码显示了一个示例。
清单 8. window.name message 技术的后端支持
testServlet.java:
protected void doGet(HttpServletRequest request,HttpServletResponse response){
//process request
String returnData = ...;
String isWindowNameReq = request.getParameter(“windowName”);
if(null !=isWindowNameReq && Boolean.parseBoolean(isWindowNameReq)){
returnData = getCrossDomainStr(returnData);
}
response.getOutputStream().print(returnData);
}
private String getCrossDomainStr(String data){
StringBuffer returnStr = new StringBuffer();
returnStr.append("<html><head><script type=\"text/javascript\">window.name='");
returnStr.append(data);
returnStr.append("'</script></head><body></body></html>");
return returnStr.toString();
}
|
将框架导航回来源域的任何页面时,您需要保证域中的这个页面是存在的。如果页面不存在,Internet Explorer 将会出现问题。在 Firefox 中,您只需要使用 blank.html。在 Dojo 中,您可以指定使用 dojo.dojoBlankHtmlUrl 属性时的回滚页面。默认情况下,将该页面设置为 Dojo 库中的 dojo/resources/blank.html。
使用 window.name 传输的数据量远远多于使用 url.hash 传输的数据。目前的大多数浏览器都支持基于 window.name 的 16M+ 数据转换。
在 HTML5 规范的草案中,新方法 window.postMessage(message, targetOrigin) 可用于安全跨域通信。调用该方法时,会分派一个消息事件,如果该窗口正在监听消息事件,那么它能够从事件中获取消息以及消息来源。图 7 显示了一个示例。
图 7. 使用 HTML5 进行跨域通信
在图 7 中,当 iframe 想要通知来自另一个源点的父窗口已成功完成加载时,它可以通过 window.postMessage 发送消息。同时,它将监测反馈消息,如清单 9 所示:
清单 9. 通过 HTML5 的新方法发送消息
http://www.otherapp.com/index.html
function postMessage(msg){
var targetWindow = parent.window;
targetWindow.postMessage(msg,"*");
}
function handleReceive(msg){
var object = dojo.fromJson(msg);
if(object.status == “ok”){
//continue to do other things
……
}else{
//retry sending msg
……
}
}
window.addEventListener("message", handleReceive, false);
window.onLoad = function(){
postMessage("already loaded");
}
|
父文档将监听消息事件。当消息到达时,会先检查它是否来自 www.otherapp.com,然后返回确认消息。
清单 10. 通过 HTML5 的新方法接收消息
http://www.myapp.com/index.html
function handleReceive(event){
if(event.origin != "http://www.otherapp.com")
return;
//process data
……
var otherAppFrame = document.getElementById(“otherApp”)
otherAppFrame.postMessage(“{status:’ok’}”,”http://www.otherapp.com”);
}
window.addEventListener("message", handleReceive, false);
|
清单 10 中的示例代码可以在 Firefox 3+、Internet Explorer 8、Google Chrome 2、Opera 9+ 以及 Safari 4 中运行。它使得来自不同来源的文档之间的通信变得更加方便。此外,如果不希望自己的文档收到来自其他文档的任何消息,那么不要添加事件监听器并删除所有消息。
学习
- “OpenAjax Hub 2.0 Specification: Managed Hub Overview”(OpenAjax Alliance,2009 ):获取 OpenAjax 集线器的概述,并了解关于托管集线器的更多信息。
- “OpenAjax Hub 2.0 Specification: Managed Hub APIs” (OpenAjax Alliance,2009):学会编写自己的跨域实现。
-
dojox.io.windowName:了解关于通过 windowName 实现 Dojo 跨域通信的更多信息。
- “使用 JSONP 实现跨域通信,第 1 部分:结合 JSONP 和 jQuery 快速构建强大的 mashup”(developerWorks,2009 年 2 月):了解如何将晦涩难懂的跨域调用技术 (JSONP) 与灵活的 JavaScript 库 (jQuery) 相结合,以便快速构建强大的混搭应用程序。
-
“HTML Living Standard: Communication: Cross-document messaging”(WHATWG.org,2011 年 6 月):获取有关 HTML5 的跨域通信标准的详细信息。
-
developerWorks 中国网站 Web 开发专区:找到介绍基于 Web 的解决方案的文章。
-
developerWorksLive! 技术讲座:随时关注 developerWorks 技术活动和网络广播。
- 查看 HTML5 专题,了解更多和 HTML5 相关的知识和动向。
获得产品和技术
-
Dojo toolkit 下载:获取 Dojo(Dojo 1.6)。
-
OpenAjax 集线器库:获取 OpenAjax 集线器。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。