JavaScript iframe 實(shí)現(xiàn)多窗口通信實(shí)例詳解
引言
我最近在完善 easyjobs 代碼共享的功能。
左側(cè)是代碼編輯器,右側(cè)下方有一個(gè)控制臺(tái)。
當(dāng)我們?cè)谧髠?cè)編輯完成代碼后,點(diǎn)擊運(yùn)行 JS,右側(cè)的控制臺(tái)就可以輸出內(nèi)容。
而右側(cè)上方有一個(gè)渲染畫布,用來作為代碼運(yùn)行的容器。
你可以打開網(wǎng)址嘗試:www.easyjobs.biz/code-sharin…。
因?yàn)橥瑫r(shí)需要運(yùn)行 JavaScript 代碼,所以需要對(duì)環(huán)境進(jìn)行隔離。也就是要有一個(gè)獨(dú)立的 JavaScript 運(yùn)行環(huán)境,也可以叫做沙箱。
該怎么做呢?
實(shí)現(xiàn) JavaScript 沙箱的方案有很多,比如 iframe、with+Proxy、還有基于 Object.freeze 的不成熟提案,如果不涉及 Web API 的話,甚至可以借助 nodejs 的 vm 模塊。
不過 JavaScript 沙箱不是本文的重點(diǎn)。我的場(chǎng)景決定了 iframe 是最好的選擇,因?yàn)槲也粌H僅需要隔離 JS 代碼,還要隔離 HTML 和 CSS 代碼。
如何做沙箱呢?
iframe 有一個(gè) srcdoc 屬性,把要執(zhí)行的代碼傳給它就可以了。
<iframe srcdoc="<script>alert('hello')</script>"></iframe>
為了方便查看 iframe 中 console 輸出的內(nèi)容,我們還需要想辦法接收 iframe 傳遞過來的消息。
這也就是本文的主要內(nèi)容,iframe 通信實(shí)戰(zhàn)。
iframe 基本通信
我在這里用代碼來演示一下 iframe 最基本的通信是如何做的。
基本的 HTML 結(jié)構(gòu)
首先我們有一個(gè) 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()">發(fā)送一條消息給子窗口</button> <p id="response"></p> </body> </html>
然后有一個(gè) 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()">發(fā)送一條消息給父窗口</button> <p id="response"></p> </body> </html>
它們的關(guān)系就是相互嵌套的關(guān)系。
打開 index.html,大概是下面這樣。
需要注意,多窗口通信需要使用 http(s) 協(xié)議。
使用 JavaScript 在窗口之間發(fā)送消息
我們來實(shí)現(xiàn)一下父窗口的 sendMessage 方法。
let sub = window.frames[0] function sendMessage() { sub.postMessage({ msg: "來自父窗口的一條消息" }) }
其中 window.frames 是獲取當(dāng)前窗口的所有 iframe 元素,它返回一個(gè)類似數(shù)組的結(jié)構(gòu)。
通過調(diào)用 sub 的 postMessage 方法可以傳遞消息。
然后我們來到 sub.html 中編寫接收端的代碼。
const responseEl = document.getElementById("response") window.addEventListener("message", function (e) { responseEl.innerHTML += `收到一條消息:${e.data.msg}` })
接收端使用 window.addEventListener 來監(jiān)聽 message 事件。當(dāng)有其他窗口通過 poseMessage 來向當(dāng)前窗口發(fā)送消息時(shí),會(huì)觸發(fā)這個(gè)事件。
我們來點(diǎn)擊父窗口的「發(fā)送一條消息給子窗口」按鈕。
可以看到子窗口可以打印父窗口的消息。
同理,我們也可以通過 parent.postMessage 反向向父窗口傳遞消息。
在 sub.html 中繼續(xù)增加 sendMessage 代碼。
function sendMessage() { parent.postMessage({ msg: "來自子窗口的一條消息" }) }
這個(gè)代碼和 index.html 中發(fā)送消息的代碼很相似,唯一的區(qū)別就是接受者變成了 parent。parent 就是指當(dāng)前窗口的父窗口。
回到 index.html 中,增加監(jiān)聽代碼。監(jiān)聽代碼與子窗口完全一致,可以直接復(fù)制過來。
const responseEl = document.getElementById("response") window.addEventListener("message", function (e) { responseEl.innerHTML += `收到一條消息:${e.data.msg}</br>` })
我們來點(diǎn)擊子窗口的「發(fā)送一條消息給父窗口」按鈕。
這樣就實(shí)現(xiàn)了 iframe 窗口間雙向通信。
注意事項(xiàng)
類型
需要注意的是,postMessage 僅支持 JSON 支持的類型。
- string
- number
- null
- boolean
- object
- array
如果傳遞 undefined 的話,會(huì)自動(dòng)轉(zhuǎn)成 null。
除了上述類型以外的其他類型都不支持,比如 function、symbol。如果傳遞了這些類型,瀏覽器會(huì)報(bào)錯(cuò)。
如何傳遞函數(shù)并執(zhí)行
傳遞函數(shù)是一個(gè)很常見的需求,我們可以通過把函數(shù)轉(zhuǎn)換為字符串的方式進(jìn)行傳遞。
比如下面這樣:
function fn () {} sub.postMessage({ fn: fn.toString() })
在接收方只需要通過 eval 就可以調(diào)用函數(shù)字符串了。
不過如果函數(shù)內(nèi)引用了外部變量的話,那就不行了。
比如下面這樣:
let name = '代碼與野獸' function fn () { console.log(name) } sub.postMessage({ fn: fn.toString() })
因?yàn)榻邮斩藷o法獲取到發(fā)送端的變量。
如果碰巧接收端也存在 name 這個(gè)變量的話,eval 在執(zhí)行時(shí)就會(huì)訪問到接收端的變量而非發(fā)送端的變量。
這里也體現(xiàn)出了純函數(shù)的優(yōu)勢(shì)。如果我們遵循函數(shù)式編程范式編寫了純函數(shù),就不會(huì)導(dǎo)致這個(gè)問題。
如何在父窗口訪問到子窗口的 console
回到文章開頭,雖然我們可以通過 iframe 通信來傳遞消息,但實(shí)現(xiàn) iframe 執(zhí)行 console 同步到父窗口,仍然是個(gè)問題。
其實(shí)非常簡(jiǎn)單,把 console 對(duì)象上的所有方法劫持,然后把這段代碼加入到 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) } }
其中會(huì)調(diào)用 funcToString 方法,這個(gè)方法就是把所有的 function 字符串化。
因?yàn)槲覀儾淮_定傳入的結(jié)構(gòu)的嵌套深度,所以需要使用遞歸來轉(zhuǎn)換。
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) } }) }
以上就是JavaScript iframe 實(shí)現(xiàn)多窗口通信實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于JavaScript iframe多窗口通信的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何在JavaScript中實(shí)現(xiàn)私有屬性的寫類方式(一)
這篇文章主要介紹了如何在JavaScript中實(shí)現(xiàn)私有屬性的寫類方式。需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-12-12js中繼承的幾種用法總結(jié)(apply,call,prototype)
本篇文章主要介紹了js中繼承的幾種用法總結(jié)(apply,call,prototype) 需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-12-12js設(shè)置文本框中焦點(diǎn)位置在最后的示例代碼(簡(jiǎn)單實(shí)用)
本篇文章主要是對(duì)js設(shè)置文本框中焦點(diǎn)位置在最后的示例代碼進(jìn)行了介紹,需要的朋友可以過來參考下,希望對(duì)大家有所幫助2014-03-03干貨分享:讓你分分鐘學(xué)會(huì)javascript閉包
干貨分享:讓你分分鐘學(xué)會(huì)javascript閉包,如何才能快速學(xué)會(huì)javascript閉包,本文為大家揭曉2015-12-12原生js實(shí)現(xiàn)each方法實(shí)例代碼詳解
這篇文章主要介紹了原生js實(shí)現(xiàn)each方法,需要的朋友可以參考下2019-05-05react-router-dom?v6?通過outlet實(shí)現(xiàn)keepAlive?功能的實(shí)現(xiàn)
本文主要介紹了react-router-dom?v6?通過outlet實(shí)現(xiàn)keepAlive功能,文中根據(jù)實(shí)例編碼詳細(xì)介紹的十分詳盡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03js字符串替換所有的指定字符或文字(推薦replaceAll方法)
要實(shí)現(xiàn)js字符串替換所有的某個(gè)字符,推薦大家使用replaceAll方法,默認(rèn)不是所有瀏覽器都兼容,所以這里給出一個(gè)解決方案,需要的朋友可以參考下2014-07-07lhgcalendar時(shí)間插件限制只能選擇三個(gè)月的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄猯hgcalendar時(shí)間插件限制只能選擇三個(gè)月的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07詳解JavaScript原生封裝ajax請(qǐng)求和Jquery中的ajax請(qǐng)求
在本篇文章中我們總結(jié)了關(guān)于JavaScript原生封裝ajax請(qǐng)求和Jquery中的ajax請(qǐng)求的知識(shí)點(diǎn)內(nèi)容,需要的朋友們學(xué)習(xí)參考下。2019-02-02