标准化客户端模板
简介
模板的概念对 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>
中可以为我们提供一些重要的属性。
在激活之前,其内容实际上处于无效状态。从本质上讲,您的标记是隐藏的 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>
来帮忙了。
更合理的方法是通过将模板内容附加到影子根来直接处理 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 创作流程变得更加合理、更易于维护且功能更全面,总是一件好事。
其他资源
- WhatWG 规范
- Web 组件简介
- <web>components</web>(视频) - 内容非常全面,由您制作的真正内容进行演示。