HTML 新模板代码

标准化客户端模板

简介

模板的概念对 Web 开发来说并不陌生。事实上,Django (Python)、ERB/Haml (Ruby) 和 Smarty (PHP) 等服务器端模板语言/引擎已经存在很长时间了。不过,在过去几年里,我们看到了 MVC 框架的爆炸式增长。它们各有不同,但大多数都采用了一种共同的机制来渲染其呈现层(也称为“视图”):模板。

毫无疑问,模板非常棒。来四处走走看看吧。就连它的定义也让人感到温暖舒适:

“…does not have to be recreated each time…” 我不知道您怎么样,但我喜欢避免额外的工作。那么,为什么 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> 来帮忙了。

更合理的方法是通过将模板内容附加到影子根来直接处理 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>

问题

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

  • 如果您使用的是 modpagespeed,请注意此 bug。定义内嵌 <style scoped> 的模板可以使用 PageSpeed 的 CSS 重写规则移至 head。
  • 无法“预渲染”模板,这意味着您无法预加载资源、处理 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 的元素,我们就无法再使用该 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
  • 不处理 - 浏览器不会将脚本内容解析为 JS,因为其类型设置为“text/javascript”以外的其他内容。
  • 安全问题 - 建议使用 .innerHTML。对用户提供的数据进行运行时字符串解析很容易导致 XSS 漏洞。

总结

还记得 jQuery 让 DOM 变得非常简单吗?结果是 querySelector()/querySelectorAll() 已添加到平台。显而易见,对吧?一个库采用 CSS 选择器和标准来提取 DOM,后来又采用了该库。这并不总是有效的,但如果能这样,我会很开心

我认为 <template> 也是类似的情况。它规范了我们进行客户端模板化的方式,但更重要的是,它消除了我们在 2008 年采用的权宜解决方法。在我看来,让整个 Web 创作流程变得更加合理、更易于维护且功能更全面,总是一件好事。

其他资源