乐趣区

关于前端:你可能需要的多文档页面交互方案

前言

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

在日常工作中,面对不同的需要场景,你可能会遇到须要进行多文档页面间交互的实现,例如在 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 ...}
})

演示成果如下:

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

storage 事件

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

  • 如果以后页面的 localStorage 值被批改,只会触发其余页面的 storage 事件,不会触发本页面的 storage 事件
  • window.onstorage 事件只对 localStorage 的批改无效,sessionStorage 的批改不能触发
  • localStorage 的值必须发生变化,如果设置成雷同的值则不会触发

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 实例。

成果演示

外围代码

>>>>>>>>>>>>>>>>>> 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 对象。

成果演示

外围代码

// A.html
<body>
    <h1>A 页面 </h1>
    <a href="/b" target="_blank"> 关上 B 页面 </a>
    <br />
    <button onclick="send()"> 发送音讯给 B 页面 </button>
    <h3>
      收到 B 页面的音讯:<small id="small"></small>
    </h3>

    <script>
      const bc = new BroadcastChannel('test_broadcast_hannel')

      // 向 B 页面发送音讯
      function send() {console.log('A 页面已发送音讯')
        bc.postMessage('你好呀!')
      }

      // 监听来着 A 页面的音讯
      bc.onmessage = ({data}) => {document.querySelector('#small').innerHTML = event.data
      }
    </script>
  </body>

// B.html
<body>
    <h1>B 页面 </h1>
    <button onclick="send()"> 发送音讯给 B 页面 </button>
    <h3>
      收到 A 页面的音讯:<small id="small"></small>
    </h3>

    <script>
      const bc = new BroadcastChannel('test_broadcast_hannel')

      // 向 A 页面发送音讯
      function send() {console.log('B 页面已发送音讯')
        bc.postMessage('还不错呦~')
      }

      // 监听来着 A 页面的音讯
      bc.onmessage = ({data}) => {document.querySelector('#small').innerHTML = event.data
      }
    </script>
  </body>

HTTP 长轮询

HTTP 长轮询 置信大家应该十分的相熟了,兴许你 过来 / 当初 正在做的 扫码登录 就是用的长轮询。

因为 HTTP1.1 协定并不反对服务端被动向客户端发送数据音讯,那么基于这种 申请 - 响应 模型,如果咱们须要服务端的音讯数据,就必须先向服务端发送对应的查问申请,因而只有每隔一段时间向服务器发动查问申请,在依据响应后果决定是持续下一步操作,还是持续发动查问。

外围很像 Web Storage 计划,只不过两头者不同:

  • Web Storage 的两头者是 浏览器,一个页面 存 / 改 数据,其余页面读取再执行后续操作
  • 长轮询 的两头者是 服务器,一个页面提交申请把指标数据提交到服务端,其余页面通过轮询的形式去读取数据再决定后续操作

因为这种计划比拟常见,这里就不再额定演示。

非同源的多文档交互

window.postMessage

通常对于两个不同页面的脚本,只有当执行它们的页面具备:

  • 雷同协定(通常为 https
  • 雷同端口号443https 的默认值)
  • 雷同主机 (两个页面的 Document.domain 设置为雷同的值)

时,这两个脚本能力互相通信。

window.postMessage() 办法能够 平安 地实现 跨源通信 ,这个办法提供了一种 受控机制 来躲避此限度,实质就是本人注册监听事件,本人派发事件。

window.postMessage() 容许 一个窗口 能够取得对 另一个窗口 的援用(比方 targetWindow = window.opener)的形式,而后在窗口上调用 targetWindow.postMessage() 办法散发一个 MessageEvent 音讯。

语法如下,具体解释可见 MDN

targetWindow.postMessage(message, targetOrigin, [transfer]);

window.open() 和 window.postMessage()

外围代码

