iframe与主窗口通信

发布时间 2023-07-27 07:20:14作者: 当时明月在曾照彩云归

1. 引言

<iframe> 元素是 HTML 中的一个标签,用于在当前页面中嵌入另一个页面

使用 <iframe> 可以实现以下功能:

  1. 嵌入其他网页:可以将其他网页嵌入到当前页面中,例如显示地图、视频、文档等
  2. 嵌入本地页面:可以将其他页面或组件嵌入到当前页面中,以实现模块化和复用

嵌入的iframe和主网页之间具有一定的独立性,无法像正常的一个网页上下文一样访问内容,如何通信成为一个问题

本文主要记述iframe和主窗口之间的通信方式,包含内置方法和使用Postmate库

2. 概述

iframe标签在JS中定义为HTMLIFrameElement,而HTMLIFrameElement.contentWindow返回当前HTMLIFrameElementWindow对象,可以使用这个Window 对象去访问这个 iframe 的文档和它内部的 DOM

Window对象可以接收消息(onmessage - Web API 接口参考 | MDN (mozilla.org))和发送消息(window.postMessage - Web API 接口参考 | MDN (mozilla.org)

另外,Window对象还存在父对象(window.parent - Web API 接口参考 | MDN (mozilla.org)),可以通过window.parent访问父对象

利用上述的方法与特性,就可以实现iframe和主窗口之间的通讯

思路之一是:将需要对方访问的属性和方法挂载到window对象上,从而主窗口通过document.querySelector('iframe').contentWindow.<xxx>访问iframe,iframe通过window.parent.<xxx>访问主窗口的属性或方法

思路之二是:主窗口向iframe发送信息postMessage(),并监听子对象的信息,iframe监听主窗口信息并向主窗口发送信息

由于安全原因与浏览器限制,非同源URL(同地址同端口)不可以使用第一种方法,第二种方法均适用

3. 内置方法

3.1 初始准备

笔者准备了这样两个网页,分别叫parent.htmlchildren.html

parent.html内容如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    }

    #parent {
      width: 100%;
      height: 100%;
      display: flex;
      flex-direction: row;
    }

    iframe {
      width: 80%;
      height: 100%;
    }

    #panel {
      width: 20%;
      height: 100%;
      background: white;
      display: flex;
      flex-direction: column;
    }

    code {
      font-size: large;
    }
  </style>
</head>

<body>
  <div id="parent">
    <iframe src="./children.html" frameborder="0"></iframe>
    <div id="panel">
      <h3>主窗口</h3>
        <code></code>
    </div>
  </div>

  <script>

  </script>
</body>

</html>

children.html内容如下:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,
    body {
      width: 100%;
      height: 100%;
      margin: 0;
      padding: 0;
    }

    #container {
      width: 100%;
      height: 100%;
      background: cadetblue;
      display: flex;
      flex-direction: column;
    }

    code {
      font-size: large;
    }
  </style>
</head>

<body>
  <div id="container">
    <h3>子窗口</h3>
    <code></code>
  </div>
  <script>

  </script>
</body>

</html>

这两网页的界面如下:

image-20230726200834842

现在,笔者准备在主窗口与iframe之间传递一个信息

3.2 访问属性

将对方访问的属性和方法挂载到window对象上,从而主窗口通过document.querySelector('iframe').contentWindow.<xxx>访问iframe,iframe通过window.parent.<xxx>访问主窗口的属性或方法

示例代码如下:

// parent.html   

const parentMessage = {
    name: '李四',
    age: 20
};
Reflect.defineProperty(window, 'parentMessage', {
    value: parentMessage
});

// 访问子窗口的全局变量
const iframe = document.querySelector('iframe');
setTimeout(() => {
    document.querySelector('code').innerHTML = JSON.stringify(iframe.contentWindow.childMessage)
}, 1000); // 等待iframe初始化完毕
// children.html

const childMessage = {
    name: '张三',
    age: 18
};
Reflect.defineProperty(window, 'childMessage', {
    value: childMessage
});

document.querySelector('code').innerHTML = JSON.stringify(parent.parentMessage)

结果如下:

image-20230727001146773

注意,这种方法只适用与同源URL,非同源URL访问对方的属性会出现类似错误:

Uncaught DOMException: Blocked a frame with origin "http://127.0.0.1:5500" from accessing a cross-origin frame

3.3 消息发送与监听

主窗口向iframe发送信息postMessage(),并监听子对象的信息,iframe监听主窗口信息并向主窗口发送信息

示例代码如下:

// parent.html

