iframe的利与弊

2,337 阅读11分钟

每当我们有需求是,这个项目里的页面要和另外一个项目中的一个页面要一模一样时,我们总是首先相当iframe。
而每当我们相当用iframe时,总感觉有些嫌弃。因为每个人都被教育iframe是个坑。
有各种安全性和性能问题,但是具体问题在哪呢?
iframe在当下是否还有应用场景?
带着这两个问题,我开始深入研究iframe。

基本用法

基本属性

以下为w3c官方文档描述,并结合自己在chrome下简单测试的结果。其中带*号的html5中的新属性。

属性 描述
align
- left
- right
- top
- middle
- bottom
不赞成使用。请使用样式代替。
规定如何根据周围的元素来对齐此框架。
frameborder
- 0
- 1
规定是否显示框架周围的边框。
width
- pixels
- %
定义 iframe 的宽度。
height
- pixels
- %
定义 iframe 的宽度。
marginheight
- pixels
定义 iframe 的顶部和底部的边距。
marginwidth
- pixels
定义 iframe 的左侧和右侧的边距。
scrolling
- yes
- no
- auto
规定是否在 iframe 中显示滚动条。
seamless* seamless 不赞成使用,兼容性较差。
规定 iframe 看上去像是包含文档的一部分。(无边框或滚动条)
src URL 规定在 iframe 中显示的文档的 URL。
srcdoc* HTML_code 规定在 iframe 中显示的页面的 HTML 内容。
longdec
- absolute URL
- relative URL
不赞成使用,h5中已经废弃,几乎所有浏览器都不支持。
规定一个页面,该页面包含了有关 iframe 的较长描述。
name frame_name 规定 iframe 的名称。
sandbox*
- ""
- allow-forms
- allow-same-origin
- allow-script
- allow-top-navigation
启用一系列对 iframe中内容的额外限制。

上面表格中前8个都是控制样式的,除了scrolling和seamless基本上没什么用,我们自己用css控制即可。seamless理论上是可以去除边框和滚动条的,但是经过实际测试,兼容性很差。所以这些控制样式的基本上都不建议使用,也不在本文的讨论范围内。
后5个基本上都是控制源和权限的,我们来简单说明一下。
src不用多说,是我们最常用的。
srcdoc是html5新出的属性,可以直接写html代码在iframe中显示,有些不是为了单纯嵌套页面的场景下会有用
longdec基本上没用,基本上所有浏览器都不支持。
name算是比较有用的属性了,可以明确iframe的名称,有点像id的感觉。可通过window.frames[iframeName]被调用。后面会具体说明应用场景。
sandbox是一个比较神秘的属性,基本上没有用到过,但是看描述貌似可以做的事情挺多。

父页面获取子页面内容(window和document)

<iframe src="/b" id="iframeb" name="iframeb"></iframe>

我们可以通过contentWindow和contentDocument两个API获取iframe的window对象和document对象。

const iframe = document.getElementById("iframeb");

// 这一步很重要,必须要等到iframe onload之后,否则idoc_ele为null
iframe.onload = function() {
    const iwindow = iframe.contentWindow; // 获取iframe的window对象
    const idoc = iframe.contentDocument; // 获取iframe的document对象
    const idoc_ele = idoc.documentElement;

    console.log("iwindow:", iwindow);
    console.log("idoc:", idoc);
    console.log("idoc_ele:", idoc_ele);
 };

刚刚我们提到了iframe的name属性,我们也可以通过window.frames[iframeName]来调用iframe。

let iframe = window.frames['iframeb']

这里补充一点:window.document其实是指向document对象的一个引用,所以其实iframe.contentDocument
iwindow.document是一个对象。

console.log(iwindow.document === idoc) // true

子页面获取父页面内容(window和document)

我们通过window.selfwindow.parentwindow.top这三个属性分别获取自身window对象,父级window对象,顶级window对象。
从网上找了一张图,很好地说明了这三者之间的关系。

image.png

由于window.document其实就是指向document对象的一个引用,所以我们可以通过window.parent.document来获取父页面的document对象。
**

父子页面传递消息

主要通过postmessage实现,用法为:

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

主要是message和targinOrigin,message为要发送的消息,targetOrigin限制了发送的URI。详细见mdn web docs

