记一次与iframe之间的抗争
iframe这个标签之前了解过这个东西,知道它可以引入外来的网页,但是实际开发中没有用到过。这一次有一个需求是说准备要在网页中嵌套另外一个网站,用iframe这个标签,让我测试一下这个可不可以在自己的网页中对引入进来的iframe框架进行操作,操作dom和css的一些东西。让我做出一个小案例看看可不可以,我信誓旦旦保证说可以的,我试过!!!
就这样交代给我之后信心满满的就开始了我的验证。
什么是同源?
同域名、 同端口、 同协议
网上是有好多这个的解释的,给出一张图片。 看下面这张图片。 引用来自 浏览器的同源策略
我直接新建了一个文件夹,在里面写了两个html页面的文件,举例说明是a.html和b.html,然后让其中的一个a.html文件中用iframe标签的src去引入b.html文件,在里面去互相操作他们的css样式和DOM元素。
a.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> html,body{ height: 100%; } body{ background: pink; } #iframe1{ width: 400px; height: 400px; margin: auto; background: blue; } </style> </head> <body> 这里是父文档 <input type="button" id="btn1" value="改变子文档的颜色"> <input type="button" id="btn2" value="删除span1"> <input type="button" id="btn3" value="改变span2的颜色"> <input type="button" id="btn4" value="修改子文档中的link标签"> <br /> <br /> <hr /> <iframe id="iframe1" src="b.html" frameborder="0"> </iframe> <script> // 只有同服务器下 同域名下才可以操作 不能更改别人的网页。。 var oBtn1 = document.getElementById('btn1'); var oIframe1 = document.getElementById('iframe1'); function fn(){ document.body.style.background = 'green'; } oBtn1.onclick = function () { console.log(oIframe1.contentWindow); // ---这个东西是子文档中的window对象 console.log(oIframe1.contentDocument); // ---- 这个东西是子文档中的document对象 oIframe1.contentWindow.document.body.style.background = 'yellow'; }; btn2.onclick = function () { var span1 =oIframe1.contentWindow.document.querySelector('.span1'); console.log(span1); span1.parentNode.removeChild(span1); }; btn3.onclick = function () { var span2 =oIframe1.contentWindow.document.querySelector('.span2'); span2.style.color = 'red'; }; btn4.onclick = function () { var iFrameWindow = oIframe1.contentWindow; console.log(iFrameWindow.document.getElementsByTagName('link')); var link0 = iFrameWindow.document.getElementsByTagName('link')[0]; console.log(link0.parentNode.removeChild(link0)); } </script> </body> </html>
b.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> body{ background: yellowgreen; } </style> <link rel="stylesheet" href="1.css"> <link rel="stylesheet" href="2.css"> </head> <body> <h1 id="h1">这里是子文档1</h1> <span class="span1">span1标签</span> <br> <br> <span class="span2">span2标签</span> <span class="span3">span3标签</span> <br /> <hr /> <input type="button" id="btn1" value="改变父文档的颜色"> <script> var oH1 = document.getElementById('h1'); var oBtn1 = document.getElementById('btn1'); oH1.onclick = function () { alert('子文档中的点击事件,我可以改变父文档'); console.log(window.parent); // -----这个parent对象是父文档中的 window对象 }; oBtn1.onclick = function () { (function (window,document) { document.body.style.background = 'skyblue'; })(window.parent, window.parent.document); }; </script> </body> </html>
样式如下
上面的两个代码中用到了一个东西,在a.html文件中 用到了iframe标签元素的 .contentWindow 和 .contentDocument 这两个东西,它们两个分别是子文档也就是b.html中的window对象和document对象,那么你说知道了这两个东西要去操作它里面的东西还不简单吗。
在 b.html文件中的 window.parent 这个东西是a.html的window对象,那么它同样也可以去操作a.html中的元素了。所以交给我的任务我感觉完成了,就去问他,这样可以。然后我给他看了一下这个东西,后来了解到这两个不是同一个域名下的,这两个网站不是在一起的,然后我就回来又来调试。
不同端口下的调试
我经常用的编辑器是webstrom,它这个东西会自启动一个 127.0.0.1:63342的端口,我又用node做了一个简单的监听 3000端口的服务器,在网页上面打开了。
还是同样的代码吧,只不过把ifreme上面的src改为了我3000端口的网页。
但是这次浏览器给了我一个惊喜,因为我感觉吧只有后端才会存在跨域什么的问题,没有想过前端的这些东西。
它的打印出来的window对象都变了,好多都是false了,和之前在同一个页面下面的东西都不一样了~~
因为一看到 origin cross-origin就感觉是跨域的那种问题。
得了吧,去百度,google查到底怎么办吧。我一直相信以我现在的水平遇到的问题其他的人同样也有人会遇到过。
这一查不要紧,感觉看得好多文章开辟除了新的天地,真的是,在文章底部会给出参考文章,昨天我只看到了一种解决方案,并且将它付诸于实践了,但是由于想要搞明白今天又找到了几种解决方案,但是并没有去试验。
还是要讲一讲同源对哪些行为有限制?
随着互联网的发展,同源策略 越来越严格。目前,如果非同源,共有三种行为受到限制。
1. Cookie、localStorage和 IndexDB无法读取
2. DOM无法获得
3. AJAX 请求不能发送
虽然这些限制是必要的,但是有时很不方便,合理的用途也会受到影响。
这个问题难道就没有办法解决了吗?有的
Cookie解决方法
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain
共享 Cookie。
举例来说,A网页是http://w1.example.com/a.html
,B网页是http://w2.example.com/b.html
,那么只要设置相同的document.domain
,两个网页就可以共享Cookie。
document.domain = 'example.com';
现在,A网页通过脚本设置一个 Cookie。
document.cookie = "test1=hello";
B网页就可以读到这个 Cookie。
var allCookie = document.cookie;
注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源政策,而要使用下文介绍的PostMessage API。
另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/
这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。
上面这种方法暂时还没有去试验,等试验过后再来修改一下这里,因为自己都不知道行不行。
iframe
如果两个网页不同源,就无法拿到对方的DOM,上面的第二个例子我已经去试验过了,也看到报错信息了。
就是父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。
document.getElementById("myIFrame").contentWindow.document // Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
上面命令中,父窗口想获取子窗口的DOM,因为跨域资源导致报错。
反之亦然,子窗口获取主窗口的DOM也会报错。
window.parent.document.body // 报错
前面讲的这些实际上我第二个例子试验过了,下面也就是我遇到问题的几种解决方法。
参考的他人的文章找到的,对于完全不同源的网站,目前有三种方法,来解决我遇到的问题。
1. 片段标识符
片段标识符呢也就是哈希值#,我们都知道当网址资源#前面不变,后面的部分变化的时候网页是不会刷新的,如果不清楚的话,可以看一下我之前写过的一篇文章 浅谈SPA ,里面有详细的介绍#。
就是父文档和子文档之间要交互时,就去改变hash值 也就是#后面的部分,然后两者再相互去监听hash变化的事件,再去自己做一些处理就好了
window.location.hash; // 这个是可以获取hash值的 window.onhashchange = function(){ // 这个是hash值改变会触发这个函数 }
举一个小例子可以去试验一下
父窗口可以把信息,写入到子窗口的片段标识符
父窗口中的代码
为了好看吧,就不用博客园自带的那个代码了,就这样把子文档的url地址给改变了吧,因为#不会刷新网页,而子文档中也可以监听到这个#值的改变,所以子文档中
浏览器中打印的东西
在这里可以看到了,我们传递过去的数据信息为 #changeColor,在子页面中可以判断#后面的带的东西,再去执行自己的逻辑。
同样的子文档给父文档传递数据
我们不是可以拿到父文档的那个 window.parent吗,就用这个去改变就可以了,但是 BUT!!!
我在子页面中使用的时候
btn1.onclick = function () { console.log(parent.location); }
这是什么嘛,你父文档都可以改子文档了,问什么这个还是要 block frame with啥啥啥的,我本来以为可以成功的,这个有知道解决方法的大佬可以帮帮忙吗。嘿嘿,暂时先这样吧,父文档已经可以给子文档传递信息了,我的解决方法也不是这种,暂时先把这个错误问题放一放,以后有解决方法了,会来这里修改的。
2. window.name
浏览器窗口有window.name属性,这个属性最大的特点是,无论是否同源,只要在同一个窗口里面,前一个窗口设置了这个属性后,后一个网页可以读取它。
父窗口先打开一次子窗口,载入一个不同源的网页,将网页信息写入window.name属性。
window.name = data;
接着,子窗口跳回一个与主窗口同域的网址
location = 'http://parent.url.com/xxx.html'
然后主窗口就可以读取子窗口的window.name了
var data = document.getElementById('myFrame').contentWindow.name;
这种方法的优点是,window.name
容量很大,可以放置非常长的字符串;缺点是必须监听子窗口window.name
属性的变化,影响网页性能。
3.跨文档通信API postMessage
我用到的解决方法是这种方法,感觉它和Vue之间的组件传值一样,不说话了,直接上代码,测试吧,记得那个子文档是 3000端口的页面
父文档
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> body{ background: pink; } #iframe1{ width: 100%; height: 400px; } </style> </head> <body> <input type="button" id="btn1" value="改变子文档的东西"> <input type="button" id="btn2" value="删除span1的颜色"> <input type="button" id="btn3" value="改变span2的颜色"> <br /> <hr /> <!--<iframe id="iframe1" src="http://localhost:3000" frameborder="0"></iframe>--> <iframe id="iframe1" src="http://localhost:3000" frameborder="0"></iframe> <script> var oIframe1 = document.getElementById('iframe1'); var a = function fn(){ document.body.style.background = 'green'; }; btn1.onclick = function () { console.log('传递的数据是','messageInfo'); oIframe1.contentWindow.postMessage('changeColor',"http://localhost:3000"); }; btn2.onclick = function () { //oIframe1.contentWindow.postMessage('changeSpan2Color',"http://localhost:3000"); oIframe1.contentWindow.postMessage('deleteSpan1',"http://localhost:3000") }; btn3.onclick = function () { oIframe1.contentWindow.postMessage('changeSPan2Color',"http://localhost:3000") }; window.addEventListener('message',function (res) { console.log(`这里是父文档`); if(res.data == 'GetWhiteLabel') document.body.style.background = 'yellow'; }) </script> </body> </html>
子文档
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> body{ background: skyblue; } </style> </head> <body> <h1>这里是我的html页面呢</h1> <span class="span1">span1标签</span> <span class="span2">span2标签</span> <br> <input type="button" id="btn1" value="改变父文档的东西"> <script> <!----> window.onload = function () { let parent = window.parent; window.addEventListener('message',function (res) { console.log(`******************这里是子页面的接收到的消息*************`); console.log(res); switch (res.data) { case 'changeColor': document.body.style.background = 'green'; break; case 'deleteSpan1': var oSpan1 = document.querySelector('.span1'); oSpan1.parentNode.removeChild(oSpan1); break; case 'changeSPan2Color': var oSpan2 = document.querySelector('.span2'); oSpan2.style.color = 'red'; break; } }); btn1.onclick = function () { parent.postMessage("GetWhiteLabel","*"); } } </script> </body> </html>
还是同样的页面吧,实现一样的功能。
上面有的地方写的不太好,存在一些安全问题,这个正是我现在正在做的地方,
oframe.contentWindow.postMessage(data,origin,false); //这个是postMessage的API // 发送的数据 子文档地址 false // 在子文档中去监听那个message的变化 window.addEventListener("message",function(res){ console.log(res.data); //这个东西就是发送过去的数据 // 去根据传过来的不同的数据 再去做相应的判断 })
子文档给父文档传数据的方式
parent.postMessage('message',origin,false); // 同样也是类似的 // 在父文档中去监听那个message的值 window.addEventListener("message",function(res){ console.log(res.data); //这个东西就是发送过去的数据 // 去根据传过来的不同的数据 再去做相应的判断 });
这样做可以实现,就是有一些安全问题,就是所有人如果查看你网站的源码的话肯定会看到这个东西的,你写的这么随意,其他任何网站只要引入你的子文档,然后就可以通过自己去写一些东西去改变你的子文档。
另外的一个缺点就感觉是 拓展性不好,你还需要去拿到子文档的网站,还需要再去修改它的源代码,感觉特别麻烦,如果有好几十个页面还要写好几十个吗。
所以想到了一种传值的方法,不穿那个要判断的东西,把要修改的元素的html代码的函数给传过去,就是子文档去定义一个接口去接收,父文档把要执行的事件都传过去,然后子文档写一个执行事件的接口。这里就会出现刚才第一个那个安全问题了,更为严重,因为你不知道要执行的是什么事件。
现在有一个想法就是后台做一个认证,就像微信的那个access_token认证一样的东西,加密,每次去操作子文档的时候,都要去请求,然后在子页面监听到这个事件之后解密再两个之间去对比,如果一样了才去执行,这个暂时还不知道如何去下手。
本文感觉特别有用的解决办法和参考的文章有如下还有更多的没有发,如果你遇到了同样的问题,看了我的解决不了问题,可以去看看下面的文章,当然还可以给我留言,必回复。
stackoverflow网页中的问答 博客园园友的文章 关于iframe的实践 阮一峰大佬的 浏览器同源政策及其规避方法
如果有更好的解决办法还望可以告知,谢谢!
如果你阅读了本文章有了一些收获,我会感到非常开心,由于能力有限,文章有的部分解释的不到位,希望在以后的日子里能慢慢提高自己能力,如果不足之处,还望指正。