HTML <template>:内容模板元素

<template> HTML 元素作为存储 HTML 片段的机制,这些片段可通过 JavaScript 稍后调用,或立即生成至shadow DOM 中。

属性

该元素包含全局属性。该属性是Element.attachShadow()方法的声明式版本,支持相同的枚举值。

open

向 JavaScript 暴露内部 shadow root DOM(推荐用于大多数使用场景)。

closed

向 JavaScript 隐藏内部 shadow root DOM。

**注意:**当节点中的首个<template>标签将此属性设置为允许值时,HTML解析器会在DOM中创建一个ShadowRoot对象。若该属性未设置、未设置为允许值,或同一父节点中已声明创建过ShadowRoot,则构造HTMLTemplateElement。已构造的HTMLTemplateElement 在解析后无法通过设置HTMLTemplateElement.shadowRootMode等方式转换为 shadow root。

**注意:**旧版教程和示例中可能出现非标准的shadowroot属性,该属性曾在Chrome 90-110版本中被支持。此属性现已移除,并由标准的shadowrootmode属性替代。

元素周期表

shadowrootclonable

将通过本元素创建的ShadowRootclonable属性值设为true。若设置此属性,使用Node.cloneNode()Document.importNode() 创建的克隆副本将包含一个 shadow root.

将通过本元素创建的ShadowRootdelegatesFocus属性值设为true。若设置此属性且 shadow tree 中不可聚焦元素被选中,焦点将委托给树中首个可聚焦元素。默认值为false

shadowrootserializable 实验性

将使用此元素创建的ShadowRootserializable属性值设为true。若设置此值,则可通过调用Element.getHTML()ShadowRoot.getHTML()方法(需将options.serializableShadowRoots参数设为true)对shadow root进行序列化。默认值为 false

使用说明

该元素不允许包含内容,因为HTML源代码中嵌套在其内部的所有内容实际上不会成为 <template> 元素的子节点。<template> 元素的 Node.childNodes 属性始终为空,您只能通过特殊的 content属性访问。但若对<template>元素调用Node.appendChild()等方法,则会将子节点插入到<template>元素本身,这违反其内容模型,且不会实际更新 content属性返回的DocumentFragment

由于 <template> 元素的解析方式,模板内部的所有 <html><head><body> 开头与结尾标签均视为语法错误并被解析器忽略,因此 <template><head><title>Test</title></head></template><template><title>Test</title></template> 效果相同。

使用<template>元素主要有两种方式。

模板文档片段

默认情况下,该元素的内容不会被渲染。对应的HTMLTemplateElement接口包含标准的content属性(无对应的content/markup属性)。该content属性为只读属性,持有包含模板所表示的DOM子树的DocumentFragment。该片段可通过cloneNode方法克隆并插入到DOM中。

使用content属性时需谨慎,因返回的DocumentFragment可能产生意外行为。详情请参阅下文规避DocumentFragment陷阱章节。

声明式 Shadow DOM

<template> 元素包含值为 openclosedshadowrootmode 属性,HTML 解析器将立即生成 Shadow DOM。该元素在 DOM 中会被替换为其内容,这些内容被包裹在 ShadowRoot 中,并附加到父元素上。这相当于通过声明方式调用Element.attachShadow()方法将 shadow root 附加到元素。

若元素的shadowrootmode属性值为其他数值,或未设置该属性,解析器将生成HTMLTemplateElement。同样地,若存在多个声明式shadow root,仅首个shadow root会被替换为 ShadowRoot 替换——后续实例将解析为 HTMLTemplateElement 对象。

示例

生成表格行

首先从示例的 HTML 部分开始。

html
<table id="producttable">
  <thead>
    <tr>
      <td>UPC_Code</td>
      <td>Product_Name</td>
    </tr>
  </thead>
  <tbody>
    <!-- existing data could optionally be included here -->
  </tbody>
</table>

<template id="productrow">
  <tr>
    <td class="record"></td>
    <td></td>
  </tr>
</template>

首先创建一个表格,后续将通过 JavaScript 代码向其中插入内容。接着是模板,它描述了代表单个表格行的 HTML 片段结构。

表格创建并定义模板后,我们使用 JavaScript 将行插入表格,每行都基于模板构建。

js
// Test to see if the browser supports the HTML template element by checking
// for the presence of the template element's content attribute.
if ("content" in document.createElement("template")) {
  // Instantiate the table with the existing HTML tbody
  // and the row with the template
  const tbody = document.querySelector("tbody");
  const template = document.querySelector("#productrow");

  // Clone the new row and insert it into the table
  const clone = template.content.cloneNode(true);
  let td = clone.querySelectorAll("td");
  td[0].textContent = "1235646565";
  td[1].textContent = "Stuff";

  tbody.appendChild(clone);

  // Clone the new row and insert it into the table
  const clone2 = template.content.cloneNode(true);
  td = clone2.querySelectorAll("td");
  td[0].textContent = "0384928528";
  td[1].textContent = "Acme Kidney Beans 2";

  tbody.appendChild(clone2);
} else {
  // Find another way to add the rows to the table because
  // the HTML template element is not supported.
}

最终结果是原始HTML表格通过JavaScript追加了两行新内容:

css

 

实现声明式Shadow DOM

本示例在标记开头包含了隐藏的支持性警告。若浏览器不支持shadowrootmode属性,该警告将通过JavaScript设置为显示状态。随后出现两个<article>元素,各自包含行为不同的嵌套<style>元素。第一个<STYLE>元素作用于整个文档。第二个元素因shadowrootmode属性的存在,其作用域限定在替代<TEMPLATE>元素生成的shadow root内。

html
<p hidden>
  ⛔ Your browser doesn't support <code>shadowrootmode</code> attribute yet.
</p>
<article>
  <style>
    p {
      padding: 8px;
      background-color: wheat;
    }
  </style>
  <p>I'm in the DOM.</p>
