postMessage源IFrame

23

我正在开发一个使用跨域iframe进行大小调整的网站,使用postMessage实现。唯一的问题是,我无法确定每个iframe的高度。目前的设置是当任何一个iframe将其高度发送给父级时,所有iframe的高度都被更改了。

父级:

var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";

eventer(messageEvent, function(e) {
    $('iframe').height(e.data);
}, false);

IFrame:

var updateHeight = function() {
    if(window.parent) {
        window.parent.postMessage($('.widget').outerHeight(), '*');
    }
};

有没有办法识别是哪个iframe发送了message事件?

6个回答

25

是的,您可以识别执行了 postMessage 操作的 IFRAME。有不同的情况:

  • 源 IFRAME 与接收到消息的 Window 具有同源 URL(例如:http://example.com/):使用以下代码识别 IFRAME:

    myIFRAME.contentWindow == event.source

  • 源 IFRAME 与父 HTML 页面具有同源但相对 URL(例如:/myApp/myPage.html):使用以下代码识别 IFRAME:

    myIFRAME.contentWindow == event.source.parent

  • 源 IFRAME 具有不同源 URL(例如:http://example.com/),且该 URL 与接收消息的页面(例如:http://example.org/)不同:以上方法均无效(比较始终返回false,访问 event.source 属性会导致Access Denied错误),必须基于 IFRAME 的来源域来识别它;

    myIFRAME.src.indexOf(event.origin)==0

为了处理这三种不同的情况,我使用以下代码:

var sourceFrame = null; // this is the IFRAME which send the postMessage
var myFrames = document.getElementsByTagName("IFRAME");
var eventSource = event.source; // event is the event raised by the postMessage
var eventOrigin = event.origin; // origin domain, e.g. http://example.com

// detect the source for IFRAMEs with same-origin URL
for (var i=0; i<myFrames.length; i++) {
    var f = myFrames[i];
    if (f.contentWindow==eventSource || // for absolute URLs
        f.contentWindow==eventSource.parent) { // for relative URLs
        sourceFrame = f;
        break;
    }
}

// detect the source for IFRAMEs with cross-origin URL (because accessing/comparing event.source properties is not allowed for cross-origin URL)
if (sourceFrame==null) {
    for (var i=0; i<myFrames.length; i++) {
        if (myFrames[i].src.indexOf(eventOrigin)==0) {
            sourceFrame = myFrames[i];
            break;
        }
    }
}

对于跨域的URL,注意如果event.origin是多个IFRAME共有的域名,则无法区分真正的来源。

有些人使用===代替==,但我在这种情况下没有发现任何区别,所以我使用最短的比较符。

此实现已经经过测试,在以下环境中工作:

  • MSIE 9
  • Firefox 17

作为一种替代(由Griffin建议),您可以使用带有唯一标识符(例如时间戳)的IFRAME src,IFRAME的Web应用程序将在发布的消息中发送回此唯一标识符。虽然IFRAME识别会更简单,但此方法需要修改IFRAME的Web应用程序(这并不总是可能的)。这也可能导致安全问题(例如IFRAME的Web应用程序试图猜测其他IFRAME应用程序的唯一标识符)。


2
你最好确保每个iframe都有一个唯一的href,使用时间戳查询字符串,并在跨文档消息中包含源href。这样做可能会更好。 - Griffin
@Griffin:你说得没错,但是还有一些缺点。我编辑了回答并加入了你的替代方案,还给你的评论点了赞。 - Julien Kronegg
不允许访问eventSource.parent,与eventSource进行比较应该没问题。 - Collin Anderson
1
@JulienKronegg 你确定第二种情况下 myIFRAME.contentWindow == event.source 不起作用吗?ev.source.parent 看起来像是两层嵌套的消息。 - leewz
大约三年前,我为以前的一个客户编写了一个复杂的应用程序框架(所以我没有代码了)。我在相对URL方面遇到了问题,因此不得不找到“ev.source.parent”解决方法。据我记得,“postMessage”对于相对URL的行为不同,并且会给出更深层次的HTMLElement作为源,因此需要“.parent”。 - Julien Kronegg
显示剩余5条评论

21

我从这里找到了解决方案:如何在JavaScript中在窗口和框架之间共享数据

父级:

var frames = document.getElementsByTagName('iframe');
for (var i = 0; i < frames.length; i++) {
    if (frames[i].contentWindow === event.source) {
        $(frames[i]).height(event.data); //the height sent from iframe
        break;
    }
}

1
@JulienKronegg,实际上这样做不够健壮,因为它将匹配任何具有相同源的iframe(除非您还要进行其他操作)。在哪些情况下?我在FF中无法匹配。 - Randy Hall
2
@RandyHall:是的,这是为了解决跨域问题而添加的。我将我的评论提升为答案以便更好地理解,详情请见https://dev59.com/R2Up5IYBdhLWcg3wXGoZ#20404180。 - Julien Kronegg
@JulienKronegg,你在哪里遇到了这个问题?我无法在IE10、Chrome33或iOS7中重现它。 - Jörn Berkefeld
@JörnBerkefeld:该问题发生在跨域URL上,请参见我的回答 - Julien Kronegg
是的,我尝试过了,但无法确认您所说的内容 - 因此我想知道您在哪些浏览器/操作系统上注意到它的存在;-) - Jörn Berkefeld
显示剩余5条评论

4
我有一个解决此问题的想法。在创建iframe时,给它一个名称/ID。在iframe内部的脚本中,将消息作为对象发送,其外观类似于:
window.parent.postMessage({"height" : $('.widget').outerHeight(), "frmname" : window.name}, '*');

在父监听器中,
eventer(messageEvent, function(e) {`enter code here`
    $(e.data.frmname).height(e.data.height);
}, false);

4
以下内容适用于我跨域:

以下内容适用于我跨域:

window.addEventListener('message', function (event) {
  if (event.data.size) {
    Array.prototype.forEach.call(document.getElementsByTagName('iframe'), function (element) {
      if (element.contentWindow === event.source) {
        element.style.height = `${event.data.size.height}px`;
      }
    });
  }
}, false);

已在Chromium 64和Firefox 59中进行测试。


1
如果源iframe嵌套在多个父iframe中,则需要递归遍历每个iframe的window.frames属性,并将其与messageEvent#source属性进行比较。
例如,如果消息是由此Dom的iframe#level3生成的。
<iframe Id=level1>
   <iframe Id=level2>
       <iframe Id=level3 />
   </iframe>
</iframe>

你应该能够使用以下方法在当前窗口中找到祖先iframe的索引。
FindMe = event.source
FrameIndex = find(window)
frames[FrameIndex].frameElement ==     getElByTagName(iframe)[FrameIndex] 

function find(target){
    for (i=0; I< target.frames.length; i ++)
       if(target.frames[i] == FindMe ||   find(target.frames[i]))
           return i
    return false 
}

重要的是要注意:
Window.frames 属性不受跨域策略限制。
无论源 iframe 嵌套多深,此技术都可以使用。
window.frames 是窗口对象的集合,而不是 iframe 元素。
访问 window.frames 集合成员的属性受同源限制(即您可能无法访问 window.frames[i] 的 frameElement 或 location 属性)。

-2
该事件还应具有一个名为“source”的属性,可以与iframe的“contentWindow”属性进行比较。

比较 event.source 与跨域 URL 不起作用,请参见 https://dev59.com/R2Up5IYBdhLWcg3wXGoZ#20404180。 - Julien Kronegg

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