JavaScript iframe 多窗口通信实战

简介: JavaScript iframe 多窗口通信实战

我最近在完善 easyjobs 代码共享的功能。

image.png

左侧是代码编辑器,右侧下方有一个控制台。

当我们在左侧编辑完成代码后,点击运行 JS,右侧的控制台就可以输出内容。

而右侧上方有一个渲染画布,用来作为代码运行的容器。

你可以打开网址尝试:www.easyjobs.biz/code-sharin…

因为同时需要运行 JavaScript 代码,所以需要对环境进行隔离。也就是要有一个独立的 JavaScript 运行环境,也可以叫做沙箱。

该怎么做呢?

实现 JavaScript 沙箱的方案有很多,比如 iframe、with+Proxy、还有基于 Object.freeze 的不成熟提案,如果不涉及 Web API 的话,甚至可以借助 nodejs 的 vm 模块。

不过 JavaScript 沙箱不是本文的重点。我的场景决定了 iframe 是最好的选择,因为我不仅仅需要隔离 JS 代码,还要隔离 HTML 和 CSS 代码。

如何做沙箱呢?

iframe 有一个 srcdoc 属性,把要执行的代码传给它就可以了。


<iframe srcdoc="<script>alert('hello')</script>"></iframe>

为了方便查看 iframe 中 console 输出的内容,我们还需要想办法接收 iframe 传递过来的消息。

这也就是本文的主要内容,iframe 通信实战。


iframe 基本通信


我在这里用代码来演示一下 iframe 最基本的通信是如何做的。


基本的 HTML 结构


首先我们有一个 index.html 文件。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>父窗口</title>
  </head>
  <body>
    <p>父窗口</p>
    <iframe src="./sub.html"></iframe>
    <button onclick="sendMessage()">发送一条消息给子窗口</button>
    <p id="response"></p>
  </body>
</html>

然后有一个 sub.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>子窗口</title>
  </head>
  <body>
    <p>子窗口</p>
    <button onclick="sendMessage()">发送一条消息给父窗口</button>
    <p id="response"></p>
  </body>
</html>

它们的关系就是相互嵌套的关系。

打开 index.html,大概是下面这样。

image.png

需要注意,多窗口通信需要使用 http(s) 协议。


使用 JavaScript 在窗口之间发送消息


我们来实现一下父窗口的 sendMessage 方法。


let sub = window.frames[0]
function sendMessage() {
  sub.postMessage({ msg: "来自父窗口的一条消息" })
}

其中 window.frames 是获取当前窗口的所有 iframe 元素,它返回一个类似数组的结构。

通过调用 sub 的 postMessage 方法可以传递消息。

然后我们来到 sub.html 中编写接收端的代码。


const responseEl = document.getElementById("response")
window.addEventListener("message", function (e) {
  responseEl.innerHTML += `收到一条消息:${e.data.msg}`
})

接收端使用 window.addEventListener 来监听 message 事件。当有其他窗口通过 poseMessage 来向当前窗口发送消息时,会触发这个事件。

我们来点击父窗口的「发送一条消息给子窗口」按钮。

image.png

可以看到子窗口可以打印父窗口的消息。

同理,我们也可以通过 parent.postMessage 反向向父窗口传递消息。

在 sub.html 中继续增加 sendMessage 代码。


function sendMessage() {
  parent.postMessage({ msg: "来自子窗口的一条消息" })
}

这个代码和 index.html 中发送消息的代码很相似,唯一的区别就是接受者变成了 parent。parent 就是指当前窗口的父窗口。

回到 index.html 中,增加监听代码。监听代码与子窗口完全一致,可以直接复制过来。


const responseEl = document.getElementById("response")
window.addEventListener("message", function (e) {
  responseEl.innerHTML += `收到一条消息:${e.data.msg}</br>`
})

我们来点击子窗口的「发送一条消息给父窗口」按钮。

image.png

这样就实现了 iframe 窗口间双向通信。


注意事项


类型


需要注意的是,postMessage 仅支持 JSON 支持的类型。

  • string
  • number
  • null
  • boolean
  • object
  • array

如果传递 undefined 的话,会自动转成 null。

除了上述类型以外的其他类型都不支持,比如 function、symbol。如果传递了这些类型,浏览器会报错。

image.png


如何传递函数并执行


传递函数是一个很常见的需求,我们可以通过把函数转换为字符串的方式进行传递。

比如下面这样:


function fn () {}
sub.postMessage({ fn: fn.toString() })

在接收方只需要通过 eval 就可以调用函数字符串了。

不过如果函数内引用了外部变量的话,那就不行了。

比如下面这样:


let name = '代码与野兽'
function fn () {
  console.log(name)
}
sub.postMessage({ fn: fn.toString() })

因为接收端无法获取到发送端的变量。

如果碰巧接收端也存在 name 这个变量的话,eval 在执行时就会访问到接收端的变量而非发送端的变量。

这里也体现出了纯函数的优势。如果我们遵循函数式编程范式编写了纯函数,就不会导致这个问题。


如何在父窗口访问到子窗口的 console


