前言
项目中有一个功能,要把列表中的某一条记录数据,传递到详情页。我能想到的方法有三个:
- 用url的query传递参数,这种方式会将query参数暴露在网址中,而且传递的参数不能过长。
- 用localStrorage传递参数,这种方式在页面销毁的时候,要清除设置的存储数据,维护起来略显麻烦。
- 第三种就与前端没多大关系了,让后端加一个查询详情的接口,调接口查详情数据。
然后我在网上查找了一下,想看看前端有没有什么好的方案实现前一个页面向后一个页面传递参数。一番查找下来,没有找到我在项目中遇到的那种场景的解决方案。但查找到两个同时打开的页面,如何传参的方法,也感觉挺有收获。先演示一下效果。
效果演示
可以看到,pageA和pageB两个页面是同源页面, 两个页面同时打开的情况下,可以相互收发消息。
现将两个同时打开的页面之间传递参数的方法,逐一列举一下。
方法1. BroadCast Channel(广播频道)
BroadcastChannel 会创建一个所有同源页面都可以共享的广播频道,每个页面都可以向其它页面发送消息,每一个页面也可以接收广播消息。BroadcastChannel主要的API有:
- 创建或加入广播频道
const bc = new BroadcastChannel('channel-name');
- 监听消息
// 正常监听
bc.onmessage = function(e) {
console.log('receive:', e.data);
};
// 异常监听
bc.onmessageerror = function(e) {
console.warn('error:', e);
};
- 发送消息
bc.postMessage({key:'value'});
- 关闭
bc.close();
介绍完BroadcastChannel API之后,我们看看如何实现一下上图的效果。
pageA和pageB的html内容大同小异,上方展示接收的消息,下方是编辑和发送消息。
<!DOCTYPE html>
<head>
<title>PageA</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link href="./style.css" rel="stylesheet" type="text/css" />
</head>
<body>
<textarea id="show-msg" placeholder="显示接收的消息" disabled></textarea>
<textarea id="input-msg-ele"></textarea>
<button id="send-msg-btn">页面A发送消息</button>
<script src="./channel.js"></script>
</body>
</html>
pageA和pageB页面都引入的channel.js
内容如下:
const bc = new BroadcastChannel("test");
const showMsgEle = document.getElementById("show-msg");
const inputMsgEle = document.getElementById("input-msg-ele");
const sendMsgBtn = document.getElementById("send-msg-btn");
// 创建/加入广播频道test
const bc = new BroadcastChannel("test");
sendMsgBtn.addEventListener("click", (event) => {
// 只能给同源界面发送消息
bc.postMessage({
form: "testA",
value: inputMsgEle.value,
});
});
bc.onmessage = function (e) {
// 只能接收同源页面的消息
showMsgEle.value = e.data.value;
};
有一处要着重说明一下,就是下面这一句。
// 创建/加入广播频道test
const bc = new BroadcastChannel("test");
这一句在pageA页面的含义是创建广播频道test,在pageB页面的含义是加入广播频道test。
方法二 Service Worker
Service Worker与普通js脚本的主要区别是它们的运行容器不同,普通js脚本中,是可以操作操作dom的,在Service Worker中无法使用与操作dom相关的document
对象,Service Worker中的全局对象也由window
变成了self
。Service Worker可以捕获客户端发出的请求,功能类似于代理服务器,允许修改请求和响应,将其替换成缓存的文件内容。出于安全原因,Service worker 仅限在 HTTPS 或localhost上运行。此外,Service Worker是完全异步的。
Service Worker使用步骤
注册 Service Worker
navigator.serviceWorker.register('service-worker.js', {scope: '/'}).then(()=>{
console.log("Service Worker 注册成功");
})
register的第一个参数是脚本文件的路径,第二个参数中的scope是脚本影响的作用域范围,默认是{scope: '/'}
,可以省略。如果像上面这样写,那么在当前路径/子路径下的页面都会受到影响。如果注册成功,Service Worker将在 ServiceWorkerGlobalScope
中执行。Service Worker只会控制那些注册register()
成功后打开的页面。判断一个页面是否受Service Worker控制,可以检测navigator.serviceWorker.controller
是不是一个Service Worker实例,如果返回的是Service Worker实例,则受到Service Worker控制。如果返回为null,则当前页面不受Service Worker控制。
Service Worker 与页面通信
Service Worker是挂载在navigator.serviceWorker.controller
对象属性上,可以使用它与Service Worker进行通讯,收发消息的操作方法如下:
// 页面向service-worker发送消息
navigator.serviceWorker.controller.postMessage({key:'value'});
// 页面接收service-worker消息
navigator.serviceWorker.addEventListener('message', function(e) {
console.log(e.data);
})
在Service Worker中使用self.clients获取受控的页面。与受控页面收发消息的方法是:
// service worker与特定页面收发消息
self.addEventListener('message', e => {
// 接收页面消息
console.log(e.data);
// service worker响应特定页面的发送消息
e.source.postMessage('响应特定页面的消息');
});
// service worker向控制的所有页面发送消息
let clients = await self.clients.matchAll();
clients .forEach(client => client.postMessage('向所有页面发消息'));
改造 Service Worker 实现广播效果
多个页面之间的 Service Worker可以共享,可将Service Worker 作为消息中转站, 实现广播效果。在每个页面注册Service Worker, 然后页面调用navigator.serviceWorker.controller.postMessage
方法将消息发送到Service Worker消息中转站,Service Worker监听ServiceWorkerGlobalScope
作用域的message
事件, 接收特定页面发送的消息, 接着通过self.clients.matchAll()
获得注册了该 Service Worker 的所有页面,然后调用每个页面的postMessage
方法,向页面群发消息。
每个页面的代码如下:
const showMsgEle = document.getElementById("show-msg");
const inputMsgEle = document.getElementById("input-msg-ele");
const sendMsgBtn = document.getElementById("send-msg-btn");
// navigator.serviceWorker 只有在https或localhost下才可见
// 在file协议下也是可见的,可是不工作
if ("serviceWorker" in navigator) {
// 在当前页面注册service worker
navigator.serviceWorker.register("./sw.js").then(()=>{
console.log("Service Worker 注册成功");
});
navigator.serviceWorker.addEventListener("message", (e)=>{
// 只接收B页面发出的消息
if (e.data.from === 'pageB') {
showMsgEle.value = e.data.msg;
}
});
}
sendMsgBtn.addEventListener("click", function () {
// 这里navigator.serviceWorker.controller初始值是null,要刷新页面才能获取有效值
navigator.serviceWorker.controller.postMessage({
from: document.title,
msg: inputMsgEle.value,
});
});
sw.js的代码如下: event.waitUntil
的用途得说一下,它是告诉浏览器Service Worker在install
或者activate
过程中,不要关闭Service Worker,不这样写的话,Service Worker有可能在任何时候被关闭。
self.addEventListener("message", function (e) {
console.log("service worker 接收的消息", e.data);
e.waitUntil(
// 像所有页面发送消息
self.clients.matchAll().then(function (clients) {
if (!clients || clients.length === 0) {
return;
}
clients.forEach(function (client) {
client.postMessage(e.data);
});
})
);
});
方法三 localStorage的StorageEvent事件
localStorage大家经常用,殊不知localStorage还有个StorageEvent
事件。
当Storage
对象发生变化时(即创建/更新/删除数据项时,重复设置相同的键值不会触发该事件,Storage.clear()
) 方法至多触发一次该事件),StorageEvent
事件会触发。在同一个页面内发生的改变不会起作用——在相同域名下的其它页面(如一个新标签或 iframe)发生的改变才会起作用。
window.addEventListener('storage', function({key,newValue,oldValue,url}) {
console.log({key,newValue,oldValue,url});
});
由于重复设置相同的键值不会触发StorageEvent
事件, 为此我们每次发送消息的时候,可以给消息对象添加一个时间戳属性,确保每次发送消息,都能触发StorageEvent
事件。如果要实现两个同源页面之间通信,可以这样实现。
const showMsgEle = document.getElementById("show-msg");
const inputMsgEle = document.getElementById("input-msg-ele");
const sendMsgBtn = document.getElementById("send-msg-btn");
// 监听storage事件接收别的页面发送的消息
window.addEventListener("storage", function (e) {
if (e.key === "share-msg") {
const data = JSON.parse(e.newValue);
showMsgEle.value = data.msg;
}
});
// 在这里调用localStorage.setItem设置属性实际相当于向别的页面发送消息
sendMsgBtn.addEventListener("click", function () {
localStorage.setItem(
"share-msg",
JSON.stringify({
from: document.title,
msg: inputMsgEle.value,
timeStamp: +new Date(),
})
);
});
非同源页面通信
先看效果
http://192.168.88.130:8080/pageA.html
和http://192.168.88.130:8081/pageA.html
两个不同源页面互发消息, 可以正常接收。
实现思路
- 第一步 让不同页面中嵌入的iframe可以相互通信
给每个页面引入一个隐藏的iframe子页面, 由于这些引入的子iframe使用的是一个url(协议+域名+端口都相同),因此它们属于同源页面,同源页面收发信息可以采用上面提到的三种方式。
在http://192.168.88.130:8080/pageA.html
和http://192.168.88.130:8081/pageA.html
引入相同的子iframe页面http://192.168.88.130:8081/bridge.html
<iframe src="http://192.168.88.130:8081/bridge.html" style="opacity: 0;" />
引入iframehttp://192.168.88.130:8081/bridge.html
文件内容:
<!DOCTYPE html>
<head>
<title>bridge iframe</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
</head>
<body>
<script>
// 创建或加入广播通道
const bc = new BroadcastChannel('bridge');
// 收到某个嵌套iframe父页面的消息后,在所有iframe间进行广播
window.addEventListener('message', function (e) {
bc.postMessage(e.data);
});
// 将接收某个父页面的信息,发送给其它嵌套iframe的父页面
bc.onmessage = function (e) {
window.parent.postMessage(e.data, '*');
};
</script>
</body>
</html>
- 第二步 让父页面和iframe页面可以相互通信
父页面用window.addEventListener("message")
接收子iframe页面消息, 父页面用window.frames[0].window.postMessage
给子iframe页面发送消息。
const showMsgEle = document.getElementById("show-msg");
const inputMsgEle = document.getElementById("input-msg-ele");
const sendMsgBtn = document.getElementById("send-msg-btn");
// 接收iframe发过来的消息
window.addEventListener("message", function (e) {
showMsgEle.value = e.data.msg;
});
// 给iframe发消息
sendMsgBtn.addEventListener("click", function () {
window.frames[0].window.postMessage(
{
form: location.origin,
msg: location.origin + "发送消息:" + inputMsgEle.value,
},
"*"
);
});
总结
若是多个同源界面相互通信,首推BroadCast Channel
方法,逻辑清晰; 其次是localStorage事件,用法简单,虽然有副作用,用完需要清除数据。最后是Service Worker方法,用法相对复杂,而且不太好用。如果是非同源页面,采用嵌套一个桥接的iframe方式解决。本文的代码已上传至码云,你可以点击这里下载学习。
原文链接:https://juejin.cn/post/7232637596945514554 作者:去伪存真