当我参加会议(当我们能够做这些事情的时候)并看到有人做关于网络组件的演示时,我一直认为它很不错(是的,显然,我来自 1950 年),但它总是看起来很复杂而且过分。一千行 JavaScript 来节省四行 HTML。演讲者不可避免地要么掩盖了大量的 JavaScript 以使其正常工作,要么他们会详细说明,而我的眼睛会变得模糊,因为我在想我的每日津贴是否包括零食。
但在最近的 参考项目 中,为了使学习 HTML 变得更容易(通过添加僵尸和愚蠢的笑话,当然),我内心完美主义者决定我必须涵盖规范中的每个 HTML 元素。除了那些会议演示之外,这是我第一次接触到 <slot>
和 <template>
元素。但当我试图写一些准确且引人入胜的东西时,我不得不更深入地研究。
并且在这个过程中我学到了一些东西:网络组件比我记忆中的容易得多。
文章系列
- 网络组件比你想象的更容易 (您当前位置)
- 交互式网络组件比你想象的更容易
- 在 WordPress 中使用网络组件比你想象的更容易
- 使用网络组件增强内置元素“比你想象的更容易”
- 上下文感知网络组件比你想象的更容易
- 网络组件伪类和伪元素比你想象的更容易
要么是网络组件自从我上次在会议上梦想着零食时已经有了很大的进步,要么是我对它们的最初恐惧阻碍了我真正了解它们——可能两者都有。
我在这里告诉你,你可以——是的,你——创建一个网络组件。让我们暂时把我们的分心、恐惧,甚至我们的零食都留在门口,一起做这件事。
<template>
开始
让我们从 <template>
是一个 HTML 元素,它允许我们创建,嗯,一个模板——网络组件的 HTML 结构。模板不必是大量的代码。它可以像这样简单:
<template>
<p>The Zombies are coming!</p>
</template>
<template>
元素很重要,因为它将所有东西都整合在一起。它就像建筑的基石;它是构建其他一切的基础。让我们将这段小的 HTML 代码作为 <apocalyptic-warning>
网络组件的模板——你知道的,当僵尸末日来临时,这是一个警告。
<slot>
然后是 <slot>
只是一个像 <template>
一样的 HTML 元素。但在这种情况下,<slot>
自定义了 <template>
在页面上呈现的内容。
<template>
<p>The <slot>Zombies</slot> are coming!</p>
</template>
在这里,我们在模板化的标记中插入了“Zombies”一词。如果我们不对插槽做任何操作,它将默认为标签之间的内容。在本例中,将是“Zombies”。
使用 <slot>
很像使用占位符。我们可以按原样使用占位符,或者定义其他东西来代替它。我们使用 name
属性来做到这一点。
<template>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
name
属性告诉网络组件模板中哪个内容对应哪个位置。现在,我们有一个名为 whats-coming
的插槽。我们假设在末日中僵尸会先来,但 <slot>
为我们提供了一些灵活性来插入其他东西,例如如果最终是机器人、狼人,甚至是网络组件末日。
使用组件
我们实际上已经“编写”了组件,可以将其放置在我们想要使用它的任何地方。
<apocalyptic-warning>
<span slot="whats-coming">Halitosis Laden Undead Minions</span>
</apocalyptic-warning>
<template>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
看到我们做了什么吗?我们将 <apocalyptic-warning>
组件放在页面上,就像任何其他 <div>
或其他元素一样。但我们还在那里放置了一个 <span>
,它引用了我们 <slot>
的 name
属性。而这个 <span>
之间的内容就是我们希望在组件渲染时替换“Zombies”的内容。
这里有一个值得注意的小问题:自定义元素名称必须包含连字符。这只是你需要了解的事情之一。 规范 规定这样做是为了防止在 HTML 发布具有相同名称的新元素时发生冲突。
到目前为止,你还能跟上吗?还不算太可怕吧?除了僵尸。我们还需要做一些工作才能使 <slot>
交换成为可能,这就是我们开始使用 JavaScript 的地方。
注册组件
正如我所说,你需要一些 JavaScript 来使这一切工作,但这并非像我之前想象的那样超级复杂、上千行的、深入的代码。希望我也可以说服你。
你需要一个注册自定义元素的构造函数。否则,我们的组件就像不死生物:它在那里,但没有完全复活。
这是我们将使用的构造函数
// Defines the custom element with our appropriate name, <apocalyptic-warning>
customElements.define("apocalyptic-warning",
// Ensures that we have all the default properties and methods of a built in HTML element
class extends HTMLElement {
// Called anytime a new custom element is created
constructor() {
// Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element
super();
// Grabs the <template> and stores it in `warning`
let warning = document.getElementById("warningtemplate");
// Stores the contents of the template in `mywarning`
let mywarning = warning.content;
const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
}
});
我在那里留下了详细的注释,逐行解释了内容。除了最后一行
const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
我们在这里做了很多事情。首先,我们获取我们的自定义元素 (this
) 并创建一个秘密特工——我的意思是,影子 DOM。mode:
open
仅仅意味着来自 :root
之外的 JavaScript 可以访问和操作影子 DOM 中的元素,这有点像为组件设置后门访问。
从那里开始,影子 DOM 已经创建,我们向其附加了一个节点。该节点将是模板的深层副本,包括模板的所有元素和文本。将模板附加到自定义元素的影子 DOM 后,<slot>
和 slot
属性将接管以将内容与应该放置的位置匹配。
看看这个。现在,我们可以放置两个相同组件的实例,只需更改一个元素即可渲染不同的内容。
为组件设置样式
您可能已经注意到该演示中的样式。正如您所料,我们绝对有能力使用 CSS 为组件设置样式。事实上,我们可以在 <template>
中直接包含一个 <style>
元素。
<template id="warningtemplate">
<style>
p {
background-color: pink;
padding: 0.5em;
border: 1px solid red;
}
</style>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
这样,由于影子 DOM,样式将直接限定到组件,并且不会泄漏到同一页面上的其他元素。
现在在我的脑海中,我认为自定义元素正在复制模板,插入您添加的内容,然后使用影子 DOM 将其注入页面。虽然在前端看起来是这样,但实际上它在 DOM 中并非如此。自定义元素中的内容会保留在原位,而影子 DOM 就像覆盖一样叠加在上面。