</article>
<article>
  <template shadowrootmode="open">
    <style>
      p {
        padding: 8px;
        background-color: plum;
      }
    </style>
    <p>I'm in the shadow DOM.</p>
  </template>
</article>
js
const isShadowRootModeSupported = Object.hasOwn(
  HTMLTemplateElement.prototype,
  "shadowRootMode",
);

document
  .querySelector("p[hidden]")
  .toggleAttribute("hidden", isShadowRootModeSupported);

声明式 Shadow DOM 与委托焦点

本示例演示了如何将 shadowrootdelegatesfocus 应用于声明式创建的 ShadowRoot,以及这对焦点产生的影响。

该元素同时显示不可聚焦的文本容器<div>和可聚焦的<input>元素。通过CSS为:focus状态的元素设置蓝色样式,并为宿主元素定义常规样式。

html
<div>
  <template shadowrootmode="open">
    <style>
      :host {
        display: block;
        border: 1px dotted black;
        padding: 10px;
        margin: 10px;
      }
      :focus {
        outline: 2px solid blue;
      }
    </style>
    <div>Clickable Shadow DOM text</div>
    <input type="text" placeholder="Input inside Shadow DOM" />
  </template>
</div>

第二个代码块完全相同,仅新增了shadowrootdelegatesfocus属性。该属性可在树中选中不可聚焦元素时,将焦点委托给树中首个可聚焦元素。

html
<div>
  <template shadowrootmode="open" shadowrootdelegatesfocus>
    <style>
      :host {
        display: block;
        border: 1px dotted black;
        padding: 10px;
        margin: 10px;
      }
      :focus {
        outline: 2px solid blue;
      }
    </style>
    <div>Clickable Shadow DOM text</div>
    <input type="text" placeholder="Input inside Shadow DOM" />
  </template>
</div>

最后通过以下CSS实现:当父级<div>元素获得焦点时,为其添加红色边框。

css
div:focus {
  border: 2px solid red;
}

结果如下所示。HTML首次渲染时,元素不带任何样式,如第一张图所示。对于未设置shadowrootdelegatesfocus的shadow root,点击除<input>外的任意位置焦点不会改变(若选中<input>元素则效果如第二张图)。

对于设置了shadowrootdelegatesfocus的shadow root,点击文本(不可聚焦)会选中<INPUT>元素,因为它是树中首个可聚焦元素。这也会使父元素获得焦点,如下图所示。

规避 DocumentFragment 陷阱

当传递 DocumentFragment 值时, Node.appendChild等方法仅将该值的_子节点_移动到目标节点。因此通常应将事件处理程序附加到DocumentFragment的子节点上,而非DocumentFragment本身。

请看以下 HTML 和 JavaScript 示例:

HTML

<div id="container"></div>

<template id="template">
  <div>Click me</div>
</template>

JavaScript

const container = document.getElementById("container");
const template = document.getElementById("template");

function clickHandler(event) {
  event.target.append(" — Clicked this div");
}

const firstClone = template.content.cloneNode(true);
firstClone.addEventListener("click", clickHandler);
container.appendChild(firstClone);

const secondClone = template.content.cloneNode(true);
secondClone.children[0].addEventListener("click", clickHandler);
container.appendChild(secondClone);

结果

由于firstCloneDocumentFragment,调用appendChild时仅其子节点会被添加到container中;firstClone的事件处理程序不会被复制。相反,由于事件处理程序被添加到secondClone的第一个_子节点_,调用appendChild时该处理程序会被复制,点击该节点时行为符合预期。

浏览器兼容性

