Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第169题:从页面 A 打开一个新页面 B,B 页面关闭(包括意外崩溃),如何通知 A 页面? #466

Open
sisterAn opened this issue Apr 7, 2021 · 3 comments

Comments

@sisterAn
Copy link
Collaborator

sisterAn commented Apr 7, 2021

No description provided.

@Yweimao
Copy link

Yweimao commented Apr 8, 2021

postmassage消息通信

@OverTree
Copy link

OverTree commented Apr 8, 2021

b.unload

@sisterAn
Copy link
Collaborator Author

本题是 html 页面通信题,可以拆分成:

  • A 页面打开 B 页面,A、B 页面通信方式?
  • B 页面正常关闭,如何通知 A 页面?
  • B 页面意外崩溃,又该如何通知 A 页面?

A 页面打开 B 页面,A、B 页面通信方式

据我所知,A、B 页面通信方式有:

  • url 传参
  • postmessage
  • localStorage
  • WebSocket
  • SharedWorker
  • Service Worker

url 传参

url 传参数没什么可说的

<!-- 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 () {// 监听 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()">发送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 传递参数与 B 页面通信,同样通过监听 hashchange 事件,在页面 B 关闭时与 A 通信

postmessage

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

A 页面打开 B 页面,B 页面向 A 页面发送消息:

<!-- 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()">发送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;存储的数据将保存在浏览器会话中。如果 A 打开的 B 页面和 A 是不同源,则无法访问同一  Storage

WebSocket

基于服务端的页面通信方式,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种

SharedWorker

SharedWorker 接口代表一种特定类型的 worker,可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。它们实现一个不同于普通 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) // 此处为了贴近其他方案的实现,剔除自己
       .forEach(p => p.postMessage(evt.data))
   }
}

Service Worker

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

// 注册
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 页面

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

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

页面正常关闭,我们有相关的 API,崩溃就不一样了,页面看不见了,JS 都不运行了,那还有什么办法可以获取B页面的崩溃?

全网搜索了一下,发现我们可以利用 window 对象的 loadbeforeunload 事件,通过心跳监控来获取 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 事件实现崩溃监控过程如下:

图片来自:https://zhuanlan.zhihu.com/p/40273861

这个方案巧妙的利用了页面崩溃无法触发 beforeunload 事件来实现的。

在页面加载时(load 事件)在 sessionStorage 记录 good_exit 状态为 pending,如果用户正常退出(beforeunload 事件)状态改为 true,如果 crash 了,状态依然为 pending,在用户第2次访问网页的时候(第2个load事件),查看 good_exit 的状态,如果仍然是 pending 就是可以断定上次访问网页崩溃了!

但有一个问题,本例中用 sessionStorage 保存状态,在用户关闭了B页面,sessionStorage 值就会丢失,所以换种方式,使用 Service Worker 来实现:

  • 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]
  }
})

参考:

原文

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants