你可能需要的多文档页面交互方案(一)

简介: 你可能需要的多文档页面交互方案

image.png


前言

欢迎关注同名公众号《熊的猫》,文章会同步更新!

在日常工作中,面对不同的需求场景,你可能会遇到需要进行多文档页面间交互的实现,例如在 A 页面跳转到 B 页面进行某些操作后,A 页面需要针对该操作做出一定的反馈等等,这个看似简单的功能,却也需要根据不同场景选择不同的方案。

这里所说的场景实际上可分为两个大方向:同源策略文档加载方式,那么本篇文章就来探讨一下这两个方面。

同源策略 & 文档加载方式

在正式开始之前,我们还是先简单聊一下同源策略和页面加载方式,如果你已经足够了解了,可以选择跳过阅读。

同源策略

基本概念

所谓的 同源策略 实际上是 浏览器 的一个重要的 安全策略,主要是用于限制 一个源 的文档 或者 其加载的脚本 是否可以与 另一个源 的资源进行交互。

注意】这里的目标是浏览器,也就是只有浏览器有同源策略的限制,例如服务端就不存在什么同源策略,这里的浏览器包括 桌面端浏览器移动端浏览器微信内置浏览器虚拟浏览器(虚拟环境中运行的网络浏览器) 等。

所谓的 就是我们常说的 协议、主机名(域名)、端口,所以所谓的 同源 也就是指两个 URL 的 协议、主机名(域名)、端口 等信息要完全匹配。

主要作用

同源策略 可以用来阻隔恶意文档,减少可能被攻击的媒介,下面还是通过一个 CSRF 例子讲解一下没有同源限制会发生什么。

CSRF 攻击

假设你在 A 网站上进行了登录并成功登入网站后,你发现 A 网站上出现了一个广告弹窗(写着:拒绝 huang,拒绝 du,拒绝 pingpangqiu),于是放纵不羁爱自由的你(为了验证真理)点开了它,发现这个网站居然不讲武德,啥也不是...


表明平静如水,背地里实则已经悄悄向 A 站点服务器 发送了请求操作,并且身份验证信息用的是你刚刚登录的认证信息(由于没有同源限制 cookies 会被自动携带在目标请求中),但服务端并不知道这是个假冒者,于是允许了本次操作,结果就是......

文档加载方式

因为这里是说多页面交互,所以前提是至少有一个页面 A 存在,那么基于 A 页面来讲有以下几种方式去加载 B 页面文档:

  • window.location.href
  • <a href="xx" target="xx">
  • window.open
  • iframe

这一部分这里先简单提及,更详细的内容放到最后作为扩展去讲,也许你会奇怪怎么没有 history.pushStatelocation.hash (如 Vue Router、React Router 中的使用),因为它们算属于在页面加载之后的路由导航,看起来虽然是页面切换了,但是切换的是文档的内容,不是整个文档,这一点还是不一样的。

同源策略下的多文档交互

Web Storage

sessionStorage & localStorage

由于多文档的方式并不适合使用 Vuex/Pinia/React Redux 等全局状态管理器,因此 Web Storage 这种应该是我们最先能想到的方式了,而 Web Storage 实际上只包含以下两种:

  • sessionStorage
  • 为每一个给定的源(given origin)维持一个独立的存储区域,该存储区域在页面 会话期间 可用,即只要浏览器处于打开状态,包括页面重新加载和恢复
  • localStorage
  • 为每一个给定的源(given origin)维持一个独立的存储区域,但是在浏览器关闭,然后重新打开后数据仍然存在,即其存储的数据是 持久化的

有些人会把 IndexedDB 也当做 Web Storage 的一种,这在规范定义上是不够准确的.

它们最基本的用法这里就不多说了,总结起来就是:在 B 页面往 Web Storage 中存入数据 X ,在 A 页面中读取数据 X 然后决定需要做什么。

这里我们可以借助 document 文档对象的 visibilitychange 事件来监听当前标签页面是否处于 可见状态,然后再决定是不是要做某些反馈操作。

核心代码:

// A 页面
document.addEventListener('visibilitychange', function () {
  if (document.visibilityState === 'visible') {
    // do something ...
  }
})
复制代码

演示效果如下:

image.png

值得注意的是,sessionStorage 在不同标签页之间的数据是不能同步,但如果 A 和 B 两个页面属于 同一浏览上下文组 可以实现初始化同步(实际算是拷贝值),后续变化不再同步。

storage 事件

当存储区域(localStorage | sessionStorage)被修改时,将会触发 storage 事件,这是 MDN 上的解释但实际是:

  • 如果当前页面的 localStorage 值被修改,只会触发其他页面的 storage 事件,不会触发本页面的 storage 事件
  • window.onstorage 事件只对 localStorage 的修改有效,sessionStorage 的修改不能触发
  • localStorage 的值必须发生变化,如果设置成相同的值则不会触发