由于父传子和子传父基本上一致,所以这里只演示了父传子。

// a.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <h1>This is Page a</h1>
    <iframe src="/b" id="iframeb" seamless></iframe>
    <script>
      let iframe = window.frames["iframeb"];
      iframe.onload = function() {
        const iwindow = iframe.contentWindow; // 获取iframe的window对象
        iwindow.postMessage("hello", "*");
      };
    </script>
  </body>
</html>

// b.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <h1 id="title">This is Page b</h1>

    <script>
      window.addEventListener(
        "message",
        function(event) {
          console.log("receiveMessageFromPageA", event);
        },
        false
      );
    </script>
  </body>
</html>

image.png

跨域问题

上面提到父页面获取子页面内容和子页面获取父页面内容时,有一个前提条件是两者必须同域,如果出现跨域问题则会出现报错,并且无法读取相关内容,报错信息如下:

image.png

什么是跨域

什么是同域什么跨域咧?同域跨域的区别在哪咧?我们一般会使用iframe来进行父子页面的通信,然鹅父子页面是否同域决定了它们之间能否进行通信。
js遵循同源策略,即同协议,同域名,同端口号,否则都算跨域。

跨域 简单的来说,指的是两个资源非同源。出于安全方面的考虑,页面中的JavaScript在请求非同源的资源时就会出 跨域问题 ——即跨域请求,这时,由于同源策略,我们的请求会被浏览器禁止。也就出现了 我们常说的 跨域 问题。

image.png

iframe跨域通讯之document.domain

对于主域相同子域不同的两个页面,我们可以通过document.domain + iframe来解决跨域通信问题。
举个例子,网页a(qq.com)和网页b(bb.qq.com),两者都设置document.domain = 'qq.com'(这样浏览器就会认为它们处于同一个域下),然后网页a再创建iframe上网页b,就可以进行通信了。

// http://qq.com
document.domain = 'qq.com';
const iframe = document.querySelector('#iframe')
ifr.onload = function(){
    let doc = iframe.contentDocument || ifr.contentWindow.document;
    // 在这里操纵b.html
};
// http://bbb.qq.com
document.domain = 'qq.com';

iframe跨域通讯之postMessage

postMessage就是用来解决跨域通讯的,上面已经提到了postMessage的具体用法,这里就不再赘述。

安全问题

我们的项目应当应当不能被其他网站嵌套,起码是不被我们授权的网站所嵌套,否则用户产生的问题可能会找到你。

前端解决方案

// 严禁嵌套
if(window != window.top){
    window.top.location.href = myURL;
}
// 严禁除了www.qq.com以外的网站嵌套
if (window.top.location.hostname !== ['www.qq.com']) {
	// 如何处理
}

服务端解决方案

X-Frame-Options

X-Frame-Options是一个响应头,主要是描述服务器的网页资源的iframe权限。目前的支持度是IE8+有3个选项:

DENY 当前页面不能被嵌套iframe里,即便是在相同域名的页面中嵌套也不允许,也不允许网页中有嵌套iframe
SAMEORIGIN iframe页面的地址只能为同源域名下的页面
ALLOW-FROM 可以在指定的origin url的iframe中加载

CSP

一个更加高度定制化的选项是内容安全策略CSP(Content-Security-Policy)。其核心思想十分简单:网站通过发送一个 CSP 头部,来告诉浏览器什么是被授权执行的与什么是需要被禁止的。其被誉为专门为解决XSS攻击而生的神器。使用主要是在后端服务器上配置,在前端,我们可以观察Response Header 里是否有这样的一个Header:

Content-Security-Policy: default-src 'self'

这就表明,你的网页是启用CSP的。
如果你未指定的话,则是使用default-src规定的加载策略。
默认配置就是同域: default-src "self"。
这里和iframe有一点瓜葛的就是 child-src 和 sandbox。
child-src就是用来指定iframe的有效加载路径。其实和X-Frame-Options中配置allow-origin是一个道理。不过,allow-origin 没有得到厂商们的支持。
而sandbox其实就和iframe的sandbox属性,是一样的,他可以规定来源能够带有什么权限,这里简单介绍一下。

