打印

HM9001: Chrome Safari Firefox 中 IFRAME 元素在文档树中发生变化后父子页面间的某些交互方式会失效

作者:陆远

标准参考

问题描述

Chrome Safari 子页面中使用 frames 集合获取其父页面中通过 innerHTML 移动位置后的 IFRAME 元素得到的对象类型为 HTMLIFrameElement,而不是正常的 Window,这将导致其下的 document 等子对象无法访问。
Firefox 中,子页面中获取在 window.onload 中移动位置后的 IFRAME 元素 window 对象内的一些子对象可能出错。

造成的影响

触发此问题后会影响 IFRAME 内外页面的交互,可能造成各种兼容性问题。

受影响的浏览器

Chrome Safari Firefox  

问题分析

window 对象中的 frames 集合可以返回当前 window 中的子框架列表,这是一个类似数组的集合对象。可以通过整型下标或者子框架元素的 name 属性获取到该集合内对应的子框架 window 对象。

IFRAME 元素对应的 DOM 对象为 HTMLIframeElement,各浏览器均支持 HTMLIframeElement 接口中的 contentWindow 属性,这个属性返回 IFRAME 引入子页面的 window 对象。

假设在当前父页面中存在一个 id 和 name 属性为 "ifr" 的 IFRAME 对象,则可以通过 window.frames["ifr"] 或者 document.getElementById("ifr").contentWindow 这两组方法获取到 IFRAME 引入页面的 window 对象。这两种方法在所有主流浏览器中均有很好的兼容性。但是却并不符合 W3C 规范。其中 window.frames 集合算 DOM Level 0 范畴,而 contentWindow 属性为 IE5.5 引入。

关于 window.frames 集合的相信资料,请参考:MSDNMozilla Developer CenterSafari Refernece Library

关于 contentWindow 属性的详细资料,请参考:MSDNMozilla Developer CenterSafari Refernece Library

下面分 4 种情况测试当 IFRAME 元素在文档树中发生变化对 IFRAME 内外的子页面与父页面交互的影响。

  1. window.onload 之前,通过 innerHTML 方法改变 IFRAME 元素在文档树中的位置:main1.html
  2. window.onload 之前,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置:main2.html
  3. window.onload 之后,通过 innerHTML 方法改变 IFRAME 元素在文档树中的位置:main3.html
  4. window.onload 之后,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置:main4.html

测试代码如下:

main1.html
<style>
    iframe { width:450px; height:1800px; }
</style>
before window.onload, innerHTML
<div id="div1">
    <iframe id="ifr1" name="ifr1" src="sub.html"></iframe>
    <iframe id="ifr2" name="ifr2" src="0.html"></iframe>
</div>
<div id="div2">
</div>
<script>
    function $(id) {
        return document.getElementById(id);
    }

    $("div2").innerHTML = $("div1").innerHTML;
    $("div1").innerHTML = "";
</script>
main2.html
<style>
    iframe { width:450px; height:1800px; }
</style>
before window.onload, appendChild
<div id="div1">
    <iframe id="ifr1" name="ifr1" src="sub.html"></iframe>
    <iframe id="ifr2" name="ifr2" src="0.html"></iframe>
</div>
<div id="div2">
</div>
<script>
    function $(id) {
        return document.getElementById(id);
    }

    var iframe1 = $("div1").children[0];
    var iframe2 = $("div1").children[1];
    $("div1").removeChild(iframe1);
    $("div1").removeChild(iframe2);
    $("div2").appendChild(iframe1);
    $("div2").appendChild(iframe2);
</script>
main3.html
<style>
    iframe { width:450px; height:1800px; }
</style>
after window.onload, innerHTML
<div id="div1">
    <iframe id="ifr1" name="ifr1" src="sub.html"></iframe>
    <iframe id="ifr2" name="ifr2" src="0.html"></iframe>
</div>
<div id="div2">
</div>
<script>
    function $(id) {
        return document.getElementById(id);
    }

    window.onload = function () {
        $("div2").innerHTML = $("div1").innerHTML;
        $("div1").innerHTML = "";
    }
</script>
main4.html
<style>
    iframe { width:450px; height:1800px; }
</style>
after window.onload, appendChild
<div id="div1">
    <iframe id="ifr1" name="ifr1" src="sub.html"></iframe>
    <iframe id="ifr2" name="ifr2" src="0.html"></iframe>