const parentMessage = {
    name: '李四',
    age: 20
};
const iframe = document.querySelector('iframe');
iframe.onload = function () {
    // 发送消息
    iframe.contentWindow.postMessage(parentMessage, '*');
};
// 接收消息
window.addEventListener('message', function (event) {
    const code = document.querySelector('code');
    code.innerHTML = JSON.stringify(event.data);
});
// children.html

const childrenMessage = {
    name: '张三',
    age: 18
};
// 接收消息
window.addEventListener('message', function (event) {
	// 发送消息
    window.parent.postMessage(childrenMessage, '*');
    const code = document.querySelector('code');
    code.innerHTML = JSON.stringify(event.data);
});

结果和上面是一样的:

image-20230727001146773

这种方法同源URL与非同源URL都适用

4. Postmate

Postmate 是一个 JavaScript 库,用于简化主窗口和 iframe 之间的跨域通,它基于 window.postMessage() 方法,并提供了一种简单的方式来在主窗口和 iframe 之间发送和接收消息

Postmate 的GitHub地址为:dollarshaveclub/postmate: ? A powerful, simple, promise-based postMessage library. (github.com)

Postmate的特性有:

  • 基于 promise 的 API,用于优雅和简单的通信
  • 安全的双向父 <-> 子握手,并带有消息验证
  • 子代暴露一个可检索的模型对象,父代可以访问
  • 子代发出事件,父代可以监听
  • 父代可以调用子代的函数
  • 零依赖性,如果需要的话,可以为 Promise API 提供自己的 polyfill 或抽象
  • 轻量级,大小约为1.6kb(缩小和压缩后)

Postmate 的主要用法就是:

  1. 主窗口建立握手程序,握手成功后可向iframe获取数据,监听事件以及远程调用函数
  2. iframe建立握手模型,可包含数据、函数等,成功握手后可向主窗口触发事件

大致用法就是如此,有个问题:iframe向主窗口发送数据倒是看起来很简单,直接在主窗口里获取即可,但是如何在iframe里获取主窗口的数据呢,如何才能将主窗口的数据发送给iframe呢?

方法之一是:使用iframe建立的握手模型里远程调用函数,将主窗口传给iframe的数据作为函数的参数(这一点感觉很别扭)

示例代码如下:

parent.html:

<script src="https://cdn.jsdelivr.net/npm/postmate@1.5.2/build/postmate.min.js"></script>
<div id="parent">
    <!-- <iframe src="./children.html" frameborder="0"></iframe> -->
    <div id="panel">
        <h3>主窗口</h3>
        <code></code>
    </div>
</div>

<script>
    const parentMessage = {
        name: '李四',
        age: 20
    };

    // 建立握手程序
    const handshake = new Postmate({
        container: document.querySelector('#parent'), // 注意将原iframe注释掉
        url: './children.html'
    });

    // 握手成功后
    handshake.then(child => {

        // 监听事件
        child.on('some-event', data => console.log(data)); // Logs "Hello, World!"

        // 远程调用函数,将主窗口传给iframe的数据作为函数的参数
        child.call('changeParentMessage', parentMessage);

        // 获取数据
        child.get('childMessage').then(data => {
            document.querySelector('code').innerHTML = JSON.stringify(data)
        }); 

    });
</script>

children.html:

<script src="https://cdn.jsdelivr.net/npm/postmate@1.5.2/build/postmate.min.js"></script>
<div id="container">
    <h3>子窗口</h3>
    <code></code>
</div>
<script>
    const childMessage = {
        name: '张三',
        age: 18
    };

    const handshake = new Postmate.Model({
        // 建立握手模型 Property values may be functions, promises, or regular values
        childMessage: childMessage,
        parentMessage: {},
        changeParentMessage: function (newMessage) {
            this.parentMessage = newMessage;
            document.querySelector('code').innerHTML = JSON.stringify(this.parentMessage);
        }
    });

    //成功握手后可向主窗口触发事件
    handshake.then(parent => {
        parent.emit('some-event', 'Hello, World!');
    });
</script>

结果如下:

image-20230727020851043

5. 参考资料

[1] <iframe> - HTML(超文本标记语言) | MDN (mozilla.org)

[2] window.parent - Web API 接口参考 | MDN (mozilla.org)

[3] HTMLIFrameElement.contentWindow - Web API 接口参考 | MDN (mozilla.org)

[4] iframe跨域通信(postMessage) - 掘金 (juejin.cn)

[5] window.postMessage - Web API 接口参考 | MDN (mozilla.org)

[6] dollarshaveclub/postmate: ? A powerful, simple, promise-based postMessage library. (github.com)

[7] 零基础学习 Postmate库 - 掘金 (juejin.cn)