配置 效果
allow-forms 允许进行提交表单
allow-scripts 运行执行脚本
allow-same-origin 允许同域请求,比如ajax,storage
allow-top-navigation 允许iframe能够主导window.top进行页面跳转
allow-popups 允许iframe中弹出新窗口,比如,window.open,target="_blank"
allow-pointer-lock 在iframe中可以锁定鼠标,主要和鼠标锁定有关
  1. 限制方式

default-src限制全局
制定限制类型
资源类型有:connect-src、mainfest-src、img-src、font-src、media-src、style-src、frame-src、script-src

  1. 应对威胁

CSP通过指定有效域——即浏览器认可的可执行脚本的有效来源——使服务器管理者有能力减少或消除XSS攻击所依赖的载体。一个CSP兼容的浏览器将会仅执行从白名单域获取到的脚本文件,忽略所有的其他脚本 (包括内联脚本和HTML的事件处理属性)。
除限制可以加载内容的域,服务器还可指明哪种协议允许使用;比如 (从理想化的安全角度来说),服务器可指定所有内容必须通过HTTPS加载。一个完整的数据安全传输策略不仅强制使用HTTPS进行数据传输,也为所有的cookie标记安全标识 cookies with the secure flag,并且提供自动的重定向使得HTTP页面导向HTTPS版本。网站也可以使用 Strict-Transport-Security HTTP头部确保连接它的浏览器只使用加密通道。

  1. CSP的分类

(1) Content-Security-Policy
配置好并启用后,不符合 CSP 的外部资源就会被阻止加载。
(2)Content-Security-Policy-Report-Only
表示不执行限制选项,只是记录违反限制的行为。它必须与report-uri选项配合使用。

  1. CSP的使用

(1) 在HTTP Header上使用(首选)

"Content-Security-Policy:" 策略
"Content-Security-Policy-Report-Only:" 策略

(2) 在HTML上使用

<meta http-equiv="content-security-policy" content="策略">
<meta http-equiv="content-security-policy-report-only" content="策略">

Meta 标签与 HTTP 头只是行式不同而作用是一致的,如果 HTTP 头与 Meta 定义同时存在,则优先采用 HTTP 中的定义。
如果用户浏览器已经为当前文档执行了一个 CSP 的策略,则会跳过 Meta 的定义。如果 META 标签缺少 content 属性也同样会跳过。

  1. 举例

(1)一个网站管理者允许内容来自信任的域名及其子域名 (域名不必须与CSP设置所在的域名相同)

Content-Security-Policy: default-src 'self' *.trusted.com

(2)一个网站管理者允许网页应用的用户在他们自己的内容中包含来自任何源的图片, 但是限制音频或视频需从信任的资源提供者(获得),所有脚本必须从特定主机服务器获取可信的代码。

Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com media2.com; script-src userscripts.example.com

(3)为降低部署成本,CSP可以部署为报告(report-only)模式。在此模式下,CSP策略不是强制性的,但是任何违规行为将会报告给一个指定的URI地址。

Content-Security-Policy: default-src 'self'; report-uri http://reportcollector.example.com/collector.cgi

性能问题

iframe对于创建 DOM 的时间固然非常恐怖,但是通常我们一个页面上很少有几十个甚至上百个iframe,所以这方面的影响可以忽略,但是onload事件的延迟却是我们无法忽略的,通常我们应该在javascript中通过动态指定src来加载iframe,这样可以避免onload阻塞。

应用场景

  • 用来实现长连接,在websocket不可用的时候作为一种替代,最开始由google发明。Comet:基于 HTTP 长连接的“服务器推”技术
  • 跨域通信。JavaScript跨域总结与解决办法 ,类似的还有浏览器多页面通信,比如音乐播放器,用户如果打开了多个tab页,应该只有一个在播放。
  • 历史记录管理,解决ajax化网站响应浏览器前进后退按钮的方案,在html5的history api不可用时作为一种替代。
  • 用iframe实现无刷新文件上传,在FormData不可用时作为替代方案
  • 在移动端用于从网页调起客户端应用(**此方法在iphone上并不安全,慎用!**具体风险看这里 iOS URL Scheme 劫持 )。比如想在网页中调起支付宝。

总体而言,iframe是个黑魔法,不仅仅是用来嵌套页面那么简单,但是现在大多数情况都有替代方案了,现在的大部分网站避免采用这种方式的。