在我们 上一篇文章 中,我们从高层次讨论了 Web Components 规范(自定义元素、Shadow DOM 和 HTML 模板)。在本文以及接下来的三篇文章中,我们将对这些技术进行测试,并更详细地检查它们,看看我们如何在当今的生产环境中使用它们。为此,我们将从头开始构建一个自定义模态对话框,以了解各种技术如何协同工作。
文章系列
- Web Components 简介
- 创建可复用的 HTML 模板(本文)
- 从头开始创建自定义元素
- 使用 Shadow DOM 封装样式和结构
- Web Components 的高级工具
HTML 模板
Web Components 规范 中一个鲜为人知但功能强大的特性是 <template>
元素。在本系列的 第一篇文章 中,我们将模板元素定义为“HTML 中的用户定义模板,只有在被调用时才会呈现”。换句话说,模板是浏览器在被告知执行其他操作之前会忽略的 HTML。
然后可以以多种有趣的方式传递和重用这些模板。出于本文的目的,我们将研究如何为一个对话框创建模板,该对话框最终将用于自定义元素中。
定义我们的模板
听起来可能很简单,<template>
是一个 HTML 元素,因此带有内容的最基本形式的模板将是
<template>
<h1>Hello world</h1>
</template>
在浏览器中运行此代码将导致屏幕为空白,因为浏览器不会渲染模板元素的内容。这变得非常强大,因为它允许我们定义内容(或内容结构)并将其保存以备后用——而不是在 JavaScript 中编写 HTML。
为了使用该模板,我们需要 JavaScript
const template = document.querySelector('template');
const node = document.importNode(template.content, true);
document.body.appendChild(node);
真正的魔力发生在 document.importNode
方法中。此函数将创建模板 content
的副本,并将其准备插入到另一个文档(或文档片段)中。函数的第一个参数获取模板的内容,第二个参数告诉浏览器对元素的 DOM 子树(即其所有子元素)进行深度复制。
我们可以直接使用 template.content
,但这样做会从元素中删除内容并将其附加到文档的 body 中。任何 DOM 节点只能连接在一个位置,因此后续使用模板内容将导致空文档片段(本质上是空值),因为内容先前已被移动。使用 document.importNode
允许我们在多个位置重用同一模板内容的实例。
然后将该节点附加到 document.body
中,并为用户呈现。这最终使我们能够做一些有趣的事情,例如为我们的用户(或程序的使用者)提供创建内容的模板,类似于我们在 第一篇文章 中介绍的以下演示
查看 CodePen 上 Caleb Williams 的示例
模板示例(@calebdwilliams)
在 CodePen 上。
在此示例中,我们提供了两个模板来呈现相同的内容——作者及其撰写的书籍。随着表单的变化,我们选择渲染与该值关联的模板。使用相同的技术,我们最终将能够创建一个自定义元素,该元素将使用稍后定义的模板。
模板的多功能性
关于模板的一个有趣的事情是,它们可以包含任何 HTML。包括脚本和样式元素。一个非常简单的例子是一个模板,它附加一个按钮,当我们点击它时会发出警报。
<button id="click-me">Log click event</button>
让我们对其进行样式设置
button {
all: unset;
background: tomato;
border: 0;
border-radius: 4px;
color: white;
font-family: Helvetica;
font-size: 1.5rem;
padding: .5rem 1rem;
}
…并使用一个非常简单的脚本调用它
const button = document.getElementById('click-me');
button.addEventListener('click', event => alert(event));
当然,我们可以使用 HTML 的 <style>
和 <script>
标签直接在模板中组合所有这些内容,而不是在单独的文件中
<template id="template">
<script>
const button = document.getElementById('click-me');
button.addEventListener('click', event => alert(event));
</script>
<style>
#click-me {
all: unset;
background: tomato;
border: 0;
border-radius: 4px;
color: white;
font-family: Helvetica;
font-size: 1.5rem;
padding: .5rem 1rem;
}
</style>
<button id="click-me">Log click event</button>
</template>
一旦此元素附加到 DOM,我们将拥有一个新的 ID 为 #click-me
的按钮,一个针对按钮 ID 的全局 CSS 选择器,以及一个简单的事件监听器,它将发出元素的点击事件警报。
对于我们的脚本,我们只需使用 document.importNode
附加内容,我们就可以获得一个大部分包含的 HTML 模板,可以从一个页面移动到另一个页面。
查看 CodePen 上 Caleb Williams 的示例
带有脚本和样式的模板演示(@calebdwilliams)
在 CodePen 上。
为我们的对话框创建模板
回到我们创建对话框元素的任务,我们想要定义模板的内容和样式。
<template id="one-dialog">
<script>
document.getElementById('launch-dialog').addEventListener('click', () => {
const wrapper = document.querySelector('.wrapper');
const closeButton = document.querySelector('button.close');
const wasFocused = document.activeElement;
wrapper.classList.add('open');
closeButton.focus();
closeButton.addEventListener('click', () => {
wrapper.classList.remove('open');
wasFocused.focus();
});
});
</script>
<style>
.wrapper {
opacity: 0;
transition: visibility 0s, opacity 0.25s ease-in;
}
.wrapper:not(.open) {
visibility: hidden;
}
.wrapper.open {
align-items: center;
display: flex;
justify-content: center;
height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 1;
visibility: visible;
}
.overlay {
background: rgba(0, 0, 0, 0.8);
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
}
.dialog {
background: #ffffff;
max-width: 600px;
padding: 1rem;
position: fixed;
}
button {
all: unset;
cursor: pointer;
font-size: 1.25rem;
position: absolute;
top: 1rem;
right: 1rem;
}
button:focus {
border: 2px solid blue;
}
</style>
<div class="wrapper">
<div class="overlay"></div>
<div class="dialog" role="dialog" aria-labelledby="title" aria-describedby="content">
<button class="close" aria-label="Close">✖️</button>
<h1 id="title">Hello world</h1>
<div id="content" class="content">
<p>This is content in the body of our modal</p>
</div>
</div>
</div>
</template>
此代码将作为我们对话框的基础。简要分解一下,我们有一个全局关闭按钮、一个标题和一些内容。我们还添加了一些行为来视觉上切换我们的对话框(尽管它尚不可访问)。不幸的是,样式和脚本内容没有作用域到我们的模板,而是应用于整个文档,导致当将多个模板实例添加到 DOM 时出现不太理想的行为。在下一篇文章中,我们将使用自定义元素并创建一个自己的元素,该元素实时使用此模板并封装元素的行为。
查看 CodePen 上 Caleb Williams 的示例
带有脚本的对话框模板(@calebdwilliams)
在 CodePen 上。
文章系列
- Web Components 简介
- 创建可复用的 HTML 模板(本文)
- 从头开始创建自定义元素
- 使用 Shadow DOM 封装样式和结构
- Web Components 的高级工具
我最近一直在做的是,而不是在模板内的
<style>
标签中编写样式,我一直在使用<style>@import '/path/to/component.css';</style>
。这样,我就可以轻松地维护样式表,还可以从那里包含其他样式表,例如您想要添加的无法直接穿透 Shadow DOM 的通用全局样式。
嗨,Laxman,这是一个完全合理的策略,如果您完全确定知道样式的路径,那么它很有意义。但是,对于此示例,我想确保涵盖最基本的内容,而不会深入探讨导入样式的最佳方法(这将在 Shadow DOM 文章中详细介绍)。
模板内的 style 和 script 元素是否以任何方式作用域化?或者它们是否只是成为文档中的额外样式和脚本,服从样式的常规级联规则,并插入到脚本的相同 JavaScript 命名空间中?如果未作用域化,脚本中的函数将每次模板使用时都重复,并且每次都会覆盖先前副本的名称?样式也会在级联中不断累积。这两者都不会使多用模板中的 style 和 script 变得特别有吸引力。
嗨,Glenn,感谢你的提问。不,模板内的 style 和 script 元素没有作用域,因此以这种方式使用该元素并不是一个很好的策略。模板节点实际上更多地用于 HTML 而不是样式或脚本,但我希望演示它们可以以这种方式使用。在接下来的两篇文章中,我们将讨论如何进一步优化此代码,利用自定义元素和 Shadow DOM。
系列不错。我真的很喜欢你能够尽可能简洁地解释它,但没有遗漏主要内容。不过有一件事让我困扰。使用
document.importNode()
而不是Node.cloneNode()
(例如fragment.content.cloneNode(true);
)是否有特定原因?请继续保持更新。
所以两者之间并没有太大的区别。如果我没记错的话,我认为使用
cloneNode
会在文档不同时隐式地采用节点(并且它们会是不同的,因为模板节点是一个文档片段)。所以在这种情况下,document.importNode
更明确。非常棒的文章!有人使用 HTML 元素模板基础设施实现了模板引擎吗?将此类模板异步加载到您的网站上也很有趣,或者这很冗长?
我有一个小问题,我是一位有经验的前端开发人员,并且我正在使用 Bootstrap 作为框架用 HTML/CSS 构建我的网站。我想做的是将我的主页元素:顶部导航栏、页脚等设置成模板,并在每个页面上调用它们。这些 Web 组件是完成此操作的好方法吗?或者是否有更简单/更少步骤的方法来实现这一点,即使是使用 Javascript?