1. 引言
使用 可以实现以下功能:
- 嵌入其他网页:可以将其他网页嵌入到当前页面中,例如显示地图、视频、文档等
- 嵌入本地页面:可以将其他页面或组件嵌入到当前页面中,以实现模块化和复用
嵌入的iframe和主网页之间具有一定的独立性,无法像正常的一个网页上下文一样访问内容,如何通信成为一个问题
本文主要记述iframe和主窗口之间的通信方式,包含内置方法和使用Postmate库
2. 概述
iframe
标签在JS中定义为HTMLIFrameElement
,而HTMLIFrameElement.contentWindow
返回当前HTMLIFrameElement的Window对象,可以使用这个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.
访问iframe,iframe通过window.parent.
访问主窗口的属性或方法
思路之二是:主窗口向iframe发送信息postMessage()
,并监听子对象的信息,iframe监听主窗口信息并向主窗口发送信息
由于安全原因与浏览器限制,非同源URL(同地址同端口)不可以使用第一种方法,第二种方法均适用
3. 内置方法
3.1 初始准备
笔者准备了这样两个网页,分别叫parent.html
和children.html
parent.html内容如下:
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>Documenttitle> <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内容如下:
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>Documenttitle> <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>
这两网页的界面如下:
现在,笔者准备在主窗口与iframe之间传递一个信息
3.2 访问属性
将对方访问的属性和方法挂载到window对象上,从而主窗口通过document.querySelector('iframe').contentWindow.
访问iframe,iframe通过window.parent.
访问主窗口的属性或方法
示例代码如下:
// 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)
结果如下:
注意,这种方法只适用与同源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); });
结果和上面是一样的:
这种方法同源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 的主要用法就是:
- 主窗口建立握手程序,握手成功后可向iframe获取数据,监听事件以及远程调用函数
- 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"> <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>
结果如下:
5. 参考资料
[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)