Webcomponents:真的就这么简单!

2025-06-07

Webcomponents:真的就这么简单!

2015年,我第一次听说了WebComponents、自定义元素以及神秘的Shadow DOM。浏览器对它们的支持——好吧——我们姑且称之为实验性的。

在如今充斥着 polyfill 的时代,Polymer 这个名字似乎很适合一个支持“Chrome 专属”技术的框架。但即使在当时,爱好者们也似乎确信:这就是未来。原因显而易见。深入理解浏览器如何解释元素,能够提供快速、流畅、可复用且可控制的用户体验。

我们在哪里

在早期采用者经历了对承诺的标准建议不断的重大变更之后,我们现在处于一个 Web 组件感觉稳定、流畅且性能极高的时代。更重要的是:它变得简单了。

设置

在这个例子中,我们不会使用任何第三方库,但我建议查看lit html以满足基本的数据绑定需求。

全部大写

所以,我们要做的是:创建一个自定义元素,将其文本内容转换为大写。虽然不太引人注目,而且与直接使用 CSS 相比,确实有点矫枉过正,但它很好地表达了我们的想法。所以我们开始吧:

测试.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test custom element</title>
    <script src="all-caps.js" type="module">
</head>
<body>

<all-caps>this is uppercase</all-caps>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

全部大写.js


// 1. create class extending HTMLElement
export class AllCaps extends HTMLElement {}

// 2. Define a new custom element
customElements.define('all-caps', AllCaps)

Enter fullscreen mode Exit fullscreen mode

关于这两行代码有很多话要说。

首先,我们要扩展 HTMLElement。我们需要遵循一些必要的规范,我们会在下一步中讲解。

接下来,我们将“全大写”定义为自定义元素(浏览器支持应该不再是问题,但如果需要,请随意规范行为要点)

构造函数

到目前为止一切顺利。现在你的类需要一个构造函数。此函数在类初始化时执行。重要的是要理解你需要考虑嵌套和继续解释。虽然了解 JavaScript 如何处理这一点很有趣,但只需遵循以下规则就足够了:始终以 开头super()。别担心,如果你忘记了,你会发现 this 不可用。话虽如此,我们的类现在看起来是这样的:

export class AllCaps extends HTMLElement {
    constructor() {
        super();
    }
}

Enter fullscreen mode Exit fullscreen mode

进入 Shadow DOM

DOM(文档对象模型)是我们经常使用的表达方式之一,无需过多思考。有人可能对 HTML 和 XML 的历史感兴趣,但让我们尝试通过示例来加深理解:

在 JavaScript 中,你可能想知道类似的东西是如何document.getElementById()在不受上下文影响的情况下工作的。毋庸置疑,这是因为“document”访问的是全局 DOM 树(就像你的浏览器一样)。任何曾经与 XPath 或 iframe 斗争过的人,在处理分离的 DOM 时都会有一段痛苦的经历。另一方面,分离的文档允许真正封装元素。Shadow DOM(有时也称为“虚拟 DOM”)就是这样。它是一种“子 DOM”,其操作方式与自身文档相同,但不受 iframe 处理数据和状态的限制。这就是为什么 Shadow DOM 不会继承样式,并且在所有上下文中都能提供安全的复用性。听起来很棒,不是吗?你甚至可以决定“外部”是否可以访问你元素的 Shadow DOM:

export class AllCaps extends HTMLElement {
    constructor() {
        super();
        // attach a shadow allowing for accessibility from outside
        this.attachShadow({mode: 'open'});
    }
}
Enter fullscreen mode Exit fullscreen mode

此时运行test.html会显示一个空白页面,因为我们使用的是“新”的 DOM。但这并不意味着内容丢失了。虽然我更喜欢使用节点,但让我们先把代码打包一下,得到我们预期输出的第一个版本:

export class AllCaps extends HTMLElement {
    constructor() {
        super();
        // attach a shadow allowing for accessibility from outside
        this.attachShadow({mode: 'open'});

        // write our uppercased text to the Shadow DOM
        let toUpper = this.firstChild.nodeValue.toUpperCase();
        this.shadowRoot.innerHTML = toUpper;
    }
}

Enter fullscreen mode Exit fullscreen mode

成功了!一切正常,刷新test.html应该就能看到预期结果了。

先进的

让我们尝试一些额外的基础知识。

应用样式

注意:我通常会以稍微不同的方式构造它,但是为了包含我们正在讨论的部分,让我们执行以下操作:

