面試官問:從頁面 A 打開一個新頁面 B,B 頁面關(guān)閉(包括意外崩潰),如何通知 A 頁面?
本題是 html 頁面通信題,可以拆分成:
A 頁面打開 B 頁面,A、B 頁面通信方式? B 頁面正常關(guān)閉,如何通知 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 關(guān)閉時與 A 通信
postmessage
postMessage 是 h5 引入的 API,postMessage() 方法允許來自不同源的腳本采用異步方式進(jìn)行有效的通信,可以實現(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
基于服務(wù)端的頁面通信方式,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話,屬于服務(wù)器推送技術(shù)的一種
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,能夠?qū)崿F(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 頁面正常關(guān)閉,如何通知 A 頁面
頁面正常關(guān)閉時,會先執(zhí)行 window.onbeforeunload ,然后執(zhí)行 window.onunload ,我們可以在這兩個方法里向 A 頁面通信
B 頁面意外崩潰,又該如何通知 A 頁面
頁面正常關(guān)閉,我們有相關(guān)的 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),在用戶關(guān)閉了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)勢,完整設(shè)計一套流程如下:
B 頁面加載后,通過 postMessage API 每 5s 給 sw 發(fā)送一個心跳,表示自己的在線,sw 將在線的網(wǎng)頁登記下來,更新登記時間; B 頁面在 beforeunload 時,通過 postMessage API 告知自己已經(jīng)正常關(guān)閉,sw 將登記的網(wǎng)頁清除; 如果 B頁面在運行的過程中 crash 了,sw 中的 running 狀態(tài)將不會被清除,更新時間停留在奔潰前的最后一次心跳; A 頁面 Service Worker 每 10s 查看一遍登記中的網(wǎng)頁,發(fā)現(xiàn)登記時間已經(jīng)超出了一定時間(比如 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沒有心跳則認(rèn)為已經(jīng) 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]
}
})
參考:
如何監(jiān)控網(wǎng)頁崩潰? 騰訊面試四問,Are you OK?
來自:https://github.com/Advanced-Frontend/Daily-Interview-Question
