【翻译】Iframe, onload 与 document.domain

原文链接:http://www.nczonline.net/blog/2009/09/15/iframes-onload-and-documentdomain/
译者:Demix

在web2.0的时代,越来越多的人开始关注使用iframe将第三方网站的内容嵌入自己的网站中。当javascript能够通过其域名进行数据交互后,iframe开始提供一系列的安全措施,使得一个嵌套于iframe中的第三方网站不可能获取到主体网站的脚本程序。这个跨域的限制同样也让父级页面无法读取嵌套内容的脚本。从所有的角度来说,父级页面和被iframe包含的页面是完全没有联系的。这个复杂的关系让javascript对象的所有权成为了许多有关iframe讨论的话题之一。

iframe 和所有权

iframe元素本身是位于父级页面中的,所以你可以像一个普通元素一样的使用和操作它。代表了iframe内容window对象是作为一个页面的属性加入到iframe中的。为了让父级页面能够以一种合适的方式获取iframe的window对象,父级页面和iframe页面的域名应该保持一致(详情)。

当域名吻合时,父级页面就可以获取到iframe的window对象了。iframe元素拥有名为contentDocument的属性,这个属性包含了iframe对象的document对象,于是我们就可以使用parentWindow这个属性取回window对象。这已经成为了获取iframe的window对象的标准方法,并被绝大多数浏览器支持。ie8以前的浏览器不支持这个属性,我们需要用以使用其专有的contentWindow属性。如

1 function getIframeWindow(iframeElement){
2     return iframeElement.contentWindow || iframeElement.contentDocument.parentWindow;
3 }

补充一点,父级页面的window对象在iframe中能够以window.parent获取。iframe元素同样也可以使用window.frameElement来获取自己的引用。由于iframe被父级元素包含但却可以直接获取到iframe的window对象,该方法广泛用于突破二者的界限。

使用iframe元素的onload事件

由于各种所有权的不同,尝试确定iframe何时装载完毕是一个很有趣的实验。非ie浏览器提供了许多有用的方法。它们让iframe元素拥有load事件,这样我们就可以确定iframe何时装载完全。由于iframe元素包含于父级页面中,你也不用担心跨域的限制。装载本地数据的iframe可以使用监听装载外部数据的iframe完成事件的相同方法。举例如下:

1 var iframe = document.createElement("iframe");
2 iframe.src = "simpleinner.htm";
3 iframe.onload = function(){
4     alert("Iframe is now loaded.");
5 };
6 document.body.appendChild(iframe);

上面的例子在所有非ie浏览器中均适用。我曾经尝试使用attachEvent方法,不过最终发现ie并不支持在iframe上的load事件

使用iframe的window对象的onload事件

看起来ie又要给我们制造难题了。不过随后,我记起来我以前没有考虑过在iframe中引用外部文件。在我的实验中,我曾经处理了同一域名下的内容。由于跨域限制不存在,我能够轻易的获取iframe对象的window对象并加上onload事件。例如:

1 var iframe = document.createElement("iframe"),
2     iframeWindow;
3 iframe.src = "simpleinner.htm";
4 document.body.appendChild(iframe);
5 iframeWindow = iframe.contentWindow || iframe.contentDocument.parentWindow;
6 iframeWindow.onload = function(){
7     alert("Local iframe is now loaded.");
8 };

有趣的是,你必须在iframe元素已经加到页面中以后才能注册事件。如果先于它,iframe的window对象将不存在,我们也当然不可能在window对象上注册事件。这个方法只在ie和ff下对于同域的两个嵌套页面有效。其他浏览器不会创建window对象并将抛出异常。

定义document.domain

我试图寻找一种可以监听ie里iframe的load事件的方法以及更多的应用于其他浏览器的方法,于是我继续了我的实验。接下来,由于我有多个需要使用iframe读取的不同二级域名的页面,我设置了父级页面的document.domain。将document.domain设定为主域名能够允许这些iframe之间以及同父级页面的通信。例如,如果我有需要读取一个地址为www2.nczonline.net的iframe,在技术上上说是不被允许的。不过,如果我在父级页面和iframe页面中均设置了document.domain为'nczonline.net',这两个页面将可以相互通讯。如下:

1 document.domain = "nczonline.net";

这个声明消除了域名的区别,我们可以像处理两个相同域名的网站一样处理这两个页面。

有一个问题又产生了。在iframe完全加载前,它将被认为是属于iframe标签中声明的src属性标志的页面的。相对地址被自动加上了父级页面的地址(www.nczonline.net)并与我们设置的document.domain相矛盾。这意味着在比较nczonline.net和www.nczonline.net时,我们将通不过同域检查,于是当我们试图获取iframe的window对象时,将引起javascript的报错。iframe页面并不会改变其关联的domain值,直到它加载完毕,届时改变domain值的脚本才会执行。当iframe已经加载完毕时,一切运行完美。但是,我们是怎么知道iframe什么时候才加载完?

换个方向思考