在构造函数之后,我们添加另一个名为“attachTemplate”的函数

attachTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
        <style>
        :host{
         color: red;
        }
        </style>`;
    this.shadowRoot.innerHTML += template.innerHTML;
}

Enter fullscreen mode Exit fullscreen mode

你可能对“:host”感到好奇。这个选择器引用的是元素本身。为了执行这个函数,我们需要在构造函数中调用它:

this.attachTemplate()

注意,你也可以使用例如“connectedCallback”作为函数名,但本教程只讲解基础知识。
我们的类现在应该如下所示:

export class AllCaps extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: 'open'});
        let toUpper = this.firstChild.nodeValue.toUpperCase();
        this.shadowRoot.innerHTML = toUpper;
        this.attachTemplate();
    }
    attachTemplate() {
        const template = document.createElement('template');
        template.innerHTML = `
        <style>
        :host{
         color: red;
        }
        </style>`;
        this.shadowRoot.innerHTML += template.innerHTML;
    }
}
Enter fullscreen mode Exit fullscreen mode

重新加载test.html现在不仅应该提供大写字母,还应该提供红色(请在实际场景中考虑单一责任)。

插槽

另一个(这里比较粗略的)介绍是使用插槽。插槽可以命名,也可以引用元素的完整内容。让我们尝试一下,掌握它的基本用法:

在我们的文件的文字字符串中,添加标签<slot></slot>,从而产生以下attachTemplate函数

attachTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
        <slot></slot>
        <style>
        :host{
         color: red;
        }
        </style>`;
    this.shadowRoot.innerHTML += template.innerHTML;
}
Enter fullscreen mode Exit fullscreen mode

刷新浏览器,您会注意到标签的原始内容已添加到 DOM 中。

属性和数据

最后,我们来看一下属性。同样,这是一个毫无意义的例子,但我认为它很好地解释了这个概念。
test.html中,我们将为标签添加属性“addition”,其值为“!”。

<all-caps addition="!">hi there</all-caps>

接下来,我们将再次编辑我们的模板字符串并添加到${this.addition}我们的插槽后面。

attachTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
        <slot></slot>
        ${this.addition}
        <style>
        :host{
         color: red;
        }
        </style>`;
    this.shadowRoot.innerHTML += template.innerHTML;
}
Enter fullscreen mode Exit fullscreen mode

现在我们需要处理该属性,至少要考虑到它未被设置的情况。为此,我们可能需要创建一个新函数,但我还是会快速“破解”它。在构造函数中,在执行“attachTemplate”之前,我们可以添加

if(this.hasAttribute('addition')){
    this.addition = this.getAttribute('addition')
} else {
    this.addition = '';
}
Enter fullscreen mode Exit fullscreen mode

我们的课程现在看起来像这样:

export class AllCaps extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({mode: 'open'});
        let toUpper = this.firstChild.nodeValue.toUpperCase();
        this.shadowRoot.innerHTML = toUpper;
        if(this.hasAttribute('addition')){
            this.addition = this.getAttribute('addition')
        } else {
            this.addition = '';
        }
        this.attachTemplate();
    }
    attachTemplate() {
        const template = document.createElement('template');
        template.innerHTML = `
        <slot></slot>
        ${this.addition}
        <style>
        :host{
         color: red;
        }
        </style>`;
        this.shadowRoot.innerHTML += template.innerHTML;
    }

}
Enter fullscreen mode Exit fullscreen mode

刷新浏览器即可查看结果。

结论

本教程旨在帮助您理解自定义元素和 Shadow DOM 的基本处理。正如开篇所述,您可能希望使用像 lit-html 这样的库来简化操作,并且您肯定希望代码更简洁一些(我在以身作则和尽可能保持代码简洁之间挣扎了很久)。但我希望这能给您一个良好的开端,并帮助您点燃深入探索的火花。

如今,我们可以假设 Web Components 将主导 Web 领域,并逐渐取代像 Angular 这样的性能密集型框架。无论您是职业生涯的起步阶段,还是久经沙场的 React 爱好者,熟悉 Web 的发展方向都是非常有意义的。尽情享受吧!

文章来源:https://dev.to/sroehrl/webcomponents-it-s-really-that-easy-96e
PREV
深入理解 JavaScript Promises 与 V8 引擎内部机制
NEXT
Map(),Filter(),reduce() 及重要面试题 Map Filter Reduce