// A.html
<body>
    <h1>A 页面 </h1>
    <button onclick="openAction()"> 关上 B 页面 </button>
    <button onclick="send()"> 发送音讯给 B 页面 </button>

    <script>
      let targetWin = null
      const targetOrigin = 'http://127.0.0.1:8082/'

      // 关上 B 页面
      function openAction() {targetWin = window.open(targetOrigin)
      }

      // 向 B 页面发送音讯
      function send() {if (!targetWin) return
        console.log('A 页面已发送音讯')
        targetWin.postMessage('你好呀!', targetOrigin)
      }

      // 监听来着 B 页面的音讯
      window.onmessage = (event) => {console.log('收到 B 页面的音讯:', event.data)
      }
    </script>
  </body>
  
 // B.html
 <body>
    <h1>B 页面 </h1>

    <button onclick="send()"> 发送音讯给 A 页面 </button>

    <script>
      const targetWin = window.opener
      const targetOrigin = 'http://127.0.0.1:8081/'
      
      // 监听来着 A 页面的音讯
      window.onmessage = (event) => {console.log("收到 A 页面的音讯:", event.data)
      }

      // 向 B 页面发送音讯
      function send() {if (!targetWin) return
        console.log('B 页面已发送音讯')
        targetWin.postMessage('还不错哟~', targetOrigin)
      }

    </script>
  </body>

iframe 和 window.postMessage()

眼前的限度

<iframe> 加载的形式有些限度,只能父页面向子页面发送音讯,子页面不能向父页面发送音讯,实质起因是在父页面中咱们能够通过 document.querySelector('#iframe').contentWindow 的形式获取到子页面 window 对象的援用,然而子页面却不能像 window.open() 的形式通过 window.opener 的形式获取父页面 window 对象的援用。

本来想通过 postMessage 将父页面的 window 的代理对象传递过来,但抛出如下异样:

次要起因是 postMessage 是不容许将 Window、Element 等对象进行复制传递,即便能够传递到了子页面中也是无奈应用的,因为能传递过来阐明你用了深克隆,但深克隆之后曾经和原来的父页面无关了。

window.parent 属性

以上思考是在没齐全没有想到 window.parent 时的方向,也感激评论区掘友的揭示,齐全能够应用这个 window.parent 化繁为简来获取父页面的 window 对象援用:

  • 如果一个窗口没有父窗口,则它的 parent 属性为 本身的援用
  • 如果以后窗口是一个 <iframe><object><frame> 的加载的内容,那么它的父窗口就是 <iframe><object><frame> 所在的那个窗口

外围代码

 // A.html
 <body>
    <h1>A 页面 </h1>
    <button onclick="send()"> 发送音讯给 B 页面 </button>
    <h3>
      收到 B 页面的音讯:<small id="small"></small>
    </h3>
    <iframe
      id="subwin"
      height="200"
      src="http://127.0.0.1:8082/"
      onload="load()"
    ></iframe>

    <script>
      let targetWin = null
      const targetOrigin = 'http://127.0.0.1:8082/'

      // B 页面加载实现
      function load() {
        // 获取子页面 window 对象的援用
        let subwin = document.querySelector('#subwin')
        targetWin = subwin.contentWindow
      }

      // 向 B 页面发送音讯
      function send() {if (!targetWin) return
        console.log('A 页面已发送音讯')
        targetWin.postMessage('你好呀!', targetOrigin)
      }

      // 监听来着 A 页面的音讯
      window.onmessage = ({data}) => {document.querySelector('#small').innerHTML = event.data
      }
    </script>
  </body>
  
 // B.html
<body>
    <h1>B 页面 </h1>
    <button onclick="send()"> 发送音讯给 B 页面 </button>
    <h3>
      收到 A 页面的音讯:<small id="small"></small>
    </h3>

    <script>
      const targetWin = window.parent
      const targetOrigin = 'http://127.0.0.1:8081/'

      // 向 A 页面发送音讯
      function send() {if (targetWin === window) return
        console.log('B 页面已发送音讯')
        targetWin.postMessage('还不错呦~', targetOrigin)
      }
      
      // 监听来着 A 页面的音讯
      window.onmessage = ({data}) => {document.querySelector('#small').innerHTML = event.data
      }
    </script>
  </body>

websocket

