时间:2023-06-15 22:27:50 点击次数:8
本文翻译自 Crafting Reusable HTML Templates | CSS-Tricks
作者:Caleb Williams
发布日期:2019-03-19
更新日期:2019-03-22
在上篇文章中,我们从高层次讨论了 Web 组件规范(自定义元素,影子 DOM,以及 HTML 模板)。在本文以及接下来的三篇文章中,我们将测试这些技术,并对它们进行更详细的研究,看看如何在现代生成中使用它们。为此,我们将从零开始构建一个自定义模态对话框,看看各种技术是如何结合到一起的。
文章系列:
第一部分:Web 组件介绍(也就是本文)第二部分:创建可重用的 HTML 模板
第三部分:从零开始创建自定义元素 第四部分:用影子 DOM 封装样式和结构 第五部分:Web 组件的高级工具Web 组件规范 中最不被认可但功能最强大的特性之一是 <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 方法中。该方法将创建模板内容的一个副本,并准备将其插入到另一个文档(或文档片段)中。函数的第一个参数获取模板的内容,第二参数告诉浏览器对元素的 DOM 子树(即其所有子树)进行深度赋值。
我们本可以直接使用 template.content,但这样做会将移除元素的内容并以后将其添加到文档的 body 中。任何 DOM 节点都只能在一个位置连接,所以后续使用模板的内容将导致一个空的文档片段(本质上是空值),因为内容先前已经被移动。使用 documnet.importNode 方法允许我们在多个位置重用相同的模板内容。
然后该节点被附加到 document.body 中,并呈现给用户。这最终允许我们做一些有趣的事情,比如为用户(或我们程序的使用者)提供创建内容的模板,类似于下面的 demo,我们在上一篇文章中提到过:
<template id="book-template"> <li><span class="title"></span> — <span class="author"></span></li> </template> <template id="book-template-2"> <li><span class="author"></span>s classic novel <span class="title"></span></li> </template> <ul id="books"></ul> <fieldset id="templates"> <legend>Choose template</legend> <label> <input type="radio" name="template" value="book-template" checked> Template One </label> <label> <input type="radio" name="template" value="book-template-2"> Template Two </label> </fieldset> label { display: block; margin-bottom: 0.5rem; } use strict; const books = [ { title: The Great Gatsby, author: F. Scott Fitzgerald }, { title: A Farewell to Arms, author: Ernest Hemingway }, { title: Catch 22, author: Joseph Heller } ]; function appendBooks(templateId) { const booksList = document.getElementById(books); const fragment = document.getElementById(templateId); // Clear out the content from the ul booksList.innerHTML = ; // Loop over the books and modify the given template books.forEach(book => { // Create an instance of the template content const instance = document.importNode(fragment.content, true); // Add relevant content to the template instance.querySelector(.title).innerHTML = book.title; instance.querySelector(.author).innerHTML = book.author; // Append the instance ot the DOM booksList.appendChild(instance); }); } document.getElementById(templates).addEventListener(change, (event) => appendBooks(event.target.value)); appendBooks(book-template);在本例中,我们提供了两个模板来呈现相同的内容——作者和他们所写的书籍。当表单更改时,我们选择呈现与该值关联的模板。我们最终会使用这种技术创建一个自定义元素,该元素将使用稍后定义的模板。
模板的一个有趣之处是,它可以包含任何 HTML 代码。包括 script 和 style 元素。举一个非常简单的例子,假设有个模板,它附加了一个按钮,当点击按钮时会弹出一个提醒。
<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,一个全局的 CSS 选择器目标时按钮的 ID,以及一个简单的事件监听器,它将提醒元素的 click 事件。
对于我们的脚本,我们简单地使用 document.inportNode 附加内容,并且我们有一个主要包含 HTML 的模板,可以在页面之间移动。
use strict; const template = document.getElementById(template); document.body.appendChild( document.importNode(template.content, true) );回到我们制作对话框元素的任务,我们想要定义模板的内容和样式。
<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 中时,会导致不理想的行为。在我们下一篇文章中,我们将用到自定义元素,创建一个我们自己实时使用此模板并封装元素行为的元素。
完整示例还包含以下 CSS 和 JS 代码:
#launch-dialog { background: tomato; border-radius: 4px; color: #fff; font-family: Helvetica, Arial, sans-serif; padding: 0.5rem 1rem; position: static; } const template = document.getElementById(dialog-template); document.body.appendChild( document.importNode(template.content, true) );结果:
点击前:
点击后:
文章系列:
第一部分:Web 组件介绍(也就是本文)第二部分:创建可重用的 HTML 模板
第三部分:从零开始创建自定义元素 第四部分:用影子 DOM 封装样式和结构 第五部分:Web 组件的高级工具Laxman March 19, 2019
我最近不在模板里的 。
这样,我可以轻松的维护样式,并且还可以从中包含其他样式,比如您想要添加的普通全局样式,它们不能直接穿影子 DOM。
Caleb Williams March 20, 2019
Hey Laxman,这是一个完全合法的策略,如果你绝对确定你知道引用样式的路径,这种策略非常有意义。 然而,对于本文示例,我希望确保涵盖最基本的内容,而不深入了解导入样式的最佳方式(这点将在影子 DOM 文章中详细介绍)。
Glenn March 19, 2019
模板中的样式和脚本元素是否以任何方式限定了作用范围?或者他们只是作为一个整体附加到整个文档中,前者遵循样式的常规级联规则,后者插入到相同的 JavaScript 命名空间中?如果没有作用域限制,那脚本中的函数将在每次使用模板时复制一次,并且每次都会覆盖上一个副本的名称?而样式将在级联中不停堆叠?这两种机制都不会使用多实例模板中的样式和脚本有啥吸引力。
Caleb Williams March 20, 2019
Hey Glenn,多谢提问。不对,模板中的样式和脚本不受范围限制,所以使用这种使用元素的方式并不是一个很好的策略。模板节点实际上更多用于 HTML,而不是样式或脚本,本文我仅仅是想证明它们可以以这种方式使用,尽管这种方式不一定好。在后续的两篇文章中,我们将讨论如何利用自定义元素和影子 DOM 来进一步优化此代码。
Konstantin March 20, 2019
不错的系列。我真的很喜欢你可以尽可能简短地解释 Web 组件,但又不遗漏主要内容。但有件事很困扰我。您使用 document.importNode() 而不是 Node.cloneNode()(例如,fragment.content.cloneNode(true);)有什么特殊原因吗?
请务必回复。
Caleb Williams March 20, 2019
这两者之间并没有太大的区别。如果我没记错的话,我认为如果文档不同,使用 cloneNode 将隐含地采用节点(它们可能是模板节点是文档片段)。所以在本例中,document.importNode 更显示。
Andrew April 20, 2019
我在前端开发中遇到一个小问题 & 我正在使用 HTML/CSS 以及 bootstrap 框架构建自己的网站。我想做的是将我的主页元素(包括顶部导航栏、页脚等)设置为模板,并在每个页面上调用它们。Web 组件是实现这一点的好方法吗?或者是否存在一种更简单/更少步骤的方法实现这一点(要用 JavaScript 也没关系)?