• 从页面 A 打开一个新页面 B,B 页面正常关闭或意外崩溃,A页面怎么更新B页面提交的参数并进行更新


    过程拆分

    • 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    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');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注: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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    代码效果
    API介绍

    • 发送数据
      otherWindow.postMessage(message, targetOrigin, [transfer]);
      
      • 1

      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);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5

    上述代码中console的结果

    • data : 指的是从其他窗口发送过来的消息对象;
    • type: 指的是发送消息的类型;
    • source: 指的是发送消息的窗口对象;
    • origin: 指的是发送消息的窗口的源

    WebSocket协议

    1. 什么是 WebSocket
    • WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
    • 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
    • Websocket是一个持久化的协议
    1. websocket的原理
    • websocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
    • 在websocket出现之前,web交互一般是基于http协议的短连接或者长连接
    • websocket是一种全新的协议,不属于http无状态协议,协议名为"ws"
    1. 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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}`))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    sharedWorkerHook.js

    const sharedWorker = new SharedWorker(new URL('./worker.js', import.meta.url), 'test')
    
    export default sharedWorker
    
    • 1
    • 2
    • 3

    文件目录结构
    在这里插入图片描述
    上述代码的效果图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');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    Service Worker - 《阮一峰 Web API 教程》

    如何监控网页崩溃?

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

    • B 页面意外崩溃,又该如何通知 A 页面
      页面正常关闭,我们有相关的 API,崩溃就不一样了,页面看不见了,JS 都不运行了,那还有什么办法可以获取B页面的崩溃?

      1. 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'));
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

    这个方案巧妙的利用了页面崩溃无法触发 beforeunload 事件来实现的。
    需要注意的是,使用sessionStorage存储状态可能会因为用户强制关闭网页或者重新打开浏览器而丢失,而将状态存储在localStorage或Cookie中可能会导致每有一次网页打开,就会有一个crash上报。因此,需要根据实际情况选择合适的存储方式。!

    1. 基于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();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      // 每 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]
        }
      })
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35

    小结

    对于同源页面
    - 广播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
    - 共享存储模式:Shared Worker / IndexedDB / cookie
    - 口口相传模式:url传参(window.open + window.opener)
    - 基于服务端:Websocket / Comet / SSE 等

    对于非同源页面:

    • 使用 iframe作为桥发送和监听消息
  • 相关阅读:
    touchGFX综合学习十四、基于cubeMX、正点原子H750开发版、RGB4.3寸屏移植touchGFX完整教程+工程(二)
    操作系统知识点总结——第二章进程管理
    网络安全原理与实践学习笔记——设计DMZ
    CloudCompare&PCL 点云SVD分解
    前端八股文之“闭包”
    解读《生成式人工智能服务管理暂行办法》
    vue学习笔记23-组件事件⭐
    自己整理博哥爱运维0817-----k8s集成GitLib流水线
    ROW_NUMBER() OVER (PARTITION BY id ORDER BY createTime DESC)的用法讲解(文心一言自动生成的博客)
    小成本搏大流量:微信/支付宝小程序搜索排名优化
  • 原文地址:https://blog.csdn.net/chengcheng9876/article/details/135968957