image.png

window.onstorage 事件配合 localStorage 很完美,但是唯独对 sessionStorage 无效,目前没有发现一个很好且详细的解释。

Cookies & IndexdeDB

这两种和上述的 Web Storage 的实现方式一致,但它们又不属于一类,因此在这里还是额外提出来讲,不过它们可都是有同源策略的限制的。

既然核心方案一致,这里就不多说了,来看看它们的一些区别,便于更好的进行选择:

  • sessionStorage
  • 会话级存储,最多能够存储 5MB 左右,不同浏览器限制不同
  • 不同标签页之间的数据不能同步,但如果 A 和 B 两个页面属于 同一浏览上下文组 可以实现初始化同步(实际算是拷贝值),后续变化不再同步
  • 不支持 结构化存储,只能以 字符串形式 进行存储
  • localStorage
  • 持久级存储,最多能够存储 5MB 左右,不同浏览器限制不同
  • 只要在 同源 的情况下,无论哪个页面操作数据都可以一直保持同步到其他页面
  • 不支持 结构化存储,只能以 字符串形式 进行存储
  • Cookie
  • 默认是 会话级存储,若想实现 持久存储 可以设置 Expires 的值,存储大小约 4KB 左右,不同浏览器限制不同
  • 只要在 同源 的情况下,无论哪个页面操作数据都可以一直保持同步到其他页面
  • 不支持 结构化存储,只能以 字符串形式 进行存储
  • IndexedDB
  • 持久存储,是一种事务型数据库系统(即非关系型),存储大小理论上没有限制,由用户的磁盘空间和操作系统来决定
  • 只要在 同源 的情况下,无论哪个页面操作数据都可以一直保持同步到其他页面
  • 支持 结构化存储,包括 文件/二进制大型对象(blobs)

同一浏览上下文组 可理解为:假设在 A 页面中以 window.open<a href="x" target="_blank">x</a> 方式 打开 B 页面,并且 A 和 B 是 同源 的,那么此时  A 和 B 就属于 同一浏览上下文组

SharedWorker — 共享 Worker

SharedWorker 接口代表一种特定类型的 worker,不同于普通的 Web Worker,它可以从 几个浏览上下文中 访问,例如 几个窗口iframe其他 worker

那么 SharedWorker 的 Shared 指的是什么?

从普通的 Web Worker 的使用来看:

  • 主线程要实例化 worker 实例:const worker = new Worker('work.js');
  • 主线程调用 worker 实例的 postMessage() 方法与 worker 线程发送消息,通过 onmessage 方法用来接收 worker 线程响应的结果
  • worker 线程(即 'work.js')中也会通过 postMessage() 方法 和 onmessage 方法向主线程做相同的事情

从上述流程看没有什么大问题,但是如果是不同文档去加载执行 const worker = new Worker('work.js'); 就会生成一个新的 worker 实例,而 SharedWorker 区别于 普通 Worker 就在这里,如果不同的文档加载并执行 const sharedWorker = new SharedWorker('work.js');,那么除了第一个文档会真正创建 sharedWorker 实例外,其他以相同方式去加载 work.js 的文档就会直接 复用 第一个文档创建的 sharedWorker 实例。

效果演示

image.png

核心代码

