核心代码
// 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)
- 相同端口号(443 为 https 的默认值)
- 相同主机 (两个页面的
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。
最后
以上就是本文的全部内容了,纵观文中原本各个看似零散的知识点,在一个需求场景下都被联系起来了,所以有些东西确实学了不一定立刻就会用到,但是真的到需要用到的时候你会发现很多知识点其实都是联系在一起的,并且它们的表现或原理何其相似。
希望本文对你有所帮助!!!