纯前端跨页面通讯的各种姿势(父子页通讯、兄弟页通讯)跨页面通讯是前端面试常考的一个经典问题,虽然说网上有很多资料介绍了一
简介
跨页面通讯是前端面试常考的一个经典问题,虽然说网上有很多资料介绍了一些跨页面通讯的方式,但是大都只是简单的罗列,并没有实际例子,所以很多小伙伴只是简单的背诵。因为没有实操,因此当面试官稍微问深一点,比如某某跨页面通讯怎么使用?有哪些坑?哪种方式最适应哪种场景?一些小伙伴们就答不上来了。
为了解决这些问题,笔者今天手把手实操,一个一个讲解常见的纯前端跨页面通讯方案,不仅会讲它们的使用还会详细说明它们的优缺点,让你们对前端跨页面通讯有更深层次的理解。
对于前端跨页面通讯,无非就是父子同页面(iframe情况)、父子异页面(window.open()打开的子页面)、兄弟页面这三种情况,下面每种跨页面通讯方式,我都会以这三种方式进行举例讲解。
postMessage
我们先来看看 postMessage
。
postMessage
是HTML5
中新引入的API
,它可以实现跨窗口以及跨域的通信。postMessage
类似于Ajax
但是它不受同源策略的限制并且通信双方都是客户端。
父子同页面
对于父子同页面的情况,postMessage
是一个很好的跨页面通信方案。
我们来看看笔者demo
的效果
可以看到,在父子同页面的情况下,postMessage
是一个很好的跨页面通讯方案。
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>father</title>
</head>
<body>
<div>
<h3>father窗口</h3>
<div>
<div>
<button onclick="send()">发送数据给子窗口</button>
</div>
<div id="content"></div>
</div>
<iframe
id="childFrame"
src="http://127.0.0.1:5500/js/%E8%B7%A8%E9%A1%B5%E9%9D%A2%E9%80%9A%E8%AE%AF/child.html"
width="100%"
height="500"
></iframe>
</div>
<script>
// postMessage 方案
const send = () => {
const childPage = document.getElementById("childFrame"); // 获取iframe
// 通过childPage.contentWindow发送数据给iframe页面
childPage.contentWindow.postMessage(
"<div>father发送给child的数据</div>",
location.origin
);
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>child</title>
</head>
<body>
<div>
<h3>child窗口</h3>
<div>
<button onclick="send()">发送数据给父窗口</button>
</div>
<div id="content"></div>
</div>
<script>
// postMessage 方案
const send = () => {
const parentPage = window.parent; // 通过window.parent获取父窗口
parentPage.postMessage("<div>child发送给father的数据</div>", location.origin);
};
const content = document.getElementById("content");
// 监听father页面发送来的数据
window.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
需要注意的是:
- 父页面获取
iframe
窗口是通过iframe.contentWindow
,在子页面获取父页面是通过window.parent
。 - 使用某窗口
postMessage
发送出来的消息,就需要在某窗口使用onmessage
事件监听。
接下来我们再来看看父子异页面
父子异页面
父子异页面就是在父页面通过window.open()
打开的子页面,而不是像上面一样,在父页面通过iframe
内嵌子页面。这种情况又稍微复杂一点点。
我们来看看笔者demo
的效果
兄弟1页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="openWindow()">打开页面</button>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
let childWindow = null;
const openWindow = () => {
childWindow = window.open( // 通过window.open的返回值获取子窗口
"http://127.0.0.1:5500/js/%E8%B7%A8%E9%A1%B5%E9%9D%A2%E9%80%9A%E8%AE%AF/brother2.html",
"brother2"
);
};
// postMessage 方案
const send = () => { // 子窗口对象发送消息
childWindow.postMessage(
"<div>brother1发送给brother2的数据</div>",
location.origin
);
};
const content = document.getElementById("content");
window.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
兄弟2页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// postMessage 方案
const send = () => {
window.opener.postMessage( // window.opener获取父窗口
"<div>brother2发送给brother1的数据</div>",
location.origin
);
};
const content = document.getElementById("content");
window.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
需要注意的是:
- 父页面获取子页面窗口是通过
wind.open()的句柄
,在子页面获取父页面是通过window.opener
。 - 使用某窗口
postMessage
发送出来的消息,就需要在某窗口使用onmessage
事件监听。
兄弟页面
对于兄弟页面,postMessage
就无能为力了。因为对于各自单独打开的页面,没法获取到窗口的句柄,也就没法进行消息的发送和监听。
优缺点分析
postMessage
优点就是简单,并且在跨域场景中依然可以使用该方案。缺点是只能在父子页面的情况下才能完成通讯,并不是万能的。
BroadcastChannel
接下来我们再来看看 BroadcastChanne
。
BroadcastChanne
接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message
事件,消息可以广播到所有监听了该频道的 BroadcastChannel
对象。
说这么多官方解释可能有些懵,别急,我们看下面的例子
父子同页面
我们来看看笔者demo
的效果
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>father</title>
</head>
<body>
<div>
<h3>father窗口</h3>
<div>
<div>
<button onclick="send()">发送数据给子窗口</button>
</div>
<div id="content"></div>
</div>
<iframe
id="childFrame"
src="http://127.0.0.1:5500/js/%E8%B7%A8%E9%A1%B5%E9%9D%A2%E9%80%9A%E8%AE%AF/child.html"
width="100%"
height="500"
></iframe>
</div>
<script>
// BroadcastChannel 方案
const channel = new BroadcastChannel("test"); // 频道为test,这个频道必须相同
const send = () => {
channel.postMessage(
"<div>BroadcastChannel father发送给child数据</div>"
);
};
const content = document.getElementById("content");
channel.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>child</title>
</head>
<body>
<div>
<h3>child窗口</h3>
<div>
<button onclick="send()">发送数据给父窗口</button>
</div>
<div id="content"></div>
</div>
<script>
// BroadcastChannel 方案
const channel = new BroadcastChannel("test"); // 频道为test,这个频道必须相同
const send = () => {
channel.postMessage(
"<div>BroadcastChannel child发送给father数据</div>"
);
};
const content = document.getElementById("content");
channel.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
这种方式就不需要父子对象窗口的句柄了,直接使用频道通信即可。不过需要注意的是父子页面必须订阅相同的频道。
父子异页面
我们来看看笔者demo
的效果。
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="openWindow()">打开页面</button>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
// BroadcastChannel 方案
const channel = new BroadcastChannel("test");
const send = () => {
channel.postMessage(
"<div>BroadcastChannel brother1发送给brother2数据</div>"
);
};
const content = document.getElementById("content");
channel.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// BroadcastChannel 方案
const channel = new BroadcastChannel("test");
const send = () => {
channel.postMessage(
"<div>BroadcastChannel brother2发送给brother1数据</div>"
);
};
const content = document.getElementById("content");
channel.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
这种方式就不需要父子对象窗口的句柄了,直接使用频道通信即可。不过需要注意的是父子页面必须订阅相同的频道。
兄弟页面
兄弟页面就是两个单独打开的页面,不存在父子关系的页面,对于这种情况,BroadcastChanne
也是支持的。
我们来看看笔者demo
的效果
兄弟1页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
// BroadcastChannel 方案
const channel = new BroadcastChannel("test");
const send = () => {
channel.postMessage(
"<div>BroadcastChannel brother1发送给brother2数据</div>"
);
};
const content = document.getElementById("content");
channel.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
兄弟2页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// BroadcastChannel 方案
const channel = new BroadcastChannel("test");
const send = () => {
channel.postMessage(
"<div>BroadcastChannel brother2发送给brother1数据</div>"
);
};
const content = document.getElementById("content");
channel.onmessage = function (e) {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
这种方式就不需要父子对象窗口的句柄了,直接使用频道通信即可。不过需要注意的是父子页面必须订阅相同的频道。
优缺点分析
BroadcastChannel
优点是简单,不依赖窗口句柄,因此支持父子和兄弟页面通信。缺点是不支持跨域,就是通信页面必须是同源,并且兼容性稍微差点点,使用的时候需要注意。
storage事件
当我们往localStorage、sessionStorage
里面存储数据的时候会触发storage
事件。所以利用这一点我们也能实现页面间的通讯。
父子同页面
我们来看看笔者demo
的效果
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>father</title>
</head>
<body>
<div>
<h3>father窗口</h3>
<div>
<div>
<button onclick="send()">发送数据给子窗口</button>
</div>
<div id="content"></div>
</div>
<iframe
id="childFrame"
src="http://127.0.0.1:5500/js/%E8%B7%A8%E9%A1%B5%E9%9D%A2%E9%80%9A%E8%AE%AF/child.html"
width="100%"
height="500"
></iframe>
</div>
<script>
// storage方案
let i = 0;
const send = () => {
localStorage.setItem("fatherKey", i++); //需要注意的是要触发storage事件,每次必须存储不一样的值,或者不同的key
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.addEventListener("storage", (e) => {
console.log(e);
content.innerHTML += "child更新了";
});
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>child</title>
</head>
<body>
<div>
<h3>child窗口</h3>
<div>
<button onclick="send()">发送数据给父窗口</button>
</div>
<div id="content"></div>
</div>
<script>
// storage方案
let i = 0;
const send = () => {
localStorage.setItem("chidlKey", i++); //需要注意的是要触发storage事件,必须存储不一样的值,或者不同的key
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.addEventListener("storage", (e) => {
console.log(e);
content.innerHTML += "father更新了";
});
</script>
</body>
</html>
对于父子同页面,我们既可以使用localStorage
还可以使用sessionStorage
来进行存储来触发storage
事件。
需要注意的是要触发storage
事件,每次存储必须存储不一样的值,或者不同的key
。并且storage
事件不会在当前窗口触发,而是会在同源的其它窗口触发。
父子异页面
我们来看看笔者demo
的效果
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="openWindow()">打开页面</button>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
// storage方案
let i = 0;
const send = () => {
localStorage.setItem("brother1Key", i++); //需要注意的是要触发storage事件,每次必须存储不一样的值,或者不同的key
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.addEventListener("storage", (e) => {
console.log(e);
content.innerHTML += "brother2发送来的数据";
});
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// storage方案
let i = 0;
const send = () => {
localStorage.setItem("brother2Key", i++); //需要注意的是要触发storage事件,每次必须存储不一样的值,或者不同的key
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.addEventListener("storage", (e) => {
console.log(e);
content.innerHTML += "brother1发送来的数据";
});
</script>
</body>
</html>
对于父子异页面,我们就只能使用localStorage
来进行存储来触发storage
事件。
需要注意的是要触发storage
事件,每次存储必须存储不一样的值,或者不同的key
。并且storage
事件不会在当前窗口触发,而是会在同源的其它窗口触发。
兄弟页面
我们来看看笔者demo
的效果
兄弟1页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
// storage方案
let i = 0;
const send = () => {
localStorage.setItem("brother1Key", i++); //需要注意的是要触发storage事件,每次必须存储不一样的值,或者不同的key
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.addEventListener("storage", (e) => {
console.log(e);
content.innerHTML += "brother2发送来的数据";
});
</script>
</body>
</html>
兄弟2页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// storage方案
let i = 0;
const send = () => {
localStorage.setItem("brother2Key", i++); //需要注意的是要触发storage事件,每次必须存储不一样的值,或者不同的key
};
const content = document.getElementById("content");
// 监听iframe页面发送来的数据
window.addEventListener("storage", (e) => {
console.log(e);
content.innerHTML += "brother1发送来的数据";
});
</script>
</body>
</html>
对于兄弟页面,我们就只能使用localStorage
来进行存储来触发storage
事件。
需要注意的是要触发storage
事件,每次存储必须存储不一样的值,或者不同的key
。并且storage
事件不会在当前窗口触发,而是会在同源的其它窗口触发。
优缺点分析
监听storage
事件优点是简单、兼容性好,并且支持父子和兄弟页面通信。缺点是不支持跨域,也就是通信页面必须是同源,并且会导致本地存储存储一些无用的数据(当然你也可以在某些情况下进行清理)。
SharedWorker
SharedWorker
接口代表一种特定类型的 worker
,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。利用这一特性,我们也能使用它来实现跨页面通讯。
父子同页面
我们来看看笔者demo
的效果
首先我们创建共享的 shared.js,这个js
的功能相当于是一个中转站。
const ports = [];
onconnect = (e) => {
const port = e.ports[0];
ports.push(port);
// 相当于是一个中转站
port.onmessage = (evt) => {
ports
.filter((v) => v !== port) // 此处为了贴近其他方案的实现,剔除自己
.forEach((p) => p.postMessage(evt.data));
};
};
然后 创建父子页面。
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>father</title>
</head>
<body>
<div>
<h3>father窗口</h3>
<div>
<div>
<button onclick="send()">发送数据给子窗口</button>
</div>
<div id="content"></div>
</div>
<iframe
id="childFrame"
src="http://127.0.0.1:5500/js/%E8%B7%A8%E9%A1%B5%E9%9D%A2%E9%80%9A%E8%AE%AF/child.html"
width="100%"
height="500"
></iframe>
</div>
<script>
// SharedWorker方案
const sharedWorker = new SharedWorker("./shared.js");
sharedWorker.port.start();
const send = () => {
sharedWorker.port.postMessage("sharedWorker father发送给child的数据");
};
const content = document.getElementById("content");
sharedWorker.port.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>child</title>
</head>
<body>
<div>
<h3>child窗口</h3>
<div>
<button onclick="send()">发送数据给父窗口</button>
</div>
<div id="content"></div>
</div>
<script>
// SharedWorker方案
const sharedWorker = new SharedWorker("./shared.js");
sharedWorker.port.start();
const send = () => {
sharedWorker.port.postMessage("sharedWorker child发送给father的数据");
};
const content = document.getElementById("content");
sharedWorker.port.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
这里需要注意的是需要一个shared.js
,用来中转消息。
父子异页面
我们来看看笔者demo
的效果
父页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="openWindow()">打开页面</button>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
// SharedWorker方案
const sharedWorker = new SharedWorker("./shared.js");
sharedWorker.port.start();
const send = () => {
sharedWorker.port.postMessage(
"sharedWorker brother1发送给brother2数据"
);
};
const content = document.getElementById("content");
sharedWorker.port.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
子页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// SharedWorker方案
const sharedWorker = new SharedWorker("./shared.js");
sharedWorker.port.start();
const send = () => {
sharedWorker.port.postMessage(
"sharedWorker brother2发送给brother1数据"
);
};
const content = document.getElementById("content");
sharedWorker.port.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
这里需要注意的是需要一个shared.js
,用来中转消息。
兄弟页面
我们来看看笔者demo
的效果
兄弟1页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother1</title>
</head>
<body>
<h3>brother1页面</h3>
<div>
<button onclick="send()">发送数据给brother2窗口</button>
</div>
<div id="content"></div>
<script>
// SharedWorker方案
const sharedWorker = new SharedWorker("./shared.js");
sharedWorker.port.start();
const send = () => {
sharedWorker.port.postMessage(
"<div>sharedWorker brother1发送给brother2数据</div>"
);
};
const content = document.getElementById("content");
sharedWorker.port.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
兄弟2页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>brother2</title>
</head>
<body>
<div>
<h3>brother2页面</h3>
<button onclick="send()">发送数据给brother1窗口</button>
<div id="content"></div>
</div>
<script>
// SharedWorker方案
const sharedWorker = new SharedWorker("./shared.js");
sharedWorker.port.start();
const send = () => {
sharedWorker.port.postMessage(
"<div>sharedWorker brother2发送给brother1数据</div>"
);
};
const content = document.getElementById("content");
sharedWorker.port.onmessage = (e) => {
console.log(e);
content.innerHTML += e.data;
};
</script>
</body>
</html>
这里需要注意的是需要一个shared.js
,用来中转消息。
优缺点分析
相较于其他方案没有优势,唯一的优点是支持父子和兄弟页面通信。此外,API
复杂而且调试不方便。而且通信页面也必须同源。
并且兼容性也需要注意
总结
这里笔者介绍了目前纯前端使用频率比较高的四种跨页面通信方案。他们各自都有优缺点,小伙伴们在使用的时候需要根据自身情况(特别是兼容性)选择最合适的方案。
postMessage
- 兼容性好,能跨源通信
- 不支持兄弟页面通信
BroadcastChannel
- 支持父子、兄弟页面通信
- 只能是同源页面,并且兼容性稍微逊色一点
storage
- 支持父子、兄弟页面通讯,兼容性好
- 只能是同源页面,并且会导致无效存储
SharedWorker
- 支持父子、兄弟页面通讯
- 只能是同源页面,但是使用复杂,并且兼容性也不怎么样
当然,我们还可以借助后端来进行跨页面通讯,常用的方案有websocket、Server-sent Events
,因为不是纯前端方案,需要后端配合,这里笔者就不再详细介绍了。如果小伙伴们还有其它优秀的方案,欢迎评论区补充。
后记
感谢小伙伴们的耐心观看,本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!
转载自:https://juejin.cn/post/7238772205516554301