本題是 html 頁面通信題,可以拆分成:
- A 頁面打開 B 頁面,A卵牍、B 頁面通信方式果港?
- B 頁面正常關閉,如何通知 A 頁面糊昙?
- B 頁面意外崩潰辛掠,又該如何通知 A 頁面?
A 頁面打開 B 頁面释牺,A萝衩、B 頁面通信方式
據(jù)我所知,A船侧、B 頁面通信方式有:
- url 傳參
- postmessage
- localStorage
- WebSocket
- SharedWorker
- Service Worker
url 傳參
url 傳參數(shù)沒什么可說的
<!-- A.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A</title>
</head>
<body>
<h1>A 頁面</h1>
<button type="button" onclick="openB()">B</button>
<script>
window.name = 'A'
function openB() {
window.open("B.html", "B")
}
window.addEventListener('hashchange', function () {// 監(jiān)聽 hash
alert(window.location.hash)
}, false);
</script>
</body>
</html>
B:
<!-- B.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B</title>
<button type="button" onclick="sendA()">發(fā)送A頁面消息</button>
</head>
<body>
<h1>B 頁面</h1>
<span></span>
<script>
window.name = 'B'
window.onbeforeunload = function (e) {
window.open('A.html#close', "A")
return '確定離開此頁嗎欠气?';
}
</script>
</body>
</html>
A 頁面通過 url 傳遞參數(shù)與 B 頁面通信,同樣通過監(jiān)聽 hashchange 事件镜撩,在頁面 B 關閉時與 A 通信
postmessage
postMessage
是 h5
引入的 API预柒,postMessage()
方法允許來自不同源的腳本采用異步方式進行有效的通信,可以實現(xiàn)跨文本文檔袁梗、多窗口宜鸯、跨域消息傳遞,可在多用于窗口間數(shù)據(jù)通信遮怜,這也使它成為跨域通信的一種有效的解決方案淋袖,簡直不要太好用
A 頁面打開 B 頁面,B 頁面向 A 頁面發(fā)送消息:
<!-- A.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A</title>
</head>
<body>
<h1>A 頁面</h1>
<button type="button" onclick="openB()">B</button>
<script>
window.name = 'A'
function openB() {
window.open("B.html?code=123", "B")
}
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
console.log('收到消息:', event.data)
}
</script>
</body>
</html>
<!-- B.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>B</title>
<button type="button" onclick="sendA()">發(fā)送A頁面消息</button>
</head>
<body>
<h1>B 頁面</h1>
<span></span>
<script>
window.name = 'B'
function sendA() {
let targetWindow = window.opener
targetWindow.postMessage('Hello A', "http://localhost:3000");
}
</script>
</body>
</html>
localStorage
// A
localStorage.setItem('testB', 'sisterAn');
// B
let testB = localStorage.getItem('testB');
console.log(testB)
// sisterAn
注意: localStorage
僅允許你訪問一個Document
源(origin)的對象 Storage
锯梁;存儲的數(shù)據(jù)將保存在瀏覽器會話中即碗。如果 A 打開的 B 頁面和 A 是不同源,則無法訪問同一 Storage
WebSocket
基于服務端的頁面通信方式陌凳,服務器可以主動向客戶端推送信息剥懒,客戶端也可以主動向服務器發(fā)送信息,是真正的雙向平等對話合敦,屬于服務器推送技術的一種
SharedWorker
SharedWorker
接口代表一種特定類型的 worker初橘,可以從幾個瀏覽上下文中訪問,例如幾個窗口充岛、iframe 或其他 worker保檐。它們實現(xiàn)一個不同于普通 worker 的接口,具有不同的全局作用域, SharedWorkerGlobalScope
崔梗。
// A.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.onmessage = evt => {
// evt.data
console.log(evt.data) // hello A
}
// B.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.postMessage('hello A')
// worker.js
const ports = []
onconnect = e => {
const port = e.ports[0]
ports.push(port)
port.onmessage = evt => {
ports.filter(v => v!== port) // 此處為了貼近其他方案的實現(xiàn)夜只,剔除自己
.forEach(p => p.postMessage(evt.data))
}
}
Service Worker
Service Worker 是一個可以長期運行在后臺的 Worker,能夠實現(xiàn)與頁面的雙向通信蒜魄。多頁面共享間的 Service Worker 可以共享扔亥,將 Service Worker 作為消息的處理中心(中央站)即可實現(xiàn)廣播效果爪膊。
// 注冊
navigator.serviceWorker.register('./sw.js').then(function () {
console.log('Service Worker 注冊成功');
})
// A
navigator.serviceWorker.addEventListener('message', function (e) {
console.log(e.data)
});
// B
navigator.serviceWorker.controller.postMessage('Hello A');
B 頁面正常關閉,如何通知 A 頁面
頁面正常關閉時砸王,會先執(zhí)行 window.onbeforeunload
,然后執(zhí)行 window.onunload
峦阁,我們可以在這兩個方法里向 A 頁面通信
B 頁面意外崩潰谦铃,又該如何通知 A 頁面
頁面正常關閉,我們有相關的 API榔昔,崩潰就不一樣了驹闰,頁面看不見了,JS 都不運行了撒会,那還有什么辦法可以獲取B頁面的崩潰嘹朗?
全網(wǎng)搜索了一下,發(fā)現(xiàn)我們可以利用 window 對象的 load
和 beforeunload
事件诵肛,通過心跳監(jiān)控來獲取 B 頁面的崩潰
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'));
}
使用 load 和 beforeunload 事件實現(xiàn)崩潰監(jiān)控過程如下:
圖片來自:https://zhuanlan.zhihu.com/p/40273861
這個方案巧妙的利用了頁面崩潰無法觸發(fā) beforeunload 事件來實現(xiàn)的屹培。
在頁面加載時(load 事件)在 sessionStorage 記錄 good_exit 狀態(tài)為 pending,如果用戶正常退出(beforeunload 事件)狀態(tài)改為 true怔檩,如果 crash 了褪秀,狀態(tài)依然為 pending,在用戶第2次訪問網(wǎng)頁的時候(第2個load事件)薛训,查看 good_exit 的狀態(tài)媒吗,如果仍然是 pending 就是可以斷定上次訪問網(wǎng)頁崩潰了!
但有一個問題乙埃,本例中用 sessionStorage 保存狀態(tài)闸英,在用戶關閉了B頁面,sessionStorage 值就會丟失介袜,所以換種方式甫何,使用 Service Worker 來實現(xiàn):
- Service Worker 有自己獨立的工作線程,與網(wǎng)頁區(qū)分開米酬,網(wǎng)頁崩潰了沛豌,Service Worker 一般情況下不會崩潰;
- Service Worker 生命周期一般要比網(wǎng)頁還要長赃额,可以用來監(jiān)控網(wǎng)頁的狀態(tài)加派;
- 網(wǎng)頁可以通過 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 發(fā)送消息
基于以上幾點優(yōu)勢,完整設計一套流程如下:
- B 頁面加載后跳芳,通過 postMessage API 每 5s 給 sw 發(fā)送一個心跳芍锦,表示自己的在線,sw 將在線的網(wǎng)頁登記下來飞盆,更新登記時間娄琉;
- B 頁面在 beforeunload 時次乓,通過 postMessage API 告知自己已經正常關閉,sw 將登記的網(wǎng)頁清除孽水;
- 如果 B頁面在運行的過程中 crash 了票腰,sw 中的 running 狀態(tài)將不會被清除,更新時間停留在奔潰前的最后一次心跳女气;
- A 頁面 Service Worker 每 10s 查看一遍登記中的網(wǎng)頁杏慰,發(fā)現(xiàn)登記時間已經超出了一定時間(比如 15s)即可判定該網(wǎng)頁 crash 了。
代碼如下:
// B
if (navigator.serviceWorker.controller !== null) {
let HEARTBEAT_INTERVAL = 5 * 1000 // 每五秒發(fā)一次心跳
let sessionId = uuid() // B頁面會話的唯一 id
let heartbeat = function () {
navigator.serviceWorker.controller.postMessage({
type: 'heartbeat',
id: sessionId,
data: {} // 附加信息炼鞠,如果頁面 crash缘滥,上報的附加數(shù)據(jù)
})
}
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]
}
})
參考: