Skip to content
This page has been auto-translated and may contain errors.View in English

嵌入与 iframe ​

docs.scrimba.com

有时候,你想放到页面上的东西其实已经存在于别处了:YouTube 上的一段视频、地图上的一个位置、某家公司提供的支付表单。你不需要重新做一个,只要把那整个页面放进你的页面里就行。做这件事的元素就是 <iframe>,它自带一整套控制手段,让借来的内容尺寸合适、能被访问、也够安全。

iframe 元素 ​

iframe 会把另一个网页嵌入到你自己的页面里。这个名字是 "inline frame"(内联框架)的缩写,这个说法很形象:页面上有一个带边框的小窗口,透过它显示的其实是完全独立的另一个页面。博客文章里的 YouTube 视频,几乎都是用 iframe 实现的。

你用 src 属性指向想显示的页面,这跟图片用来指定文件的属性是一样的:

html
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ"></iframe>

就这一个标签,就能把 YouTube 播放器搬到你的页面上。可以把它想象成在墙上开了一个窗:墙是你的页面,透过这个窗户,你看到的是完全另一个房间。

<iframe> 会加载一个独立的完整网页,并把它显示为你页面里的一个矩形区域。src 属性存放的是那个页面的地址,而框架内部的一切——它自己的 HTML、CSS 和 JavaScript——都是独立运行的,与你的页面互不干扰。

html
<iframe src="https://www.openstreetmap.org/export/embed.html"></iframe>

写在开始标签和结束标签之间的任何内容都只是兜底内容,只有那些太老、不支持 frame 的浏览器才会显示它,实际上现在已经没有这样的浏览器了。这个框架完全靠属性来配置,接下来的部分会讲到你实际会用到的那些属性。

<iframe> 会创建一个**嵌套浏览上下文**:一个完整、独立的文档,被嵌入到你的文档里(浏览上下文是单个页面运行所处的环境,拥有自己的历史记录、自己的全局作用域,以及自己那一份 DOM)。这个框架并不真正算是你页面 DOM 树的一部分;它是另一个独立页面,只是恰好被绘制在你页面上的一个方框里。