由于一致没有找到一种可以跨浏览器解决判定iframe是否加载完毕的方法,我决定转变一下我的想法。如果我们让iframe告诉父级页面它已经加载完毕,而不是让父级页面去获取iframe的load事件,也许能够解决问题。我希望这种方法能够与注册一个事件句柄一样简单,所以我采用了下面的想法:我在iframe元素上声明一个方法,然后,当iframe页面加载完毕之后会执行这个函数。当然,这个方法是被声明到iframe元素本身而不是iframe的window对象上的,后一方法在前面的研究中被证明不能兼容所有的浏览器。结果看起来像这样:

1 var iframe = document.createElement("iframe");
2 iframe.src = "simpleinner.htm";
3 iframe._myMethod = function(){
4     alert("Local iframe is now loaded.");
5 };
6 document.body.appendChild(iframe);

上面的代码在iframe元素上声明了_myMethod的方法。iframe中的页面加入如下方法:

1 window.onload = function(){
2     window.frameElement._myMethod();
3 }

由于上述代码是在我们声明document.domain之后运行的,于是我们便不用担心任何安全限制的问题。这种方法在同一主域名下工作得很完美。它能够兼容所有的浏览器,这也正是我所需要的。但是,监听包含第三方页面的iframe的load事件仍然在困扰我。

使用iframe的onreadystatechange

我决定研究一下ie浏览器关于iframe的接口文档。如果在onload事件中声明某些事件显而易见不能达到我们想要的效果,但是我觉得肯定会有类似的方法。我尝试使用attachEvent方法去增加事件句柄,但是仍然没有用。ok,显然ie中的iframe并不支持load事件。有其他方法吗?

接下来我使用了ie的一种怪异的方法——readystatechange事件。显然它与xhr对象的readystatechange事件完全不一样。我想知道是否iframe元素也支持这个事件,它会在iframe嵌套的内容加载完全前变成'interactive',随后变成'complete'。同时,由于它是注册到iframe元素而不是iframe的window对象上,这理所当然不会存在跨域的问题。最后我整理出来的代码如下:

 1 var iframe = document.createElement("iframe");
 2 iframe.src = "simpleinner.htm";
 3 
 4 if (navigator.userAgent.indexOf("MSIE"> -1 && !window.opera){
 5     iframe.onreadystatechange = function(){
 6         if (iframe.readyState == "complete"){
 7             alert("Local iframe is now loaded.");
 8         }
 9     };
10 else {
11     iframe.onload = function(){
12         alert("Local iframe is now loaded.");
13     };
14 }
15 
16 document.body.appendChild(iframe);

判断浏览器是否为ie浏览器稍稍有些麻烦。本来我更偏向使用判断iframe.readystate是否存在来进行浏览器的检测。但是,当试图获取未加入到页面中的iframe的属性时会抛出一个错误。我也尝试使用document.readyState去判断是否使用readystatechange,然而,已经有很多浏览器支持前一属性了,所以它并不是一个有效的划分手段。

ie 对onload事件的支持

在发表这篇文章后短暂的时间里, Christopher留言说在iframe元素上使用attachEvent是能够在ie下工作的。我发誓我之前已经尝试过这样的方法,但是由于他的提示,我尝试了另外一个实验。随后发现,他是正确的。随后我研读了msdn上的文档,最后终于发现了一段文档说明了这个问题。它最终让我们的代码变成了下面这个样子

 1 var iframe = document.createElement("iframe");
 2 iframe.src = "simpleinner.htm";
 3 
 4 if (iframe.attachEvent){
 5     iframe.attachEvent("onload"function(){
 6         alert("Local iframe is now loaded.");
 7     });
 8 else {
 9     iframe.onload = function(){
10         alert("Local iframe is now loaded.");
11     };
12 }
13 
14 document.body.appendChild(iframe);

以上的代码仍然能正常运行于所有的浏览器之上,并能回避readystatechange事件与load事件潜在的冲突可能。

综合

在一小段调研之后,我们发现确定一个iframe对象何时加载完成的跨浏览器的方法是存在的。这让我们对iframe的监听和错误控制变得容易的多。感谢所有的浏览器厂商看到了在iframe元素上添加这些事件的好处,而不是去依赖iframe的window对象或者认为我们平时并不关心iframe何时完成加载。



很久没有翻译了,草草翻译出上面这篇文章,错误一定不少。Zakas这篇文章很搞笑,写出来几分钟后有人留言说里面有错误,又改掉了。这里我们又看到写博客的一个好处——共同成长。如果没有后来的评论,也许Zakas会一直使用不太优雅的监听readystatechange事件来实现。另外通过这篇文章,我们看到了大师的细致之处。虽然整篇文章所描述的问题也许我们平时都会有接触,但是又有哪一个人会有这样的细致。PPK也如此,老道也如此,所有的大师都是如此。成功,重在细节。
posted @ 2009-09-16 16:30  demix  阅读(5107)  评论(7编辑  收藏  举报