如何在iFrame中访问DOM元素

Dav*_*Sag 19 javascript iframe cross-domain

我正在编写一个jQuery插件,需要能够在iFrame中针对DOM元素运行.我现在正在本地测试这个(即url是file://.../example.html),在Chrome中我一直在点击"SecurityError:无法从'HTMLIFrameElement'读取'contentDocument'属性:阻止了一个框架原点"null"来访问跨域框架." 在Safari中我只得到一个空文档.

鉴于父文件和iFrame的文件都来自我的本地磁盘(在开发中)并且将从相同的服务器(生产中)下来,我认为我不会受到跨源问题的影响.

有没有办法可以说服浏览器我的本地文件实际上是同一个域?

<旁边> 有趣的是在Safari中,直接使用控制台,我可以输入$("iframe").get(0).contentDocument.find("ol"),并愉快地找到我的列表.在Chrome中,同一行会抛出安全错误,就像它正在执行一样.</一旁>

更新

基于下面的建议,我已经启动了一个简单的本地Web服务器来测试这个,现在我没有得到跨源错误 - yay - 但我也没有得到任何内容.

我的Javascript看起来像

$(document).ready(function(){
  var myFrame = $("iframe"),
      myDocument = $(myFrame.get(0).contentDocument),
      myElements;
  myDocument.ready(function(){
    myElements = myDocument.find("ul, ol");
    console.debug("success - iFrame", myFrame, "document", myDocument, "elements", myElements);
  });
});
Run Code Online (Sandbox Code Playgroud)

myDocument.ready是那里只是为了确保iframe的文件准备-在现实中它没有什么区别.

我总是以myElements空虚为结局.([]在safari或jQuery.fn.init[0]Chrome中)

但是,如果我手动将其键入控制台:

$($("iframe").get(0).contentDocument).find("ol, ul")
Run Code Online (Sandbox Code Playgroud)

我按预期得到了我的清单.Safari和Chrome现在都是这种情况.

所以我的问题变成了:为什么我的脚本不能看到DOM元素,但是当直接输入浏览器的控制台时,相同的代码可以很高兴地看到DOM元素?

jfr*_*d00 28

Chrome具有默认安全限制,即使它们在技术上是相同的来源,也不允许您从硬盘访问其他窗口.Chrome中有一个标志可以放宽安全限制(Windows上的命令行参数是我记得的),但我不建议使用该标志运行以进行快速测试.有关命令行参数的信息,请参阅此文章本文.

如果您从Web服务器(即使它是本地Web服务器)而不是您的硬盘驱动器运行文件,您将不会遇到此问题.

或者,您可以在其他不受限制的浏览器中进行测试.


现在您已将问题更改为不同的内容,您必须等待iframe窗口加载才能访问其中的内容,并且您不能.ready()在不同的文档上使用jQuery (它不适用于其他文档).

$(document).ready(function() {
    // get the iframe in my documnet
    var iframe = document.getElementById("testFrame");
    // get the window associated with that iframe
    var iWindow = iframe.contentWindow;

    // wait for the window to load before accessing the content
    iWindow.addEventListener("load", function() {
        // get the document from the window
        var doc = iframe.contentDocument || iframe.contentWindow.document;

        // find the target in the iframe content
        var target = doc.getElementById("target");
        target.innerHTML = "Found It!";
    });
});
Run Code Online (Sandbox Code Playgroud)

测试页面在这里.


编辑:经过进一步的研究,我发现jQuery会像这样为你做一些这样的工作,jQuery解决方案似乎适用于所有主流浏览器:

$(document).ready(function() {
    $("#testFrame").load(function() {
        var doc = this.contentDocument || this.contentWindow.document;
        var target = doc.getElementById("target");
        target.innerHTML = "Found It!";
    });
});
Run Code Online (Sandbox Code Playgroud)

测试页面在这里.

在查看jQuery实现时,它真正做的就是load在iFrame上设置一个事件监听器.


如果您想了解调试/解决上述第一种方法的细节:

在我试图解决这个问题时,我发现了一些非常奇怪的东西(在Chrome中)与iFrames.当你第一次看到iframe的窗口时,有一个文件,它说readyState === "complete"你认为它已经完成加载,但它正在撒谎.<body>通过URL加载到iframe的文档中的实际文档和实际标记实际上还没有.我通过在自定义属性上添加自定义属性<body data-test="hello">并检查该自定义属性来证明这一点.瞧,看哪.尽管如此document.readyState === "complete",<body>标签上没有自定义属性.因此,我总结(至少在Chrome中)iFrame最初有一个虚拟的空文档和正文,这些文档和正文一旦将URL加载到iFrame中,就不会是实际的文档和正文.这使得整个过程检测到什么时候它已经准备好让人感到困惑(这需要花费我几个小时来解决这个问题).事实上,如果我设置一个间隔计时器和轮询iWindow.document.body.getAttribute("data-test"),我会看到它显示为undefined重复,然后最终它将显示正确的值,所有这一切document.readyState === "complete"意味着它完全在说谎.

我认为发生的事情是iFrame以虚拟和空文档开始,然后在内容开始加载后替换.另一方面,iFrame window是真正的窗口.因此,我发现实际等待加载内容的唯一方法是监视loadiFrame上的事件,window因为这似乎并不存在.如果您知道有一些您正在等待的特定内容,您也可以进行轮询直到该内容可用.但是,即便如此,你也必须小心,因为你不能iframe.contentWindow.document太快拿到它,因为如果你太快拿到它,那将是错误的文件.整件事情非常糟糕.我找不到任何方法可以DOMContentLoaded在iFrame文档本身之外使用,因为您无法知道实际document对象是否到位,您可以将事件处理程序附加到它.所以...我load在iFrame窗口上安排了这个活动似乎有效.


如果您实际控制了iFrame中的代码,那么您可以更轻松地从iFrame本身触发事件,方法是$(document).ready()在iFrame代码中使用jQuery 和它自己的jQuery版本,或者通过在脚本中调用父窗口中的函数位于目标元素之后(从而确保目标元素已加载并准备就绪).


进一步编辑

经过一系列的研究和测试,这里有一个函数可以告诉你何时iFrame命中DOMContentLoaded事件而不是等待load事件(图像和样式表可能需要更长时间).

// This function ONLY works for iFrames of the same origin as their parent
function iFrameReady(iFrame, fn) {
    var timer;
    var fired = false;

    function ready() {
        if (!fired) {
            fired = true;
            clearTimeout(timer);
            fn.call(this);
        }
    }

    function readyState() {
        if (this.readyState === "complete") {
            ready.call(this);
        }
    }

    // cross platform event handler for compatibility with older IE versions
    function addEvent(elem, event, fn) {
        if (elem.addEventListener) {
            return elem.addEventListener(event, fn);
        } else {
            return elem.attachEvent("on" + event, function () {
                return fn.call(elem, window.event);
            });
        }
    }

    // use iFrame load as a backup - though the other events should occur first
    addEvent(iFrame, "load", function () {
        ready.call(iFrame.contentDocument || iFrame.contentWindow.document);
    });

    function checkLoaded() {
        var doc = iFrame.contentDocument || iFrame.contentWindow.document;
        // We can tell if there is a dummy document installed because the dummy document
        // will have an URL that starts with "about:".  The real document will not have that URL
        if (doc.URL.indexOf("about:") !== 0) {
            if (doc.readyState === "complete") {
                ready.call(doc);
            } else {
                // set event listener for DOMContentLoaded on the new document
                addEvent(doc, "DOMContentLoaded", ready);
                addEvent(doc, "readystatechange", readyState);
            }
        } else {
            // still same old original document, so keep looking for content or new document
            timer = setTimeout(checkLoaded, 1);
        }
    }
    checkLoaded();
}
Run Code Online (Sandbox Code Playgroud)

这简单地称为:

// call this when you know the iFrame has been loaded
// in jQuery, you would put this in a $(document).ready()
iFrameReady(document.getElementById("testFrame"), function() {
    var target = this.getElementById("target");
    target.innerHTML = "Found It!";
});
Run Code Online (Sandbox Code Playgroud)