过程拆分
- A页面中打开B页面,A,B页面通信方式?
- B页面正常关闭,如何通知A页面?
- B页面意外崩溃,如何通知A页面?
A、B页面的通信方式
- url传参
- localStorage本地存储
- postmessage
- WebSocket协议
- SharedWorker
- Service Worker
url传参
A.vue
<template>
<div>
<div>{{ count }}</div>
<button @click="handleNewDialog">B弹窗</button>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from 'vue-router' //引入useRouter
const count = ref(1);
const router = useRouter();
const {href} = router.resolve({ //使用resolve
name:'B', //这里是跳转页面的name
path: '/B',
query: {
count: count.value,
}
})
window.name = 'A'
function handleNewDialog() {
window.open(href, '_blank', centerStyle(400, 400)+',toolbar=no,menubar=no,resizeable=no,location=no,status=no,scrollbars=yes')
}
// 子方法
var centerStyle = function (height, width) {
var iTop = (window.screen.height - 30 - height) / 2; //获得窗口的垂直位置;
// var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置; 不生效
let iLeft = (window.screenX || window.screenLeft || 0) + (window.screen.width - width) / 2;
return 'height=' + height + ',width=' + width + ',top=' + iTop + ',left=' + iLeft
};
window.addEventListener('hashchange', function () {// 监听 hash
let hash = window.location.hash;
let index = hash.indexOf('?');
let searchData = hash.substring(index + 1);
let arr = searchData.split('=');
count.value = arr[1];
}, false);
</script>
<style>
</style>
B.vue
<template>
<div>
<div>{{ newCount }}</div>
<button @click="handelAdd">增加count</button>
<button type="button" @click="sendA()">发送A页面消息,关闭B弹窗,newCount变更后并显示在A页面上</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRoute, useRouter } from 'vue-router'
window.name = 'B'
let newCount = ref(0);
const route = useRoute()
newCount.value = route.query.count
function handelAdd() {
newCount.value ++
}
// 窗口崩溃
window.onbeforeunload = function (e) {
sendA()
return '确定离开此页吗?';
}
function sendA() {
let href = window.location.origin + '/#/?count=' + newCount.value
window.open(href, 'A')
}
</script>
<style>
</style>
localStorage本地存储
// A页面存储count, 监听获取数据,更新页面
localStorage.setItem('count', count.value);
// A页面 监听获取数据 storage事件只有在值发生变化时才会触发。
window.addEventListener('storage', function (e) {
})
// B页面获取count B修改之后更新localStorage存储的count
let testB = localStorage.getItem('count');
注:localStorage仅允许方位同源,存储的数据将保存在浏览器会话中,如果A打开的B页面和A是不同源的,则无法方位同一Storage
postMessage
postMessage 是 html5 引入的API,postMessage()方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档、多窗口、跨域消息传递,多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案。
A.vue
<template>
<div>
<div>{{ count }}</div>
<button @click="handleNewDialog">new弹窗</button>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from 'vue-router' //引入useRouter
const count = ref(1);
const router = useRouter();
const {href} = router.resolve({ //使用resolve
name:'B', //这里是跳转页面的name
path: '/B',
query: {
count: count.value,
}
})
window.name = 'A'
function handleNewDialog() {
window.open(href, '_blank', centerStyle(400, 400)+',toolbar=no,menubar=no,resizeable=no,location=no,status=no,scrollbars=yes')
}
// 子方法
var centerStyle = function (height, width) {
var iTop = (window.screen.height - 30 - height) / 2; //获得窗口的垂直位置;
// var iLeft = (window.screen.width - 10 - width) / 2; //获得窗口的水平位置; 不生效
let iLeft = (window.screenX || window.screenLeft || 0) + (window.screen.width - width) / 2;
return 'height=' + height + ',width=' + width + ',top=' + iTop + ',left=' + iLeft
};
window.addEventListener("message", receiveCount, false);
</script>
<style>
</style>
B.vue
<template>
<div>
<div>{{ newCount }}</div>
<button @click="handelAdd">增加count</button>
<button type="button" @click="sendA()">发送A页面消息</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRoute, useRouter } from 'vue-router'
window.name = 'B'
let newCount = ref(0);
const route = useRoute()
newCount.value = route.query.count
function handelAdd() {
newCount.value ++
}
function sendA() {
// window.opener----是window.open打开的子页面对象调用父页面对象
// 通常在使用window.opener的时候要去判断父窗口的状态,如果父窗口被关闭或者更新,就会出错,解决办法是加上如下的验证if(window.opener && !window.opener.closed)
let targetWindow = window.opener
targetWindow.postMessage(newCount.value, window.location.href);
}
</script>
<style>
</style>
API介绍
- 发送数据
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow:窗口的引用,例如:比如执行window.open返回的窗口对象 iframe的contentWindow属性或者是命名过的或数值索引的window.frames.
message:要发送给其他窗口的数据
targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,指定后只有对应origin下的窗口才可以接收到消息,设置为通配符"*“表示可以发送到任何窗口,但通常处于安全性考虑不建议这么做.如果想要发送到与当前窗口同源的窗口,可设置为”/"
transfer (可选属性):是一串和message同时传递的Transferable对象,这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权. - 接收数据:监听message事件的发生
window.addEventListener("message", receiveMessage, false) ; function receiveMessage(event) { var origin= event.data; console.log(event); }
- data : 指的是从其他窗口发送过来的消息对象;
- type: 指的是发送消息的类型;
- source: 指的是发送消息的窗口对象;
- origin: 指的是发送消息的窗口的源
WebSocket协议
- 什么是 WebSocket
- WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
- 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
- Websocket是一个持久化的协议
- websocket的原理
- websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
- 在websocket出现之前,web交互一般是基于http协议的短连接或者长连接
- websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"
- websocket的使用场景: 社交聊天、弹幕、多玩家游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、基于位置的应用、在线教育、智能家居等 需要高实时的场景。
SharedWorker
SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域,SharedWorkerGlobalScope 。
page.vue
<template>
<div>{{ count }}</div>
<div @click="sendMessage">点击1</div>
</template>
<script setup>
import sharedWorkerHook from './sharedWorkerHook.js'
import { ref, onMounted } from "vue";
let count = ref(1);
onMounted(() => {
sharedWorkerHook.port.start()
// 接收SharedWorker返回的结果
sharedWorkerHook.port.onmessage = event => {
count.value = event.data;
console.log(event.data, '11111111111111')
}
})
function sendMessage() {
count.value ++
sharedWorkerHook.port.postMessage({ value: count.value })
}
</script>
page2.vue
<template>
{{ count }}
<div @click="sendMessage">点击2</div>
</template>
<script setup>
import sharedWorkerHook from './sharedWorkerHook'
import { ref, onMounted } from "vue";
let count = ref(100);
onMounted(() => {
sharedWorkerHook.port.start()
// 接收SharedWorker返回的结果
sharedWorkerHook.port.onmessage = event => {
count.value = event.data
console.log(event.data, '22222222222')
}
})
function sendMessage() {
count.value ++
sharedWorkerHook.port.postMessage({ value: count.value })
}
</script>
worker.js
/**
* @description 所有连接这个worker的集合
*/
const portsList = []
/**
* @description 连接成功回调
*/
self.onconnect = (event) => {
// 当前触发连接的端口
const port = event.ports[0]
// 添加进去
portsList.push(port)
// 接收到消息的回调
port.onmessage = (event) => {
// 获取传递的消息
const { type, message, value } = event.data
// 计算
let result = 0
result = value
portsList.forEach((port) => port.postMessage(`${result}`))
}
}
sharedWorkerHook.js
const sharedWorker = new SharedWorker(new URL('./worker.js', import.meta.url), 'test')
export default sharedWorker
文件目录结构
ShareWorker的Web API 接口
Service Worker
Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。
// 注册 Service Worker
navigator.serviceWorker.register('./sw.js').then(function () {
console.log('Service Worker 注册成功');
})
// 其中./sw.js是对应的Service Worker脚本。Service Worker本身并不具备“广播通信”的功能, 需要我们将其改造成消息中转站:
self.addEventListener('message', function (e) {
console.log(e.data);
e.waitUntil(
self.clients.matchAll().then(function (clients) {
if (!clients || clients.length === 0) {
return;
}
clients.forEach(function (client) {
client.postMessage(e.data);
});
})
);
});
// A 在需要获取的页面监听Service Worker发送来的消息:
navigator.serviceWorker.addEventListener('message', function (e) {
console.log(e.data)
});
// B 发送消息,可以调用Service Worker的postMessage方法:
navigator.serviceWorker.controller.postMessage('Hello A');
Service Worker - 《阮一峰 Web API 教程》
如何监控网页崩溃?
-
B 页面正常关闭,如何通知 A 页面
页面正常关闭时,会先执行 window.onbeforeunload ,然后执行 window.onunload ,我们可以在这两个方法里向 A 页面通信 -
B 页面意外崩溃,又该如何通知 A 页面
页面正常关闭,我们有相关的 API,崩溃就不一样了,页面看不见了,JS 都不运行了,那还有什么办法可以获取B页面的崩溃?- window 对象的 load 和 beforeunload 事件,通过心跳监控来获取 B 页面的崩溃
- 在页面加载时(load事件)在sessionStorage记录goodexit状态为pending。
- 如果用户正常退出(beforeunload事件)状态改为true。
- 如果crash了,状态依然为pending。
- 在用户第2次访问网页的时候(第2个load事件),查看goodexit的状态,如果仍然是pending就是可以断定上次访问网页崩溃了。
window.addEventListener('load', function () { sessionStorage.setItem('good_exit', 'pending'); setInterval(function () { sessionStorage.setItem('time_before_crash', new Date().toString()); }, 1000); }); window.addEventListener('beforeunload', function () { sessionStorage.setItem('good_exit', 'true'); }); if(sessionStorage.getItem('good_exit') && sessionStorage.getItem('good_exit') !== 'true') { /* insert crash logging code here */ alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash')); }
这个方案巧妙的利用了页面崩溃无法触发 beforeunload 事件来实现的。
需要注意的是,使用sessionStorage存储状态可能会因为用户强制关闭网页或者重新打开浏览器而丢失,而将状态存储在localStorage或Cookie中可能会导致每有一次网页打开,就会有一个crash上报。因此,需要根据实际情况选择合适的存储方式。!
- 基于Service Worker的崩溃统计方案:
- 在页面的脚本中创建Service Worker工作线程。
- 定时向该线程发送消息,即使网页奔溃了,线程还能存活。
- 在线程中接收消息并比对时间,当间隔时间大于15秒时,就认为超时没有心跳了,页面处于奔溃阶段,向监控系统上报相关信息。
优点- Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
- Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;
- 网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息
完整的设计流程 - B 页面加载后,通过 postMessage API 每 5s 给 sw 发送一个心跳,表示自己的在线,sw 将在线的网页登记下来,更新登记时间;
- B 页面在 beforeunload 时,通过 postMessage API 告知自己已经正常关闭,sw 将登记的网页清除;
- 如果 B页面在运行的过程中 crash 了,sw 中的 running 状态将不会被清除,更新时间停留在奔溃前的最后一次心跳;
- A 页面 Service Worker 每 10s 查看一遍登记中的网页,发现登记时间已经超出了一定时间(比如 15s)即可判定该网页 crash 了。
// B if (navigator.serviceWorker.controller !== null) { let HEARTBEAT_INTERVAL = 5 * 1000 // 每五秒发一次心跳 let sessionId = uuid() // B页面会话的唯一 id let heartbeat = function () { navigator.serviceWorker.controller.postMessage({ type: 'heartbeat', id: sessionId, data: {} // 附加信息,如果页面 crash,上报的附加数据 }) } window.addEventListener("beforeunload", function() { navigator.serviceWorker.controller.postMessage({ type: 'unload', id: sessionId }) }) setInterval(heartbeat, HEARTBEAT_INTERVAL); heartbeat(); }
// 每 10s 检查一次,超过15s没有心跳则认为已经 crash const CHECK_CRASH_INTERVAL = 10 * 1000 const CRASH_THRESHOLD = 15 * 1000 const pages = {} let timer function checkCrash() { const now = Date.now() for (var id in pages) { let page = pages[id] if ((now - page.t) > CRASH_THRESHOLD) { // 上报 crash delete pages[id] } } if (Object.keys(pages).length == 0) { clearInterval(timer) timer = null } } worker.addEventListener('message', (e) => { const data = e.data; if (data.type === 'heartbeat') { pages[data.id] = { t: Date.now() } if (!timer) { timer = setInterval(function () { checkCrash() }, CHECK_CRASH_INTERVAL) } } else if (data.type === 'unload') { delete pages[data.id] } })
小结
对于同源页面
- 广播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
- 共享存储模式:Shared Worker / IndexedDB / cookie
- 口口相传模式:url传参(window.open + window.opener)
- 基于服务端:Websocket / Comet / SSE 等
对于非同源页面:
- 使用 iframe作为桥发送和监听消息