这种隔离正是它存在的意义,而它是靠**同源策略**来强制实现的:你的页面和被嵌入的页面只有在共享同一个源(origin)时,才能相互读取或操作对方(源是协议、主机和端口的组合,所以 https://example.com 和 https://other.com 是不同的源)。对于同源的框架,你可以通过 iframe.contentWindow 用 JavaScript 伸手进去操作;而跨源的框架则是一个密封的盒子,你只能把它摆在那儿,却看不进去。你放的几乎每一个嵌入内容——视频、地图、小组件——都是跨源的,正因为如此,把它嵌入进来才是安全的。

Junoiframe 元素<iframe> 会在你的页面里显示完整的另一个网页,就像开了一个小窗户,透过它看到另一个房间。你用 src 指定要显示的地址,跟图片一样。博客上那个 YouTube 播放器?几乎全都是这么做的。
Junoiframe 元素<iframe> 会加载一个独立、自成一体的页面,并把它显示在你页面上的一个方框里。它自己的 HTML、CSS 和 JavaScript 都与你的页面隔开。你完全靠属性来配置它,从 src 开始。
Junoiframe 元素 框架是一个嵌套浏览上下文:一个完全独立的文档,有自己的作用域和 DOM,不是你页面的一个分支。同源策略负责把它封起来,所以跨源的嵌入内容可以显示出来,但你的页面没法操作它。这道墙是一个特性,不是需要绕过去的限制。

嵌入视频、地图和小组件 ​

好消息是:你很少需要自己手写嵌入代码。你要嵌入的那个网站会替你写好。在 YouTube 上,你点一下"分享",再点"嵌入",它就会给你一段现成的 <iframe> 代码,直接复制就行。地图、音乐播放器、日历,都是同样的用法。复制它们给你的代码块,粘贴到你的 HTML 里就行了。

html
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" width="560" height="315"></iframe>

width 和 height 用来设置这个框架的大小,单位是像素。改动这两个数字,视频在你页面上就会变大或变小。

大多数嵌入代码都是直接从源网站拿来的。找到"分享"或"嵌入"按钮,服务商就会给你一段已经设置好尺寸和地址的 <iframe> 代码块。有三个属性值得你真正搞懂,而不只是照抄:

html
<iframe
  src="https://www.youtube.com/embed/aqz-KE-bpKQ"
  width="560"
  height="315"
  title="Big Buck Bunny short film"
  loading="lazy"
></iframe>

width 和 height 以像素设置框架的尺寸。title 属性给这个框架起了个名字,供屏幕阅读器使用——它们播报这个名字的方式,跟播报链接文本的方式一样,所以没有 title 的框架会被读成一个没有标签的"frame",对谁都没有帮助。loading="lazy" 会告诉浏览器,先别急着下载这个框架里的页面,等读者滚动到它附近再加载,这样页面下方的嵌入内容就不会拖慢首次加载速度。

如果你想让框架跟随容器伸缩,而不是固定像素大小,那就去掉 width 和 height 属性,用 CSS 来设置尺寸,并用 aspect-ratio 来保持视频的比例正确。

服务商给你的嵌入代码片段只是一个还算合理的默认值,不是可以照单全收的最终方案。有几个属性决定了它在生产环境里表现得好不好。

html
<iframe
  src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ"
  title="Big Buck Bunny short film"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen
  style="width: 100%; aspect-ratio: 16 / 9; border: 0;"
></iframe>

title 在实际使用中并不是可选项:辅助技术(比如把页面读出来的屏幕阅读器之类的软件)会把框架当作一个地标,并播报它的 title,所以缺了 title,阅读顺序里就会出现一段空白区域。loading="lazy" 会把框架里文档的加载推迟到它接近视口(页面的可见区域)时才进行,这一点在这里格外重要,因为每个框架都是一次完整的页面加载,不是单个资源。referrerpolicy 控制的是你页面的 URL 在 Referer 请求头(浏览器用来告知服务器请求来源的字段)里会泄露多少信息给被嵌入的服务,收紧这个设置能减少泄露给用户的信息。用 aspect-ratio 做响应式尺寸,而不是固定像素,这样框架在小屏幕上也能自适应重排。最值得养成的一个习惯是:粘贴之前先读一读这段代码,因为你继承的是提供方选定的所有默认设置。

Juno嵌入视频、地图和小组件 你几乎从来不需要自己写嵌入代码。在 YouTube、地图或播放器上找到"分享"或"嵌入"按钮,它就会给你一段可以直接粘贴的 <iframe>。改动 width 和 height 就能调整大小。基本上这就是全部工作了。
Juno嵌入视频、地图和小组件 从源网站拿到嵌入代码后,再整理一下。一定要加上 title,这样屏幕阅读器才能读出这个框架的名字;再加上 loading="lazy",这样页面下方的框架不会拖慢首屏渲染。想要灵活的尺寸,就去掉固定的像素宽高,改用 CSS 的 aspect-ratio。
Juno嵌入视频、地图和小组件 服务商给的代码片段只是个起点。为阅读顺序加上 title,因为框架相当于整整一次页面加载而不是单个资源,所以要加上 loading="lazy",用严格一点的 referrerpolicy 来减少用户信息的泄露,再用 aspect-ratio 做响应式尺寸。粘贴前先看看内容,因为你继承的是对方选定的每一个默认值。

沙箱与权限 ​

因为框架里运行的是完完整整的另一个页面,你可能想限制那个页面能做什么。sandbox 属性正是干这个的:它给框架加上一道安全围栏。

html
<iframe src="widget.html" sandbox></iframe>

单独写上 sandbox,被嵌入的页面就会受到最严格的限制:不能运行自己的脚本,不能提交表单,也不能弹出新窗口。之后你再一项一项地把权限还给它,只给嵌入内容真正需要的那些。可以把这想象成把房间借给别人住,但把你不想让人碰的柜子锁起来。

有两个属性控制着框架被允许做什么。sandbox 用来限制框架,allow 用来授予设备和浏览器功能的访问权限。

只加 sandbox、不带任何值,几乎会禁止框架里页面能做的所有事情:脚本、表单、弹窗,以及把自己当作同源对待。你可以通过列出用空格分隔的权限令牌来放宽限制:

html
<iframe
  src="https://example.com/widget"
  sandbox="allow-scripts allow-forms"
></iframe>

allow-scripts 允许框架运行 JavaScript,allow-forms 允许它提交表单,allow-popups 允许它打开新窗口。只给嵌入内容真正需要的权限。另一个独立的**allow**属性,管的是像全屏、摄像头、麦克风这类敏感的浏览器功能:

html
<iframe src="https://example.com/call" allow="camera; microphone; fullscreen"></iframe>

如果某个功能没有列在 allow 里,即使框架内部的代码去请求它,被嵌入的页面也用不了。

这里其实有两种不同的模型,值得分开来看。sandbox 是从框架身上拿掉能力;allow 是授予被限制的功能访问权。它们不是同一件事的两种写法。

不带值的 sandbox 会一次性施加所有限制:不能运行脚本、不能提交表单、不能弹窗、不能自动播放媒体,而且重要的是,这个框架会被强制变成一个独一无二的不透明源(opaque origin)(这种源跟任何其他源都不匹配,所以这个框架没法声称自己跟任何人同源,包括加载它的那个站点本身)。你可以用令牌把能力一项一项加回来:

html
<iframe
  src="https://example.com/widget"
  sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"
></iframe>

有一种组合要特别避开。如果一个框架加载的是你自己站点的内容,同时给它 allow-scripts 和 allow-same-origin 这两个权限,就等于让框架里的页面能运行代码,反过来把自己的沙箱解除,这就完全违背了沙箱的初衷。只有当框架里的内容完全可信时,才能把这两个权限配对使用。

allow 属性是一种**权限策略(Permissions Policy)**(一种浏览器机制,用来决定哪些页面可以使用摄像头、麦克风、地理位置、全屏等敏感功能)。它的默认行为是拒绝:没有在 allow 里点名的功能,无论框架里的脚本怎么请求,都用不了。这就是为什么你可以放一个需要摄像头的视频通话小组件,却不必把摄像头权限也交给页面上的其他每一个框架。

Juno沙箱与权限 给框架加上 sandbox,被嵌入的页面就会被锁死:不能跑脚本,不能弹窗,什么坏事都干不了。然后你再把它真正需要的那些权限还给它。这就像把房间借给别人,但有些柜子还是锁着的。
Juno沙箱与权限sandbox 用来限制框架,你可以一个一个令牌地放宽它,比如 allow-scripts 和 allow-forms。摄像头、麦克风或全屏这些,是靠另一个独立的 allow 属性来授权的。每一项都只给嵌入内容真正要用到的权限。
Juno沙箱与权限 把这两套模型分清楚:sandbox 是拿掉能力,allow 是授予被限制的功能,而且默认是拒绝的。要小心的陷阱是,在同源框架上同时给 allow-scripts 和 allow-same-origin,因为这会让页面解开自己的沙箱。只有在你完全信任里面的内容时才能这么做。

安全与性能方面的注意事项 ​

嵌入内容很方便,但每个框架都要加载一整个额外的页面,一堆框架叠在一起,会让你的页面感觉很慢。养成两个习惯就能避开麻烦:只嵌入你信任的网站的内容,因为这相当于把对方的页面请到你家里来;还有,不要加超出实际需要的框架数量。

前面提到的 loading="lazy" 属性在这里也帮得上忙,它会让框架等读者滚动到附近才加载。当嵌入能帮你省下真正的工作量时,比如一段视频或一张地图,就用它;如果一个普通链接就能达到同样的效果,就跳过嵌入。

你加的每一个框架都是一次完整的页面加载:有自己的 HTML、自己的 CSS,往往还有一大堆自己的 JavaScript,全都是从另一台服务器上抓取的。一个页面里有三个第三方嵌入内容,实际上就相当于同时加载四个页面,这就是为什么嵌入内容多的页面会显得很沉重。给不在屏幕上的框架加上 loading="lazy",是成本最低的优化;而少用一些嵌入内容,比调优某一个嵌入内容更管用。

除了速度之外,还有第二个需要关心的问题。你可以保护自己的页面,不让它被别人的框架嵌进去,这样能防住一种把戏:攻击者把你的站点嵌进一个框架里,诱骗用户点击他们看不见的东西。这种保护是在服务器端设置的,Deep Dive 部分会讲到负责这件事的两个响应头。

这里有两项代价值得你认真看待:框架对你的性能有什么影响,以及被别人的框架嵌入会带来什么安全隐患。

先说性能,第三方框架并不是一次简单的请求;它是一次**嵌套文档加载**,会在你的页面里跑起服务商的整套东西(他们的标记、样式、字体、统计代码和脚本)。它跟你的页面争抢同一份网络带宽和主线程,它的脚本在自己的上下文里运行,但用的是同一台设备,所以一个很重的嵌入内容,可能会拖慢你自己内容变得可交互的时间。loading="lazy" 能推迟屏幕外框架的加载,而把嵌入内容合并、精简,比单独调优某一个更划算。先量一量实际代价,再判断嵌入是不是"免费"的。

再说安全,风险在于**点击劫持(clickjacking)**:攻击者在自己的站点上用一个框架加载你的页面,把它变得不可见或叠在别的内容上面,诱使已登录的用户在不知不觉中点击你页面上一个真实存在的控件。防御手段是明确声明谁——如果有的话——可以框住你的页面,而这是通过服务器上的响应头来设置的,不是在 HTML 里:

  • X-Frame-Options: DENY 会拒绝让你的页面在任何框架里加载。SAMEORIGIN 只允许你自己的站点来框住它。这是比较老、比较粗粒度的控制手段。
  • Content-Security-Policy: frame-ancestors 'self' 是现代的替代方案。frame-ancestors 精确列出了哪些源可以框住你的页面(Content-Security-Policy,简称 CSP,是一种响应头,告诉浏览器允许哪些内容来源和行为)。它取代了 X-Frame-Options,可以设置具体的允许列表,而不是一条统一的规则,凡是带登录或表单的页面,都应该设置这个头。

注意方向:sandbox 和 allow 控制的是你嵌入的那些页面,而 frame-ancestors 和 X-Frame-Options 控制的是谁可以嵌入你。两个方向都很重要,而且它们的配置位置是不一样的。

Juno安全与性能方面的注意事项 每个框架都要加载一整个额外的页面,一堆框架叠在一起会拖慢你的页面。只嵌入你信任的站点,控制好数量,并用上 loading="lazy"。如果一个普通链接就能达到同样效果,就用链接。
Juno安全与性能方面的注意事项 三个嵌入内容意味着同时加载四个页面,所以要把每个框架都当成一笔真实的成本。给屏幕外的框架加 loading="lazy" 是最省力的优化,少用几个嵌入内容比调优某一个更划算。另外还有一种框架相关的风险值得防范,Deep Dive 部分会用两个服务器响应头来讲解。
Juno安全与性能方面的注意事项 第三方框架会在你的页面里跑起服务商的整套东西,并跟你争抢网络和主线程,所以别想当然地以为它"免费",先量一量再说。安全方面,要靠声明谁可以框住你来防住点击劫持:CSP 里的 frame-ancestors 是现代的做法,X-Frame-Options 是老一点的做法。记住方向:sandbox 守护的是你嵌入的页面,frame-ancestors 守护的是谁能嵌入你。