如何访问 iFrame 中的 DOM 元素

IT技术 javascript iframe cross-domain
2021-03-02 00:22:01

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

鉴于父文件和 iFrame 的文件都从我的本地磁盘(在开发中)和将在同一个服务器上(在生产中)中分离出来,我本以为我不会受到跨域问题的影响.

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

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

更新

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

我的 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 元素?

2个回答

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

测试页面在这里


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

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

测试页面在这里

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


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

在我试图解决这个问题的过程中,我发现了一些非常奇怪的东西(在 Chrome 中)与 iFrames。当您第一次查看 iframe 的窗口时,有一个文档,它说readyState === "complete"您认为它已完成加载,但它在撒谎。<body>通过 URL 加载到 iframe 的文档中的实际文档和实际标签实际上还不存在。我通过在 上放置自定义属性<body data-test="hello">并检查该自定义属性来证明这一点瞧,瞧。即使document.readyState === "complete"该自定义属性不在<body>标签。因此,我得出结论(至少在 Chrome 中)iFrame 最初有一个虚拟的空文档和正文,这些文档和正文并不是在将 URL 加载到 iFrame 后将就位的实际文档。这使得检测何时准备就绪的整个过程变得非常混乱(我花了好几个小时来弄清楚这一点)。事实上,如果我设置一个间隔计时器和 poll iWindow.document.body.getAttribute("data-test"),我会看到它undefined重复显示,然后最后它会显示正确的值,所有document.readyState === "complete"这些都意味着它完全在撒谎。

我认为正在发生的事情是 iFrame 以一个虚拟的空文档和正文开始,然后在内容开始加载后替换它。另一方面,iFramewindow是真正的窗口。因此,我发现实际等待加载内容的唯一方法是监视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();
}

这只是这样调用:

// 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!";
});
@DaveSag - 经过进一步研究,我开发了一个iFrameReady()函数(现在包含在我的答案中),它会在 iFrame 命中时通知您,DOMContentLoaded而无需等到所有图像和样式表都加载完毕(这就是load事件对 iFrame 的作用)。
2021-04-26 00:22:01
@DaveSag - 啊,是的,问题是在 Chrome 和 Firefox 中,在iFrameReady()调用其回调之前,您无法获取文档您的代码myFrame.contents()之前正在获取这在我的回答中都有解释,但这是因为您正在获得一个临时文档,该文档最终不会成为真正的文档,因此您永远不会在这个临时的空文档中找到任何内容。 在调用回调时iFrameReady()传递实际文档,this因此您可能应该从回调中使用$(this).find(...).
2021-04-26 00:22:01
每当我在回答有关事件的问题时看到 1 毫秒超时时,我都会使用凝固汽油弹烧掉一只小狗。所以要小心。
2021-05-03 00:22:01
@DaveSag - 仅供参考,您在iFrameReady(). 当变量应该firedready()函数高一级时,它被声明为函数的局部变量这允许多次调用相同的回调iFrame(您的实现正在执行),这不是它应该如何工作。
2021-05-06 00:22:01
很公平。在 Mac 上运行并且无法控制我的用户将运行的内容(即使在测试中)。
2021-05-11 00:22:01

鉴于父文件和 iFrame 的文件都从我的本地磁盘(在开发中)和将在同一个服务器上(在生产中)中分离出来,我本以为我不会受到跨域问题的影响.

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

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

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

@DaveSag — 这是一个完全不同的问题。这可能是因为父文档的 DOM 在框架中的文档加载之前很久就准备好了。使用更合适的事件(如加载)。
2021-04-27 00:22:01
谢谢 - 我已经运行了一个本地网络服务器,现在 Chrome 和 Safari 的行为一致,尽管问题尚未解决。见上面的更新。
2021-05-09 00:22:01