标准化客户端模板
简介
模板的概念对于 Web 开发并不陌生。事实上,Django (Python)、ERB/Haml (Ruby) 和 Smarty (PHP) 等服务器端模板语言/引擎已经存在很长时间。但在过去几年里,MVC 框架层出不穷所有这些视图略有不同,但大多数都采用相同的呈现层(也称为 da 视图)渲染机制:模板。
毫无疑问,模板非常棒。快点,随意询问。即使是它的定义,也会让您感到温暖舒适:
“...无需每次都重新创建...”不知道您,但我喜欢避免额外的工作。那么,为什么 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>
内的任何位置,并且可以包含这些元素中允许的任何类型的内容。请注意,“任何位置”意味着可以安全地在 HTML 解析器禁止使用除内容模型子项之外的位置使用<template>
。也可以将其放置为<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);
取消模板后,其内容会“发布”。在此特定示例中,系统会克隆内容、发出图片请求,并呈现最终的标记。
样本歌曲
示例:inert 脚本
此示例演示了模板内容的固定性。只有在按下该按钮后,模板才会运行 <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 重写规则移至最头部。 - 无法“预渲染”模板,也就是说,您无法预加载素材资源、处理 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
可防止块显示。 - 非 inert - 即使内容被隐藏,仍会针对图片发出网络请求。
- 设置复杂的样式和主题 - 嵌入页面必须在其所有 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 年的做法。让整个网络创作过程更简洁、更易于维护、功能更全面,始终是我书籍的一件好事。
其他资源
- WhatWG 规范
- Web 组件简介
- <web>components</web>(视频) - 由你真正制作的详尽全面的演示。