等待子窗口加载完成

31
有没有一种简单的方法来检测由脚本打开的窗口是否已经加载完成?基本上,我想要与 onLoad() 钩子相当的东西,但我无法直接设置它——假设子文档是给定的,并且实际上我无法在其中放置任何代码。
例如,假设我有以下两个文件: parent.html:
<html>
  <head>
    <title>Parent</title>
  </head>
  <script type="text/javascript">
    var w;
    function loadChild() {
      w = window.open();
      w.location.href="child.html";
      // block until child has finished loading... how?
      w.doSomething();
    } 
  </script>
</html>
<body>
  I am a parent window. <a href="javascript:loadChild()">Click me</a>.
</body>

child.html:

<html>
  <head>
    <title>Child</title>
  </head>
  <script type="text/javascript">
    function doSomething() {
      alert("Hi there");
    }
  </script>
</html>
<body>
  I am a child window
</body>

由于设置location.href是非阻塞的,因此w.doSomething()尚未定义,而doSomething()调用失败。我该如何检测子页面何时加载完成?

3个回答

29

如果新打开窗口的位置与当前页面同源,那么这将有效:

var w = window.open('child.html')
w.addEventListener('load', w.doSomething, true); 

1
你能澄清一下“同源策略”吗?在我的本地环境中,它似乎无法与 file:/// URL 一起使用,但也许这是出于安全考虑? - David Moles
1
是的,很可能。在浏览器中,同源基本上意味着目标必须在相同的主机和相同的协议上。也许你把它和 http:// 混淆了? - Øystein Riiser Gundersen
我忘了提到addEventLister在IE中的支持不太好,因此像gabtub建议的那样使用jQuery(或另一个修复事件处理程序浏览器不兼容性的JS框架)可能是个好主意... - Øystein Riiser Gundersen
不混合使用,我猜Chrome只是没有将两个“file:///”URL视为同一来源?我无法在本地文件上让IE7运行脚本...也许我应该尝试在运行的服务器上进行此操作。 - David Moles
可能是这种情况,是的。我相信浏览器中file:/// URL的安全策略非常严格(在某些情况下,甚至是有缺陷的)。尝试使用适当的Web服务器并查看是否解决了问题。此外,请尝试检查IE / Chrome控制台是否有任何错误消息。 - Øystein Riiser Gundersen
如果窗口已经打开,那么这种方法就不起作用了,在这种情况下不会触发任何事件。 - Johan Danforth

10

被接受的答案并不能解决原来的问题:

  w = window.open();
  w.location.href="child.html";
  // block until child has finished loading... how?
  w.doSomething();
为了解决这个问题,我们需要更多地了解页面在后台如何加载。实际上,它是异步的,所以当你写下 w.location.href="child.html"; 并立即调用 w.doSomething(); 后,它不会等待新页面加载完成。虽然浏览器开发人员已经很好地解决了iframe的加载问题,但对于子窗口则不是这样。没有相应的API,所以你只能编写一些方法来解决这个问题。你可以使用适当的延迟函数从子窗口的unload事件监听器中解决这个问题。
w.addEventListener("unload", function (){
    defer(function (){
        w.doSomething();
    });
});

通常情况下,由于客户端js开发,每个浏览器的工作方式都完全不同。

enter image description here

最好的解决方案是使用延迟函数,在子窗口文档处于加载就绪状态时调用回调函数,这时您可以添加一个加载处理程序。如果它在此之前调用了回调,则在当前浏览器中,新文档尚未创建,因此加载处理程序将添加到旧文档中,该旧文档稍后将被新文档替换,因此加载处理程序将会丢失。唯一的例外是Firefox浏览器,因为该浏览器保留了添加到窗口的加载处理程序。如果浏览器在加载就绪状态之后调用延迟回调,则在某些现有浏览器中,页面实际加载后可能会有超过2500毫秒的延迟。我不知道为什么某些浏览器在子窗口文档加载时会有如此巨大的延迟。我搜索了一段时间,但没有找到任何答案。一些浏览器没有任何“加载”延迟,因此您只能使用“延迟”延迟,并希望获得最佳效果。根据我的测试结果,基于MessageChannel的解决方案是最好的多浏览器“加载”延迟方案:

function defer (callback) {
    var channel = new MessageChannel();
    channel.port1.onmessage = function (e) {
        callback();
    };
    channel.port2.postMessage(null);
}

那么您可以这样做:

