ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iframe深入浅出:从嵌套到动态采集的完整实战

iframe深入浅出:从嵌套到动态采集的完整实战 很多人一看到 iframe 这三个字母第一反应往往是“知道这个东西但好久没用过了”。翻十几年前的网页源码几乎每个页面里都能看到 iframe 的影子从广告位、留言板到音乐播放器全是它的地盘。到了现在iframe 依然是前端绕不开的基础能力你可能正在写一个 Vue 或 React 单页应用但为了嵌入第三方地图、视频、报表或者做一个多系统集成门户还是会硬生生把 iframe 拉回来用。这篇内容的定位是实操路线。我打算用最直白的语言把 iframe 是什么、怎么嵌套页面、怎么处理内部外部调用、怎么隐藏滚动条以及在爬虫场景下怎么用 Playwright 抓动态 iframe 内容完整过一遍。目标读者包括刚接触前端的新人也包括做数据采集、自动化测试的同行。不管你是哪种角色看完应该能直接照搬大部分方案。1. iframe 是什么先搞清楚它到底解决了什么问题1.1 一句话理解 iframeiframe 是 HTML 提供的一个内联框架标签官方名称是 inline frame。它的作用是在当前页面里再开一个“页面窗口”窗口的 src 指向另一个 HTML 地址浏览器就会把这个地址对应的文档渲染在这个窗口里。从视觉上看它是页面里的一块矩形区域从结构上看它是嵌套在父文档里的完整子文档树。我习惯用“动态画框”来理解它。iframe 像一个画框画框里不是静态图片而是一个独立的小世界。这个小世界有自己的 DOM、自己的脚本执行环境甚至有自己的存储空间。外面的人想递东西进去或者想看看里面的情况只能走特定的通道。这个通道就是 iframe 提供的通信接口。把这个抽象理解了后面所有属性、方法、坑都能围绕这个模型推断出来不会一看代码就晕。1.2 iframe 和普通页面元素的本质区别很多人不把 iframe 当一个特殊标签所以遇到问题就懵。普通 HTML 元素比如 div、p、img它们和主页面共享同一个 window 对象、同一个 document你可以随时随地通过 JavaScript 修改、读取、监听。iframe 则完全不一样它内部的 window、document 和外层主页面是彻底隔离的两个世界。举一个最常见的例子。你在主页面的 localStorage 里存了一个登录 tokeniframe 里加载的外部页面默认拿不到这个 token因为两个不同源的页面有各自的存储空间。反过来iframe 内部初始化变量、修改样式、清理缓存外层主页面全都感知不到。这种隔离既是 iframe 安全性所在内容不会互相污染也是 iframe 使用过程中最大的麻烦来源。几乎所有人遇到的 iframe 坑最后都能归结到“边界没处理好”这一点上。注意iframe 与普通标签之间的区别不在于外观而在于上下文。普通标签活在父文档的上下文里iframe 则拥有独立执行上下文。1.3 什么场景最值得用 iframe我见过不少开发者为了做一个“看起来漂亮”的集成页面用 iframe 把所有模块都包进去结果页面加载又慢又乱。这里给大家一个经验判断内容是不是外部提供的。你不需要、也不方便直接操作内容内部逻辑的时候iframe 通常是最合适的选择。典型场景有三个。第一第三方内容嵌入。地图、视频、聊天插件、在线协作文档、支付页面这些功能不是你开发的但产品里必须要用。对方给你一个嵌入地址你用 iframe 一行标签就能引入不用重写轮子也不用跟对方商量接口对齐。第二多系统集成门户。企业内部的管理平台常常是多个子系统并行OA、CRM、BI 报表、监控大屏各自独立开发、独立部署。主系统只负责导航和身份认证右侧内容区用 iframe 加载不同子系统页面切换菜单时替换 src 即可。这样一来各系统之间的发布时间、技术栈、部署环境都不用互相牵连。第三隔离不信任的内容。渲染用户提交的富文本、第三方广告、不可控脚本时放进 iframe 能在一定程度上隔离双方上下文防止恶意脚本直接操作父页面。这种隔离不能当作绝对安全但比直接在主页面上混着跑要扎实不少。判断标准简化成一句话这个内容是不是“外部提供的、你不需要也不方便直接操作它内部逻辑”如果是iframe 通常就是成本最低的选择。2. 最基础的 iframe 用法嵌套页面与内部外部调用2.1 最小可用写法先把最小可用的例子摆出来这段代码大家应该都很眼熟iframe srchttps://example.com/page.html width800 height600 loadinglazy /iframesrc 是必填的指向要加载的页面地址width 和 height 控制 iframe 的矩形尺寸默认单位是像素也可以用百分比loading 属性是后来新增的取 lazy 表示 iframe 滚动到视口附近才开始加载资源放在首屏之下时能明显减少主页面初始化时间。除了这几个还有几个高频属性要记住。name 给 iframe 命名配合 a 标签的 target 能实现“点击链接后在 iframe 里打开新页面”。allow 用来开启摄像头、麦克风、全屏等权限嵌入地图或视频时比较常见。allowfullscreen 允许 iframe 内部触发全屏是嵌入视频播放器时的常客。sandbox 则是一把大锁可以用它限制 iframe 内部的脚本执行、表单提交、弹窗等能力在嵌入第三方不可控内容时非常实用。2.2 内部页面调用与外部网站嵌入iframe 加载的地址可以分成两大类一类是内部页面也就是跟主站同源、属于你自己发布体系的页面另一类是外部页面来自第三方服务器。内部调用的典型场景是后台管理系统。主框架页面只负责布局左侧菜单点击后右侧内容区不断切换不同的 src每个功能模块独立开发、独立维护。因为同源你可以在主页面脚本里直接操作 iframe 内部元素自由度很高。外部调用的例子就更多了视频平台的分享代码本质上就是一段 iframe地图平台提供的“嵌入地图”功能输出的也是一段 iframe 地址。你在自己页面里贴一行 src就能把一个功能完整、跨站的第三方区域整合进来。!-- 内部调用相对路径即可 -- iframe src/admin/report.html namemain/iframe !-- 外部调用需要完整 URL -- iframe srchttps://player.example.com/video/12345 allowfullscreen/iframe这里有个重要区别必须点明内部页面同源主页面可以直接读取 iframe 内部 DOM、调用内部函数、修改样式外部页面受同源策略限制主页面脚本没有权限窥探 iframe 内部的一丝一毫。想通信只能走 postMessage 这类跨文档通道第 3 章我会把完整写法给出来。2.3 控制跳转方向name 加 target 的巧妙组合iframe 的 name 属性新手常常忽略但它是一个很聪明的老技巧。给 iframe 设置 name 后页面里的 a 链接只要把 target 指向这个名字点击时浏览器就不会在顶层窗口打开新链接而是直接在 iframe 窗口内打开链接地址。iframe namecontentFrame srcwelcome.html/iframe pa hreflist.html targetcontentFrame跳转到列表页/a/p pa hrefdetail.html targetcontentFrame跳转到详情页/a/p在没有前端框架的年代这种做法是实现“局部刷新”的常见手段。现在虽然不推荐用它替代路由系统但在旧系统改造、第三方页面跳转方向控制的场景里依然能派上用场。理解了 name target 的关系你也更容易排查“点击链接后为什么 iframe 被整页顶掉”这类问题。3. 把 iframe 调教成你真正想要的样子3.1 自适应高度同源与跨域两套方案最开始用 iframe 做嵌入大概率都会碰到高度问题。如果 iframe 尺寸小于内部内容浏览器就会在 iframe 内部生成滚动条外层页面又有一个滚动条体验非常割裂。理想状态是 iframe 的高度跟着内容变化内容有多少它就撑多高。同源情况下解决方案很简单。iframe 加载完成后读取内部文档的高度再把这个高度覆盖到 iframe 标签上const iframe document.getElementById(myFrame); iframe.addEventListener(load, () { const innerDoc iframe.contentDocument || iframe.contentWindow.document; const height innerDoc.documentElement.scrollHeight; iframe.style.height height px; });这里的关键是 contentDocument 能拿到 iframe 内部文档对象。同源下不会触发权限限制documentElement.scrollHeight 就是内部页面的完整高度。这段代码实战里非常常用配合窗口 resize 事件一起监听基本上能覆盖绝大多数内部页面场景。跨域情况就没这么幸运了。跨域 iframe 的内部文档高度外层脚本没有权限读取只能靠通信方案也就是“子页面报高度、父页面改高度”。子页面里写入这样一段脚本function reportHeight() { const h document.documentElement.scrollHeight; parent.postMessage({ type: iframeHeight, height: h }, *); } window.addEventListener(resize, reportHeight); reportHeight();父页面监听消息window.addEventListener(message, (event) { if (event.data event.data.type iframeHeight) { const frame document.getElementById(myFrame); frame.style.height event.data.height px; } });这段示例里我先用了 * 便于说明但生产环境绝不能这么做一定要校验 event.origin。后面 3.3 节我会把完整的来源校验写法放出来。跨域自适应高度的核心思路就八个字子页报高父页改高。3.2 隐藏滚动条先避开一个伪方案“iframe 隐藏滚动条”在搜索热词里长期靠前。很多人发现 iframe 内外两层滚动条就想直接用 CSS 藏掉结果发现根本没效果。先说原因给 iframe 标签写 overflow: hidden控制的只是 iframe 这个元素作为容器时的滚动行为iframe 内部文档的滚动条是独立渲染的外层样式默认进不去。这个方向从一开始就错了。靠谱的做法分三类按推荐程度排序。第一个也是最推荐的让 iframe 和内部内容等高内容不溢出滚动条自然没有。也就是 3.1 节自适应高度的思路治标治本。第二个修改内部页面样式前提是内部页面由你控制/* 内部页面里的样式 */ html, body { margin: 0; height: 100%; overflow: hidden; } .content { height: calc(100vh - 60px); overflow-y: auto; }这样把 html 和 body 的滚动关闭需要滚动的区域转移到内部一个指定容器上滚动条样式也可以在此基础上继续自定义。很多后台系统里的“无边框嵌入”效果都是这么实现的。第三个设置 iframe 的 scrolling 属性为 no。这个属性虽然被标准废弃但大多数浏览器还会认。注意它只是隐藏滚动条并不能阻止内容溢出时用户无法滚动的尴尬。你如果隐藏滚动条但内部内容又超出高度用户就会完全卡死这种“假隐藏”比不隐藏更坑。我的建议是隐藏滚动条一定要结合交互设计来决策。要么内容短到不需要滚动要么把滚动统一到外层页面要么保留内部滚动条但隐藏外框。为了美观而砍掉滚动能力是最容易被用户吐槽的处理方式。3.3 postMessage父子页面通信的正确姿势前面跨域章节反复提到 postMessage这里完整展开。postMessage 是 HTML5 引入的跨文档消息 API解决的就是不同源窗口之间传消息的问题。发送消息的格式是targetWindow.postMessage(messageData, targetOrigin);messageData 是任意可序列化的数据targetOrigin 是目标窗口的源写法是协议加域名加端口比如https://parent.example.com。最不建议的写法是 *它表示允许任何窗口接收。写明确的目标源等于给消息做了一次安全过滤。接收端注册 message 事件window.addEventListener(message, (event) { if (event.origin ! https://child.example.com) { return; } const data event.data; // 处理数据 const iframe document.getElementById(childFrame); iframe.contentWindow.postMessage({ ack: true, payload: data }, event.origin); });这里的 event.origin 是消息发送方的源一定要完整校验协议、域名、端口。尤其提醒一句不要只做前缀匹配比如判断 event.origin.indexOf(example.com) ! -1这会放过 example.com.evil.com 这种伪造源。很多安全事故都出在这种偷懒写法上。顺便说一个容易踩的坑message 事件不只 iframe 会触发用 window.open 打开的窗口、以及任何能拿到你窗口引用的脚本都可能触发消息。所以收发两端都要把来源校验写严谨。你要是嫌麻烦省掉校验将来被页面里嵌的广告 iframe 塞一堆垃圾数据排查起来要花好几倍的时间。4. 动态 iframe渲染原理与自动化采集实战4.1 动态 iframe 是这么来的前面讲的 iframesrc 都写在 HTML 源码里。打开网页源码能直接看到标签搜索引擎和普通用户访问都能及时拿到内容。但现代 Web 应用经常用 JavaScript 动态创建 iframeHTML 源码里根本找不到标签的影子必须等脚本执行到特定时机才会在运行时把 iframe 挂进 DOM。典型代码是div idreport/div script const wrap document.getElementById(report); const frame document.createElement(iframe); frame.src https://data.example.com/dashboard/2024; frame.width 100%; frame.height 600; wrap.appendChild(frame); /script这种方式在数据报表、埋点统计、单页应用里特别普遍。好处是加载时机可控不会让 iframe 拖慢首屏渲染。坏处是假如你只是发一个 HTTP 请求拿 HTML 字符串根本拿不到 iframe 内部的最终数据。要抓动态 iframe就得有一个能执行 JavaScript 的完整浏览器环境等 iframe 插入 DOM、内部资源加载完成、数据渲染完毕再去读取内容。4.2 Playwright 处理动态 iframe 的完整套路Playwright 是目前处理动态页面最顺手的工具之一。它对 iframe 的支持做得很完善核心思路是先用 frame_locator 定位目标 iframe再在 iframe 上下文里继续查找元素、点击按钮、读取文本。一个完整的 Python 示例from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com/report, wait_untilnetworkidle) # 等待动态 iframe 出现 frame_locator page.frame_locator(iframe[src*dashboard]) frame_locator.locator(text加载完成).wait_for() # 读取 iframe 内部的关键内容 title frame_locator.locator(.dashboard-title).inner_text() rows frame_locator.locator(table tbody tr).count() print(title:, title) print(rows:, rows) browser.close()frame_locator 是一个“懒加载”定位器每次操作前会自动等待 iframe 出现并重试对动态渲染场景极其友好。如果想要遍历页面上所有 frame也可以直接用 page.framesfor frame in page.frames: if dashboard in frame.url: content frame.locator(body).inner_text() print(content)page.frames 返回当前页面里所有已存在的 frame包括嵌套 iframe。用它做遍历可以处理多层嵌套的动态 iframe只要能从 url 或其他特征识别出目标 frame 就行。这里要提醒的是page.frames 拿到的 frame 对象不会自动等待未知的 iframe 加载所以遇到页面里有实时渲染、异步插入的 iframe 时还是 frame_locator 更省心。在实际采集任务里我建议把等待时间显式设置出来。比如 frame_locator.locator(...).wait_for(timeout10000)避免网络抖动导致脚本反复失败。也不要盲目用 sleep 当等待sleep 只会浪费采集时间wait_for 才是正经做法。4.3 Scrapy 与 Playwright 结合抓 iframe 内容老牌的 Scrapy默认下载器完全基于 HTTP 请求拿到的只是 HTML 字符串没有执行 JS 的能力。遇到动态 iframe只靠 Scrapy 是抓不到内容的所以要么在 Spider 里调用 Playwright要么在下载器层集成渲染能力。最轻量的做法是在 Spider 的解析函数里直接调用一个渲染提取函数from playwright.sync_api import sync_playwright def render_and_extract(url: str) - str: with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, wait_untilnetworkidle) page.wait_for_selector(iframe[src*data]) content page.frame_locator(iframe[src*data]).locator(body).inner_text() browser.close() return content写好之后在 Scrapy 的 parse 方法里对包含动态 iframe 的页面调用它把结果封装成 Item 继续向下流转。这种方案简单直观适合中小规模采集但每次请求都新起一个 Playwright 进程并发一高效率就下来。工程化程度更高的做法是安装 scrapy-playwright让 Scrapy 的下载器直接具备渲染能力pip install scrapy playwright scrapy-playwright playwright install chromium在 settings.py 里配置DOWNLOAD_HANDLERS { http: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, https: scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler, } PLAYWRIGHT_LAUNCH_OPTIONS { headless: True, }Spider 代码可以写成这样import scrapy class IframeSpider(scrapy.Spider): name iframe_spider start_urls [https://example.com/report] def start_requests(self): for url in self.start_urls: yield scrapy.Request(url, meta{playwright: True}, callbackself.parse) def parse(self, response): page response.meta[playwright_page] page.wait_for_selector(iframe[src*data], timeout15000) text page.frame_locator(iframe[src*data]).locator(body).inner_text() yield {url: response.url, iframe_text: text}要点是 meta 里的 playwright 字段。它告诉下载器这个请求需要渲染渲染完成后浏览器实例挂在 response.meta 的 playwright_page 上parse 里可以直接继续操作页面。纯 HTTP 请求的旧方案拿到这种页面iframe 内容永远是空的除非对方把数据一起写进了初始 HTML。注意scrapy-playwright 的配置项在不同版本间有过调整我写的是当前的主流写法。真正生产环境接入时请对照你所安装版本的官方文档确认配置键名避免出现键名对不上的情况。5. iframe 常见坑与排查清单5.1 iframe 加载空白先看这两个响应头iframe 打开一片空白多数情况下不是你的代码问题而是目标页面设置了安全响应头。这时候打开浏览器开发者工具的 Network 面板找到 iframe 对应的请求查看响应头里的两个字段X-Frame-Options 和 Content-Security-Policy。X-Frame-Options 有三个取值DENY 表示任何页面都不能把它嵌入 iframeSAMEORIGIN 表示只有同源页面可以嵌入ALLOW-FROM 后接域名这个取值已被大多数浏览器废弃基本不用考虑。只要响应头里出现 DENY 或 SAMEORIGIN外部页面想嵌它就会被浏览器直接拦截。Content-Security-Policy 的 frame-ancestors 指令是新一代的替代方案。浏览器会在渲染 iframe 前先检查这些响应头不满足条件就直接拒绝外部表现就是空白 iframe。如果是你自己部署的页面解决办法是把响应头配置成只放行你信任的域名。如果是第三方页面正规做法是优先寻找官方嵌入入口比如视频平台的 embed 链接、地图平台的嵌入 API这类页面专门为 iframe 设计不会设置拦截。退一步的方法是让你的后端服务把第三方页面内容请求回来再由你的域名输出但这个前提是内容授权和版权协议允许。别为了抓数据去硬绕过对方的安全策略合规意识是底线。5.2 跨域报错别想着绕过在开发者工具里执行一段 JS尝试读取 iframe 内部元素控制台往往会报这个错Blocked a frame with origin https://a.com from accessing a cross-origin frame。这句话就是同源策略在拦截。同源的规则是协议、域名、端口三者完全一致。只要有一项不同两边就被视为互不信任的源。这种限制防止恶意页面通过 iframe 读取用户在其他网站上的隐私数据。比如你嵌了一个支付页面的 iframe如果父页面能随意读取 iframe 内部 DOM攻击者就能在你不知情的情况下拿到卡号输入框的内容。所以遇到跨域报错正确思路不是绕过而是换方案。同源但只是子域不同的场景部分老项目可以尝试把两边 document.domain 设置为相同主域但新项目不推荐。完全跨域的页面唯一稳妥的通道就是 postMessage写法在 3.3 节。还有一种常规替代是后端中转让服务端帮你去跨域通信、再返回结果但这已经不属于 iframe 内部交互的范畴了。5.3 常见问题速查表结合上面所有内容我整理了一张快速对照表适合开发时直接查现象可能原因解法iframe 空白X-Frame-Options / CSP 拦截检查响应头改用官方嵌入地址双层滚动条iframe 固定高度与内容高度不匹配同源用 contentDocument跨域用 postMessage隐藏滚动条无效外层 overflow 管不到内部文档改内部滚动容器或让内容不溢出跨域报错同源策略禁止跨源 DOM 访问用 postMessage 或后端中转爬虫抓不到 iframe 内容纯 HTTP 请求无法执行 JS用 Playwright 渲染后再提取动态 iframe 脚本空跑没等 iframe 出现就操作frame_locator 配合 wait_foriframe 影响 SEO搜索引擎对 iframe 内容索引很有限核心内容不要放进 iframe5.4 关于 SEO 与体验补充三条经验第一核心内容千万别用 iframe。如果你的文章正文、商品标题、价格、评论等关键数据全塞在 iframe 里搜索引擎爬虫很可能只看得到外层一个空壳。即使能爬到内部 URL也很难把两者关联起来做权重传递。做内容站、电商站的朋友尤其要注意这点。第二注意焦点管理和键盘可达性。用户按 Tab 进入 iframe焦点可能被内部页面接管按多少次都跳不出去。这对无障碍体验是很大的减分项。如果确实需要 iframe尽量在里面放简单静态内容复杂交互页面最好用组件方案替代。第三移动端要记得单独适配。iframe 内部页面如果是桌面版宽度手机上会出现横向滚动观感很糟。常见做法是根据屏幕宽度动态调整 iframe 尺寸或者干脆在移动端隐藏某些不重要的 iframe 区域。这些经验看着简单都是做了几个项目之后被用户反馈催着改出来的。写代码时觉得 iframe 省事交付后才发现每个滚动条、每次焦点跳转都可能成为投诉点。提前把这些场景想清楚比事后返工划算太多。最后再分享一点个人心得。用 iframe 这么多年我的判断标准一直很简洁这个内容我是不是只有使用权、没有修改权。如果是iframe 就是首选如果不是就优先把内容合并进主文档流用组件化方案做。遇到 iframe 相关报错时先回答三个问题同源还是跨域动态还是静态内容是否受控想清楚这三件事九成的问题不用查资料也能定位。如果你在做数据采集我建议把 Playwright 的 frame_locator 练熟它帮你省掉的不只是等待代码还有大量排查疑难杂症的精力。
返回列表