回到文章开头,虽然我们可以通过 iframe 通信来传递消息,但实现 iframe 执行 console 同步到父窗口,仍然是个问题。

其实非常简单,把 console 对象上的所有方法劫持,然后把这段代码加入到 iframe 最顶部就可以了。


var fns = new Map()
for(let key in console) {
  fns.set(key, console[key])
  console[key] = (...args) => {
    funcToString(args)
    window.parent.postMessage({ type: 'console.' + key, args }, "*")
    return fns.get(key)(...args)
  }
}

其中会调用 funcToString 方法,这个方法就是把所有的 function 字符串化。

因为我们不确定传入的结构的嵌套深度,所以需要使用递归来转换。


function funcToString(args) {
  Object.keys(args).forEach((key) => {
    const arg = args[key]
    if (typeof arg === "function") {
      args[key] = arg.toString()
    } else if (typeof arg === "object") {
      funcToString(arg)
    }
  })
}



相关文章
|
28天前
|
前端开发 JavaScript Java
【JavaScript】JavaScript 防抖与节流:以游戏智慧解锁实战奥秘
【JavaScript】JavaScript 防抖与节流:以游戏智慧解锁实战奥秘
27 3
|
2月前
|
数据可视化 JavaScript 架构师
D3.js实战:数据可视化高级技巧实例应用
本文介绍了D3.js入门,包括创建HTML文件引入库、绘制简单线图、柱状图和饼图。示例展示了数据绑定、交互性和动画效果,如柱状图的悬停效果和线图的数据平滑过渡。此外,还提及力导向图和地图可视化的实现,以及使用Enter, Update, Exit模式进行动态更新。最后提到了复杂图表和高级技巧,如使用组件库、动画、交互性和性能优化。
43 0
|
2月前
|
JSON JavaScript 前端开发
【JavaScript技术专栏】JavaScript的跨域通信方法
【4月更文挑战第30天】本文探讨了JavaScript中的跨域通信方法,包括:同源策略和跨域通信的概念,以及JSONP、CORS、WebSockets、`window.postMessage()`、代理服务器和WebAssembly的使用。这些技术各有优劣,适用于不同的场景,是Web开发者解决跨域问题的关键工具。随着Web技术的演进,跨域通信的解决方案也将不断更新。
|
26天前
|
设计模式 JavaScript 前端开发
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
JavaScript的继承机制基于原型链,它定义了对象属性和方法的查找规则。每个对象都有一个原型,通过原型链,对象能访问到构造函数原型上的方法。例如`Animal.prototype`上的`speak`方法可被`Animal`实例访问。原型链的尽头是`Object.prototype`,其`[[Prototype]]`为`null`。继承方式包括原型链继承(通过`Object.create`)、构造函数继承(使用`call`或`apply`)和组合继承(结合两者)。ES6的`class`语法是语法糖,但底层仍基于原型。继承选择应根据需求,理解原型链原理对JavaScript面向对象编程至关重要
33 7
【JavaScript】深入浅出JavaScript继承机制:解密原型、原型链与面向对象实战攻略
|
12天前
|
存储 前端开发 JavaScript
Next.js 实战 (一):项目搭建指南
这篇文章介绍了作者在2024年下半年计划使用Next.js从零开始搭建一个后台模板,以探索Next.js的奥秘。文章包含了项目搭建、目录结构、APP路由约定、配置Eslint、Prettierrc、Husky等项目提交规范、使用release-it自动管理版本号和生成CHANGELOG、import排序规则、安装NextUI等内容。作者还提到会在开发过程中记录遇到的问题和解决方法,并计划在后期使用Prisma+Supabase数据库存储数据,最终完成一个基于Next.js的全栈项目。
Next.js 实战 (一):项目搭建指南
|
19天前
|
移动开发 前端开发 JavaScript
HTML5+CSS3+JavaScript网页实战
HTML5+CSS3+JavaScript网页实战
|
1月前
|
JavaScript 前端开发 API
JavaScript基础-BOM与窗口交互
【6月更文挑战第12天】本文介绍了BOM(浏览器对象模型),它是JavaScript与浏览器交互的API。核心对象包括顶级对象window、document、location、navigator和history。常见问题涉及window全局作用域、location.href编码、history使用和navigator.userAgent检测。提供了代码示例,如设置页面标题、页面跳转及利用history实现无刷新跳转。掌握BOM基础和最佳实践对前端开发至关重要。
|
9天前
|
JavaScript
JS【实战】跨域的网页链接跳转
JS【实战】跨域的网页链接跳转
6 0
|
10天前
|
前端开发 JavaScript
js【详解】Promise(含 Promise 的三种状态及其变化,创建 Promise, Promise.all 语法、Promise.all 实战范例、手写 Promise.all)
js【详解】Promise(含 Promise 的三种状态及其变化,创建 Promise, Promise.all 语法、Promise.all 实战范例、手写 Promise.all)
22 0
|
1月前
|
数据库 数据安全/隐私保护 Python
Web实战丨基于django+html+css+js的电子商务网站
Web实战丨基于django+html+css+js的电子商务网站
43 3