ARTICLE DETAIL

资讯详情

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

HTML框架详解:frameset与iframe的对比及通信实践

HTML框架详解:frameset与iframe的对比及通信实践 1. 从一次面试聊起为什么要懂HTML框架两年前我去一家公司面试前端岗技术面聊得挺顺最后面试官突然问了一句“frameset和iframe有区别吗现在还有没有人在用frameset”我愣了一下因为说实话工作后一直在用Vue和React这两个标签在我日常开发里基本没出现过。那次面试我没答好但对方告诉我老项目维护里经常会碰到历史遗留的框架页面不懂这些标签就看不懂别人的代码结构。后来我确实在接外包时遇到过一个2008年左右搭建的老后台系统整个页面就是一个frameset布局左侧菜单栏一个frame右侧内容区另一个frame顶部还有一个固定导航栏。第一次打开源码时确实有点懵但理清结构后反而觉得这种布局方式很直观——每个区域独立加载HTML文档互不干扰改左侧菜单完全不用动右侧页面。这篇文章我想把HTML框架这三个标签讲透。内容会分成两条线一条是已经被Web标准废弃但历史项目中大量存在的frameset/frame老框架布局另一条是今天依然活跃在各种后台管理系统、富文本编辑器、第三方嵌入场景里的iframe。两者虽然都叫“框架”但设计思想和应用场景完全不同。同时我会把框架页之间如何通信、如何互相调用函数、如何隐藏滚动条、如何适配老项目这些问题一并解决。适合谁看刚学HTML、对框架标签只有一个模糊概念的初学者接手老项目需要快速读懂frameset布局的维护者以及工作中频繁和iframe打交道、想彻底搞懂跨页面通信机制的前端开发。这篇文章不涉及任何框架库纯原生HTMLJavaScript只要浏览器就能跑通所有例子。2. 框架页设计思路frameset老布局与iframe嵌入的本质区别2.1 frameset的诞生逻辑把浏览器窗口切分成多个独立文档frameset翻译过来叫“框架集”核心思想是把浏览器整个可视区域按行或者按列切割成若干块每一块独立加载一个HTML文件。想象一下把你的显示器划分成几个显示器拼接屏每个屏各自显示不同的内容对外看起来还是一个整体。这在2000年前后特别流行因为那时候网页还是纯静态的服务端模板技术不成熟CMS还不普及用frameset可以轻松实现“左边菜单、右边内容”的经典后台布局菜单和内容彼此独立不用每次点击都刷新整个页面。一个标准的frameset页面长这样!DOCTYPE html html langzh-cn head meta charsetutf-8 title经典后台布局/title /head frameset cols200px,* frame srcnav.html nameleftFrame frame srccontent.html namemainFrame /frameset /html注意几个当时的硬性要求使用frameset后不能再写body标签cols是按列切割rows按行切割每一个frame必须通过src指定加载的页面name属性是给每个子窗口起名字方便a标签的target属性指定在哪个窗口打开链接。这套组合拳在当时已经足够解决大部分网页布局需求尤其适合管理后台这种“功能多、结构固定、交互不复杂”的场景。2.2 iframe的设计定位页面内部的嵌入式窗口iframe全称是inline frame翻译为内联框架。它和frameset最大的区别在于iframe不需要独立占据整个页面你可以把它嵌在任何普通HTML页面的任意位置——段落中间、侧边栏、弹窗里都行。父页面本身就是完整的文档iframe只是文档里一个“可以独立加载其他页面的容器”。这种设计彻底解放了布局思路。你不必为了左右结构做一个专门的外壳页面直接在普通页面里插入一个iframe用CSS控制它的宽高、位置、边框、滚动条。从HTML5开始浏览器官方支持iframe的所有核心特性直到今天依然是后端管理系统集成报表、地图、第三方支付页面的标配方案。两者的选择逻辑其实很简单整个页面都是框架结构用frameset页面中某一块区域需要嵌入外部页面用iframe。需要说明的是frameset和frame标签在HTML5标准中已经被移除只是浏览器出于兼容性考虑仍然支持渲染。新项目别再用了老项目也必须清楚这一点。3. frameset与frame核心实操搭建一个可用的框架页3.1 行列切割规则与星号、像素的关系frameset的cols和rows属性支持三种单位固定像素、百分比、星号*。星号表示“剩余空间分配”注意是分配不是均分。举个例子frameset rows80px,*,100px frame srctop.html frame srcmiddle.html frame srcbottom.html /frameset这个结构就是一个经典的上中下三行布局顶栏80像素底栏100像素中间部分占据剩余全部高度。如果没有星号只有两个frame比如rows50%,50%那么两个窗口各占一半。如果是一个星号和两个固定像素同时出现星号就是“吃掉所有剩余空间”。星号还能带系数rows2*,1*表示把剩余空间分成三份第一个frame拿两份第二个拿一份。这个特性在老的报表系统里很常见比如左侧导航占三分之一、右侧内容占三分之二的布局直接写cols1*,2*就可以。实操中建议固定宽度用px自适应区域用星号不要全部用百分比。原因很简单百分比在不同屏幕分辨率下可能因为整除产生1到2像素的偏差左右两个frame之间会露出一条难看的body背景色。星号是浏览器自动计算的整数像素值基本不会有这个问题。3.2 嵌套frameset实现复杂布局框架页完全可以套框架页。当需要更复杂的布局时——比如上方是顶部导航栏下方左边是菜单、右边是内容区——直接在frame的src里再放一个frameset页面即可或者在同一个frameset内部再次嵌套。frameset rows60px,* frame srcheader.html nameheaderFrame frameset cols200px,* frame srcmenu.html namemenuFrame frame srccontent.html namecontentFrame /frameset /frameset这个写法比拆成两个页面更清晰结构层级一目了然第一层按行分顶部和主体第二层在主体区域内部按列分菜单和内容。浏览器渲染时会自然形成嵌套的窗口结构每个frame的window对象可以互相访问。嵌套时要注意一点每一层frameset内部的所有frame尺寸之和必须等于外层分配的空间百分比或像素可以简单理解为“一个100%的空间被下层frameset再次分成若干份占比总和等于100%”即可。这个容易在多浏览器适配时出问题建议固定宽度部分优先写死自适应部分放最后。3.3 noframes老框架页的兼容性保底方案当浏览器不支持框架时noframes标签可以显示备用内容用法类似body的替代品。frameset cols200px,* frame srcnav.html frame srccontent.html noframes body您的浏览器不支持框架请点击a hrefcontent.html这里/a访问内容。/body /noframes /frameset今天来看noframes已经没有实际意义因为所有现代浏览器都支持frameset渲染。但维护老代码时我建议保留它理由很实际nostalgia企业内网里偶尔还有老版IE或定制化浏览器这种兜底提示虽然丑但至少用户知道页面不是白屏而是确实存在需要升级浏览器的明确信号。3.4 框架集页面与子页面之间的链接控制frameset布局的核心交互是“点击左侧菜单右侧内容区变化”。如果不做任何处理链接会在当前frame窗口内部打开结果就是整个页面变成了只有右侧内容那一小块布局瞬间崩掉。正确的做法是使用target属性配合frame的name。左侧菜单页面nav.html中的链接这样写ul lia hrefpage1.html targetmainFrame页面1/a/li lia hrefpage2.html targetmainFrame页面2/a/li /ultarget值为mainFrame时链接指向的页面会替换namemainFrame的frame窗口内容左侧菜单和顶部导航保持不动。类比成电视频道切换的话整个电视机是浏览器窗口频道是各个frame换台只换当前选中的那个频道其他屏幕区域不受影响。如果希望链接在整个浏览器窗口中打开脱离框架结构target值用_top希望在父级frameset窗口中打开用_parent希望新开浏览器标签页用_blank。这几个值和普通iframe里的情况完全一致理解target和name的配合机制后老后台的页面跳转逻辑就没什么秘密了。4. iframe深入解析现代网页里的嵌入式窗口4.1 iframe基础语法与常用属性iframe的使用比frameset简单很多就是在普通HTML文档中插入一个元素!DOCTYPE html html langzh-cn head meta charsetutf-8 titleiframe容器页面/title style .embed-container { width: 100%; height: 600px; border: 1px solid #ddd; } /style /head body h1报表区域/h1 div classembed-container iframe srcreport.html width100% height100% frameborder0 namereportFrame scrollingauto/iframe /div /body /html常用属性逐个说srciframe加载的页面地址必填。width/height宽高单位px或百分比现代用法大多通过CSS控制属性值往往写个100然后交给CSS覆盖。nameiframe名称配合form或a标签的target使用。frameborder0或10表示无边框HTML5标准已不支持这个属性但老项目中还很常见拿到旧代码别觉得奇怪。scrollingyes/no/auto控制滚动条显示。HTML5同样废弃了该属性现代方案用CSS的overflow属性控制。sandboxHTML5新增的安全属性限制iframe内的行为比如不允许表单提交、不允许脚本执行、不允许导航等。嵌入不可信第三方内容时强烈建议启用。allow权限策略可以控制摄像头、麦克风、全屏等权限。比如嵌入一个视频会议页面需要写allowcamera; microphone。referrerpolicy控制iframe请求时的Referer信息用于隐私保护和防止泄露内部地址。4.2 主页面调用iframe内函数iframe调用主页面函数框架页之间互相调用函数是高频需求。同源前提下方法和frameset时代几乎一样。假设主页是parent.html里面嵌入namechildFrame的iframechildFrame加载child.html。主页要调用iframe中的函数// 主页parent.html中 const childWin document.getElementById(childFrame).contentWindow; childWin.refreshTable(); // 调用iframe内的全局函数反过来iframe内部要调用主页面的函数// 子页面child.html中 window.parent.refreshMenu(); // 调用父页面的全局函数如果要跨级访问——比如三层嵌套iframe里面再套iframe——可以逐级使用window.parent或者window.top。比如第三层要访问最外层主页用window.top.refreshHeader()。注意一个前提同源策略。只有父页面和iframe加载的页面协议、域名、端口完全一致时这些调用才会生效。如果跨域浏览器会直接报错Blocked a frame with origin... from accessing a cross-origin frame. 此时需要通过postMessage通信这个后面专门讲。我的一个实操习惯是所有跨页面调用都先判断是否存在再判断函数类型。因为iframe异步加载页面还没执行完脚本时contentWindow里的函数可能还不存在直接调用会抛TypeError。稳妥写法如下const childWin document.getElementById(childFrame).contentWindow; if (childWin typeof childWin.refreshTable function) { childWin.refreshTable(); }4.3 onload事件的正确使用时机iframe的onload事件在子页面完全加载完成后触发。需要等iframe内部DOM就绪后再调函数时这个时机非常关键。const iframe document.getElementById(childFrame); iframe.onload function() { const childDoc iframe.contentDocument; const table childDoc.getElementById(dataTable); console.log(table.textContent); };这里有一个经常踩的坑iframe已经加载完成之后你才给onload赋值事件永远不会触发。这在动态创建iframe时尤其容易踩坑。解决方法是先检查iframe的readyState或contentDocument是否可访问再决定直接执行还是等待onload。更现代的做法是用Promise包装加载状态function loadIframe(url) { return new Promise((resolve, reject) { const iframe document.createElement(iframe); iframe.src url; iframe.onload () resolve(iframe); iframe.onerror () reject(new Error(iframe加载失败)); document.body.appendChild(iframe); }); }这种方式在需要按顺序加载多个外部页面时特别好用。4.4 获取iframe内的DOM元素同源情况下除了访问contentWindow全局对象还可以直接操作iframe内部的DOMconst iframe document.getElementById(childFrame); const iframeDoc iframe.contentDocument; // 获取iframe内的document对象 const input iframeDoc.getElementById(username); input.value 张三;contentDocument是标准API在现代浏览器中兼容性很好。老项目的IE版本可能需要用iframe.contentWindow.document替代但现在基本不用担心。获取到document后所有常规DOM查询、修改、事件绑定都可以正常进行。跨域时contentDocument为null可以通过iframe.contentWindow.length判断是否跨域——这个值是当前页面内iframe数量跨域时仍可读取。不过别依赖这种特性做跨域突破我只是提醒遇到空白null时先排查是不是跨域导致的。4.5 隐藏滚动条的正确打开方式隐藏iframe滚动条看起来很基础但实际做的时候经常出现怎么做都留一道白边的情况。先说最直接的方案在iframe对应的内部页面CSS里处理/* 子页面中被嵌入的容器 */ body { overflow: hidden; }这个方案有一个理解成本overflow: hidden写在父页面中设置iframe本身没用因为滚动条属于iframe内部文档的视口不是iframe元素本身。子页面把body的overflow设为hidden后内容超出部分会被裁切不出现滚动条。还有一种常见场景需要保留滚动能力但隐藏滚动条外观适合做沉浸式全屏嵌入效果。方案是用CSS把滚动条宽度设为0/* 子页面中 */ ::-webkit-scrollbar { width: 0; height: 0; } html, body { scrollbar-width: none; /* Firefox */ }不过要提醒一下隐藏滚动条会降低页面的可发现性用户不知道往下还有内容对长页面来说体验未必是好事。如果是少量溢出我更推荐让滚动条正常显示系统默认样式不丑可访问性也好。4.6 让iframe自适应高度的常见方案iframe高度自适应是老生常谈的话题。嵌入内容的实际高度往往未知把iframe写死一个高度会留白或者截断于是各种自适应方案层出不穷。最简单可靠的做法是等iframe加载后用contentDocument取内容高度再设置iframe高度const iframe document.getElementById(childFrame); iframe.onload function() { const innerHeight iframe.contentDocument.documentElement.scrollHeight; iframe.style.height innerHeight px; };这方法能解决八成需求但有两个限制只能同源使用如果iframe内部内容在加载后动态增高——比如用户展开折叠面板、新插入图片——高度不会自动更新。这时需要用ResizeObserver监听子页面body的高度变化const observer new ResizeObserver(() { const innerHeight iframe.contentDocument.documentElement.scrollHeight; iframe.style.height innerHeight px; }); observer.observe(iframe.contentDocument.body);跨域场景下上述方法全部失效。可以改用postMessage让子页面高度变化时主动通知父页面这是目前跨域iframe自适应的标准做法。这个过程看起来像// 子页面中 function notifyHeight() { const h document.documentElement.scrollHeight; window.parent.postMessage({type: resize, height: h}, *); } // 在页面加载和DOM变化时调用notifyHeight // 父页面中 window.addEventListener(message, function(event) { if (event.data event.data.type resize) { document.getElementById(childFrame).style.height event.data.height px; } });postMessage的targetOrigin参数建议不要用*明确指定父页面的具体origin更安全。5. 框架页通信与安全细节绕过同源限制的几种思路5.1 同源判断与跨域报错排查“同源”指协议、域名、端口三者完全一致。我遇到过不少“明明域名一样为什么还报跨域”的案例最后检查发现是端口不一致一个页面通过http://localhost:8080访问内嵌的iframe却指向http://localhost:9090跨域报错没跑了。还有个坑是域名解析方式www.example.com和example.com也属于不同源。排查思路分三步走打开浏览器的开发者工具Console面板看具体报错内容。跨域错误通常描述得很清楚会同时指出两个页面的完整地址。核对协议http和https混用时几乎必报跨域这种情况要优先把混合内容问题解决掉。确认端口生产环境最常见的还是iframe外链被拒绝。一些知名网站明确设置X-Frame-Options响应头禁止被嵌入比如DataEase社区版就明确禁止通过iframe嵌入作为开源集成方案。遇到这种第三方限制技术上没有绕过的好办法应该从产品层面调整方案比如跳转新窗口或使用后端API获取数据在前端自绘。5.2 postMessage通信机制跨域页面对话的官方通道postMessage是HTML5提供的跨窗口通信API专治跨域。基本用法分为发送方和接收方两步。发送方// iframe子页面中 window.parent.postMessage(这是来自子页面的消息, https://parent.example.com);接收方// 父页面中 window.addEventListener(message, function(event) { // 重要验证消息来源 if (event.origin ! https://child.example.com) return; console.log(收到消息, event.data); });接受消息后第一件事一定是校验event.origin否则任何网站都能给页面发消息恶意消息可能导致严重安全问题。发送时指定targetOrigin同理确保消息不会发到非预期的窗口。实际项目里我通常是约定一套简单的消息协议用对象结构承载业务数据{ type: navigator, path: /user/list, params: { page: 2 } }接收方根据type字段分发处理逻辑。这样即使嵌入方和被嵌入方来自不同团队、不同域名也能通过一套约定稳定协作。5.3 sandbox属性给iframe上一道安全锁sandbox是一个布尔属性可以设置多个值之间用空格分隔。空sandbox表示“完全锁定”——子页面里不能执行脚本、不能提交表单、不能弹窗、不能导航父页面。具体到某个能力需要显式放开。常用取值组合sandboxallow-scripts允许执行脚本但不允许其他操作。sandboxallow-same-origin允许保持同源否则iframe被视为独立源。如果不加这个即使父页面和iframe域名完全一样也会被当成跨域处理。sandboxallow-forms允许表单提交。sandboxallow-popups允许弹出新窗口。sandboxallow-top-navigation允许iframe导航顶级窗口也就是修改父页面的URL。注意allow-same-origin和allow-scripts同时使用时有风险——iframe内的脚本可以修改自身src从而跳出sandbox限制。嵌入完全不可信的内容时千万别同时开这两个权限。5.4 X-Frame-Options与CSP对iframe的限制即使你在页面里写了iframe目标服务器也完全可以拒绝被嵌入。常见的响应头是X-Frame-Options取值如下DENY所有页面都无法嵌入。SAMEORIGIN同源页面可以嵌入。ALLOW-FROM uri仅指定来源可以嵌入但这个值兼容性不好现代浏览器基本已废弃。更现代的方案是Content-Security-Policy的frame-ancestors指令Content-Security-Policy: frame-ancestors self这个指令指定哪些来源可以嵌入当前页面。self只允许同源允许指定域名可以写frame-ancestors https://trusted.example.com。当服务器配置了这类响应头前端无论怎么写iframe都会被浏览器拦截。做第三方嵌入类产品需要提前和对方确认响应头策略否则辛辛苦苦做好了集成联调最后上线发现被CSP拦了整套方案推倒重来。6. 常见问题排查与避坑技巧实录6.1 框架页常见问题速查表现象可能原因解决方法frameset页面不显示内容白屏页面中仍有body标签删除body标签frameset下直接写frame点击链接后整个框架页消失target未设置或值不正确给目标frame加name链接target指向该nameiframe加载的页面显示空白跨域或服务器拒绝嵌入检查响应头X-Frame-Options/CSPiframe内部函数调用无效同源策略或加载未完成用postMessage通信或等onload后再调用子页面高度变化后iframe底部出现滚动条iframe高度固定未动态更新使用scrollHeight检测或postMessage通知更新iframe内页面的Cookie/登录态失效浏览器第三方Cookie策略确认SameSite设置必要时后端调整Set-Cookie属性动态创建iframe后onload不触发onload赋值晚于加载完成创建后若contentDocument可访问则直接执行6.2 踩坑记录iframe事件绑定失败的背后之前做一个客服聊天窗项目用户在主页点按钮希望iframe内部的聊天插件自动弹出。我在主页里直接写document.getElementById(chatFrame).contentWindow.showChat();结果控制台报错showChat is not a function。问题在于iframe的聊天插件是异步加载的按钮点击时插件还没加载完函数还不存在。后面改成按钮点击后先检查是否存在不存在就等iframe onload再调用问题解决。代码写成了这样function showChatInFrame() { const win document.getElementById(chatFrame).contentWindow; if (win typeof win.showChat function) { win.showChat(); } else { const iframe document.getElementById(chatFrame); iframe.onload function() { iframe.contentWindow.showChat(); }; } }这个模式在嵌入第三方报表、地图、聊天组件时几乎都用得上。6.3 踩坑记录iframe打印时内容被裁切项目需求是主页面有一个打印按钮打印iframe里的报表内容。刚开始直接用window.print()结果发现打印出来的内容被裁切。原因在于iframe内部的打印样式和控制打印区域的CSS没有正确设置。解决办法是调用iframe内部的打印函数const iframe document.getElementById(printFrame); iframe.contentWindow.focus(); iframe.contentWindow.print();如果子页面本身有专门的打印样式这种方式会直接打印子页面完整内容。如果打印的是父页面某个区域但其中含有iframe需要确认iframe内的高度是否在打印视口内完整展示必要时把iframe在打印时展开成全高。6.4 实操心得老框架项目维护的五个建议第一不要轻易重构。老项目如果运行稳定业务团队习惯了非必要不迁移。框架页虽然老但胜在结构简单只要理解了frame嵌套关系维护成本并不高。第二改造前先画一张框架结构图。把所有frameset嵌套、frame名称、每个frame加载的URL整理成一张表改动前先对照避免改错窗口。第三跨域通信统一走postMessage。即使当前域相同也建议用postMessage而不是直接访问contentWindow为以后可能的域名拆分留后路。第四iframe嵌入第三方页面之前一定要先做响应头检查。用curl命令一眼就能看到curl -I https://example.com/report看响应头里有没有X-Frame-Options或者Content-Security-Policy提前判断能否嵌入别等开发完联调才发现。第五文档化的价值远大于临时记忆。老框架项目的结构不是一眼能看懂的把自己的理解写成简单文档对后续接手的人非常重要。6.5 遭遇iframe被拒绝嵌入时的应对方案遇到第三方不允许iframe嵌入比如DataEase社区版明确禁止iframe嵌入有几个替代路线思路一后端代理。通过后端接口转发目标页面内容再把返回的HTML作为iframe的src或直接通过srcdoc渲染。这个方案只在目标页面没有额外接口鉴权和动态渲染时可行如果页面重度依赖前端JavaScript调用API代理得到的只是一纸空壳。思路二新窗口打开。用window.open或a target_blank打开目标页面虽然交互上不如iframe流畅但功能完整且不受限制。思路三调API拿数据自己画。很多平台虽然禁止iframe嵌入但提供了开放API。后端获取数据前端自己渲染图表和表格成本高一些但可控性最强可以完全贴合自身的UI风格。每次遇到这种限制我的决策习惯是先用最快方案验证curl看响应头能嵌入就直接嵌入不能嵌入就评估数据敏感程度和开发成本选上面三条路中合适的一条走下去。写在最后最近一个项目里我调试iframe跨域通信调试了一下午。最后发现是端口问题——父页面是8080子页面是9090所有postMessage都收了消息却没触发回调。我在控制台里把两个页面的window.location打印出来对比发现问题后改掉端口一切正常。这种排查过程比任何文档都更能帮助理解同源策略的边界。HTML框架这套技术frameset作为一面镜子反映出早期网页开发的思维模式——用空间分割来组织信息iframe则从诞生之后一直占据着网页嵌入场景的核心位置。学习的时候不用纠结于谁先进谁落后关键是看明白设计者要解决的问题。以后如果在老项目中震撼于那些写了二十年的框架页面可以顺着这套逻辑重新推演一遍设计者为什么这么分每个frame的name为什么取这个名字链接的target为什么指向这里想通了老项目也就没那么可怕了。
返回列表