HTML 的新模板标记

标准化客户端模板

简介

模板化概念在 Web 开发中并不新鲜。事实上,Django (Python)、ERB/Haml (Ruby) 和 Smarty (PHP) 等服务器端模板语言/引擎已经存在了很长时间。不过,在过去几年中,我们看到 MVC 框架如雨后春笋般涌现。它们都略有不同,但大多数都使用一种共同的机制来渲染其展示层(又称视图):模板。

毫无疑问,模板非常实用。尽管提问。甚至连它的定义都让人感到温暖舒适:

“…无需每次都重新创建…”我不知道您怎么想,但我喜欢避免额外的工作。那么,为什么 Web 平台缺乏对开发者显然关心的功能的原生支持呢?

WhatWG HTML 模板规范就是答案。它定义了一个新的 <template> 元素,该元素描述了一种基于 DOM 的标准客户端模板方法。借助模板,您可以声明标记片段,这些片段会解析为 HTML,在网页加载时不会使用,但稍后可在运行时实例化。引用 Rafael Weinstein 的话:

它们是放置大量 HTML 的地方,您不希望浏览器以任何理由对其进行任何处理。

Rafael Weinstein(规范作者)

功能检测

如需检测 <template>,请创建 DOM 元素并检查 .content 属性是否存在:

function supportsTemplate() {
    return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
    // Good to go!
} else {
    // Use old templating techniques or libraries.
}

声明模板内容

HTML <template> 元素表示标记中的模板。它包含“模板内容”;本质上是可克隆 DOM 的惰性块。您可以将模板视为可在应用的整个生命周期内使用(和重复使用)的基架。

如需创建模板化内容,请声明一些标记并将其封装在 <template> 元素中:

<template id="mytemplate">
    <img src="" alt="great image">
    <div class="comment"></div>
</template>

核心要素

将内容封装在 <template> 中可为我们提供一些重要属性。

  1. 其内容在激活之前实际上处于非活动状态。从本质上讲,您的标记是隐藏的 DOM,不会呈现。

  2. 模板中的任何内容都不会产生副作用。脚本不会运行,图片不会加载,音频不会播放…直到模板被使用。

  3. 内容被视为不在文档中。在主页面中使用 document.getElementById() 或 querySelector() 不会返回模板的子节点。

  4. 模板可以放置在 <head>、<body> 或 <frameset> 内的任何位置,并且可以包含这些元素中允许的任何类型的内容。请注意,“任何位置”意味着 <template> 可以安全地用于 HTML 解析器不允许的位置…内容模型子级除外。它也可以作为 <table> 或 <select> 的子项放置:

<table>
  <tr>
    <template id="cells-to-repeat">
      <td>some content</td>
    </template>
  </tr>
</table>

启用模板

如需使用模板,您需要将其激活。否则,其内容将永远不会呈现。 最简单的方法是使用 document.importNode() 创建其 .content 的深层副本。.content 属性是一个只读 DocumentFragment,其中包含模板的内容。

var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';

var clone = document.importNode(t.content, true);
document.body.appendChild(clone);

模板实例化后,其内容就会“上线”。在此特定示例中,系统会克隆内容、发出图片请求并呈现最终标记。

演示

示例:惰性脚本

此示例演示了模板内容的惰性。<script> 仅在按下按钮时运行,从而生成模板。

<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
  function useIt() {
    var content = document.querySelector('template').content;
    // Update something in the template DOM.
    var span = content.querySelector('span');
    span.textContent = parseInt(span.textContent) + 1;
    document.querySelector('#container').appendChild(
      document.importNode(content, true)
    );
  }
</script>

<template>
  <div>Template used: <span>0</span></div>
  <script>alert('Thanks!')</script>
</template>

示例:通过模板创建 Shadow DOM

大多数人通过将标记字符串设置为 .innerHTML,将 Shadow DOM 附加到宿主:

<div id="host"></div>
<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.innerHTML = '<span>Host node</span>';
</script>

这种方法的问题在于,Shadow DOM 越复杂,您要执行的字符串串联就越多。它无法扩缩,事情很快就会变得一团糟,婴儿开始哭泣。这种方法也是 XSS 最初诞生时的攻击方式!<template>来帮忙。

