标准化客户端模板
简介
模板化概念在 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> 中可为我们提供一些重要属性。
其内容在激活之前实际上处于非活动状态。从本质上讲,您的标记是隐藏的 DOM,不会呈现。
模板中的任何内容都不会产生副作用。脚本不会运行,图片不会加载,音频不会播放…直到模板被使用。
内容被视为不在文档中。在主页面中使用
document.getElementById()或querySelector()不会返回模板的子节点。模板可以放置在
<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 创作过程更合理、更易于维护且功能更全面,始终是一件好事。
其他资源
- WhatWG 规范
- Web 组件简介
- <web>组件</web>(视频)- 由本人亲自呈现的精彩全面的演示。