【问题标题】:How can I access the DOM elements within an iFrame如何访问 iFrame 中的 DOM 元素
【发布时间】:2014-07-07 05:31:38
【问题描述】:

我正在编写一个 jQuery 插件,它需要能够针对 iFrame 中的 DOM 元素运行。我现在只是在本地测试这个(即 url 是 file://.../example.html),在 Chrome 中我一直在点击“SecurityError: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Blocked a frame访问跨域框架的来源为“null”。”在 Safari 中,我只得到一个空文档。

鉴于父文件和 iFrame 的文件都来自我的本地磁盘(开发中)并且将来自同一个服务器(生产中),我原以为我不会受到十字架的影响-起源问题。

有没有办法让浏览器相信我的本地文件实际上属于同一个域?

更新

根据下面的建议,我启动了一个简单的本地网络服务器来测试它,现在没有收到跨域错误 - 是的 - 但我也没有收到任何内容。

我的 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);
  });
});

myDocument.ready 只是为了确保 iFrame 的文档准备就绪 - 实际上它没有任何区别。

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

但如果我在控制台中手动输入:

$($("iframe").get(0).contentDocument).find("ol, ul")

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

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

【问题讨论】:

    标签: javascript iframe cross-domain


    【解决方案1】:

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

    如果您从网络服务器(即使它是本地网络服务器)而不是硬盘驱动器上运行文件,则不会出现此问题。

    或者,您可以在其他没有限制的浏览器中进行测试。


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

    $(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!";
        });
    });
    

    测试页面here


    编辑:经过进一步研究,我发现 jQuery 会像这样为您完成一些工作,并且 jQuery 解决方案似乎适用于所有主要浏览器:

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

    测试页面here

    在查看 jQuery 实现时,它实际上所做的只是在 iFrame 本身上设置一个 load 事件侦听器。


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

    在我试图解决这个问题的过程中,我发现了一些非常奇怪的东西(在 Chrome 中)与 iFrames。当您第一次查看 iframe 的窗口时,有一个文档,上面写着它的 readyState === "complete",因此您认为它已完成加载,但它在撒谎。通过 URL 加载到 iframe 中的文档中的实际文档和实际 <body> 标记实际上还不存在。我通过在<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 是真正的窗口。因此,我发现实际等待加载内容的唯一方法是监视 iFrame window 上的 load 事件,因为这似乎不是谎言。如果您知道您正在等待某些特定内容,您还可以轮询直到该内容可用。但是,即使那样你也必须小心,因为你不能太快获取iframe.contentWindow.document,因为如果你太早获取它将会是错误的文档。整个事情已经很破碎了。我无法从 iFrame 文档本身之外找到使用 DOMContentLoaded 的任何方法,因为您无法知道实际的 document 对象是否已到位,您可以将事件处理程序附加到它。所以...我选择了 iFrame 窗口上的 load 事件,这似乎确实有效。


    如果您实际控制 iFrame 中的代码,那么您可以更轻松地从 iFrame 本身触发事件,方法是在 iFrame 代码中使用带有 $(document).ready() 的 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();
    }
    

    这只是这样称呼:

    // 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!";
    });
    

    【讨论】:

    • 很公平。在 Mac 上运行并且无法控制我的用户将运行什么(即使在测试中)。
    • @DaveSag - 经过进一步研究,我开发了一个 iFrameReady() 函数(现在包含在我的答案中),当 iFrame 命中 DOMContentLoaded 时会通知您,而无需等到所有图像和样式表都加载完毕(这就是 load 事件对 iFrame 所做的。
    • @DaveSag - 啊,是的,一个问题是在 Chrome 和 Firefox 中,直到iFrameReady() 调用它的回调之后,您才能获取文档。您的代码之前正在获取myFrame.contents()。这一切都在我的回答中进行了解释,但这是因为您获得的临时文档最终不会成为真正的文档,因此您永远不会在这个临时且空的文档中找到任何内容。 iFrameReady() 在调用回调时将实际文档作为 this 传递,因此您可能应该使用 $(this).find(...) 在回调中在 iFrame 中进行搜索。
    • @DaveSag - 仅供参考,您在实现iFrameReady() 时也存在实现错误。变量fired 被声明为ready() 函数的局部变量,而它应该更高一级。这允许为同一个iFrame(您的实现正在执行)多次调用回调,这不是它应该如何工作的。
    • 每当我在回答有关事件的问题时看到 1 毫秒超时,我就会使用凝固汽油弹烧死一只小狗。所以要小心。
    【解决方案2】:

    鉴于父文件和 iFrame 的文件都来自我的本地磁盘(开发中)并且将来自同一个服务器(生产中),我原以为我不会受到十字架的影响-起源问题。

    “请打开我附在这封电子邮件中的这个完全无害的 HTML 文档。”浏览器有充分的理由对本地文件应用跨域安全性。

    有没有办法让浏览器相信我的本地文件实际上属于同一个域?

    安装网络服务器。通过http://localhost 进行测试。作为奖励,可以获得 HTTP 服务器的所有其他好处(例如能够使用以 / 开头的相对 URI 并使用服务器端代码进行开发)。

    【讨论】:

    • 谢谢 - 我已经运行了本地网络服务器,现在 Chrome 和 Safari 的行为一致,尽管问题尚未解决。请参阅上面的更新。
    • @DaveSag — 这是一个完全不同的问题。这可能是因为父文档的 DOM 在框架中的文档加载之前很久就准备好了。使用更合适的事件(如加载)。
    猜你喜欢
    • 2019-06-15
    • 1970-01-01
    • 2013-03-22
    • 2017-05-04
    • 1970-01-01
    • 2012-12-29
    • 2018-11-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多