更合理的做法是,通过将模板内容附加到 shadow root 来直接使用 DOM:

<template>
<style>
  :host {
    background: #f8f8f8;
    padding: 10px;
    transition: all 400ms ease-in-out;
    box-sizing: border-box;
    border-radius: 5px;
    width: 450px;
    max-width: 100%;
  }
  :host(:hover) {
    background: #ccc;
  }
  div {
    position: relative;
  }
  header {
    padding: 5px;
    border-bottom: 1px solid #aaa;
  }
  h3 {
    margin: 0 !important;
  }
  textarea {
    font-family: inherit;
    width: 100%;
    height: 100px;
    box-sizing: border-box;
    border: 1px solid #aaa;
  }
  footer {
    position: absolute;
    bottom: 10px;
    right: 5px;
  }
</style>
<div>
  <header>
    <h3>Add a Comment
  </header>
  <content select="p"></content>
  <textarea></textarea>
  <footer>
    <button>Post</button>
  </footer>
</div>
</template>

<div id="host">
  <p>Instructions go here</p>
</div>

<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.appendChild(document.querySelector('template').content);
</script>

Gotchas

以下是我在实际使用 <template> 时遇到的一些陷阱:

  • 如果您使用的是 modpagespeed,请注意此错误。定义内嵌 <style scoped> 的模板,许多模板会通过 PageSpeed 的 CSS 重写规则移至头部。
  • 无法“预渲染”模板,这意味着您无法预加载资源、处理 JS、下载初始 CSS 等。服务器和客户端都是如此。模板仅在发布时才会呈现。
  • 请谨慎使用嵌套模板。它们的行为可能与您的预期不符。例如:

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    启用外部模板不会启用内部模板。也就是说,嵌套模板要求其子模板也必须手动激活。

标准制定之路

我们不能忘记自己的根。基于标准的 HTML 模板的发展历程 可谓漫长。多年来,我们想出了一些非常巧妙的方法来创建可重复使用的模板。以下是我遇到过的两种常见情况。 我会在本文中介绍这些方法,以便进行比较。

方法 1:屏幕外 DOM

长期以来,人们一直使用的一种方法是创建“屏幕外”DOM,并使用 hidden 属性或 display:none 将其隐藏。

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

虽然这种方法可行,但存在一些缺点。此技巧的简要说明:

  • 使用 DOM - 浏览器知道 DOM。它很擅长。我们可以轻松克隆它。
  • 不呈现任何内容 - 添加 hidden 会阻止显示该块。
  • 非惰性 - 即使内容处于隐藏状态,系统仍会针对图片发出网络请求。
  • 样式和主题设置繁琐 - 嵌入页面必须为其所有 CSS 规则添加 #mytemplate 前缀,以便将样式范围缩小到模板。这种方法很脆弱,无法保证我们将来不会遇到命名冲突。例如,如果嵌入页面已经有一个具有该 ID 的元素,我们就会遇到问题。

方法 2:重载脚本

另一种方法是重载 <script> 并将其内容作为字符串进行操作。John Resig 可能是第一个在 2008 年通过其微模板实用程序展示此功能的人。现在还有许多其他模板引擎,包括一些新出现的模板引擎,例如 handlebars.js。

例如:

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

此技巧的简要说明:

  • 不呈现任何内容 - 浏览器不会呈现此块,因为 <script> 默认为 display:none。
  • Inert - 浏览器不会将脚本内容解析为 JS,因为其类型设置为“text/javascript”以外的值。
  • 安全问题 - 鼓励使用 .innerHTML。 对用户提供的数据进行运行时字符串解析很容易导致 XSS 漏洞。

总结

还记得 jQuery 让 DOM 操作变得多么简单吗?最终,querySelector()/querySelectorAll() 被添加到该平台。这显然是双赢,对吧?一个库普及了使用 CSS 选择器提取 DOM 的做法,后来标准也采用了这种做法。虽然并非总是如此,但我非常喜欢这种感觉。

我认为<template>的情况类似。它不仅规范了客户端模板的使用方式,更重要的是,它让我们不再需要使用 2008 年的那些技巧。 在我看来,让整个 Web 创作过程更合理、更易于维护且功能更全面,始终是一件好事。

其他资源