w.addEventListener("unload", function (){
    // note: Safari supports pagehide only
    defer(function (){
        if (w.document.readyState === "loading")
            w.addEventListener("load", function (){
                w.doSomething();
            });
        else
            w.doSomething();
    });
});

如果您想支持Safari,那么应该使用pagehide而不是unload。pagehide事件从IE 11开始得到支持,因此如果您想支持甚至更旧的浏览器,则必须同时使用unload和pagehide,并且只有在两者都可用时才使用其中之一启动defer。

var awaitLoad = function (win, cb){
    var wasCalled = false;
    function unloadListener(){
        if (wasCalled)
            return;
        wasCalled = true;
        win.removeEventListener("unload", unloadListener);
        win.removeEventListener("pagehide", unloadListener);
        // Firefox keeps window event listeners for multiple page loads
        defer(function (){
            win.document.readyState;
            // IE sometimes throws security error if not accessed 2 times
            if (win.document.readyState === "loading")
                win.addEventListener("load", function loadListener(){
                    win.removeEventListener("load", loadListener);
                    cb();
                });
            else
                cb();
        });
    };
    win.addEventListener("unload", unloadListener);
    win.addEventListener("pagehide", unloadListener);
    // Safari does not support unload
});


w = window.open();
w.location.href="child.html";
awaitLoad(w, function (){
    w.doSomething();
});

如果支持 Promise 和 async 函数,则可以使用以下类似的代码:

w = window.open();
await load(w, "child.html");
w.doSomething();

但那是另外一个故事……


你救了我的一天,谢谢!我一直在寻找一种方法来关闭从弹出窗口引发的事件,但我总是收到警告,说“只有创建窗口的脚本才能关闭它”。但这段代码允许我从打开者中放置一个事件监听器!因此,对于每个正在寻找方法的人,这就是正确的方法。 - Nenri
@Nenri 你好............ :-) 请注意,如果您输入错误的地址,您将无法访问浏览器错误页面,例如Chrome错误页面。这些页面与打开者页面不同源。据我所知,目前没有解决方法。我曾经在使用window.open和Karma进行端到端测试项目时遇到了这个问题,因此只能部分停止。https://github.com/inf3rno/e2e 我可能会稍后重新开始。 - inf3rno
@Nenri 对于每个跨框架跨源请求都是如此。例如,如果 www.example.com 在新窗口中打开 sub.example.com,那么您将无法访问该窗口并等待页面加载。只有禁用浏览器安全和隔离才能解决这个问题,但这是一个仅适用于Chromium的功能,而且它仅用于测试,而不是生产。通常,您只能使用 postMessage 并要求子窗口关闭自身。因此,在您的情况下,我建议您这样做。 - inf3rno
在我的情况下,打开的弹出窗口来自另一个来源,但是它是静态的,永远不会变成我无法编辑的东西,所以我没有这个问题,但是谢谢你告诉我这些 :-) 我正在等待明天给这个答案一些赏金。 - Nenri
1
@Nenri 请在不同的浏览器中尝试,特别是最近的Chrome。如果您在子窗口中打开了不同的来源,则无法访问childWindow.document。这应该会导致安全错误。请注意,我正在谈论这种类型的来源:https://en.wikipedia.org/wiki/Same-origin_policy 如果您不想访问它,请关闭它,然后简单地使用延迟即可,但在这种情况下不要使用document.readyState和事件侦听器。 - inf3rno
显示剩余4条评论

6

how about

parent.html:

<html>
<head>
<title>Parent</title>
</head>
<script type="text/javascript">
  var w;
  function loadChild() {
    w = window.open();
    w.location.href="child.html";
    // like this (with jquery)
    $(w).ready(function()
    {
      w.doSomething();
    });
  } 
</script>
</html>
<body>
  I am a parent window. <a href="javascript:loadChild()">Click me</a>.
</body>

2
似乎什么都没发生。:( 我之前没有见过$运算符(我大部分时间都在Java中);它应该做什么?Chrome JavaScript调试器给出了未捕获的异常ReferenceError:$未定义 - David Moles
哎呀,我打错了。是的,jQuery,这就解释了为什么我没看到它。似乎应该有一些方法可以直接使用 DOM 来完成它。 - David Moles
1
如果不使用JS框架,最好使用gunderwonder的解决方案。正如他所说,“addEventListener”在IE中不受支持,但是您可以进行浏览器检查,并在IE的情况下使用微软添加事件侦听器的实现=>“w.attachEvent('onload',doSomething);” - gabtub

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接