首页 / 资讯中心 / 文章详情

HTML代码复用方案全指南:从ifram到Web Components的选型之路

HTML代码复用方案全指南:从ifram到Web Components的选型之路 ★ FEATURED ARTICLE
做前端这些年我几乎每隔一段时间就会被问到同一个问题网站导航栏、页脚、侧边栏在几十个页面上重复出现产品经理改一句话我要全局替换十几个文件有没有办法让HTML代码真正复用起来这问题看着简单但真要落到技术选型上坑比想象中多。HTML代码复用从来不是“能抽出来就行”它牵涉到部署方式、开发协作、动态数据注入、缓存更新甚至SEO解析不同场景下的最优解可能完全不同。这篇文章我把实际项目里用过和梳理过的复用方法从头到尾捋一遍把每种方案的原理、适用边界和容易翻车的地方讲清楚给你一份可以直接落地参考的选型指南。1. 先想清楚HTML代码复用到底在复用什么很多人一上来就纠结“用哪个工具”但我的习惯是先分清楚要复用的东西属于哪一层。HTML代码复用表面上是“把一段标记存下来在别处引用”但实际项目里被复用的东西通常有三种完全不同的形态。1.1 三种复用对象静态片段、结构模板、功能组件第一种是静态片段比如站点公共头尾、备案信息、版权声明。这些内容没有变化不依赖数据也不带交互纯粹是为了少写重复标签。处理这种对象最简单的include、服务端包含甚至复制粘贴都能解决。第二种是结构模板比如商品卡片、用户列表项、弹出框。它们的HTML骨架是固定的但里面的文字、图片、链接需要根据数据动态填充。这种对象单靠“引入一段HTML”不够还得解决“模板加数据”的问题所以模板字符串、template标签、渲染函数是更合适的手段。第三种是功能组件比如日期选择器、折叠面板、带校验的搜索框。它不仅有固定的HTML结构还包含内部样式、事件绑定、生命周期逻辑甚至要能接受外部参数。这种级别需要真正的封装能力已经到了Web Components或前端框架组件化的范畴。把这三类混为一谈是很多方案做到一半做不下去的根本原因。你拿iframe去复用静态片段会平白引入隔离和性能问题你又拿模板字符串去做功能组件代码会慢慢变成一团谁都看不懂的拼接逻辑。1.2 为什么HTML比JavaScript难复用JavaScript之所以容易复用是因为它天生有模块系统文件可以import函数可以传参对象可以继承。HTML不一样它是一段静态标记浏览器解析完就成了DOM树没有“运行时加载模块”的原生能力。想复用HTML本质上只有两条路要么在“交付给浏览器之前”把它拼好要么在“浏览器运行之后”把它插进去。前者靠服务端模板或构建工具后者靠JavaScript动态创建DOM。还有一个容易被忽略的问题HTML一旦涉及动态数据就要连带考虑事件绑定和样式的生命周期。复用一个“按钮”不是把标签复制过去就行还要保证点击事件、视觉样式、焦点状态都在。这就导致了纯标签层面的复用很简单但一旦叠加交互和样式难度会指数上升。1.3 选型前先判断四个维度我每次设计复用方案都会先问四个问题复用的内容需不需要动态数据填充需不需要把内部样式和外部页面隔离团队是纯前端还是能控制服务端模板或构建流程这个页面需不需要被搜索引擎收录这四个维度基本上能圈定方案范围。不需要动态数据的静态站服务端包含或构建工具就够需要动态数据但不想上框架模板字符串加渲染函数最能解决问题既要动态数据又要样式隔离Web Components是原生路线里最合适的选择如果团队已经用了组件化框架那就没必要绕过框架另搞一套用好框架自身的组件能力才是最高效的HTML代码复用。2. 从最原始到最简单iframe、服务端包含与fetch直插先说几种体积不大、理解成本最底的方案。它们不是最时髦的但在特定场景下依然有效。我不是为了凑篇幅才提它们而是因为这些方案在存量项目里太常见了你总得知道它们为什么存在、什么时候该果断放弃。2.1 iframe复用彻底隔离但也彻底隔离了麻烦iframe算是最无脑的复用方式把公共的头部、侧边栏或某个功能模块单独做成一个HTML文件需要的地方用iframe srcheader.html引进来。这样做的好处是隔离性极强iframe内部的脚本和样式无论如何都不会污染宿主页面宿主页面的全局样式也不会影响iframe内部。如果你的团队要嵌入第三方提供的一段内容iframe仍然是最安全的选择。但隔离是把双刃剑。iframe的典型问题很多首先大量使用iframe会让页面产生额外的网络请求和渲染开销一个页面上挂五六个iframe性能基本不会好看。其次iframe高度默认不会跟随内容自适应经常出现两层滚动条需要脚本去量高度再设定高度非常麻烦。再者搜索引擎对iframe内的内容解析有限大量关键内容放在iframe里会直接影响SEO。最后宿主页面和iframe之间通信只能靠postMessage主项目想直接操作iframe里的DOM需要绕一条跨文档的路在合规和不跨源的情况下都很别扭。我的结论是iframe适合“真正独立的模块”比如在线编辑器、地图、第三方支付页面但不适合用来复用站内的公共头部和卡片列表。2.2 服务端包含老方案但服务端渲染项目依然好用如果你的页面是服务端渲染的那么服务端模板引擎或SSI服务端包含其实是成本最低的HTML代码复用方式。以SSI为例只需要在页面里写!--#include virtual/common/header.html --服务器在返回HTML之前就会把公共文件拼好。用户拿到的是一段完整的HTML浏览器不需要额外请求也不存在动态插入导致的闪烁。服务端模板引擎的原理类似只是把“包含文件”升级成了“包含模板块”还能带参数渲染。这类方案的优势是顺带解决了SEO问题因为最终返回的HTML是完整的。代价是依赖服务端环境本地纯静态预览时没法直接看到效果需要起一个支持对应规则的服务。另外服务端渲染的HTML如果有大量公共脚本缓存策略做得不好每次改动都可能要全站刷缓存这块在后面避坑清单里细说。2.3 纯前端fetch加innerHTML简单直接但细节多如果项目没有服务端又不想为了一个公共头尾引入构建工具最常见的手段就是用JavaScript把公共HTML片段fetch进来再塞到页面容器里。这个思路很朴素也确实能解决一部分问题。async function loadComponent(el, url) { try { const response await fetch(url); const html await response.text(); el.innerHTML html; } catch (err) { console.error(加载失败, err); } }用法上就是给目标元素加个约定好的数据属性或id然后逐一调用。我在一个老后台系统里就见过这种写法确实用很少的代码把公共头和菜单抽了出去。但这个方案有几个细节不处理就会踩坑。第一fetch请求存在跨域限制如果页面和公共片段不在同一个源需要服务端配合CORS。第二通过innerHTML插入的script标签在很多浏览器里不会自动执行公共头里的菜单初始化脚本可能直接失效解决思路是把初始化逻辑封装成函数在插入完成后显式调用。第三公共片段如果被多个页面引用很容易出现“改完公共文件用户浏览器里还是旧内容”的情况因为HTML片段本身可能被缓存这时需要在请求URL上加版本号比如header.html?v20250101。第四页面加载完再去fetch公共头会出现短暂的空白或布局抖动体验不好所以要在页面渲染早期就发起请求或者至少给容器留一个最小占位高度。站在今天的视角fetch加innerHTML更适合做“页面局部的轻量复用”和“不常改的静态排行榜、公告条”不适合做复杂组件。如果你已经开始要维护多段动态模板就进入下一章。3. 用模板字符串和数据驱动实现“写一次渲染多次”当复用对象从“一段完整的HTML”变成“一个会被数据重复填充的卡片”时核心问题就不再是“引入文件”而是“数据和结构怎么结合”。这章说的都是不依赖框架也能用好的方法。3.1 模板字符串最直觉的拼接方式ES6的模板字符串让HTML拼接舒服了很多。过去写字符串拼接要用加号还要小心引号嵌套现在可以这样写function renderCard(item) { return div classcard h3${item.title}/h3 p${item.description}/p a href${item.link}查看详情/a /div ; }对简单场景来说模板字符串就是模板本身变量用${}往里塞代码很直白。列表渲染也不复杂function renderList(items) { return items.map(renderCard).join(); }这样写的好处是没有额外依赖浏览器原生支持调试时看到的就是一段可读的HTML字符串。缺点也同样明显模板字符串里的逻辑和展示是耦合的一旦业务复杂函数里会塞满循环、条件判断和嵌套三元表达式可维护性迅速下降。另外把用户数据直接插进HTML时必须格外小心否则就是XSS漏洞。3.2 必须做的HTML转义别让模板功亏一篑动态数据进入HTML之前一定要做转义。以item.comment为例如果用户提交的内容包含script标签或img onerror...直接插入模板字符串这段内容会被浏览器当成真正的HTML解析轻则页面布局被破坏重则被注入恶意脚本。我常用的转义函数长这样function escapeHtml(str) { return String(str).replace(/[]/g, function (match) { return { : amp;, : lt;, : gt;, : quot;, : #39; }[match]; }); }原理很简单把HTML里有特殊含义的字符替换成对应的实体字符。等浏览器解析这段HTML时实体字符会还原成普通文本而不是被当作标签或属性分隔符。用这个函数包住所有外部数据之后再放进模板字符串才算安全。3.3 原生template标签让模板在HTML里而不是在字符串里纯用模板字符串模板代码藏在JavaScript文件里要让非技术同事理解模板长什么样很费劲。原生HTML里其实提供了一个专门干这事的标签template。它里面的内容不会被浏览器渲染也不会占据布局但可以让JavaScript随时“复制”一份出来使用。template idcard-tpl div classcard h3 classcard-title/h3 p classcard-desc/p /div /template对应的JavaScript代码const tpl document.getElementById(card-tpl); function renderCard(item) { const fragment tpl.content.cloneNode(true); fragment.querySelector(.card-title).textContent item.title; fragment.querySelector(.card-desc).textContent item.description; return fragment; }这段代码的关键是cloneNode(true)。template.content是一个DocumentFragment直接使用同一个节点会导致它只能被插入一个位置但深克隆之后每个卡片都拿到一份全新的DOM结构互不影响。和模板字符串比template的优点是HTML结构留在HTML文件里维护成本更低同时用textContent赋值而不是拼字符串天然规避了XSS问题因为textContent只会设置文本节点不会解析标签。它的局限是仍然需要手动写查询和填充逻辑数据和模板之间的绑定关系不够直观不适合嵌套特别深的结构。3.4 渲染函数从“拼HTML”到“渲染DOM”越复杂的复用我越不建议用大段字符串拼接而是改用渲染函数直接构建DOM节点。比如一个列表项先用document.createElement创建根节点再用innerHTML设置内部静态结构这样既保留了HTML的可读性又避免了整体字符串拼接带来的转义风险。function createCard(item) { const card document.createElement(div); card.className card; card.innerHTML h3/h3p/p; card.querySelector(h3).textContent item.title; card.querySelector(p).textContent item.description; card.addEventListener(click, () handleCardClick(item)); return card; }这个方案的逻辑跟template很像但更灵活因为事件绑定、属性设置、状态初始化都可以集中在渲染函数里。之后无论是插入列表还是替换内容只要调用函数就能拿到一个“活”的DOM节点。缺点是要写的原生DOM操作代码偏多每个新组件都得从头写一遍查节点、绑事件的样板代码复用性停留在“函数复用”还不是“自定义元素”级别的封装。4. Web Components让复用从“贴代码”升级为“封装成组件”如果你既不想用重型框架又希望HTML、样式、脚本三者能打包成一个真正可复用的单元那Web Components就是原生路线里的正解。它不是一个单一API而是由自定义元素、Shadow DOM、HTML模板和插槽共同组成的一套标准。4.1 自定义元素先把“标签”变成自己的自定义元素让你能注册一个新标签比如user-card然后浏览器在遇到这个标签时调用你定义的类来构建对应的DOM。class UserCard extends HTMLElement { connectedCallback() { this.innerHTML div classuser-card h3/h3 p/p /div ; } } customElements.define(user-card, UserCard);在页面里直接写user-card/user-card这段HTML就会被“升级”成对应的自定义组件。注意自定义元素名必须包含连字符这是为了避免和原生HTML标签冲突也方便在语法层面识别。光做到这一步其实和“用一个类包装渲染函数”没有本质区别。真正提升复用层面的是Shadow DOM。4.2 用Shadow DOM实现样式和DOM隔离Shadow DOM是Web Components里最值钱的部分。它能让组件内部的DOM和样式与外部页面隔离通俗地说组件内部是一间“自带墙的房间”外边的样式进不来里面的样式也出不去。class UserCard extends HTMLElement { constructor() { super(); const shadow this.attachShadow({ mode: open }); shadow.innerHTML style .user-card { border: 1px solid #ddd; padding: 12px; } h3 { margin: 0 0 4px; } /style div classuser-card h3/h3 p/p /div ; } } customElements.define(user-card, UserCard);我把样式直接写在shadow.innerHTML里这样.user-card这个类名在其它页面随便用都不会和组件内部的样式冲突。类似的组件内部的DOM查询不会意外选到外部元素外部脚本也很难误操作内部节点。attachShadow的mode参数选open表示外部可以通过element.shadowRoot访问内部调试方便如果选closed外部就无法直接拿到内部DOM但这种“隐藏”更多是工程约束不是真正的安全边界。4.3 属性监听和生命周期让组件能接收参数复用组件不能只是一张死脸它得接受外部传入的数据并响应变化。Web Components提供了observedAttributes和attributeChangedCallback来处理属性变化。class UserCard extends HTMLElement { static get observedAttributes() { return [name, desc]; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue newValue) return; if (name name) { this.shadowRoot.querySelector(h3).textContent newValue; } if (name desc) { this.shadowRoot.querySelector(p).textContent newValue; } } }使用方式很直观user-card name张三 desc前端开发者/user-card属性变化时组件能自动更新。但要注意属性值只能是字符串如果需要传对象或数组要么用.setAttribute(data, JSON.stringify(data))要么直接通过组件实例的属性赋值比如读取this.config {...}再手动触发渲染。这也是我在实际项目中经常看到的困惑点。4.4 插槽和组合让复用不只是“固定的盒子”一个真正复用的组件往往需要允许使用者在中间塞自定义内容。Web Components里的slot就是干这个的。组件内部定义一个插槽外部内容会被“投影”进去。shadow.innerHTML style ... /style div classcard h3卡片标题/h3 slot/slot /div ;外部使用user-card p这里是自定义内容/p /user-card没有插槽的组件像一台封死的机器有插槽的组件才像一个可扩展的容器。更高级的用法还可以给插槽命名让外部内容指定放到组件内的不同位置。这个模式在框架组件里叫slot原理和原生Web Components是相通的。4.5 Web Components实际项目里的坑第一Shadow DOM内部的部分CSS属性具有一定穿透性比如color、font-family这类继承属性在边界处仍然可能影响内部文本。这不是BUG但新手常被迷惑需要显式在组件内部设置继承属性。第二表单相关组件如果要做在Shadow DOM里要注意表单提交时内部控件不会自动参与外部表单需要手动处理值和事件。第三组件的可访问性需要额外照顾比如焦点管理、ARIA属性都需要自己写浏览器不会自动帮你补齐。第四旧一些的浏览器环境可能需要引入老牌的polyfill库不过对现代前端项目来说这个问题已经越来越小。如果项目兼容目标是近几年的浏览器原生Web Components已经可以直接用。5. 框架与构建工具里的复用用工程化手段消灭重复聊到这儿必须把前端框架和构建工具这条更主流的路放进来了。毕竟对大多数项目来说HTML代码复用最终是靠组件化和构建流程解决的而不是用原生API“硬造轮子”。5.1 把页面拆成组件树现代组件化框架Vue、React等的核心思想是组件化页面是一棵组件树树的每个节点独立管理自己的模板、样式和状态。拿一个列表页来说它可以拆成页面容器、筛选栏、列表区、空状态提示、分页器等多个组件。筛选栏和分页器这类通用组件在不同页面挪过来直接用这才是真正大规模的HTML代码复用。框架组件比原生方案强在几个地方模板支持条件判断、循环、事件绑定不用手写DOM操作状态变化能自动触发视图更新不用手动查DOM节点改内容组件之间通过props或插槽传数据接口清晰。考虑到大多数人团队内部已经在用这类框架复用公共UI这件事早就被框架内置能力接管了。但组件化不是银弹。组件拆得越细状态传递和通信成本越高。我在一个后台项目里见过把按钮都封装成组件的结果一个页面上几十个组件实例每个都要传几个props任何一次改动都要跨组件联调查。我的经验是复用应该发生在“真正重复两次以上”的地方而不是“未来可能用到”的假设上。5.2 构建工具层面的HTML包含就算不用框架构建工具也能帮我们把HTML片段当作模块来复用。比如常见的静态站点生成器和HTML包含插件允许你维护一个公共头文件然后构建时统一插入所有页面。和运行时fetch相比构建时拼接的优势是页面最终交付的HTML完整且不依赖JavaScript执行对SEO和首屏更友好。这类方案的原理不复杂你在源文件里写一个类似!-- include common/header.html --的占位注释构建工具看到后读取对应文件把内容替换进去最终生成一个个已经拼好公共代码的HTML文件。改公共头的时候只需要改一个文件重新构建就能全站更新。对于没有技术背景的静态站维护者来说这比要求他们写JavaScript模板更友好。5.3 服务端模板引擎参与复用如果项目是服务端渲染那么服务端模板引擎也属于HTML代码复用的重要成员。它做的事情和SSI类似但表达能力更强支持循环、条件、布局继承和局部模板。所有这些处理都在服务端完成响应给浏览器的就是完整HTML不需要额外请求也不会出现运行时拼接导致的闪烁。服务端模板复用的核心优势是“全链路可被搜索”因为HTML到达浏览器之前就已经定稿。对于内容型网站来说这是运行时JS渲染没法比的。缺点是模板维护在服务端前端改动模板需要走部署流程模板引擎的语法各有各的写法团队成员如果前后端分离惯了上手成本需要评估。5.4 混用策略不一定非要“选边站”实际项目里很少只用一种方案。我自己在维护一个小型落地页时会这样组合用服务端模板或静态构建去拼公共头和页脚保证SEO和首屏页面里的交互卡片用Web Components或轻量渲染函数来承载如果某个区域需要完全独立的三方模块再用iframe隔开。这种混用听起来不够“纯粹”但在真实业务里最稳。关键是不让某个技术方案绑架全部需求每层选最合适的手段复用的维护成本才会降下来。6. 方案对比与避坑清单读到这里你已经见过了从iframe到Web Components再到框架组件的各类方法。最后一章我把它们放在一起做对比并整理一份实际项目里最常遇到的坑。6.1 各HTML代码复用方案对比表方案动态数据样式隔离SEO友好实现复杂度推荐场景iframe弱强差低独立第三方模块服务端模板/SSI强无好中服务端渲染内容站fetch innerHTML中无视预渲染而定低老项目轻量改造模板字符串/渲染函数强无视渲染时机而定中简单动态列表和卡片template标签中无视渲染时机而定中需要集中管理HTML结构Web Components强强视SSR方案而定中高跨框架复用的原生组件框架组件强强可SSR高大型应用协作开发看表之余我建议你按自己的项目实际情况去圈定方案页面是否会被搜索引擎收录团队是否能接受构建或服务端部署组件是否需要跨框架使用。这几个问题的答案往往比任何一张对比表都要准。6.2 我踩过的坑第一公共头用fetch加innerHTML引入后页面突然多出一条“未定义变量”的报错。原因就是公共HTML里的script被插入后没有执行。后来我把公共逻辑抽成一个全局初始化函数在fetch完成后显式调用问题才消失。第二模板字符串里拼用户评论时忘了转义测试环境里还没问题上线后一个用户在昵称里放了尖括号整个页面的样式全乱。从那以后我把转义函数做成了公共工具所有拼HTML的入口都强制过一遍。这属于不遇到一次就记不住的教训。第三Web Components在某个页面能正常渲染在另一个页面却完全不显示。排查了半天发现是因为另一个页面在组件类定义之前就渲染了DOM而页面别处写了一句document.write把脚本执行顺序搞乱了。后来我把组件脚本改成在页面底部统一加载并在使用前检查customElements.get(user-card)是否存在才彻底稳定。第四公共片段做了版本号更新但服务端返回的HTML里还带旧的缓存头导致线上至少半天内拿不到最新公共头。不仅要在片段请求上加版本号还要确认服务端对HTML本身的缓存策略。6.3 我现在的选型思路如果是纯静态内容型网站我会尽量把组件往服务端模板或构建阶段靠减少浏览器运行时的负担如果是工具型、应用型网站我会明确使用组件化框架并把复用粒度控制在“至少重复两次”的模块上如果是一个需要跨项目复用、甚至希望脱离主框架独立存在的业务组件我才优先考虑Web Components。全站尽早统一“模板存放位置”和“更新流程”也很重要。哪怕是最简单的方案只要确定下来就能避免项目后期出现“这个页面用iframe、那个页面用模板字符串、另一个页面又是构建时注入”的混乱局面。技术选型讲究的是在团队能接受的成本和维护边界内可预期地解决重复问题。最后分享一个实际操作中的小技巧无论用哪种方案我都会给复用的组件或片段加一个稳定的命名前缀比如x-开头或mc-开头。这个前缀既是视觉标记也方便做统一的查找和替换。下次产品再提“把导航栏文案统一改一下”你不需要翻着十几个页面逐个改只需要定位一个入口剩下的交给复用机制就好。
阅读完成 · 觉得有帮助?
咨询建站