>>>>>>>>>>>>>>>>>> pubilc/worker.js <<<<<<<<<<<<<<
// 保存多个 port 对象
let ports = []
// 每个页面进行连接时,就会执行一次
self.onconnect = (e) => {
  // 获取当前 port 对象
  const port = e.ports[0]
  // 监听消息
  port.onmessage = ({ data }) => {
    switch (data.type) {
      case 'init': // 初始化页面信息
        ports.push({
          port,
          pageId: data.pageId,
        })
        port.postMessage({
          from: 'init',
          data: '当前线程 port 信息初始化已完成',
        })
        break
      case 'send': // 单播 || 广播
        for (const target of ports) {
          if(target.port === port) continue
          target.port.postMessage({
            from: target.pageId,
            data: data.data,
          })
        }
        break
      case 'close':
        port.close()
        ports = ports.filter(v => data.pageId !== v.pageId)
        break
    }
  }
}
>>>>>>>>>>>>>>>>>> pubilc/worker.js <<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>> initWorker.ts <<<<<<<<<<<<<<
import { v4 as uuidv4 } from 'uuid'
export default (store) => {
  const pageId = uuidv4()
  const sharedWorker = new SharedWorker('/worker.js', 'testShare')
  store.sharedWorker = sharedWorker
  // 初始化页面信息
  sharedWorker.port.postMessage({
    pageId,
    type: 'init'
  })
  // 接收信息
  sharedWorker.port.onmessage = ({ data }) => {
    if (data.from === 'init') {
      console.log('初始化完成', data)
      return
    }
    store.commit('setShareData', data)
  }
  // 页面关闭
  window.onbeforeunload = (e) => {
    e = e || window.event
    if (e) {
      e.returnValue = '关闭提示'
    }
    // 清除操作
    sharedWorker.port.postMessage({ type: 'close', pageId })
    return '关闭提示'
  }
}
>>>>>>>>>>>>>>>>>> initWorker.js <<<<<<<<<<<<<<
>>>>>>>>>>>>>>>>>> store/indext.js <<<<<<<<<<<<<<
import { createStore } from 'vuex'
import initWorker from '../initWorker'
const store: any = createStore({
  state: {
    shareData: {}
  },
  getters: {
  },
  mutations: {
    setShareData (state, payload) {
      state.shareData = payload
      console.log('收到的消息:', payload)
    }
  },
  actions: {
    send (state, data) {
      store.sharedWorker.port.postMessage({
        type: 'send',
        data
      })
      console.log('发送的消息:', data)
    }
  },
  modules: {
  }
})
// 初始化 worker
initWorker(store)
export default store
>>>>>>>>>>>>>>>>>> store/indext.js <<<<<<<<<<<<<<
复制代码

BroadcastChannel

BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 浏览上下文 来订阅它,并允许 同源 的不同浏览器 窗口、Tab 页、frame/iframe 下的不同文档之间相互通信,通过触发一个 message 事件,消息可以 广播 到所有监听了该频道的 BroadcastChannel 对象。

效果演示

image.png


目录
相关文章
如何关掉Parsed mapper file日志打印
如何关掉Parsed mapper file日志打印
487 1
|
消息中间件 自然语言处理 容灾
实时或者准实时的说法
假期重新把之前在新浪博客里面的文字梳理了下,搬到这里。本文从个人理解出发,探探实时或者准实时搜索。
2357 0
|
1月前
|
Ubuntu 关系型数据库 MySQL
MySQL包安装 -- Debian系列(离线DEB包安装MySQL)
本文详细介绍了在Ubuntu 24.04、22.04、20.04及Debian 12系统上,通过离线DEB包安装MySQL 8.0和8.4版本的完整步骤。涵盖下载地址、依赖处理、dpkg安装顺序、配置方法及服务启动验证,确保用户可顺利部署MySQL数据库。
686 0
MySQL包安装 -- Debian系列(离线DEB包安装MySQL)
|
6月前
|
前端开发 JavaScript Android开发
《深度剖析:React Native与Flutter在社交应用中混合原生组件开发》
React Native通过JavaScript桥接机制调用原生组件,适合快速开发社交应用功能,如分享、相机等,但性能上可能在高并发场景下存在瓶颈。Flutter采用Dart语言和Skia引擎自绘制UI,跨平台一致性更强,热重载支持状态保留,提升开发效率,但在特定原生功能集成时需更多适配工作。两者各有优势:React Native社区成熟、上手容易;Flutter性能优越、代码统一。开发者需根据项目需求、团队技术栈及性能要求选择合适框架。
199 25
|
Prometheus 运维 监控
智能运维实战:Prometheus与Grafana的监控与告警体系
【10月更文挑战第26天】Prometheus与Grafana是智能运维中的强大组合,前者是开源的系统监控和警报工具,后者是数据可视化平台。Prometheus具备时间序列数据库、多维数据模型、PromQL查询语言等特性,而Grafana支持多数据源、丰富的可视化选项和告警功能。两者结合可实现实时监控、灵活告警和高度定制化的仪表板,广泛应用于服务器、应用和数据库的监控。
1143 3
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的校园二手交易平台系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的校园二手交易平台系统的详细设计和实现(源码+lw+部署文档+讲解等)
368 1
|
JSON 缓存 前端开发
阿里开发手册 嵩山版-编程规约 (十) 前后端规约
《阿里开发手册 嵩山版》中关于前后端规约的部分,涵盖了前后端交互的API设计、数据格式、错误处理、安全性等关键编程规约,目的是确保前后端开发高效协同,提升软件交付质量。
 阿里开发手册 嵩山版-编程规约 (十) 前后端规约
|
SQL 关系型数据库 MySQL
批量提交SQL语句的技巧与方法
在数据库管理和开发中,经常需要执行大量的SQL语句,如批量插入、更新或删除数据
|
JavaScript 前端开发 数据管理
|
机器学习/深度学习 自然语言处理 PyTorch