</div>
<div id="div2">
</div>
<script>
    function $(id) {
        return document.getElementById(id);
    }

    window.onload = function () {
        var iframe1 = $("div1").children[0];
        var iframe2 = $("div1").children[1];
        $("div1").removeChild(iframe1);
        $("div1").removeChild(iframe2);
        $("div2").appendChild(iframe1);
        $("div2").appendChild(iframe2);
    }
</script>
sub.html
<html>
<head>
<style>
    * { font:12px Arial; }
    body { margin:0; }
    span { font-weight:bold; }
    .g { color:green; }
    .r { color:red; }
</style>
</head>
<body>
<dl>
<script>
  function myEval (code) {
    var script = document.createElement("script");
    script.text = "var ret =" + code;
    document.body.appendChild(script);
    var x = ret;
    document.body.removeChild(script);
    return x;
  }

    function tryObj (obj_text) {
        var ok = '<span class="g">OK</span>';
        var fail = '<span class="r">FAIL</span>'
        try {
            var f = "";
            var ev = myEval(obj_text);
            try {
              f = ev.toString();
            } catch(e) {
              f = "";
            }
            return ev ? ok + " " + f : fail + " " + f;
        } catch(e) {
          return fail + " " + f;
        }
    }

    var arr = [
        'parent',
        'parent.document',
        'parent.document.getElementById("ifr1")',
        'parent.document.getElementById("ifr1").contentWindow',
        'parent.document.getElementById("ifr1").contentWindow.document',
        'parent.document.getElementById("ifr1").contentWindow.history',
        'parent.document.getElementById("ifr1").contentWindow.location',
        'parent.document.getElementById("ifr1").contentWindow.navigator',
        'parent.document.getElementById("ifr1").contentWindow.screen',
        'parent.document.getElementById("ifr1").contentWindow.alert',
        'parent.document.getElementById("ifr2")',
        'parent.document.getElementById("ifr2").contentWindow',
        'parent.document.getElementById("ifr2").contentWindow.document',
        'parent.document.getElementById("ifr2").contentWindow.history',
        'parent.document.getElementById("ifr2").contentWindow.location',
        'parent.document.getElementById("ifr2").contentWindow.navigator',
        'parent.document.getElementById("ifr2").contentWindow.screen',
        'parent.document.getElementById("ifr2").contentWindow.alert',
        'parent.frames',
        'parent.frames["ifr1"]',
        'parent.frames["ifr1"].document',
        'parent.frames["ifr1"].history',
        'parent.frames["ifr1"].location',
        'parent.frames["ifr1"].navigator',
        'parent.frames["ifr1"].screen',
        'parent.frames["ifr1"].alert',
        'parent.frames["ifr2"]',
        'parent.frames["ifr2"].document',
        'parent.frames["ifr2"].history',
        'parent.frames["ifr2"].location',
        'parent.frames["ifr2"].navigator',
        'parent.frames["ifr2"].screen',
        'parent.frames["ifr2"].alert'
    ];
    for (var i in arr) {
        document.write('<dt>' + arr[i] + ':</dt>');
        document.write('<dd>' + tryObj(arr[i]) + '</dd>');
    }
</script>
</dl>
</body>
</html>
0.html
<html></html>

上面代码中有 4 个主页面 main1.htmlmain2.htmlmain3.htmlmain4.html,分别对应本文分析的 4 中情况,每组代码均包含两个 DIV 元素【div1】与【div2】,其中初始状态【div1】包含 IFRAME 元素【ifr1】及【ifr2】,【div2】中为空。
【ifr1】引入了子页面 "sub.html" ,【ifr2】引入了子页面 "0.html" 。通过 JavaScript 将【div1】中的【ifr1】及【ifr2】移动到【div2】内。但是 4 个主页面采取了不同的移动方式。

子页面中,分别判断了 33 个对象的状态,若存在该对象,则显示“OK" 及对象类型,否则显示 "FAIL" 。

在本地构建 Web 服务器1,将测试代码放入服务器进行测试。

  window.onload 之前,通过 innerHTML 方法改变 IFRAME 元素在文档树中的位置 window.onload 之前,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置 window.onload 之后,通过 innerHTML 方法改变 IFRAME 元素在文档树中的位置 window.onload 之后,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置
