如何通过 postMessage 执行一个函数

12

我有一个放在iframe里面的代码:

window.addEventListener('message', function(e){

  if(e.data == 'test')
    console.log(e);

}, false);

在父文档中嵌套该内容:

$('#the_iframe').get(0).contentWindow.postMessage('test', 'http://localhost/');

所以父文档向 iframe 发送了一条“测试”消息,它可以正常工作。

但是我该如何在父文档中定义一个函数,并通过 postMessage 将该函数发送到 iframe,并在本地执行该函数呢?

该函数对文档进行了如下更改:

var func = function(){
  $("#some_div").addClass('sss');
}

(#some_div 存在于iframe中,而不是父文档中)


我认为有一个 window.parent 变量应该可以从 iframe 中访问。主窗口也会有一个 parent,但我相信它将指向自身。 - sparebytes
5个回答

16

你可以将一个字符串化的函数作为postmessage事件数据进行传递,没有任何阻碍。实现起来很简单,对于如下任何函数声明:

function doSomething(){
    alert("hello world!");
}

您可以使用encodeURI对其字符串表示进行编码:

console.log(encodeURI(doSomething.toString()));
//function%20doSomething()%20%7B%0A%20%20%20%20alert(%22hello%20world!%22);%0A%7D

它可以作为闭包的一部分执行 - 比如说并不需要过于想象力的东西,像这样:

eval('('+decodeURI(strCallback)+')();');

这里有一个概念验证的可行性示例,没有跨框架结构 - 我会尝试制作一个postMessage版本,但在jsfiddle上托管它将是非常困难的。

更新

正如承诺的那样,下面是一个完整的模拟,它可以工作(链接在下面)。通过正确的event.origin检查,它足以防范攻击,但我知道我们的安全团队绝不会像这样把eval放到生产环境中 :)

如果有选择的话,我建议在两个页面之间规范化功能,以便只需要传递参数信息(即传递参数而不是函数);但肯定有一些情况下这种方法更加优选。

父级代码:

document.domain = "fiddle.jshell.net";//sync the domains
window.addEventListener("message", receiveMessage, false);//set up the listener

function receiveMessage(e) {
    try {
        //attempt to deserialize function and execute as closure
        eval('(' + decodeURI(e.data) + ')();');
    } catch(e) {}
}

IFrame 代码:

document.domain = "fiddle.jshell.net";//sync the domains
window.addEventListener("message", receiveMessage, false);//set up the listener

function receiveMessage(e) {
    //"reply" with a serialized function
    e.source.postMessage(serializeFunction(doSomething), "http://fiddle.jshell.net");
}

function serializeFunction(f) {
    return encodeURI(f.toString());
}

function doSomething() {
    alert("hello world!");
}

原型模型: 父代码iframe 代码


这真的有效吗?我的意思是,你可以通过调用function.toString()获取每个函数的代码吗? - Alex
2
@Alex:是的,它适用于您定义的任何函数 - 或者说,任何非本地代码编写的函数(即您无法看到window.alert源代码的字符串表示形式)。 - Oleg
3
请注意,最终结果可能会出现最差的代码实践(如eval、在全局上下文中执行的函数等)。 - Dziad Borowy

10
您无法直接发送函数对象,至少不能保留它们的作用域。虽然(草案)postMessage规范提到了结构化对象,例如嵌套的对象和数组,[...] JavaScript值(字符串、数字、日期等)以及[...]某些数据对象,如File Blob、FileList和ArrayBuffer对象,但大多数浏览器仅允许字符串(包括JSON)。请在MDNdev.opera阅读更多信息。
如果您真的想从父窗口执行一些代码,则需要将函数转换为字符串,并在iframe中使用eval()方法执行它。然而,我认为没有理由让任何应用程序允许评估任意代码(即使是来自注册域的代码);最好建立一个消息API,可以接收(JSON)字符串命令并调用其自己的方法。

2
在这种情况下,我建议尝试不同的方法。为什么呢?因为Bergi已经解释了为什么它不能按照你想要的方式工作。
您可以在父页面中定义(并重新定义)您的函数:
<html>                                                                  
<head>                                                                  
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> 
<script type="text/javascript">                                        
    var fun = function() { alert('I am a function!'); };

    $(function() {
        // use this function to change div background color
        fun = function() { 
            // get desired div
            var theDiv = $('#frame').contents().find('#some_div');
            theDiv.css('background', '#ff0000');
        };

        // or override it, if you want to change div font color
        fun = function() { 
            var theDiv = $('#frame').contents().find('#some_div');
            theDiv.css('color', '#ff0000');
        };

        // in this example second (font color changing) function will be executed
    });
</script>                                                               
</head>                                                                 
<body>                                                                  
    <iframe id="frame" src="frame.htm"></iframe>
</body>                                                                 
</html>

并从frame-page中调用您的函数:

<html>                                                                  
<head>                                                                  
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> 
<script type="text/javascript">                                         
    $(function() {
        parent.fun();
    });
</script>                                                               
</head>                                                                 
<body>                                                                  
    <div id="some_div">I am a div (but inside frame)!</div>
</body>                                                                 
</html>

虽然不太方便,但它确实有效。


1

下面的问题上进行扩展,您可以将函数字符串化,使用postMessage将函数主体发送过去,然后使用eval来执行它。

本质上,您正在对函数进行编组,以便可以将其发送到iframe,然后在另一端对其进行解组。要这样做,请使用以下代码:

Iframe:

window.addEventListener("message", function (event) {
    var data = JSON.parse(event.data);
    var callback = window[data.callback];
    var value = data.value;

    if (data.type === "function")
        value = eval(value);

    var callback = window[data.callback];

    if (typeof callback === "function")
        callback(value);
}, false);

function callFunction(funct) {
    funct();
}

父级:

var iframe = $("#the_iframe").get(0).contentWindow;

postMessage(function () {
    $("#some_div").addClass("sss");
}, "callFunction");

function postMessage(value, callback) {
    var type = typeof value;

    if (type === "function")
        value = String(value);

    iframe.postMessage({
        type: type,
        value: value,
        callback: callback
    }, "http://localhost");
}

一旦您在iframe中获取了函数体并使用eval,您就可以像普通函数一样使用它。您可以将其分配给变量,传递它,对其使用callapplybind它等。

但请注意,函数的作用域是动态的(即函数中的任何非局部变量必须已在您的iframe窗口中定义)。

在您的情况下,您在函数中使用的jquery $变量是非局部的。因此,iframe必须已经加载了jquery。它不会使用父窗口中的jquery $变量。


现在是2015年,你不应该建议在生产环境中使用eval。 - Yagiz
1
@Yagiz 那是一个愚蠢的论点。这就像说,“现在是2015年,你不应该鼓励人们系鞋带。” - Aadit M Shah

0

您始终可以发送 postMessage 回到 iframe 并让 iframe 处理消息。当然,您必须考虑到它可能在 iframe 中的下一个命令之前不会被执行。


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