跨窗口通信
同源策略限制了窗口 window 和 frame 的互相访问。
在非同源的情况下,如果一个用户有两个打开的页面:一个来自 john-smith.com,另一个是 gmail.com,那么 john-smith.com 的脚本不可以访问 gmail.com 中的内容,以免用户信息被盗窃。
同源
如果两个 URL 具有相同的协议,域名和端口,则称它们是“同源”的。
以下的几个 URL 都是同源的:
http://site.comhttp://site.com/http://site.com/my/page.html
但是下面这几个不是:
http://www.site.com(另一个域:www.影响)http://site.org(另一个域:.org影响)https://site.com(另一个协议:https)http://site.com:8080(另一个端口:8080)
同源策略规定:
- 如果我们有对另外一个窗口(
window.open创建的弹窗,或者窗口中的iframe)的引用,并且该窗口是同源的,那么我们具有对该窗口的全部访问权限 - 如果不是同源的,那么我们无法访问窗口中的内容。唯一的例外是虽然我们无法读取 location,但是可以修改它。
iframe
使用<iframe>标签能够在当前页面单独嵌入一个窗口,它具有自己的document和window。
iframe.contentWindow获取<iframe>中的 window。iframe.contentDocument获取<iframe>中的 document,是iframe.contentWindow.document的简写形式。
当我们访问嵌入的窗口中的东西时,浏览器会检查 iframe 是否具有同样的源。如果不是,则会拒绝读写(可以改 location)
html1<body> 2 <iframe id="iframe" src="https://www.bilibili.com" frameborder="0"></iframe> 3 <script defer> 4 iframe.onload = () => { 5 let iframeWindow = iframe.contentWindow 6 console.log(iframeWindow) // 可以拿到 iframe 的window 7 let doc = iframe.contentDocument 8 console.log(doc) // null 拿不到 document 9 try { 10 let href = iframe.contentWindow.location.href 11 } catch (error) { 12 console.log(error) // Error 13 } 14 15 iframe.contentWindow.location = 'https://www.youtube.com/' // 可以修改 16 iframe.onload = null // 记得清除 onload,否则会一直执行 onload 事件,并修改 location 17 } 18 </script> 19</body>
上述代码只能做以下事情:
- 通过
iframe.contentWindow读取iframe的window - 改
location
如果是同源的,则可以做任何事情:
html1<iframe id="iframe" src="./iframe.html" frameborder="0"></iframe> 2<script defer> 3 iframe.onload = () => { 4 let doc = iframe.contentDocument // #document 读到了 5 } 6</script>
iframe.onload事件与iframe.contentWindow.onload基本相同,都是在嵌入的窗口所有资源加载完成后触发。区别在于:
iframe.onload访问的是当前窗口下的iframe,iframe.contentWindow.onload访问的是iframe的window对象- 如果不同源的
iframe,我们就没办法使用iframe.contentWindow.onload了。
相同二级域跨窗口
根据同源策略,域名不同则属于不同的源,会有同源限制。
但是,如果窗口的二级域名是相同的,比如 john.site.com,peter.site.com 和 site.com(它们共同的二级域是 site.com),我们可以通过设置让浏览器把它们当做同源来对待,这样就可以做到无限制的跨窗口通信了。
方法是这样的,让每个窗口都执行以下代码
javascript1document.domain = 'site.com'
这仅适用于具有相同二级域的页面。
同源下访问 doc 的陷阱
当 iframe 来自于同一个源时,我们可能会访问其document,但是这里有一个陷阱:
在创建 iframe 时,iframe 会立即有一个document,但是该文档不同于加载到其中的文档!
因此,如果我们要对文档进行操作,可能会出问题。
下面的代码能够反映出这个陷阱:
html1<iframe id="iframe" src="./iframe.html" frameborder="0"></iframe> 2<script defer> 3 let oldDoc = iframe.contentDocument 4 iframe.onload = () => { 5 let newDoc = iframe.contentDocument 6 // 加载后的文档与初始的文档不同!!! 7 console.log(oldDoc === newDoc) // false 8 } 9</script>
我们不应该对尚未加载完成的 iframe 进行处理,那是错误的。
正确的文档在iframe.onload时就已经就位了。
这里有个小缺点,iframe.onload 只有在整个 iframe 和它所有资源都加载完成后才触发,如果我们希望更早一点获取到新的文档,则可以使用setInterval来检查
html1<iframe src="/" id="iframe"></iframe> 2 3<script> 4 let oldDoc = iframe.contentDocument 5 6 // 每 100ms 检查一次文档是否为新文档 7 let timer = setInterval(() => { 8 let newDoc = iframe.contentDocument 9 if (newDoc == oldDoc) return 10 11 alert('New document is here!') 12 13 clearInterval(timer) // 取消 setInterval,不再需要它做任何事儿 14 }, 100) 15</script>
window.frames 集合
获取<iframe>的 window对象的另外一个方法是从命名集合window.frames中获取
- 通过索引获取:
window.frames[0]—— 文档中第一个 iframe 的 window 对象 - 通过名称获取:
window.frames.iframeName—— 获取name='iframeName'的 iframe 的 window 对象。
html1<iframe src="/" style="height:80px" name="win" id="iframe"></iframe> 2 3<script> 4 alert(iframe.contentWindow == frames[0]) // true 5 alert(iframe.contentWindow == frames.win) // true 6</script>
一个 iframe 内可能嵌套了其他的 iframe。相应的 window 对象会形成一个层次结构(hierarchy)。
可以通过以下方式获取:
window.frames—— “子”窗口的集合(用于嵌套的 iframe)。window.parent—— 对“父”(外部)窗口的引用。window.top—— 对最顶级父窗口的引用。
例如:
javascript1window.frames[0].parent === window // true
我们可以使用 top 属性来检查当前的文档是否是在 iframe 内打开的:
javascript1if (window == top) { 2 // 当前 window == window.top? 3 alert('The script is in the topmost window, not in a frame') 4} else { 5 alert('The script runs in a frame!') 6}
iframe 特性 sandbox
sandbox特性允许在<iframe>中禁止某些特定的行为。它通过对 iframe 应用一些限制来实现“沙盒化”。
假设 iframe 的标签是这样写的
html1<iframe sandbox src="..."></iframe>
那么就会对这个 iframe 应用最严格的限制,但是我们可以用一个空格分割的列表,来列出想要移除的限制。
例如:
html1<iframe sandbox="allow-forms allow-popups"></iframe>
上面的列表表示也就是允许表单提交和允许在 iframe 中使用 window.open 打开弹窗。
以下是限制列表:
-
allow-same-origin
默认情况下,
"sandbox"会为 iframe 强制实施“不同来源”的策略。即使其src指向的是同一个网站也是如此。这样浏览器就会对 iframe 实行所有非同源下的限制。这个选项会移除这个限制 -
allow-top-navigation
允许
iframe更改parent.location。 -
allow-forms
允许在
iframe中提交表单。 -
allow-scripts
允许在
iframe中运行脚本。 -
allow-popups
允许在
iframe中使用window.open打开弹窗。
更多信息可以查看 MDN-iframe
请注意:
"sandbox" 特性的目的仅是 添加更多 限制。它无法移除这些限制。尤其是,如果 iframe 来自其他源,则无法放宽同源策略。
跨窗口通信
postMessage允许窗口之间互相通信,无论来自什么源。
因此,它允许来自于 john-smith.com 的窗口与来自于 gmail.com 的窗口进行通信,并交换信息,但前提是它们双方必须均同意并调用相应的 JavaScript 函数。这可以保护用户的安全。
postMessage
如果当前窗口想要给接收窗口发送信息,可以调用接收窗口的postMessage方法。举个 🌰,如果我们想要把消息发送给win,我们需要调用win.postMessage(data,targetOrigin)
参数:
-
data
要发送的数据。可以是任何对象,数据会被通过使用“结构化序列化算法(structured serialization algorithm)”进行克隆。IE 浏览器只支持字符串,因此我们需要对复杂的对象调用
JSON.stringify方法进行处理,以支持该浏览器。
-
targetOrigin
指定目标窗口的源,以便只有来自给定的源的窗口才能获得该消息。
指定 targetOrigin 可以确保窗口仅在当前仍处于正确的网站时接收数据。
例如,这里的 win 仅在它拥有来自 http://example.com 这个源的文档时,才会接收消息:
html1<iframe src="http://example.com" name="example"> 2 <script> 3 let win = window.frames.example 4 5 win.postMessage('message', 'http://example.com') 6 </script></iframe 7>
如果我们不希望做这个检查,可以将 targetOrigin 设置为 *。
html1<iframe src="http://example.com" name="example"> 2 <script> 3 let win = window.frames.example 4 5 win.postMessage('message', '*') 6 </script></iframe 7>
使用 window.open打开新窗口并发送一个消息
主窗口
html1<button id="btn">open iframe</button> 2<script defer> 3 btn.onclick = () => { 4 let win = window.open('./iframe.html') 5 win.onload = () => { 6 win.postMessage('123', '*') 7 } 8 } 9</script>
被打开窗口
html1<script> 2 window.addEventListener('message', (e) => { 3 alert(`message ${e.data} from ${e.origin}`) 4 }) 5</script>
message 事件
目标窗口监听message事件。当postMessage被调用时触发这个事件(并且 targetOrigin 检查成功)
event 对象具有特殊属性:
-
data
从
postMessage传递来的数据 -
origin
发送方的源
-
source
对发送方窗口的引用。我们可以立即
source.postMessage(...)回去
应该使用addEventListener来监听 message事件
javascript1window.addEventListener('message', function (event) { 2 if (event.origin != 'http://javascript.info') { 3 // 来自未知的源的内容,我们忽略它 4 return 5 } 6 7 alert('received: ' + event.data) 8 9 // 可以使用 event.source.postMessage(...) 向回发送消息 10})
总结
要调用另一个窗口的方法或者访问另一个窗口的内容,我们应该首先拥有对其的引用。
对于弹窗,我们有两个引用:
window.open打开新的窗口,返回一个新窗口的引用window.opener弹窗中对打开此弹窗的窗口的引用
对于 iframe,我们可以使用以下方式访问父/子窗口
window.frames嵌套的子窗口window对象的集合,能够访问iframe的windowwindow.parent,window.top对父窗口和顶级窗口的引用iframe.contentWindow是<iframe>标签内的window对象
如果窗口是不同源的,那么只能做以下事情:
- 更改 location
postMessage发送消息
例外情况:
- 二级域名相同的窗口可以通过均设置
domain属性的方式,使它们能被浏览器认为是同源的 - 如果 iframe 具有 sandbox 特性,那么默认非同源,除非指定
allow-same-origin
postMessage允许任何源的窗口之间互相通信:
- 发送方调用
targetWin.postMessage - 如果
targetOrigin不是'*',那么浏览器会检查窗口targetWin是否具有源targetOrigin。 - 如果它具有,
targetWin会触发具有特殊的属性的message事件:origin—— 发送方窗口的源source—— 对发送方窗口的引用data—— 数据,可以是任何对象。但是 IE 浏览器只支持字符串,因此我们需要对复杂的对象调用JSON.stringify方法进行处理,以支持该浏览器。
应该使用addEventListener在目标窗口监听message事件