我们都有我们不会从事的项目。代码变得难以管理,范围不断扩大,快速修复应用在其他修复之上,结构在其意大利面条代码的重压下崩溃了。编码可能是一项混乱的业务。
项目受益于使用具有单一职责的简单、独立的模块。模块化代码被封装,因此无需担心实现。只要您知道在给定一组输入时模块将输出什么,您就不一定需要了解它是如何实现该目标的。
将模块化概念应用于单一编程语言很简单,但 Web 开发需要多种技术组合。浏览器解析HTML、CSS 和 JavaScript 以呈现页面的内容、样式和功能。
它们并不总是很容易混合,因为:
- 相关代码可以在三个或更多文件之间拆分,并且
- 全局样式和 JavaScript 对象可能会以意想不到的方式相互干扰。
这些问题是语言运行时、框架、数据库和服务器上使用的其他依赖项所遇到的问题之外的问题。
什么是 Web 组件?
Web 组件是一种创建可在任何页面上重用的封装的、单一职责的代码块的方法。
考虑 HTML<video>
标签。给定一个URL,查看者可以使用诸如播放、暂停、后退、前进和调整音量等控件。
提供了样式和功能,但您可以使用各种属性和 JavaScript API 调用进行修改。任何数量的<video>
元素都可以放在其他标签内,它们不会冲突。
如果您需要自己的自定义功能怎么办?例如,显示页面上单词数的元素?没有 HTML<wordcount>
标签(还)。
React和Vue.js等框架允许开发人员创建 Web 组件,其中内容、样式和功能可以在单个 JavaScript 文件中定义。这些解决了许多复杂的编程问题,但请记住:
- 您必须学习如何使用该框架并随着代码的发展更新您的代码。
- 为一个框架编写的组件很少与另一个框架兼容。
- 框架在流行中起起落落。您将变得依赖于开发团队和用户的突发奇想和优先事项。
- 标准的 Web Components 可以添加浏览器功能,这是单靠 JavaScript 难以实现的(例如 Shadow DOM)。
幸运的是,库和框架中引入的流行概念通常会进入 Web 标准。这花了一些时间,但 Web 组件已经到来。
将模块化概念应用于单一编程语言很简单,但 Web 开发需要多种技术组合。👩💻 在本指南中了解更多信息⬇️点击推文
Web 组件简史
在许多特定于供应商的错误开始之后,标准 Web 组件的概念由Alex Russell 在 2011 年的 Fronteers Conference 上首次提出。Google 的Polymer 库(基于当前提案的 polyfill)在两年后问世,但直到 2016 年,Chrome 和 Safari 才出现早期的实现。
浏览器供应商花时间协商细节,但 Web Components 在 2018 年被添加到 Firefox 中,在 2020 年被添加到 Edge 中(当时微软切换到 Chromium 引擎)。
可以理解的是,很少有开发人员愿意或能够采用 Web 组件,但我们终于通过稳定的 API 达到了良好的浏览器支持水平。并非一切都是完美的,但它们越来越成为基于框架的组件的可行替代方案。
即使你还不愿意放弃你最喜欢的,Web Components 与每个框架都兼容,并且 API 将在未来几年内得到支持。
每个人都可以查看预构建 Web 组件的存储库:
…但是编写自己的代码更有趣!
本教程完整介绍了不使用 JavaScript 框架编写的 Web 组件。您将了解它们是什么以及如何将它们用于您的 Web 项目。您将需要一些HTML5、CSS 和 JavaScript 知识。
Web 组件入门
Web 组件是自定义 HTML 元素,例如<hello-world></hello-world>
. 名称必须包含破折号,以免与 HTML 规范中正式支持的元素发生冲突。
您必须定义一个 ES2015 类来控制元素。它可以命名任何东西,但 HelloWorld 是常见的做法。它必须扩展HTMLElement 接口,它表示每个 HTML 元素的默认属性和方法。
注意: Firefox允许您扩展特定的 HTML 元素,例如 HTMLParagraphElement、HTMLImageElement 或 HTMLButtonElement。这在其他浏览器中不受支持,并且不允许您创建 Shadow DOM。
为了做任何有用的事情,该类需要一个名为connectedCallback()的方法,当元素添加到文档时会调用该方法:
class HelloWorld extends HTMLElement {
// connect component
connectedCallback() {
this.textContent = 'Hello World!';
}
}
在此示例中,元素的文本设置为“Hello World”。
该类必须向CustomElementRegistry注册以将其定义为特定元素的处理程序:
customElements.define( 'hello-world', HelloWorld );
现在,当您的 JavaScript 被加载(例如)时,浏览器会将<hello-world>
元素与您的HelloWorld<script type="module" src="./helloworld.js"></script>
类相关联。
您现在有一个自定义元素!
这个组件可以像任何其他元素一样在 CSS 中设置样式:
hello-world {
font-weight: bold;
color: red;
}
添加属性
这个组件没有好处,因为无论如何都会输出相同的文本。像任何其他元素一样,我们可以添加 HTML 属性:
<hello-world name="Craig"></hello-world>
这可能会覆盖文本,因此“Hello Craig!” 被展示。为此,您可以向HelloWorld类添加一个constructor()函数,该函数在创建每个对象时运行。它必须:
- 调用super()方法来初始化父 HTMLElement,并且
- 进行其他初始化。在这种情况下,我们将定义一个设置为默认值“World”的名称属性:
class HelloWorld extends HTMLElement {
constructor() {
super();
this.name = 'World';
}
// more code...
您的组件只关心name属性。静态observedAttributes()属性应返回要观察的属性数组:
// component attributes
static get observedAttributes() {
return ['name'];
}
当在 HTML 中定义属性或使用 JavaScript 更改属性时,将调用attributeChangedCallback()方法。它传递了属性名称、旧值和新值:
// attribute change
attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) return;
this[ property ] = newValue;
}
在此示例中,只会更新name属性,但您可以根据需要添加其他属性。
最后,您需要在connectedCallback()方法中调整消息:
// connect component
connectedCallback() {
this.textContent = `Hello ${ this.name }!`;
}
生命周期方法
浏览器在 Web 组件状态的整个生命周期中自动调用六个方法。此处提供了完整列表,尽管您已经在上面的示例中看到了前四个:
构造函数()
它在组件第一次初始化时被调用。它必须调用super()并且可以设置任何默认值或执行其他预渲染过程。
静态observedAttributes()
返回浏览器将观察到的属性数组。
属性更改回调(属性名称,旧值,新值)
每当观察到的属性发生变化时调用。在 HTML 中定义的那些会立即传递,但 JavaScript 可以修改它们:
document.querySelector('hello-world').setAttribute('name', 'Everyone');
发生这种情况时,该方法可能需要触发重新渲染。
连接回调()
当 Web 组件附加到文档对象模型时调用此函数。它应该运行任何所需的渲染。
断开回调()
当从文档对象模型中删除 Web 组件时调用它。如果您需要清理,例如删除存储的状态或中止Ajax 请求,这可能很有用。
通过回调()
当 Web 组件从一个文档移动到另一个文档时调用此函数。尽管我一直在努力考虑任何情况,但您可能会发现它的用途!
Web 组件如何与其他元素交互
Web 组件提供了一些您在 JavaScript 框架中找不到的独特功能。
影子 DOM
虽然我们在上面构建的 Web 组件可以正常工作,但它不能免受外部干扰,CSS 或 JavaScript 可以修改它。同样,您为组件定义的样式可能会泄漏并影响其他人。
Shadow DOM 通过将分离的 DOM 附加到 Web 组件来解决这个封装问题:
const shadow = this.attachShadow({ mode: 'closed' });
模式可以是:
- “open” ——外部页面中的 JavaScript 可以访问 Shadow DOM(使用Element.shadowRoot),或者
- “关闭” ——Shadow DOM 只能在 Web 组件中访问。
Shadow DOM 可以像任何其他 DOM 元素一样被操作:
connectedCallback() {
const shadow = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
p {
text-align: center;
font-weight: normal;
padding: 1em;
margin: 0 0 2em 0;
background-color: #eee;
border: 1px solid #666;
}
</style>
<p>Hello ${ this.name }!</p>`;
}
该组件现在在元素内呈现“Hello”文本<p>
并为其设置样式。它不能被组件外部的 JavaScript 或 CSS 修改,尽管某些样式(例如字体和颜色)是从页面继承的,因为它们没有明确定义。
此 Web 组件范围内的样式不会影响页面上的其他段落甚至其他<hello-world>
组件。
请注意,CSS选择器可以在 Web 组件:host
中设置外部元素的样式:<hello-world>
:host {
transform: rotate(180deg);
}
您还可以设置元素使用特定类时应用的样式,例如<hello-world class="rotate90">
:
:host(.rotate90) {
transform: rotate(90deg);
}
HTML 模板
对于更复杂的 Web 组件,在脚本中定义 HTML 可能变得不切实际。模板允许您在页面中定义 Web 组件可以使用的 HTML 块。这有几个好处:
- 您可以调整 HTML 代码,而无需在 JavaScript 中重写字符串。
- 无需为每种类型创建单独的 JavaScript 类即可自定义组件。
- 在 HTML 中定义 HTML 更容易——并且可以在组件呈现之前在服务器或客户端上对其进行修改。
模板是在<template>
标签中定义的,并且可以分配一个 ID,以便您可以在组件类中引用它。本例三段显示“Hello”消息:
<template id="hello-world">
<style>
p {
text-align: center;
font-weight: normal;
padding: 0.5em;
margin: 1px 0;
background-color: #eee;
border: 1px solid #666;
}
</style>
<p class="hw-text"></p>
<p class="hw-text"></p>
<p class="hw-text"></p>
</template>
Web 组件类可以访问该模板,获取其内容,并克隆元素,以确保您在使用它的任何地方都创建一个唯一的 DOM 片段:
const template = document.getElementById('hello-world').content.cloneNode(true);
可以修改 DOM 并将其直接添加到 Shadow DOM:
connectedCallback() {
const
shadow = this.attachShadow({ mode: 'closed' }),
template = document.getElementById('hello-world').content.cloneNode(true),
hwMsg = `Hello ${ this.name }`;
Array.from( template.querySelectorAll('.hw-text') )
.forEach( n => n.textContent = hwMsg );
shadow.append( template );
}
模板槽
插槽允许您自定义模板。假设您想使用<hello-world>
Web 组件,但将消息放在 Shadow DOM 中的<h1>标题中。您可以编写以下代码:
<hello-world name="Craig">
<h1 slot="msgtext">Hello Default!</h1>
</hello-world>
(注意slot属性。)
您可以选择添加其他元素,例如另一个段落:
<hello-world name="Craig">
<h1 slot="msgtext">Hello Default!</h1>
<p>This text will become part of the component.</p>
</hello-world>
现在可以在您的模板中实现插槽:
需要一个可以为您提供竞争优势的托管解决方案?Kinsta 为您提供令人难以置信的速度、最先进的安全性和自动缩放功能。查看我们的计划
<template id="hello-world">
<slot name="msgtext" class="hw-text"></slot>
<slot></slot>
</template>
一个元素槽属性设置为“msgtext”(<h1>
)被插入到有一个<slot>
名为“msgtext”的位置。<p>
没有分配插槽名称,但在下一个可用的 unnamed 中使用<slot>
。实际上,模板变为:
<template id="hello-world">
<slot name="msgtext" class="hw-text">
<h1 slot="msgtext">Hello Default!</h1>
</slot>
<slot>
<p>This text will become part of the component.</p>
</slot>
</template>
实际情况并非如此简单。<slot>
Shadow DOM中的元素指向插入的元素。您只能通过定位 a<slot>
然后使用.assignedNodes() 方法返回一个内部子级数组来访问它们。更新的connectedCallback()方法:
connectedCallback() {
const
shadow = this.attachShadow({ mode: 'closed' }),
hwMsg = `Hello ${ this.name }`;
// append shadow DOM
shadow.append(
document.getElementById('hello-world').content.cloneNode(true)
);
// find all slots with a hw-text class
Array.from( shadow.querySelectorAll('slot.hw-text') )
// update first assignedNode in slot
.forEach( n => n.assignedNodes()[0].textContent = hwMsg );
}
此外,您不能直接设置插入元素的样式,尽管您可以针对 Web 组件中的特定插槽:
<template id="hello-world">
<style>
slot[name="msgtext"] { color: green; }
</style>
<slot name="msgtext" class="hw-text"></slot>
<slot></slot>
</template>
模板槽有点不寻常,但一个好处是,如果 JavaScript 无法运行,您的内容将被显示。此代码显示了仅在 Web 组件类成功执行时才被替换的默认标题和段落:
<hello-world name="Craig">
<h1 slot="msgtext">Hello Default!</h1>
<p>This text will become part of the component.</p>
</hello-world>
因此,您可以实现某种形式的渐进增强——即使它只是“您需要 JavaScript”的消息!
声明性 Shadow DOM
上面的示例使用 JavaScript 构建了一个 Shadow DOM。这仍然是唯一的选择,但正在为Chrome开发一个实验性的声明性 Shadow DOM 。这允许服务器端渲染并避免任何布局变化或无样式内容的闪烁。
HTML 解析器检测到以下代码,它会创建与您在上一节中创建的相同的 Shadow DOM(您需要根据需要更新消息):
<hello-world name="Craig">
<template shadowroot="closed">
<slot name="msgtext" class="hw-text"></slot>
<slot></slot>
</template>
<h1 slot="msgtext">Hello Default!</h1>
<p>This text will become part of the component.</p>
</hello-world>
该功能在任何浏览器中都不可用,并且不能保证它会到达 Firefox 或 Safari。您可以找到有关声明性 Shadow DOM 的更多信息,并且 polyfill 很简单,但请注意实现可能会发生变化。
影子 DOM 事件
您的 Web 组件可以像在页面 DOM 中一样将事件附加到 Shadow DOM 中的任何元素,例如监听所有内部子级的点击事件:
shadow.addEventListener('click', e => {
// do something
});
除非您stopPropagation,否则该事件将冒泡到页面 DOM 中,但该事件将被重新定位。因此,它似乎来自您的自定义元素,而不是其中的元素。
在其他框架中使用 Web 组件
您创建的任何 Web 组件都可以在所有JavaScript 框架中工作。他们都不知道也不关心 HTML 元素——你的<hello-world>
组件将被视为与 a 相同,<div>
并被放置在类将激活的 DOM 中。
custom-elements-everywhere.com提供了框架列表和 Web 组件说明。大多数都是完全兼容的,尽管 React.js 有一些挑战。可以<hello-world>
在 JSX 中使用:
import React from 'react';
import ReactDOM from 'react-dom';
import from './hello-world.js';
function MyPage() {
return (
<>
<hello-world name="Craig"></hello-world>
</>
);
}
ReactDOM.render(<MyPage />, document.getElementById('root'));
…但:
- React 只能将原始数据类型传递给 HTML 属性(而不是数组或对象)
- React 无法侦听 Web 组件事件,因此您必须手动附加自己的处理程序。
Web 组件批评和问题
Web 组件已显着改进,但某些方面可能难以管理。
造型困难
样式化 Web 组件会带来一些挑战,尤其是在您想要覆盖作用域样式时。有很多解决方案:
- 避免使用 Shadow DOM。您可以将内容直接附加到您的自定义元素,尽管任何其他 JavaScript 都可能意外或恶意更改它。
- 使用
:host
类。正如我们在上面看到的,当一个类应用于自定义元素时,作用域 CSS可以应用特定的样式。 - 查看 CSS 自定义属性(变量)。自定义属性级联到 Web 组件中,因此,如果您的元素使用
var(--my-color)
,您可以--my-color
在外部容器(例如:root
)中设置它,它将被使用。 - 利用阴影部分。新的::part() 选择器可以为具有 part 属性的内部组件设置样式,即组件
<h1 part="heading">
内部<hello-world>
可以使用 selector 设置样式hello-world::part(heading)
。 - 传入一串样式。您可以将它们作为属性传递以在
<style>
块内应用。
没有一个是理想的,您需要仔细计划其他用户如何自定义您的 Web 组件。
忽略的输入
Shadow DOM 中的任何<input>
、<textarea>
或<select>
字段都不会自动关联到包含的表单中。早期的 Web 组件采用者会将隐藏字段添加到页面 DOM 或使用FormData 接口来更新值。两者都不是特别实用和破坏 Web 组件封装。
新的 ElementInternals 接口允许 Web 组件与表单挂钩,因此可以定义自定义值和有效性。它在 Chrome 中实现,但polyfill 可用于其他浏览器。
为了演示,您将创建一个基本<input-age name="your-age"></input-age>
组件。该类必须将静态formAssociated值设置为 true,并且可以选择在关联外部表单时调用formAssociatedCallback()方法:
// <input-age> web component
class InputAge extends HTMLElement {
static formAssociated = true;
formAssociatedCallback(form) {
console.log('form associated:', form.id);
}
构造函数现在必须运行attachInternals()方法,该方法允许组件与表单和其他想要检查值或验证的JavaScript 代码进行通信:
constructor() {
super();
this.internals = this.attachInternals();
this.setValue('');
}
// set form value
setValue(v) {
this.value = v;
this.internals.setFormValue(v);
}
ElementInternal 的setFormValue()方法在此处为用空字符串初始化的父表单设置元素的值(也可以将其传递给具有多个名称/值对的 FormData 对象)。其他属性和方法包括:
- form : 父表单
- labels:标记组件的元素数组
- 约束验证 API选项,例如 willValidate、checkValidity 和 validationMessage
connectedCallback()方法像以前一样创建一个 Shadow DOM,但还必须监视该字段的更改,因此可以运行setFormValue() :
connectedCallback() {
const shadow = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>input { width: 4em; }</style>
<input type="number" placeholder="age" min="18" max="120" />`;
// monitor input values
shadow.querySelector('input').addEventListener('input', e => {
this.setValue(e.target.value);
});
}
您现在可以使用这个 Web 组件创建一个 HTML 表单,它的行为方式与其他表单字段类似:
<form id="myform">
<input type="text" name="your-name" placeholder="name" />
<input-age name="your-age"></input-age>
<button>submit</button>
</form>
它有效,但诚然感觉有点令人费解。
在CodePen 演示中查看它
有关更多信息,请参阅这篇关于功能更强大的表单控件的文章。
想要更好地了解 Web 组件及其工作原理?✅ 点击潜入⬇️点击推文
概括
在 JavaScript 框架的地位和功能不断增长的时候,Web 组件一直在努力获得共识和采用。如果您来自 React、Vue.js或Angular,Web 组件可能看起来复杂而笨重,尤其是当您缺少数据绑定和状态管理等功能时。
有一些问题需要解决,但 Web Components 的未来是光明的。它们与框架无关、轻量级、快速,并且可以实现仅在 JavaScript 中不可能实现的功能。
十年前,几乎没有人能够处理没有 jQuery 的网站,但浏览器供应商采用了优秀的部分并添加了本地替代方案(例如 querySelector)。JavaScript 框架也会发生同样的情况,而 Web 组件是第一步。