由于内容实际上位于模板外部,因此我们在模板的 <style>
元素中使用的任何后代选择器或类都不会对插槽内容产生影响。这无法像我希望或预期的那样实现完全封装。但由于自定义元素是一个元素,因此我们可以在任何 CSS 文件中将其用作元素选择器,包括页面上使用的主样式表。虽然插入的材料实际上并不在模板中,但它在自定义元素中,来自 CSS 的后代选择器将起作用。
apocalyptic-warning span {
color: blue;
}
但要注意!主 CSS 文件中的样式无法访问 <template>
或影子 DOM 中的元素。
让我们把所有这些拼凑起来
让我们看一个例子,比如一个僵尸约会服务的个人资料,比如你在末日之后可能需要的一个。为了对默认内容和任何插入的内容进行样式化,我们需要在<template>
中同时使用<style>
元素和 CSS 文件中的样式。
JavaScript 代码完全相同,只是现在我们正在使用不同的组件名称,<zombie-profile>
。
customElements.define("zombie-profile",
class extends HTMLElement {
constructor() {
super();
let profile = document.getElementById("zprofiletemplate");
let myprofile = profile.content;
const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));
}
}
);
这是 HTML 模板,包括封装的 CSS
<template id="zprofiletemplate">
<style>
img {
width: 100%;
max-width: 300px;
height: auto;
margin: 0 1em 0 0;
}
h2 {
font-size: 3em;
margin: 0 0 0.25em 0;
line-height: 0.8;
}
h3 {
margin: 0.5em 0 0 0;
font-weight: normal;
}
.age, .infection-date {
display: block;
}
span {
line-height: 1.4;
}
.label {
color: #555;
}
li, ul {
display: inline;
padding: 0;
}
li::after {
content: ', ';
}
li:last-child::after {
content: '';
}
li:last-child::before {
content: ' and ';
}
</style>
<div class="profilepic">
<slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>
</div>
<div class="info">
<h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>
<span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>
<span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>
<div class="interests">
<span class="label">Interests: </span>
<slot name="z-interests">
<ul>
<li>Long Walks on Beach</li>
<li>brains</li>
<li>defeating humanity</li>
</ul>
</slot>
</div>
<span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>
</div>
</template>
这是我们主 CSS 文件中<zombie-profile>
元素及其后代的 CSS。请注意,其中存在重复,以确保替换的元素和模板中的元素具有相同的样式。
zombie-profile {
width: calc(50% - 1em);
border: 1px solid red;
padding: 1em;
margin-bottom: 2em;
display: grid;
grid-template-columns: 2fr 4fr;
column-gap: 20px;
}
zombie-profile img {
width: 100%;
max-width: 300px;
height: auto;
margin: 0 1em 0 0;
}
zombie-profile li, zombie-profile ul {
display: inline;
padding: 0;
}
zombie-profile li::after {
content: ', ';
}
zombie-profile li:last-child::after {
content: '';
}
zombie-profile li:last-child::before {
content: ' and ';
}
现在一起!
虽然仍然有一些问题和其他细微差别,但我希望你现在比几分钟前更有能力使用 Web 组件。像我们这里一样试水。也许可以在你的工作中撒上一些自定义组件,以了解它以及它在何处有意义。
实际上就是这些。现在,你更害怕什么,Web 组件还是僵尸末日?在不久的过去,我可能会说 Web 组件,但现在我很自豪地说,僵尸是唯一让我担心的事情(好吧,还有我的每日津贴是否够买零食……)
关于自定义元素的命名,您可以在这里检查它是否有效且未被其他框架使用。
https://raw.githack.com/nuxodin/web-namespace-registry/main/web/index.html
不错。感谢分享这个资源!
不错!但浏览器支持情况如何?
实际上还不错。不是在 IE 这个不死浏览器中,但它们在其他几乎所有地方都能正常工作。https://caniuse.cn/custom-elementsv1
不错的文章!
关于自定义元素名称中的连字符,我不会说规范还在不断变化;这部分已经稳定多年,不太可能改变。但是,新的 Web 组件规范正在开发中,比如 AOM 等。
抓住了要点。通用规范比连字符更趋于变化,但从上下文来看并不清楚。感谢您的澄清!
好文章!感谢您抽出时间解释 Web 组件的工作原理!
不过,到最后我发现自己仍在思考“为什么要使用它?”
我不是在批评。我认为您在解释方面做得很好。我只是在想一个使用它的现实世界场景。
在示例中,每个都有相当多的 HTML,因此似乎我们并没有节省很多代码,但它确实增加了一层抽象,以及一个 JavaScript 依赖项。
通常,我期望用 PHP 做这样的事情,然后用 CSS 对其进行样式化。但我无法弄清楚为什么我想在其之上添加一层 Web 组件。
我想我可能只是遗漏了什么。
你能想到任何简单的现实世界应用程序吗?我很想听听您的想法。
添加交互式元素使用 Web 组件比仅仅用纯 Vanilla JS 编写它们要容易得多。WC 有点像是 React 的原生版本。
我一直都在想同样的事情(同时想着零食),我会同意 PHP 可以处理许多最低级的用例。但我认为有一些地方它可以闪耀。
当您需要隔离 CSS 样式时。虽然它不是像上面讨论的那样完美的封装,但您可以隔离很多。唯一其他方法是使用 iframe。
跨上下文的一致性。是的,这也可以用 PHP/服务器端语言来完成,但是如果有一些 1 的上下文,或者您想做 2,那么您可以在这些上下文中获得更高的一致性。
我喜欢把 Web 组件看作是工具箱中的一种工具。它不能解决所有问题,但有时它只是解决问题的正确方法。
组件就像您习惯使用的 PHP 模板。只有当您在同一个页面上或跨多个页面使用它们的多个实例时,它们才能节省代码。
最重要的是,即使它不节省代码行数,它也为您提供了一种将 HTML 分割成更简单块的方法,从而使其更易于管理和阅读
想象一下一个 4000 行的 PHP 文件,您会想以某种方式清理它,对吧?
为大块代码(例如 zombie-profile)命名使它更容易阅读和在搜索东西时跳过。
范围内的 CSS 是很多人都很兴奋的东西。就我个人而言,我更希望有一种方法可以从全局 CSS 中更改所有组件的样式,但这只是我个人的想法。
Web 组件是设计系统的良好用例。如果您在一个更大的团队中工作,而他们希望在不同的团队中使用不同的 JavaScript 库,那么可能很难保持组件设计的一致性。
您可以在任何框架(例如 React 或 Vue)中使用 Web 组件。因此,开发人员只需添加“”。组件带有内置的设计。您可以更新 Web 组件库的更改,所有团队在从 NPM 或通过 CDN 重新加载库时都会获得新设计。
您好,好文章!我受到您的教程的启发,在 Observable 上重新创建它来学习如何使其工作。这是一个链接 https://observablehq.com/@triptych/web-component-test
感谢您!由于这篇文章,我将开始更频繁地使用 Web 组件。
我想说学习 Web 组件比现在学习任何新的框架/库都要容易得多。
我甚至自己做了一些。
这是我的 switch-component
对于应用程序开发人员来说,非常好的信息。这些混合工具可以更快地为所有平台同时开发应用程序。这些功能会在一段时间内逐渐改进。尽管如此,对于快速开发应用程序来说,这是一个良好的开端
感谢这篇文章!非常有信息量且有趣:) 这个示例可能是 CSS 伪元素 ::slotted() 的良好用例,它允许您从 shadow DOM 内对插槽的内容进行样式化 https://mdn.org.cn/en-US/docs/Web/CSS/::slotted
好发现,Rose。以前没见过 ::slotted()。我看了 ::part(),但这没有解决封装问题。看来我应该停止幻想零食了,我还有很多东西要学 :)
如果您有任何关于如何在 Drupal 8 或 9 网站中包含 Web 组件的提示,我很想听听。感谢您澄清了否则很复杂的一个主题。
我没有使用过 Drupal 8 或 9(几年前我使用过 Drupal 7,但那项技能已经退化了)。我的(可能非常幼稚的)假设是,您会像在主题或模块中包含任何其他 JS/CSS 一样包含 JS/CSS。抱歉我帮不了太多。
不幸的是,对于我的组织和其他组织来说,仍然支持 IE。
希望有一些关于支持那里的 polyfills 的建议。似乎有 Polymer 这样的完整框架,以及少数其他 polyfill 库。指导会很棒。
你可怜,可怜的人。我自己没有使用过 polyfills,但 webcomponents.org 有一篇关于它的好文章 https://www.webcomponents.org/polyfills
怀旧的旁注,以前在 IE6 中是可以实现的(自定义元素和 htc)。
看到人们像这样构建一次性 Web 组件很有趣。很多人都没有学习“框架”的意愿,但我认为 Web 组件的真正力量体现在设计系统等更大型的东西中。我用 Stencil 创建组件,Stencil 非常像框架——我之前对 node.js 和现代工具的背景了解很少,所以学习曲线很陡峭。Stencil 为你带来的好处是,对 IE11/旧版 Edge 和更新的 JS 特性都提供了强大的 polyfills。我感觉被宠坏了,能够使用最新的 JS 特性(大多数情况下),而不必担心兼容性问题,因为所有这些都将被编译成可用的、带 polyfills 的浏览器 JS。
很棒的文章。这让我更加高兴能学习 Svelte!组件和使用原生 JS 的所有好处,但从开发的角度来看,它更容易。
他们新的 kit.svelte.dev 项目尤其令人兴奋。
这篇文章没有提到的是,如何使用数据对象属性来使代码更简洁。
虽然这种级别的封装很好
这更好
我还没有找到任何原生实现此功能的好方法,但似乎 Lit 框架在 Web 组件周围创建了一个薄包装,以提供这些功能。可能还有其他我不知道的选择,但这是我见过的最有希望的。
模板部分很糟糕。如果你要在一个页面上共享组件,它应该与 JavaScript 文件一起共享:并且你将不得不使用纯文本。这很糟糕,至少在还没有一些编译器工具之前是这样的。
或者,它“可以”是你可以为不同页面提供不同模板的优势,但…至少你需要提供一个“默认”模板。