我们先看下以下场景:
开启两个本地服务器,页面A为localhost:9800,其中嵌套了iframeB localhost:9000,页面A想使用页面B的数据,例如调用它的方法,会报以下错误
如图所示, Protocols,domains,and ports must match. 译为:协议、主机和端口号必须符合,否则,就是跨域。
下面我们来具体谈谈。
我们都知道,浏览器有个同源策略,也就是这个策略,限制了两个源中的资源相互交互。
Two pages have the same origin if the protocol, port (if one is specified), and host are the same for both pages.
译为:如果两个页面有相同的协议、端口号和主机,那么这两个页面就属于同一个源。
也就是说,只有同一个源才可以进行资源共享。
我们来举几个例子,如果想和 http://www.test.com/index.html 进行通信:
URL | Result | Reason |
---|---|---|
http://www.test.com/page/othe... | 允许 | |
http://www.test.com/index.js | 允许 | |
http://a.test.com/index.html | 不允许 | 子域不同 |
http://www.other.com/index.html | 不允许 | 主域不同 |
https://www.test.com/index.html | 不允许 | 协议不同 |
http://www.test.com:3000/inde... | 不允许 | 端口不同 |
我们可以看到,协议、端口、主机缺一不可,必须完全匹配,上文就是由于端口号不同而报错。
那为什么要有同源策略的限制呢?原因也很简单,就是为了保证用户信息的安全,防止恶意的网站窃取数据。
试想一下,如果我们可以随意访问一个网站的cookie,那么岂不是随意窃取别别人登陆的cookie?如果没有同源策略,那互联网就太危险了。
同源策略的限制范围有以下几种:
Cookie、LocalStorage 和 IndexDB 无法读取。
DOM 无法获得。
AJAX 请求不能发送。
而跨域访问,也无非两种情况:一是请求不同源下的接口(如上第3种);二是请求不同源的页面的资源(如上1、2,通常是页面中嵌套不同源的iframe要进行通信)。本文主要讨论第二种情况下造成的跨域,方法很多,主要介绍以下几种: document.domain
、 location.hash
、 window.name
、 postMessage
。
适用于:主域相同子域不同的页面,例如上例中的第三种
方法:只需将这两个页面的 document.domain
设置为相同的父域名,即可实现不同子域名之间的跨域通信。
<!-- http://www.test.com/index.html --> <button>发送消息</button> <div></div> <iframe name="ifr" src="http://a.test.com/index.html" style="width: 100%;"></iframe> <script type="text/javascript"> document.domain = 'test.com'; function sendMsg(msg) { document.querySelector('div').innerHTML = msg; } document.querySelector('button').onclick = function (){ window.frames['ifr'].sendMsg('Hello son'); } </script>
<!-- http://a.test.com/index.html --> <div></div> <script type="text/javascript"> document.domain = 'myapp.com'; function sendMsg(msg) { document.querySelector('div').innerHTML = msg; parent.sendMsg('Hello father'); } </script>
一般来说,URL的任何改变都重新会加载一个新的网页,除了 hash
的变化, hash
的任何改变都不会导致页面刷新。
在跨域解决方案中, hash
也非常有用,来自不同源的页面可以相互设置对方的 URL,包括 hash
值,但不能获取对方的 hash
值。文档之间可以通过 hash
来相互通信。
流程(如上图):
主页面A
中嵌入 iframeB
,两个来自不同域
在 主页面A
中,将想要传递给B的字段,作为hash,将它与B的url连接起来,然后将B的src设置为连接后的url
在 iframeB
中,就可以通过获取自己url的hash值,从而得到主页面传递的值,但在iframeB中,需设置一个 定时器 监听hash值的变化
除了设置定时器,还可以通过监听 window.onhashchange
事件
例子:启动两个本地服务器,主页面 localhost:9800
,子页面 localhost:9000
,主页面向子页面发消息
<!-- 主页面 localhost:9800 --> <script type="text/javascript"> function sendMsg(msg) { var data = encodeURI(JSON.stringify({msg: msg})); var target = "http://localhost:9000"; var src = target + "#" + data; document.getElementById("ifr").src = src; } function onClick() { sendMsg("来自'localhost: 9800': 你好, 这是你要的数据"); } </script> <body> <iframe id="ifr" src="http://localhost:9000" style="width: 100%"></iframe> <button onclick="onClick()">发送消息</button> </body>
<!-- 子页面 localhost:9000 --> <script type="text/javascript"> var oldHash = ""; function checkHash() { var newHash = location.hash.length > 1 ? location.hash.substring(1) : ''; if (newHash != oldHash) { oldHash = newHash; var msg = JSON.parse(decodeURI(newHash)).msg; document.getElementById("container").innerText = msg; clearInterval(timer); } } //设置定时器监听 hash 的变化 var timer = setInterval(checkHash, 1000); </script> <body> <div id="container"></div> </body>
结果如下:点击button后,子页面将收到主页面的消息
注意:使用 hash
时最好对其进行编码、解码,否则在 Firefox
中会报错。因为 Firefox
会自动将 hash
值进行编码,如果不进行解码就无法 JSON.parse()
.
两个页面不同源的情况下,IE、Chrome不允许修改 parent.location.hash
的值(Firefox可以),所以如果主页面想从子页面获取消息,只能借助一个代理iframe设置。
流程如下(两种):
子页面
创建隐藏的代理iframe,与主页面同源,并将消息作为 hash
,设置到iframe的src中
代理页面
将主页面的 hash
值设置为自身的 hash
主页面
使用定时器监听 hash
的变化
例子如下
<!-- 主页面 localhost:9800 --> <script type="text/javascript"> var oldHash = ''; function checkHash() { var newHash = location.hash.length > 1 ? location.hash.substring(1) : ''; if (newHash != oldHash) { oldHash = newHash; var msg = JSON.parse(decodeURI(newHash)).msg; document.querySelector("div").innerText = msg; clearInterval(timer); } } //设置定时器监听 hash 的变化 var timer = setInterval(checkHash, 1000); </script> <body> <iframe id="ifr" name="ifr" src="http://localhost:9000" style="width: 100%;"></iframe> <div></div> </body>
<!-- 子页面 localhost:9000 --> <script type="text/javascript"> function sendMsg(msg) { try { parent.location.hash = encodeURI(JSON.stringify({msg: msg})); } catch(e) { //由于IE, Chrome的安全机制 无法修改 parent.location.hash //因此创建代理页面, 与主页面同源, 然后修改代理页面的hash var ifrProxy = document.createElement("frame"); ifrProxy.style.display = "none"; ifrProxy.src = "http://localhost:9800/public/proxy.html" + '#' + encodeURI(JSON.stringify({msg: msg})); document.body.appendChild(ifrProxy); } } function onClick() { sendMsg("Hello Father"); } </script> <body> <button onclick="onClick()">发送消息</button> </body>
<!-- 代理页面 localhost:9800/public/proxy.html --> <script type="text/javascript"> parent.parent.location.hash = window.location.hash.substring(1); </script>
结果如图:
这种方法的劣处就是将消息暴露在url中,因此也可以采用下文将讲述利用代理iframe跨域的方法,这边就不赘述了。
由于现在许多网站的 hash
已经被用于其他用途,此时想用 hash
跨域就会比较复杂了。这种情况下,我们可以使用一个同域的代理页面来完成
页面A想向页面B发送数据,流程如下:
页面A
创建一个隐藏的 代理iframeC
,这个iframe与 页面B
同域
页面A
中,将要发送的数据,作为hash,与 页面C
的 url
连接起来
在 代理iframeC
中,它与B同域,可以直接调用 页面B
中的方法,这样便可以将hash值传递给B了
例子如下
<!-- 页面A localhost:9800 --> <body> <iframe name="ifr" src="http://localhost:9000" style="width: 100%;"></iframe> <button onclick="onClick()">发送消息</button> <div></div> <script type="text/javascript"> function sendMsg(msg) { //创建隐藏的iframe, 与页面B同源 var frame = document.createElement('frame'); var target = 'http://localhost:9000/static/page/proxy.html'; var data = {frameName: 'ifr', msg: msg}; frame.src = target + "#" + encodeURI(JSON.stringify(data)); frame.style.display = 'none'; document.body.appendChild(frame); } function onClick() { sendMsg('你好, 我是localhost: 9800'); } </script>
<!-- 代理页面C localhost:9000/static/page/proxy.html --> <script type="text/javascript"> var hash = location.hash.length > 1 ? location.hash.substring(1) : ''; if (hash) { var data = JSON.parse(decodeURI(hash)); //处理数据 parent.frames[data.frameName].receiveData(data.msg); } </script>
<!-- 页面B localhost:9000 --> <script type="text/javascript"> function receiveData(msg) { document.querySelector('div').innerHTML = "接收到数据:"+ msg; } </script> <body> <div></div> </body>
结果如下:
缺点:
数据直接暴露在url中,安全性较低
url大小是有限制的,它支持传递的数据量较小
加载任何页面 window.name 的值始终保持不变
当页面A想从页面B中获取数据时:
页面A
,创建一个 隐藏的iframeC
,将C的 src
指向 页面B
页面C
加载完成后,把响应的数据附加到 window.name
上
C 取到数据后,将 src
设为任何一个与 A同源的页面
,这时 A 就能获取到 B 的 name
属性值
A 取到数据后,随时可以删掉 C
例子,启动两个本地服务器,页面A localhost:9800
,页面B localhost:9000
,页面A想从页面B中获取数据
<!-- 页面A localhost:9800 --> <script type="text/javascript"> function sendMsg(msg) { var state = 0, data; //1. 创建隐藏的iframe, 与页面B同源 var frame = document.createElement("frame"); frame.src = "http://localhost:9000"; frame.style.display = "none"; frame.onload = function () { if (state === 1) { //3. 此时iframe与页面A同源, 页面A可以获取到数据 data = frame.contentWindow.name; document.querySelector('.res').innerHTML = "响应数据:" + data; //4. 删除iframe frame.contentWindow.document.write(''); frame.contentWindow.close(); document.body.removeChild(frame); } else { //2. iframe加载完成后, 响应的数据已附加到此iframe上, 再将其导航至与页面A同源 state = 1; frame.src = "http://localhost:9800/test.html"; } }; document.body.appendChild(frame); } function onClick() { var val = 'hi, 我是页面A'; sendMsg(val); document.querySelector('.val').innerHTML = "请求数据:" + val; } </script> <body> <button onclick="onClick()">发送消息</button> <div class="val"></div> <div class="res"></div> </body>
<!-- 页面B localhost:9000 --> <script type="text/javascript"> window.name = 'hi, 我是页面B'; </script>
结果如图
另外,两个页面还可相互通信
页面A通过 hash
,将数据传递给页面B,页面B仍通过 window.name
向页面A传递数据
场景如下:页面B存储了一些人的信息,页面B通过页面A的输入,获取不同人的信息
<!-- 页面A localhost:9800 --> <script type="text/javascript"> function sendMsg(msg) { var state = 0, data; //1. 创建隐藏的iframe, 与页面B同源 var frame = document.createElement("frame"); frame.style.display = "none"; //将获取的数据通过hash传递给页面B frame.src = "http://localhost:9000" + "#" + encodeURI(JSON.stringify({data: msg})); frame.onload = function () { if (state === 1) { //3. 此时iframe与页面A同源, 页面A可以获取到数据 data = JSON.parse(frame.contentWindow.name); var html = ''; for (var key in data) { html += key + ": " + data[key] + " "; } document.querySelector('.res').innerHTML = html; //4. 删除iframe frame.contentWindow.document.write(''); frame.contentWindow.close(); document.body.removeChild(frame); } else { //2. iframe加载完成后, 响应的数据已附加到此iframe上, 再将其导航至与页面A同源, state = 1; // !!!注意 '/test.html'不可缺少,否则在firefox不会触发frame.onload, 在chrome虽会触发但会报错 frame.src = "http://localhost:9800/test.html"; } }; document.body.appendChild(frame); } function onClick() { var val = document.querySelector('.text').value; sendMsg(val); document.querySelector('.val').innerHTML = "id为 " + val + " 的信息如下:"; } </script> <body> <input class="text" type="text" /> <button onclick="onClick()">发送消息</button> <div class="val"></div> <div class="res"></div> </body>
<!-- 页面B localhost:9000 --> <script type="text/javascript"> var information = { '1': { name: '狐狸', age: 2 }, '2': { name: '馒头', age: 1 }, '3': { name: '端午', age: 3 } }; var hash = location.hash.length > 1 ? location.hash.substring(1) : ''; if (hash) { //获取hash值 并进行转换 var obj = JSON.parse(decodeURI(hash)); var data = obj.data; //传递的数据为object, 需进行JSON转换 window.name = JSON.stringify(information[data]); } </script>
输入想获得的数据的id值,即可得到相应的信息
总结:
优点:容量很大,可以放置很长的字符串(2M左右,比url大得多)
缺点:必须要监听window.name属性的变化
HTML5规范中的新方法 window.postMessage()
可以用于安全跨域通信。当该方法调用时,将 分发 一个消息事件。对应的窗口通过 事件监听 来获取消息。
语法 :otherWindow.postMessage(message, targetOrigin)
otherWindow
代表其他窗口的引用,例如iframe的 contentWindow
属性,通过 window.open
返回的窗体,通过 window.frames[]
返回的ifame对象。
message
表示发送给其他窗口的数据
targetOrigin
指定哪些来源的窗口可以接收到消息事件,其值可以是字符串"*"(表示无限制)或"/"(表示与父窗口同源)或一个URI。发送消息时,只有目标窗口的 协议
、 主机
、 端口
这三项都匹配targetOrigin提供的值,消息才会发送。
接收消息的窗口可以通过 监听message事件
,去接收消息
window.addEventListener("message", receiveMessage, false);
receiveMessage
为接收到消息后的操作
message事件的事件对象 event
,提供三个属性:
event.source
:发送消息的窗体(可以用来引用父窗口)
event.origin
:消息发向的网址(可以过滤不是发给本窗口的消息)
event.data
:消息内容
例子:启动两个本地服务器,页面A localhost:9800
,页面B localhost:9000
,页面A根据页面B发来的数据改变颜色
<!-- 页面A localhost:9800 --> <body> <iframe name="ifr" src="http://localhost:9000" style="width: 100%; display: none"></iframe> <div style="width: 200px; height: 200px; background-color: #ccc"></div> <button onclick="onClick()">改变颜色</button> <script type="text/javascript"> function onClick() { var otherFrame = window.frames["ifr"]; otherFrame.postMessage("getColor", "*"); } function handleReceive(event){ //判断来源 if(event.origin != "http://localhost:9000") return; //处理页面B发送的数据 var data = JSON.parse(event.data); document.querySelector('div').style.backgroundColor = data.color; } window.addEventListener("message", handleReceive, false); </script>
<!-- 页面B localhost:9000 --> <script type="text/javascript"> function handleReceive(event){ //判断来源 if (event.origin != "http://localhost:9800") { return; } if(event.data == "getColor"){ //给页面A发送数据 var targetWindow = parent.window; targetWindow.postMessage(JSON.stringify({color:'blue'}), "http://localhost:9800"); } } window.addEventListener("message", handleReceive, false); </script>
结果如下:点击按钮后,页面的div由灰变蓝
关于跨域的就先聊到这啦~