晚期 HTTP(超文本传输协定)次要目标就是传输超文本,因为过后网络上绝大多数的资源都是纯文本,许多通信协议也都应用纯文本,因而 HTTP 在设计上不可避免地受到了时代的限度,即 HTTP 没有齐全的利用 TCP 协定的 全双工通信 能力,这就是为什么 HTTP 是 半双工通信 的起因。

因为 HTTP 存在晚期设计上的限度,但随着互联网的一直倒退,越来越须要这种 全双工通信 的性能,因而须要一种新的基于 TCP 实现  全双工通信 的协定,而这个协定就是 WebSocket

具体应用这里不再独自介绍,如果你想理解更多,能够查看往期文章《HTTP,WebSocket 和 聊天室》.

不过这里还是简略介绍一下,实现的外围就是 不同的页面 同一个 websocket 服务 建设连贯,多个页面间的通信在 websocket 服务 中进行转发,即页面发送音讯到 websocket 服务 依据标识进行 单博 播送 的模式下发到其余指定页面

不同文档加载形式

后面提到的不同的文档加载形式如下:

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

下面曾经列举了最常见的计划,跨源计划最全能,这是毋庸置疑的,对于不同文档加载形式也在某些层面上与上述计划挂钩,上面次要讲一些不同文档加载形式的异同点。

window.location.href 和 <a href="x" target="_self">

最常的用法就是通过 window.location.href = x 将以后的文档的 url 进行替换,但其实它是有一些规定的:

  • 无效 url
  • hash 模式
  • 其余模式

x = 无效 url 时,以后文档的内容会被新的 url 指向的内容替换:

x = hash 模式 时,会将以后文档的 hash 局部间接替换为 x 指向的内容:

x = 其余模式 时,会将 x 的内容作为以后文档 url 子门路 进行替换:

以上三种模式与 <a href="x" target="_self"> 的体现统一。

window.open 和 <a href="x" target="_blank">

window.open(x)<a href="x" target="_blank"> 的形式都会新关上一个标签页,而后去加载 x 指向的资源,当然其中 x 的加载模式同上。

window.open() 的毛病

浏览器出于平安的思考,会拦挡掉 非用户操作 关上的新页面,也就是指如果咱们想在某个异步操作之后主动通过 window.open(x) 的模式关上新页面就会失败,例如:

fetch(url,option).then(res=>{window.open('http://www.test.com') // 关上失败
})

setTimeout(() => {window.open('http://www.test.com') // 关上失败
}, 1000)

解决办法

  • 将 window.open() 办法放在用户事件中

    • 如在异步操作完结后弹窗提供按钮,让用户手动点击
  • 间接提供 <a> 标签的模式进行跳转

    • 不要妄想通过主动创立 a 标签,而后再通过 a.click() 的形式实现跳转,你能想到浏览器平安限度中也能思考到
  • window.open() 配合 window.location.href

    • 如下的 clickHandle 实质还是须要用在 用户事件 中,间接主动执行该函数还是会生效,因为毕竟不是由用户动作产生的后果

      const clickHandle = () => {const newWin = window.open('about:blank')
        ajax().then(res => {newWin.location.href = 'http://www.baidu.com'}).catch(() => {newWin.close()
        })
      }

    <iframe>

    <iframe> 可能将另一个 HTML 页面嵌入到 以后页面 中,每个嵌入的 浏览上下文 都有本人的 会话历史记录 DOM 树

蕴含嵌入内容的浏览上下文称为 父级浏览上下文 顶级浏览上下文(没有父级)通常是由 Window 对象示意的浏览器窗口。

多余的货色在这也不开展了,下面咱们应用过的 contentWindow 属性只在 <iframe> 元素上存在,它返回的是以后 iframe 元素HTMLIFrameElement)所加载文档的 Window 对象的援用。

contentWindow 属性是 可读属性,它所指向的 Window 对象能够去拜访这个 iframe 的文档和它外部的 DOM

最初

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

以上就是本文的全部内容了,纵观文中本来各个看似零散的知识点,在一个需要场景下都被分割起来了,所以有些货色的确学了不肯定立即就会用到,然而真的到须要用到的时候你会发现很多知识点其实都是分割在一起的,并且它们的体现或原理何其相似。

心愿本文对你有所帮忙!!!

退出移动版