在本文中,我提供了对 Web 组件是什么以及如何使用它们的基本理解。使用现实生活中的示例,我将展示 Web 组件如何帮助使应用程序更可预测和更易于维护。此外,我分享了有关如何通过将 HTML/CSS/JS 代码隔离为……等待它……隔离的组件以供重用来重新利用某些代码编写的技巧。我还介绍了如何在应用程序布局中多次组合 Web 组件,以及如何在整个应用程序中执行相同的行为。
好的,让我们跳过所有的理论,直接进入几个代码示例。
示例 1 - 两个字段的简单原生 HTML 表单
<form>
<div class="form-field">
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required> <span class="error">This field is required!</span>
</div>
<div class="form-field">
<label for="email">Enter your email: </label>
<input type="email" name="email" id="email" required> <span class="error">This field is required!</span>
</div>
<div class="form-field">
<input type="submit" value="Subscribe!">
</div>
</form>
虽然上面的例子不是很令人兴奋,但这完美地说明了我关于可预测、易于维护的代码的观点。相信我。😉 我们已经可以预见,如果添加更多字段,代码将变得难以维护,一大堆字段、标签等。没有乐趣!
想象一下……有人要求在每个字段附近用额外的东西来扩充这个表格。他们要求提供信息文本。好家伙。
相反,一个好的起点是使用 Web 组件。它们允许我们仅扩展一个组件,该组件贯穿所有编码。甜的!
值得注意的是,Firefox(63 版)、Chrome 和 Opera 默认支持 Web 组件。Safari 支持许多 Web 组件功能,但提供的功能少于上述浏览器。Edge 的支持正在进行中。
示例 2:两个字段的相同形式,但使用 Web 组件实现。
<form>
<db-form-field type="text" label="Enter your name" required />
<db-form-field type="email" label="Enter your email" required />
<db-form-submit value="Subscribe!" />
</form>
看起来不错,对吧?🙂 此代码可读且可重用。这很棒,因为代码现在更易于维护,这也使未来的更改更新速度更快,错误更少。
实现表单域组件的分步指南
在本节中,我展示了部分代码示例,详细描述了如何创建可重用的表单字段,并注意如何在布局中使用它。(要查看成品,请到文末。😉)
请记住,Web 组件由 HTML 模板和使用影子 DOM 的自定义元素组成。这些特性有助于代码变得更加可维护和可预测。逻辑在一个地方实现。然后在整个程序中重复使用并应用于相同的组件。
第 1 步:创建 Web 组件的初始类,其中添加了所有逻辑。
class FormField extends HTMLElement {
constructor() {
super();
}
}
引入组件模板。模板是出现在 shadow DOM 中的组件的一部分。它可以在使用该组件的 HTML 中定义,也可以在同一个 .js 文件中定义。在大多数情况下,我更喜欢使用 .js 文件,因为它在一个地方包含所有相关的组件信息。
const template = document.createElement('template');
template.innerHTML = "It works!";
class FormField extends HTMLElement {
...
第 2 步:使用下面的模板并使用 shadow DOM。
注意:这旨在封装元素逻辑,但可以通过` shadowRoot`选择器访问。
定义影子根属性,它类似于所有 Web 组件标记的外壳。
...
class FormField extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({
'mode': 'open'
});
this._shadowRoot.appendChild(template.content.cloneNode(true));
}
}
第 3 步:定义自定义元素。
这是必要的步骤,以便浏览器知道如何对 HTML 布局中使用的自定义标签名称做出反应。在此之后,该组件就可以在 HTML 中使用并具有自定义标签名称。看看名称自定义元素如何有意义?!
...
class FormField extends HTMLElement {
...
}
window.customElements.define('db-form-field', FormField);
If you opt to use <db-form-field>in HTML layout, you need to include the component file via script tag like this:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Form</title>
</head>
<body>
<db-form-field></db-form-field>
<script src="dbFormField.js"></script>
</body>
</html>
然后,在浏览器中打开 index.html。(在本例中,我使用了 Chrome。)您应该会看到如下内容:
由于模板使用了shadow root,可以看到组件已经成功创建并在布局中使用。您还可以更新模板以保存标签、输入和注释错误消息。
...
template.innerHTML = `
<div class="form-field">
<label>Enter name:</label>
<input type="text" />
<span class="error">This field is required!</span>
</div>`;
...
你还没有完成!标签文本、输入字段类型和错误消息具有需要配置的静态值。通过在类中声明一个观察到的属性方法来检查自定义元素上提供的属性。它应该返回一个您想要跟踪更改的所需属性名称的数组:
static get observedAttributes() {
return ["label", "type", "error-message"];
}
如果这些属性值中的任何一个发生了更改,它就会触发一个 attributeChangedCallback 方法。在构造函数中将组件元素分配给这些变量,并在回调方法中设置属性值。
constructor() {
...
this.$label = this.shadowRoot.querySelector("label");this.$input = this.shadowRoot.querySelector("input");this.$error = this.shadowRoot.querySelector(".error");
}
static get observedAttributes() {
return ["label", "type", "error-message"];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "label":
this.$label.innerText = newValue;
break;
case "type":
this.$input.type = newValue;
break;
case "error-message":
this.$error.innerText = newValue;
break;
default:
break;
}
}
将属性传递给index.html中的db-form-field。
<db-form-field error-message="This field is required ❗" label="Custom Field" type="text"></db-form-field>
布局应如下所示:
第四步:给组件添加一些样式,默认隐藏错误信息。
有几种方法可以向自定义元素添加样式。一个简单的选项是使用此处显示的样式标签:
template.innerHTML=`<style>:host {
margin-bottom: 10px;
display: block;
}
.form-field {
display: table;
}
label,
input {
display: table-cell;
}
label {
padding-right: 10px;
}
.error {
display: block;
}
.hidden {
display: none;
}
</style>
...
看到这一点后,请继续在布局中添加一个字段。您应该会看到下面的结果。
确保包含一个 .hidden 类,用于字段初始化的错误消息以及另一个属性 - 无效 - 以观察列表。结果,组件能够从有效状态切换到无效状态,反之亦然。
...
<span class="error hidden"></span></div>`;
class FormField extends HTMLElement {
...
static get observedAttributes() {
return ["label", "type", "error-message", "invalid"];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
...
case "invalid":
newValue !== null ? this.$error.classList.remove("hidden") : this.$error.classList.add("hidden");
break;
...
在下图中,我重构了一个无效的案例并提取到一个单独的方法中。(您会注意到示例代码发生了变化。我将逻辑提取到另一个方法中以使其更具可读性。)这显示了无效状态切换的实现。
...
case "invalid": this._handleInvalidState(newValue);
break;..._handleInvalidState(value) {
if (value !== null) {
this.$error.classList.remove("hidden");
this.$input.classList.add("invalid-field");
} else {
this.$error.classList.add("hidden");
this.$input.classList.remove("invalid-field");
}
}
请务必以编程方式设置该字段的无效状态,并将该属性标记为无效。但是,在这种情况下,不会调用attributeChangedCallback,因为更改的是组件属性而不是属性。您需要使用组件类来更改设置属性,并通过创建 getter 和 setter 将属性反映到属性。
get invalid() {
return this.hasAttribute("invalid");
}
set invalid(value) {
if (!!value) {
this.setAttribute("invalid", "");
} else {
this.removeAttribute("invalid");
}
}
以编程方式设置属性后,属性也会更新并触发回调。
在下图中,我通过开发工具控制台设置了元素的属性,组件对其做出反应。
第 5 步:在组件中包含必填字段验证。
此功能要求程序员同时为输入字段引入必需属性和模糊事件侦听器。这样,您就知道何时检查和验证字段值。要添加事件监听器,请使用组件生命周期的 connectedCallback 方法。此操作将组件连接到影子 DOM。可以肯定的是,检查输入字段 isConnected 属性。
connectedCallback() {
if (this.$input.isConnected) {
this.$input.addEventListener("blur", (event) => {
if (!event.target.value && this.hasAttribute("required")) {
this.invalid = true;
this.$error.innerText = "This field is required."
} else {
this.invalid = false;
this.value = event.target.value;
}
});
}
}
value 属性也以与无效属性相同的方式制作,现在应该反映在属性中。将值添加到输入字段时,它也会传递到 db-form-field 值。这个动作允许程序员在不深入 shadow DOM 的情况下获取 shadow DOM 中的输入字段的值,并从提交的所有表单字段中提取值,而无需查询 shadow DOM 和挖掘组件的结构。该代码仅从 db-form-field 中获取值。
为了获得更大的灵活性,您可以使用 slot 元素将内容插入到组件中。它创建了一个空白空间,其中出现了组件标签之间的嵌套内容。在此处查看为字段添加信息文本的示例。
template.innerHTML=`<style>
...
::sloted(span) {
color: grey;
font-style: italic;
padding-left: 10px;
}
</style>
...
综上所述
查看整个代码。
我承认在这个例子中还有一些地方可以改进。提醒一下,本文的目的是在创建 Web 组件时尝试多种技术:添加模板、在布局中使用组件、使用 shadow DOM 将组件与外部环境隔离、检测属性更改、将属性反映到属性以及从子元素到根组件。使用这些方法可以改进本博客或您的 Web 组件中的组件创建。
其他建议和资源:
查看这篇文章以深入了解 Web 组件。
考虑研究 polyfill 以获取更多浏览器支持信息。
注意:本文不包含此详细信息,因为其目的是介绍原生 Web 组件以及如何创建它们。当然,现在使用 Polymer 或 SkateJs 等框架/库之一并支持 IE11+ 是合理的。在不久的将来可能不需要它们,因为所有现代浏览器都在全力支持 Web 组件。