共有 84 条评论

  1. 我在Timelinize[0]中大量使用<template>标签,该项目拥有相当复杂的用户界面,我完全用原生JavaScript编写——甚至没用jQuery。仅引入了少量库(用于地图绘制、日期时间处理,以及Bootstrap+Tabler实现美观样式),仅此而已。

    确实写过些通用JS模板,但我完全掌控前端实现,理解其运作原理,且无需编译。

    总之目前运行良好!<template>标签能高效布局组件,再用简单JS函数填充内容。

    代码注释中记录了一个细节:

        // 哇哦,克隆模板标签内容时必须使用firstElementChild(!!!!):
        // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template#avoiding_documentfragment_pitfalls
        // 为此耗费了太多时间。
        const elem = $(tplSelector);
        if (!elem) return;
        return elem.content.firstElementChild.cloneNode(true);
    

    弄明白这个之后,一切就顺风顺水了,而且功能非常强大。

    哦,不过我还没整理代码,因为当时只是在实验/赶工,所以有点像意大利面 🙂 如果你去查看代码,别介意乱七八糟的样子。

    [0]: https://github.com/timelinize/timelinize – 一款自托管应用,可将所有数据整合到本地计算机并组织成单一时间线:社交媒体账号、照片库、短信记录、GPS轨迹、浏览历史等。即将正式发布…

    • 克隆模板时应使用document.importNode()。

      模板内容与主文档分离存储,这正是其保持惰性的原因。importNode()会将节点导入文档,使其立即获得正确的原型(包括自定义元素)。否则元素初次附加时会触发采用步骤,这会为插入/追加操作增加额外的树遍历,耗费额外时间。

      因此:

          document.importNode(elem.content, true);
      

      此时即可获取可提取节点的DocumentFragment。或直接追加整个片段。

      • 太棒了——马上试试。感谢建议!顺便说lit-html看起来很酷。

        更新:现在改用importNode()了——效果同样出色。

    • Timelinize,简直_哇_。一直渴望这样的功能。超赞的创意!

      关于GPX支持…不想误导你,但你见过https://www.sethoscope.net/heatmap/吗?

      • 谢谢!希望它能满足需求。

        其实我没见过这个——看起来很不错。我们确实已实现热力图功能,但发现要调试出最具实用价值/吸引力的热力图效果相当棘手。我会参考该项目的经验。

    • 最初考虑用基础模板构建ActivityPub前端[1],但最终认为lit-js为web组件增添的增强功能值得文件体积的增加。

      [1] https://github.com/mariusor/oni

      • 我是lit-html的作者。

        没错,Lit的标签化模板字面量和render()方法本质上是生成<template>元素的简写:标记表达式位置、克隆模板,再填入对应内容。

    • > 我使用了几个库(用于映射、日期时间处理)

      我非常期待 Temporal [0] 达到更广泛的基础支持。结合它和近期大量国际化组件,我们距离不再需要任何日期时间相关的库已经近在咫尺。

      https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe

      • 没错,JS里处理日期时间一直是个大麻烦。我还没试过Temporal,但希望它能更好。

        • 我已经用过它一段时间了。在Deno中需要启用实验性功能才能使用。它大量借鉴了Java 8+引入的java.time库(包括但不限于),就像JS原生Date类与最初的Java Date类的关系那样,因此它基本上实现了对Java 8+版本的追赶。

          它支持几乎所有你能想到的日期运算、比较和日历转换功能。其toLocaleString()格式化选项设计精妙,基于不可变对象构建,彻底规避了当前JS Date类固有的整类漏洞。

    • 老兄,这项目太酷了!几年前我也考虑过开发类似工具,但除非开源且支持自托管(非常感谢采用AGPLv3协议!),否则我绝不会使用。后来我陷入分析瘫痪,所以对你的成果超级佩服 🙂

      • 哈哈,谢谢。这确实很难,“卡住”很正常。我为此反复打磨了十多年,未来还会继续优化…但最重要的是,我真心希望家人能用上它。

    • 什么是Tabler?

      • 就是这个:https://tabler.io/admin-template

        不同版本间仍存在较高不稳定性,但作为免费工具,其效果出色且极具灵活性。在本项目中我对其表现相当满意。

        • 在我看来,Tabler实现的很多功能如今用“原生”CSS网格就能轻松完成,后者甚至更具灵活性。(我曾仅通过简单修改CSS类名和CSS网格实现过动态仪表盘。仅需调整grid-template-areas就能实现丰富效果,其以ASCII图示呈现布局的特性让我特别欣赏——在CSS文件中快速浏览多种布局方案并辨别差异变得异常轻松。)

          目前我已几乎在所有场景中弃用其他布局方案(Bootstrap网格、Flexbox等),转而采用纯CSS网格,效果令人惊叹。若尚未掌握此技术,我认为绝对值得投入精力学习。

          • 我使用网格布局(虽然仍觉得难以完全掌握)——但Tabler远不止是布局工具。我欣赏它组件化的样式体系,高度可定制且易于使用。

    • 这几乎肯定会被用于不受欢迎的监控行为。

      • 它实际上根本不会获取你的任何数据,更别说他人的数据了。它只是个组织工具,请具体说明你的担忧。

  2. 模板机制很稳健,性能上比在Web组件中嵌入HTML-in-JS略胜一筹。但话说回来,我真心希望存在某种Web组件文件格式,能整合:

    – HTML
    – JS
    – CSS

    以某种结构化形式打包,便于作为库加载分发。我不喜欢当前Web组件的实现方式——必须先通过import加载逻辑,再内联HTML部分。

    • HTML模块规范目前存在一个待解决问题[1],并有若干提案。该概念需要有动力推动者来完成。微软近期对此表现出兴趣。

      用户空间存在单文件组件项目,如我的Heximal[2]项目。不过Heximal并未解决模块格式问题——它需要某种HTML模块polyfill支持。

      不过我认为现状并不糟糕。容器格式对组件使用者而言基本无关紧要。无论组件定义位于.js还是.html文件中,你都需要导入该定义。导入后使用方式完全一致。

      文件格式真正重要的场景应该少之又少。除非在JavaScript禁用时,且HTML基底的Web组件被认为安全可执行(其模板表达力可能存在攻击隐患)。

      [1]: https://github.com/WICG/webcomponents/issues/645

      [2]: https://heximal.dev/(抱歉移动端显示异常,我得修复它!)

      • 老实说,我喜欢heximal的设计,但读完文档后开始怀念简单的PHP式模板。

    • 其实你可以在模板中直接添加style标签,这些样式会局限在模板内生效。这是实现局部CSS规则的唯一方法。

      你也可以在任意位置添加script标签。它们会在加入DOM时执行,但不会被局限在特定作用域内。

      • 样式并不局限于模板范围。

        你可能想的是Shadow DOM:若将模板克隆到ShadowRoot中,则所有包含的样式都会被限定在该ShadowRoot范围内。

    • Salesforce其实有个很棒的模块格式叫LWC,正好实现了这个功能。还支持单向绑定。可惜他们从未在客户使用之外大力推广。

  3. 看了第一个示例,我实在看不出这样有什么好处。用模板创建表格行和从头开始创建的工作量完全一样。

    • 当然,示例的目的是通过逻辑合理的案例进行教学。模板可以任意扩展,显然随着模板规模增大,其与手动调用document.createElement的差异会显著放大。

      • 差异体现在哪里?老实说我也没看懂目的。无论哪种方式都需要用到JavaScript,而为document.createElement创建一个能设置属性并添加子元素的简写函数根本不费事…每个项目顶多10行代码。

        我原以为这是种优化方案,但实际测试发现document.createElement明显更快(火狐浏览器)。

        而且在HTML中使用template意味着模板/使用场景的变更更具全局性。

        感觉这种改进只有深陷React生态的人才会觉得合理。

        • React生态仍在深度使用document.createElement,短期内似乎不会投入模板标签的开发。

          我不清楚你的具体基准测试方法,但我的测试(同样主要在火狐浏览器中进行)表明,对于我遇到的许多树形结构,克隆模板比document.createElement快得多。(并非每次只测试单个元素的微基准测试,而是同时处理整个组件和“页面”的树形内容。) 这还是在今天阅读本帖前,我尚未发现一个被忽略的性能优化技巧(使用document.importNode克隆模板内容,可避免多次树遍历操作来将克隆节点附加到文档)。

    • 该示例是刻意设计的简化版本,旨在说明其易于克隆为DOM节点以便后续操作的特性。

      或许结合<slot>元素的用法能让你更易理解,示例见此:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/

    • 我同意,这似乎比直接在JS中定义模板更复杂。反正你仍需用JS与模板交互,本质上只是将固定字符串映射到DOM节点生成器,却不如后者灵活。其优势似乎仅在于当你不用JS操作模板,而直接在HTML中内联使用时。

    • 同样不理解React/Vue的Hello World示例意义何在,它们比直接编写HTML更冗长。

    • 想象一个包含大量类名和结构的大型模板。当需要多次渲染时,只需更新特定实例的少量内容,而非反复重建整个结构和类名。

      • 正是如此,还涉及关注点分离。HTML应留在HTML或你偏好的SSR模板中,各司其职。

        我喜欢<template>的另一原因是原型设计便捷。最直接的做法是用占位符模拟HTML+CSS中可能填充的元素样式。无需将其转换为JS元素生成代码,只需移除填充数据,将基础元素包裹在<template>标签中即可。

        回到JS端,只需一个简单的初始化器按需添加这些元素,再配一个(重新)填充数据的函数即可。

        简单明了,无需任何库或框架。

    • 我认为它更适合Web组件的设计。

  4. 这究竟试图解决什么问题?它真的成功解决了问题吗?我难以理解其吸引力,毕竟JS仍需建模模板内部结构才能填充槽位。

    • 当Web组件在主DOM中存在槽填充器时,Shadow DOM可自动填充槽位。虽然仍需JS调用/创建Shadow DOM,但此时JS代码可能极简且无需建模内部结构。

      但模板真正解决的核心问题在于构建已解析但未在当前文档中“生效”的DOM片段。在模板标签出现前,除了JS配合createElement/createElementNS外别无良策,而这种方式始终比浏览器高度优化的HTML解析器效率更低。

      此外,slot标签确实解决了小问题——它是首个默认(浏览器CSS)行为为display: contents;的标签。虽然相较于CSS一行代码的提升有限,但在模板之外仍有实用场景。

  5. 通过运用模板元素等标准网络技术,规避困扰多数现代框架的臃肿抽象层,构建现代网站存在巨大潜力。本质上,我们需要将传统网络开发的简洁性与现代网络特性相结合。

    已有若干现代库与框架朝此方向发展:Nue、Datastar、Lit。它们带来的开发体验与用户体验令人耳目一新。

    我本人也通过Miu[1]实践了这种方法。虽然它尚未与Web Components完美融合,但已接近目标。我对WC标准有些异议,不过这是另一回事。

    我衷心期盼现代网页开发终能回归初始状态——无需依赖任何第三方库或框架。开发者之所以习惯使用这些工具,源于早期原生网页API的局限性,但如今网络技术已取得飞跃性发展。在现代工作流成为标准之前,打开文本编辑器几分钟内就能生成可运行的网页,这种体验堪称神奇:无需依赖库、无需构建步骤、毫无冗余操作。让我们重拾这份纯粹吧。

    [1]: https://github.com/hackfixme/miu

    • 我曾撰写数篇博文阐述为何应为DOM添加原生模板功能,从而减少对库的依赖。

      现在正是推出DOM模板API的时机:https://justinfagnani.com/2025/06/26/the-time-is-right-for-a

      原生DOM模板API应具备哪些特性?:https://justinfagnani.com/2025/06/30/what-should-a-dom-templ

      • 我对类似plates[1]库的API很感兴趣,但希望它能支持事件绑定(而非仅限于一次性渲染纯HTML)。我的初步想法是这种方案可能难以维护,最多只能用于小型模板或依赖大量微数据[2]的模板。

        但这种绑定机制的独特魅力,让我仍想认真尝试一次,至少在彻底放弃前给它一个机会。

        [1]: https://github.com/flatiron/plates

        [2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Mic

        • Plates的风格并非开发者对现代模板语法的期待。当今所有主流语法都支持内联表达式。

      • 我浏览了你的博文和WICG提案,主要反对意见在于其依赖JavaScript。

        现有JavaScript方案已能实现多种HTML模板功能。如你所提,最简单的标记模板字面量方案甚至无需外部依赖。

        这种方案的问题在于,它使除静态网站和服务器端模板处理外的所有场景都必须依赖 JavaScript。这种情况本不该存在。

        作为平台的网络应当具备灵活性,提供构建可维护网站和应用的工具,而不应强迫开发者和用户依赖特定运行时环境。JavaScript 应当成为可选依赖项,如同当今的 CSS 一样。随着WebAssembly的普及和浏览器转型为软件交付平台,连HTML都应成为可选项。

        更关键的是,让HTML核心功能依赖JS严重违背关注点分离原则。HTML模块本应是独立特性,我们应当能够定义HTML模块树、导入模块、复用模块等。这才是构建模板系统的基础架构。

        自90年代起我们就拥有服务器端包含和XSLT技术,如今HTML模板却要依赖JS?这简直荒谬。

        况且,将逻辑最小化甚至完全排除在模板之外具有充分合理性。Mustache和Handlebars已证明这种方案既简洁又优雅。每当看到HTML属性中嵌入JS代码,我都忍不住诅咒这个创意的发明者。这严重损害代码可读性、可维护性,甚至危及开发者的理智。

        HTML本可具备更强大的能力而不必依赖JS。推动该方案落地时,我认为应回归第一性原理思考,而非受现代Web框架和JavaScript特性启发。

    • > 同时规避困扰多数现代框架的臃肿抽象层。本质上,我们需要将传统网页开发的简约性与现代网络特性相结合。

      若倾听实际构建框架者(Solid、Preact、Vue、Svelte)的经验,你会发现所谓“现代网络特性”中真正能简化开发的特性寥寥无几。所有框架都渴望用真正的浏览器内置功能取代“臃肿的抽象层”。可惜这些功能完全脱离了除少数Chrome开发者外的用户需求而存在。

      > 网页开发者因原生API的局限而习惯了这些框架,但网络技术早已突飞猛进。

      事实并非如此。至少未朝着你期望的方向发展。

      > 无依赖、无构建步骤、无冗余操作。请让我们重拾这种体验。

      现在没有任何因素阻碍你实现这一点。

      • > 若倾听实际构建框架者(Solid、Preact、Vue、Svelte)的心声,你会发现所谓“现代网页特性”中真正能简化开发的实属寥寥。

        这取决于“更轻松”的主观定义,但多数框架采用的抽象层过于复杂,其价值远不及带来的负担。讽刺的是,这些框架反而增加了开发难度——更多动态组件、更复杂的概念体系、更易出错的环节,最终导致用户体验大幅恶化。

        编写虚假HTML以供JavaScript解析、编译和解释,从而在运行时操作DOM的整个思路是错误的。虚拟DOM、差异比较、协调机制以及一堆相关概念都是错误的。围绕解决客户端状态管理构建的整个工具生态系统简直疯狂。将包括CSS在内的所有内容都迁移到JS中解释的趋势更是荒谬至极。像Tailwind这样的抽象工具本就不该存在,更别提像daisyUI这类可憎之物对其进行二次抽象。无数框架承诺模糊前端与后端开发界限,最终在方法论的荒谬性暴露时惨遭滑铁卢——但在此之前,它们早已积累了庞大的用户群体。多数网页开发者已默认:要在浏览器里显示“Hello World!”就得依赖100MB+的库。每个网站都必须是SPA的观念,以及为实现传统网站功能而堆砌的各种权宜之计。这样的例子不胜枚举…

        这些工具和理念的流行,绝不意味着它们能构建出理性的开发环境,更遑论最优方案。这仅说明该领域已被有影响力的开发者主导,以至于初入行的新手甚至不了解其他构建网页应用的方式。毕竟在简历上,React和Vue的亮眼程度远胜于JavaScript本身。如今若不精通这些框架,几乎不可能被视为合格的网页开发者——这形成了一个自我强化的循环。

        > 事实并非如此。至少未朝着你期望的方向发展。

        这种说法有误。浏览器端的JavaScript已是稳定现代的语言。HTML历经多年变革性更新,例如我们正在讨论的template元素。CSS的演进更是令人瞠目,其发展速度令人难以追踪。跨浏览器的怪癖与兼容性问题已不复存在——这主要归功于谷歌主导网络的局面(虽与本文主题无关)。Web服务器变得更灵活且易于部署。诸如此类。

        诸如Datastar这类框架证明,网页开发可以变得更简单,在远低于传统框架复杂度的条件下提供更优质的用户体验。这并非因为它重新发明了其他地方使用的抽象概念,而是因为它采取了第一性原理的方法,拒绝在现有的抽象概念堆栈上构建。它利用了服务器发送事件等成熟技术,没有重复造轮子。若您看过演示和实例,成果自会说明一切。

        不难想象,类似的简化工作流终将成为Web客户端与服务端的行业标准。只需全行业共同推动,将现有技术整合为统一体系即可。技术层面我们已接近这个目标,因此Datastar这类框架仍有必要,但我们绝对不需要昔日那些臃肿的框架。

        • > 通过编写伪HTML,再由JavaScript在运行时解析、编译并解释以操作DOM的整个思路是错误的。

          因为Web标准中根本不存在其他实现方式。

          > 虚拟DOM、差异比较、协调机制及相关概念都是错误的。

          我提到的框架中没有一个使用虚拟DOM或差异比较。它们都转向了更高效的方式——猜猜看,这种方式正成为浏览器原生标准:https://github.com/tc39/proposal-signals

          > 围绕客户端状态管理构建的整个工具生态系统简直疯狂。

          动态UI必然伴随状态管理。我曾期盼网页保持静态,但这艘船早已远航。而现有网页标准中,没有任何内容能解决状态管理问题。

          > 这意味着领域已被有影响力的开发者主导,以至于初入行的开发者甚至不知道还有其他构建网页应用的方式。

          奇怪的是,你完全忽视了Solid、Vue和Svelte——这些框架与React相比,都提供了构建网页应用的不同路径。

          > 网页浏览器中的JavaScript是稳定的现代语言。

          仅有语言是不够的。你还需要_库_以及浏览器实际暴露的实用功能。

          多年来涌现如此多库与框架的根本原因,正是因为没人愿意耗费数百行代码来正确设置和修改某个DOM元素。而极少有库/框架(包括新晋者)采用“浏览器那些惊艳的新特性”,是因为这些特性存在着长达数英里的问题清单和注意事项。仅举一例:https://x.com/Rich_Harris/status/1841467510194843982

          > 多年来,HTML经历了诸多变革性更新,例如我们正在讨论的`template`元素。

          请问模板能带来什么?它本身毫无用处。至少需要借助JS才能操作它。

          > 但它秉持第一性原理,拒绝在现有抽象体系之上堆砌功能。

          你不是刚说过:“编写虚假HTML在JavaScript中解析、编译并解释以运行时操作DOM的整个思路是错误的”吗?

          那么Datastar的实现本质上不正是_完全相同的做法_吗?你认为这段代码是如何运作的:

              <div data-signals="{counter: 0, message: 'Hello World', allChanges: [], counterChanges: []}">
              <div class="actions">
                  <button data-on-click="$message = `Updated: ${performance.now().toFixed(2)}`">
                      Update Message
                  </button>
                  <button data-on-click="$counter++">
                      Increment Counter
                  </button>
                  <button
                      class="error"
                      data-on-click="$allChanges.length = 0; $counterChanges.length = 0"
                  >
                      Clear All Changes
                  </button>
              </div>
              <div>
                  <h3>Current Values</h3>
                  <p>Counter: <span data-text="$counter"></span></p>
                  <p>Message: <span data-text="$message"></span></p>
              </div>
              <div
                  data-on-signal-patch="$counterChanges.push(patch)"
                  data-on-signal-patch-filter="{include: /^counter$/}"
              >
                  <h3>Counter Changes Only</h3>
                  <pre data-json-signals__terse="{include: /^counterChanges/}"></pre>
              </div>
              <div
                  data-on-signal-patch="$allChanges.push(patch)"
                  data-on-signal-patch-filter="{exclude: /allChanges|counterChanges/}"
              >
                  <h3>All Signal Changes</h3>
                  <pre data-json-signals__terse="{include: /^allChanges/}"></pre>
              </div>
            </div>
          

          它确实(滥)用浏览器进行初步解析,但后续操作与其他方案并无二致(只是可能更糟)。例如通过字面解析属性内容来重构半套JavaScript实现“响应式表达式”:https://github.com/starfederation/datastar/blob/develop/libr

          • > 因为Web标准中根本没有其他实现方式。

            当然有。自Web诞生起我们就用JavaScript操作DOM,后来jQuery等项目让操作更符合人体工学。现存大量JS库能简化DOM操作,完全无需在JavaScript中编写伪HTML。

            > 我提到的框架中没有一个使用虚拟 DOM 或差异比较。它们都转向了更高效的方式

            哦,对,可观察对象才是正道。不,等等,钩子才是正确方案。不,等等,信号绝对是未来趋势。

            我们竟指望这些追逐潮流的人来定义网络未来?别开玩笑了。

            > 凡涉及动态界面,必然存在状态管理。

            此言不假,但状态管理之所以演变成重大难题,正是源于单页应用和胖客户端的流行趋势。若能更多依赖服务器端,诸多复杂性本可缓解。开发者曾痴迷于规避传统HTTP请求引发的整页刷新,于是涌现出大量库与框架来解决这些自找的麻烦。

            再加上JS生态的持续演变,开发者们不断相互攀比、追逐潮流,最终演变成我所说的疯狂局面。

            > 你完全忽视了Solid、Vue和Svelte这些与React截然不同的Web应用构建方案,这很奇怪。

            我几年前就不再关注前端框架了。顺带一提,我确实尝试过早期版本的Svelte,其开发体验令人愉悦,但归根结底,表面的简洁不过是掩盖底层疯狂黑魔法的幌子——正是这些黑魔法支撑着整个框架运行。如今随着SvelteKit的出现和Svelte 5的改版,它感觉不过是又一个React克隆体。

            我近期接触过Vue,它堪称现代网页开发所有弊病的缩影。只是我实在抽不出时间精力专门讨论它。

            重申一次,我的问题不在于具体框架本身,而在于整个开发理念——为何构建网站需要如此庞大的框架?

            > 这些年来涌现出如此多库和框架的根本原因,正是因为没人愿意花时间编写数百行代码,只为正确设置和修改某个DOM元素。

            这是稻草人谬误。正如本讨论串所示,有人能用极少模板代码和抽象层构建高级交互网站。对于那些你不想亲力亲为的粗糙边缘,还有库和轻量级框架可供选择。没人强迫你使用单体框架。框架之所以流行,不过是赶时髦罢了,其实存在大量更简洁的替代方案。

            > 这里仅举一例

            啊,我确信那些热门前端框架的作者们都是公正无私的…

            听着,当前的网页标准——尤其是Web Components——确实不完美。我对某些设计决策也持保留态度。但这些工具组合起来能覆盖大量功能,而现代网页框架却要么忽视这些功能,要么重新发明轮子。_这_正是我所说的错误。

            > 请问模板能提供什么?单凭模板毫无用处,至少需要JS来操作它。

            `模板`只是构建模块之一。你可以将其与其他模块结合使用,实现框架能做的事——只不过…无需框架!这个概念很新颖,我明白。

            > Datastar不正是做着完全相同的事吗?

            并非如此。那段代码是标准HTML,通过数据属性嵌入的JS会在运行时解析,但它并非被解析编译成JavaScript的HTML类DSL——它本身就是HTML。

            需要澄清的是:我既没用过Datastar,也没研究过其内部实现。个人不赞成这种滥用HTML属性承载JavaScript逻辑的做法,更倾向于Backbone和Knockout这类传统框架的处理方式。我对Vue的挫败体验部分促使我思考并提出自己的方案,若感兴趣可参考我上面提供的链接。

            • >这话没错,但状态管理之所以成为大问题,正是源于单页应用和胖客户端的流行趋势。

              我感到奇怪的是,为何众多开发者在维护页面状态时如此吃力,最终只能求助于“魔法”框架来代劳。使用规范对象或其他数据结构来表示状态有何不可?为何有人会想要一个泛化框架来(错误地)呈现应用程序的核心模型?

              几十年来我们一直在创建具有内部状态的应用程序。早在Web出现之前,就存在具有内部状态的GUI应用程序。通常这通过代码规范和合理的抽象来实现。例如,模板元素将文档结构与逻辑层分离。

              然而在现代JS世界里,展示层、状态管理乃至后端都被堆砌成难以驾驭的怪兽。为掩盖这种混乱,人们又添加更多“魔法”进行临时拼凑。结果导致当前行业标准竟是:为展示静态页面而堆砌10MB的冗余代码。

              • 状态管理向来不易。图形界面应用同样为此困扰。因此衍生出多种方案:MVC、MVVM、各类流与可观察对象的尝试、状态机、流程图…这些概念皆源于Web之外。而如同Web领域多数事物,这些理念要么姗姗来迟,要么因种种原因未能妥善实现。

                问题更因人们试图将原生应用范式强加于本质仍是文档渲染系统的网络而加剧。当移动设备能以120Hz刷新率渲染数千对象时,网络在台式机上显示并动画化数百个节点都举步维艰。因此必须构思整套机制,以尽可能减少DOM节点的变更。

                至于难度所在…

                问题通常源于人们混淆了两个概念:数据状态与UI状态。

                例如:音乐播放器需呈现当前播放歌曲的数据状态,但存在若干纯UI元素。比如:控制面板隐藏计时器(按钮点击时需重置/禁用该计时器)

                • >图形界面应用同样深受其困

                  而我们在此犯错时,往往是因为偷工减料而非遵循清晰的抽象原则。

                  >因此必须设计整套机制,以尽可能少的DOM节点变更实现效果。

                  这正是我对JS前端框架持保留态度的根源。我们应当掌握数据状态,也应了解展示组件的状态——无论是渲染复杂数据的画布元素,还是简单的表格。这里适用KISS原则。

                  框架的“所有机制”并非处理二者同步的必要条件。尤其当它强制要求荒谬的构建步骤、成千上万的依赖项、不透明的“魔法”、巨型压缩JS文件和加载转圈圈时。

                  >真正的难点往往源于人们混淆了两个概念:数据状态与UI状态。

                  完全赞同。这正是我青睐<template>元素的原因——它以清醒的逻辑实现代码与文档的明确分离。我猜这正是另一位网友反对在JS中嵌入HTML的根源。

            • > 许多JS库能简化DOM操作,无需在JavaScript中编写伪HTML。

              任意划线的弊端在于其随意性:除了主观臆断外毫无标准可循。

              你不能一边痛斥“伪HTML”,转头又讨论某个框架——它明明在非伪HTML中嵌入了伪JS。

              > 哦对了,可观察对象才是正道。不,等等,钩子才是正确方案。不,等等,信号绝对是未来趋势。

              1. 这叫技术演进。我们不可能从一开始就掌握所有最佳方案。

              2. 猜猜datastar底层用了什么。

              说实话,我真不确定是否该继续讨论——你显然对所谈论的内容一无所知。你早已放弃追踪技术动态,既没用过Datastar也没钻研过其内部实现,还把可验证的事实斥为框架开发者的偏见等等。

              > 听着,当前的网页标准(尤其是Web Components)确实不完美。我对某些设计决策也持保留态度。但这些工具组合起来能覆盖大量功能,而现代Web框架要么忽视这些功能,要么重新发明轮子——这正是我所说的错误。

              这个“不完美却必须使用的工具”存在几个问题: 需注意,该工具今年已历经14年开发,而所有“臃肿庞大的框架”根本不存在这些问题。请注意这些问题均出自Web组件工作组报告——正是他们自己撰写了这些问题。更值得注意的是,这些问题早在报告问世前,就被“有偏见”的框架开发者讨论了多年:

              – CSS级联与主题功能失效。需多个新规范才能实现,其中多数仅支持JS实现,部分已发布,部分仍在开发中:https://w3c.github.io/webcomponents-cg/2022.html#constructab… 及 https://w3c.github.io/webcomponents-cg/2022.html#css-module-… 以及 https://w3c.github.io/webcomponents-cg/2022.html#css-propert… 以及 https://w3c.github.io/webcomponents-cg/2022.html#custom-css-… 以及 …

              – 文档选择功能失效 https://w3c.github.io/webcomponents-cg/2022.html#composed-se

              – 表单参与功能失效,因每个组件需额外加载JavaScript才能参与表单交互。位于shadow root内的按钮仍无法作为提交按钮使用:https://w3c.github.io/webcomponents-cg/2022.html#form-associ… 及 https://github.com/WICG/webcomponents/issues/814

              更别说它们还破坏了许多其他功能。比如你称之为偏见的事实——使用支持自定义元素的API会导致性能下降30%。

              > template只是一个构建模块。你可以将其与其他模块结合使用,实现框架能做的事情,只不过…无需框架!这个概念很棒,我明白。

              最终你可能在过程中创建了自己的库/框架。当然,除非你打算对上述所有问题视而不见。

              > 该代码是标准HTML,其数据属性中嵌入了JS,确实会在运行时被解释执行,但它并非被解析编译成JavaScript的HTML类DSL——它本身就是HTML。

              这既非JS,也非运行时解释执行。它需通过正则表达式进行繁琐解析,再根据解析结果执行特定代码。

              不知为何“伪HTML”被视为禁忌,而伪JS?哦,请便。

  6. Shopify在Storefront Web Components项目中大量使用<template>元素,但模板本身作为子元素传递给Web组件,该组件会动态查询渲染模板所需的数据,然后才将元素挂载到DOM上:https://shopify.dev/docs/api/storefront-web-components

  7. 最近我频繁使用模板元素在妻子摄影博客上展示商品。商品数据从无头MedusaJS实例加载,嵌入匹配博客主题的模板后,最终渲染到页面中。我刻意避免使用React这类框架,因为页面加载本就因图片体积而相当沉重。从这点来看,Template的使用体验相当轻松。

    • 坦白说,Preact只有4KB大小,相比任何图片都微不足道。

      • 我之前没考虑过Preact,老实说总会忘记它。这可能是不错的折中方案。谢谢!

  8. <template>标签的实际意义是什么?仅仅是创建文档片段吗?毕竟在HTML中它确实只能实现这个功能。如果使用JS,任何元素都能附加影子DOM。

    或许Web组件的插槽功能是另一用途?

    • 我曾用它让服务器生成独立于常规页面加载的片段,再由JavaScript填充内容作为模板。相比SPA能实现更快的初始加载速度,加载后动态性却毫不逊色。

      这种方案效果不错,但确实存在一些粗糙之处,我真心希望标准制定过程能重点解决这些问题。像JSX这类方案虽然速度慢/占用内存大,但人们仍因其便捷性而使用,若能提升易用性就太好了。

    • > 仅指创建文档片段吗?

      基本如此。不过我自己尚未实际使用过,所以我的理解可能存在偏差,请酌情参考。

      相比在模板外部使用非显示HTML标签实现,这种方式具有性能优势(解析器会特殊处理这些尚未成为活动DOM元素的内容)和便捷性优势(据我所知,这些内容不会被querySelector/querySelectorAll选中)。

      相较于在JS中构建文档片段,模板能更便捷地创建和编辑,同时保持设计与代码的松耦合(因此团队中不愿接触JS的设计师(或你不希望其修改JS代码的设计师)也能在一定程度上自由调整)。其他实现相同分离的方法可能需要额外的构建步骤才能部署。

    • Alpine.js使用<template>标签处理状态化数据的if语句和for循环。例如:<template x-if=“myVariable === 1”>

      1

      </template>

      这种模板处理方式比slots和fragments更简洁直观。

    • 好问题。确实存在未沿袭HTML5路线的Web组件方案,而模板元素正是其衍生产物。由于规范中的元素难以轻易移除,它们往往会随着时间推移被赋予新用途。

  9. 我想问,模板元素是否适合存储页面中供JS使用的json数据?

    • 不行。我会使用<script type=”-json”>

      <script>会将其内容解析为文本,而<template>会解析为DOM。这意味着你只需转义`</script>`,而无需转义`<`。

      我和一些浏览器工程师正在研究提案,允许内联模块(包括JSON)通过常规import语句导入到其他模块中。

      因此我推荐使用“-json”类型——这样就不会与未来原生的“json”类型发生冲突。

      • 为什么不使用更规范的MIME类型,比如<script type="application/mytype+json">之类的?你的建议似乎不符合规范推荐:https://html.spec.whatwg.org/multipage/scripting.html#attr-s

        • 我理解的是,在实现中任何未知类型都会生成“数据块”,本质上就是未处理的文本。

          我不会使用application/json,以防浏览器开始支持该格式后,其语义与你自定义方案产生冲突,导致原生功能上线时出现webcompat问题。

          不过使用JSON时,语义差异的可能性很低。JavaScript中的JSON模块本质就是纯JSON块,既无特殊附加内容也无命名导出项——内联版本同样如此。

          • 但像我示例中的`mytype`这类子类型(`application/mytype+json`)不仍是有效的MIME类型吗?这样既能避免你的顾虑,我之前也用过类似方案。

      • 感谢分享。这个提案确实很有意思。请问是否有公开可访问的链接?

    • 你可能对数据属性感兴趣,它适用于此场景https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/Use…,不过这取决于你设想的JSON数据结构。

      • 您所说的“结构”具体指什么?是否涉及转义等处理?

        • 我理解若数据嵌套层级深,标记可能变得复杂,但HTML结构不必完全复刻JSON格式。这里还有其他示例可能更清晰地说明实现方式:https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/

          • 感谢指点。我正在探索向自定义元素传递配置数据的通用方案,由于其他开发者可能有不同需求,我无法预先确定数据结构。目前支持简单类型的数据属性,但想确认是否可通过其他方式支持任意JSON对象。

            • 由于 JSON 是 JavaScript 的子集,您只需使用 script 元素即可,添加 json MIME 类型也毫无妨碍:

                  <script type="application/json" id="myJSONData">
                    {
                      “name”: “John Doe”,
                      “age”: 30
                    }
                  </script>
              
                  <script>
                    const data = JSON.parse(document.getElementById(‘myJSONData’).textContent);
                    console.log(data.name + “ is ” + data.age); // John Doe is 30
                  </script>
              

              注意:JSON数据必须以字面形式存在于script元素内。若尝试通过src属性从外部请求数据,则无法通过textContent或其他机制获取响应[0],此时需使用fetch/ajax/xhr等方法获取数据。

              [0] https://stackoverflow.com/questions/17124713/read-response-o

              • 关于在script标签中嵌入JSON,我最近读到一篇文章,指出JSON内部若包含闭合标签</script>可能导致脚本失效的风险。

                脚本标签中的安全JSON:如何避免破坏网站 – https://sirre.al/2025/08/06/safe-json-in-script-tags-how-not

                与所有不可信内容相同,根据 JSON 字符串的来源,值得考虑对输出进行净化处理。

                • 该文章相当复杂;我理解其历史背景,但坦率地说,Web 遗留体系过于复杂,不必过多纠结“为什么”——最终许多设计本就不合逻辑,纯属历史偶然。

                  这里提供另一种简明替代方案列表。值得注意的是,“&” 符号同样需要转义:https://pkg.go.dev/encoding/json#HTMLEscape

                  • 这篇总结很好地阐明了将JSON安全嵌入脚本标签所需的处理步骤。

                    我同意“为什么”背后的故事过于冗长——那些看似合理的决策层层累积,最终形成了这团怪异规则。这些细节实在难以全部记住,正是应该交由专家开发并维护的专用函数来处理的事务。

                • 精彩文章!我认为在data-*属性或其他HTML文档部分也需要采取类似(但不同的)预防措施。

    • 效果堪比type为application/json的script元素。

      • 我好奇浏览器是否会尝试验证type为json的script标签内容,而非将其视为仅在解析/使用时才验证的blob数据。这样做是否会增加加载时的性能开销?目前没有机器可验证。

      • 关键区别在于application/json脚本仍受CSP策略约束

        • 为何如此?我从未见过相关问题。CSP政策反而会引导开发者采用这种方式(而非直接在内联脚本中赋值给JS变量)

          • 我原以为自己清楚,但CSP的实际情况似乎并不明确。两种观点都找不到权威依据

            • CSP会阻止执行/包含操作,但由于json本身不执行且任何json MIME类型都不会触发执行,因此不存在问题。

              任何CSP允许的脚本都能读取该application/json脚本标签并解码,但这与读取其他可访问数据(如HTML元素或属性)并无本质区别。

    • 在我的作品集(miler.codeberg.page)及其重构重设计过程中(希望很快完成!),我通过类似数据库的json文件为<template>元素提供数据。我感觉用原生JS查询节点并设置数据的过程仍显原始,面对海量数据或大量节点时会很麻烦。但至少现代JS无需通过XMLHTTPRequest等方式获取JSON——它能像普通JS模块那样直接导入,这点还算便利。

  10. 特殊标签有何意义?

    难道不是所有内容都只是JSON、CSS、JavaScript,以及大量DIV和SPAN元素吗?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


京ICP备12002735号