关键词: 页面通讯

问题区:

从页面 A 打开一个新页面 B,B 页面关闭(包括意外崩溃),如何通知 A 页面

拆分问题:

  1. A页面打开B页面,A、B页面如何通讯
  2. B页面正常关闭,如何通知A页面
  3. B页面意外奔溃,又该如何通知A页面

A页面打开B页面,A B页面之间的通讯


A,B页面的通讯方式:

  1. URL 传值
  2. postmessage
  3. localStorage
  4. WebSocket
  5. SharedWorker
  6. Service Worker

url 传参

A页面 监听 window.addEventListener(‘hashchange’) 事件
注: window.open(‘a.html’, ‘A’) 的第二个参数,self 和 _blank 和 已经有名字的窗口打开

  1. <!-- A.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>A</title>
  8. </head>
  9. <body>
  10. <h1>A 页面</h1>
  11. <button type="button" onclick="openB()">B</button>
  12. <script>
  13. window.name = 'A'
  14. function openB() {
  15. window.open("B.html", "B")
  16. }
  17. window.addEventListener('hashchange', function () {// 监听 hash
  18. alert(window.location.hash)
  19. }, false);
  20. </script>
  21. </body>
  22. </html>

B页面 监听 window.onbeforeunload 事件

  1. <!-- B.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>B</title>
  8. <button type="button" onclick="sendA()">发送A页面消息</button>
  9. </head>
  10. <body>
  11. <h1>B 页面</h1>
  12. <span></span>
  13. <script>
  14. window.name = 'B'
  15. window.onbeforeunload = function (e) {
  16. window.open('A.html#close', "A")
  17. return '确定离开此页吗?';
  18. }
  19. </script>
  20. </body>
  21. </html>

postmessage

postMessage是h5引入的 API,postMessage()方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档、多窗口、跨域消息传递,可在多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案,简直不要太好用

A 页面打开 B 页面,B 页面向 A 页面发送消息
A 页面打开,监听 message 事件

  1. <!-- A.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>A</title>
  8. </head>
  9. <body>
  10. <h1>A 页面</h1>
  11. <button type="button" onclick="openB()">B</button>
  12. <script>
  13. window.name = "A"
  14. function open () {
  15. window.open('b.html', 'b')
  16. }
  17. window.addEventListener("message", receiveMessage, false)
  18. function receiveMessage(e) {
  19. console.log('收到消息', event.data)
  20. }
  21. </script>
  22. </body>
  23. </html>

B 页面 targetPage.postMessage

  1. <!-- B.html -->
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>B</title>
  8. <button type="button" onclick="sendA()">发送A页面消息</button>
  9. </head>
  10. <body>
  11. <h1>B 页面</h1>
  12. <span></span>
  13. <script>
  14. window.name = 'B'
  15. function sendA() {
  16. let targetWindow = window.opener
  17. targetWindow.postMessage('Hello A', "http://localhost:3000");
  18. }
  19. </script>
  20. </body>
  21. </html>

localStorage

A 页面打开之后通过监听 window.addEvenetListener(“storage”, funciton(e) { e.data})

注意:localStorage 仅允许你访问一个Document 源(origin)的对象 Storage;存储的数据将保存在浏览器会话中。如果 A 打开的 B 页面和 A 是不同源,则无法访问同一 Storage

SharedWorker

SharedWorker接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 worker 的接口,具有不同的全局作用域, SharedWorkerGlobalScope

  1. // A.html
  2. var sharedworker = new SharedWorker('worker.js')
  3. sharedworker.port.start()
  4. sharedworker.port.onmessage = evt => {
  5. // evt.data
  6. console.log(evt.data) // hello A
  7. }
  8. // B.html
  9. var sharedworker = new SharedWorker('worker.js')
  10. sharedworker.port.start()
  11. sharedworker.port.postMessage('hello A')
  12. // worker.js
  13. const ports = []
  14. onconnect = e => {
  15. const port = e.ports[0]
  16. ports.push(port)
  17. port.onmessage = evt => {
  18. ports.filter(v => v!== port) // 此处为了贴近其他方案的实现,剔除自己
  19. .forEach(p => p.postMessage(evt.data))
  20. }
  21. }

Service Worker

Service Worker 是一个可以长期运行在后台的 Worker,能够实现与页面的双向通信。多页面共享间的 Service Worker 可以共享,将 Service Worker 作为消息的处理中心(中央站)即可实现广播效果。

  1. // 注册
  2. navigator.serviceWorker.register('./sw.js').then(function () {
  3. console.log('Service Worker 注册成功');
  4. })
  5. // A
  6. navigator.serviceWorker.addEventListener('message', function (e) {
  7. console.log(e.data)
  8. });
  9. // B
  10. navigator.serviceWorker.controller.postMessage('Hello A');

B 页面正常关闭,如何通知 A 页面

页面正常关闭时,会先执行window.onbeforeunload,然后执行window.onunload,我们可以在这两个方法里向 A 页面通信

B 页面意外崩溃,又该如何通知 A 页面

使用 ServiceWorker

完整设计一套流程

  1. B 页面加载后,通过 postMessage API 每 5s 给sw 发送一个心跳,表示自己在线,sw 将在线的网页登记下来,更新登记时间
  2. B 页面在 beforunload 时,通过 postMessage API 告诉自己已经正常关闭,sw 将登记的页面清除;
  3. A 页面 Sevice Worker 每 10S 查看一遍登记中的网页,发现登记时间已经超出一定是时间(比如 15s),即可判定该网页崩溃(crash)了
  1. // B 页面
  2. if (navigator.serviceWorker.controller !== null) {
  3. let HEARTBEAT_INTERVAL = 5 * 1000 // 每五秒发一次心跳
  4. let sessionId = uuid() // B页面会话的唯一 id
  5. let heartbeat = function () {
  6. navigator.serviceWorker.controller.postMessage({
  7. type: 'heartbeat',
  8. id: sessionId,
  9. data: {} // 附加信息,如果页面 crash,上报的附加数据
  10. })
  11. }
  12. window.addEventListener("beforeunload", function() {
  13. navigator.serviceWorker.controller.postMessage({
  14. type: 'unload',
  15. id: sessionId
  16. })
  17. })
  18. setInterval(heartbeat, HEARTBEAT_INTERVAL);
  19. heartbeat();
  20. }
  1. // A 页面
  2. const CHECK_CRASH_INTERVAL = 10 * 1000
  3. const CRASH_THRESHOLD = 15 * 1000
  4. const pages = {}
  5. let timer
  6. function checkCrash() {
  7. const now = Date.now()
  8. for (var id in pages) {
  9. let page = pages[id]
  10. if ((now - page.t) > CRASH_THRESHOLD) {
  11. // 上报 crash
  12. delete pages[id]
  13. }
  14. }
  15. if (Object.keys(pages).length == 0) {
  16. clearInterval(timer)
  17. timer = null
  18. }
  19. }
  20. worker.addEventListener('message', (e) => {
  21. const data = e.data;
  22. if (data.type === 'heartbeat') {
  23. pages[data.id] = {
  24. t: Date.now()
  25. }
  26. if (!timer) {
  27. timer = setInterval(function () {
  28. checkCrash()
  29. }, CHECK_CRASH_INTERVAL)
  30. }
  31. } else if (data.type === 'unload') {
  32. delete pages[data.id]
  33. }
  34. })