IE6 IE7 IE8 Opera Firefox Chrome Safari IE6 IE7 IE8 Opera Firefox Chrome Safari IE6 IE7 IE8 Opera Firefox Chrome Safari IE6 IE7 IE8 Opera Firefox Chrome Safari
parent OK OK OK OK OK OK OK OK OK OK OK OK
parent.document OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1") OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1").contentWindow OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById( "ifr1") .contentWindow.document OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1") .contentWindow.history OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1") .contentWindow.location OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1") .contentWindow.navigator OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1") .contentWindow.screen OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr1") .contentWindow.alert OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow.document OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow.history OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow.location OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow.navigator OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow.screen OK OK OK OK OK OK OK OK OK OK OK OK
parent.document .getElementById("ifr2") .contentWindow.alert OK OK OK OK OK OK OK OK OK OK OK OK
parent.frames OK OK OK OK OK OK OK OK OK OK OK OK
parent.frames["ifr1"] OK OK OK2 OK OK OK OK OK OK2 OK OK OK
parent.frames["ifr1"].document OK OK FAIL OK OK OK OK FAIL FAIL OK FAIL OK
parent.frames["ifr1"].history OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr1"].location OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr1"].navigator OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr1"].screen OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr1"].alert OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr2"] OK OK OK2 OK OK OK OK OK OK2 OK OK OK
parent.frames["ifr2"].document OK OK FAIL OK OK OK OK FAIL FAIL OK FAIL OK
parent.frames["ifr2"].history OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr2"].location OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr2"].navigator OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr2"].screen OK OK FAIL OK OK OK OK OK FAIL OK OK OK
parent.frames["ifr2"].alert OK OK FAIL OK OK OK OK OK FAIL OK OK OK

注1. Chrome 中认为本地页面为跨域,IFRAME 元素父子页面之间的脚本交互是不安全的,会在控制台提示错误:Unsafe JavaScript attempt to access frame with URL [XXX] from frame with URL [XXX]. Domains, protocols and ports must match.
注2. 与其他浏览器不同,Chrome 和 Safari 此时虽然返回一个有效对象,但此对象类型不是 [window] 而是 [HTMLIframeElement]。

从上表中的结果可见,通过 document.getElementById("IFRAME").contentWindow 的方式获取 IFRAME 元素引入子页面的 window 对象,各浏览器均没有任何问题。而 window.frames 方式则产生了差异:

  1. window.onload 之前,通过 innerHTML 方法改变 IFRAME 元素在文档树中的位置:
    • IE6 IE7 IE8 Firefox Opera 中,子页面均可以获得移动后的【ifr1】及【ifr2】相关对象;
    • Chrome Safari 中,虽然可以获取到父页面的【ifr1】及【ifr2】相关对象,但是与 IE Firfox Opera 中不同,该对象类型为 "HTMLIframeElement" ,而不是 "Window" ,所以该对象的子对象无法获取。
  2. window.onload 之前,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置:
    • 各浏览器中,子页面均可以获得移动后的【ifr1】及【ifr2】相关对象。
  3. window.onload 之后,通过 innerHTML 方法改变 IFRAME 元素在文档树中的位置:
    • IE6 IE7 IE8 Opera 中,子页面均可以获得移动后的【ifr1】及【ifr2】相关对象;
    • Firefox 中,在 window.onload 中移动 IFRAME 却使 IFRAME 的 window 对象中 "document" 对象失效;
    • Chrome Safari 中,虽然可以获取到父页面的【ifr1】及【ifr2】相关对象,但是与 IE Firefox Opera 中不同,该对象类型为 "HTMLIframeElement" ,而不是 "Window" ,所以该对象的子对象无法获取。
  4. window.onload 之后,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置:
    • IE6 IE7 IE8 Opera Chrome Safari 中,子页面均可以获得移动后的【ifr1】及【ifr2】相关对象;
    • Firefox 中,在 window.onload 中移动 IFRAME 却使 IFRAME 的 window 对象中“document" 对象失效。

可见,对于 window.frames 方式,使用情况 2,即 window.onload 之前,通过 removeChild、AppendChild 方法改变 IFRAME 元素在文档树中的位置不会出现兼容性问题。

解决方案

根据上面所得的结果,推荐使用 document.getElementById("IFRAME").contentWindow.document 获取 IFRAME 元素内页面的 document 对象,且对于在文档树中移动位置后的 IFRAME 元素也有很好的兼容性。同时应避免对跨域的父子页面交互。

参见

知识库

相关问题

测试环境

操作系统版本: Windows 7 Ultimate build 7600
浏览器版本: IE6
IE7
IE8
Firefox 3.6.7
Chrome 6.0.472.0 dev
Safari 5.0
Opera 10.60
测试页面: main1.html
main2.html
main3.html
main4.html
本文更新时间: 2010-07-22

关键字

IFRAME parent frames collection 文档树 移动 交互 contentWindow