W

Web 组件:从零到英雄 Web 组件:从零到英雄

2025-05-28

Web 组件:从零到英雄

Web 组件:从零到英雄

Web 组件:从零到英雄

编写原始 Web 组件的介绍

Web 组件正受到越来越多的关注。随着 Edge 团队最近宣布实现自定义元素和 Shadow DOM,所有主流浏览器很快都将原生支持 Web 组件。Github、Netflix、Youtube 和 ING 等公司甚至已经在生产环境中使用 Web 组件。太棒了!然而,令人惊讶的是,这些成功的大型公司中,没有一家实现了(你猜对了)待办事项应用!

所以今天,我们将开发一款待办事项应用,因为目前市面上还没有足够多的待办事项应用。你可以在这里查看我们将要开发的内容。

在开始之前,我想先声明一下:这篇博文旨在帮助您更好地掌握Web Components 的基础知识。Web Components 是底层的,如果不使用任何辅助库,可能不适合用来编写完整的应用程序,也不应该将其与成熟的框架进行比较。

🙋 什么是 Web 组件?

  • [x] 制作演示
  • [ ] 无聊的东西
  • [ ] 设置属性
  • [ ] 设置属性
  • [ ] 将属性反映到特性
  • [ ] 事件
  • [ ] 总结

首先:Web 组件是一套标准,允许我们编写模块化、可重用且封装的 HTML 元素。它们最棒的地方在于:由于它们基于 Web 标准,我们无需安装任何框架或库即可使用它们。现在,您就可以使用原生 JavaScript 编写 Web 组件了!

但在我们开始动手之前,让我们先看看允许我们编写 Web 组件的规范。

自定义元素

定义元素API 允许我们创建自己的 DOM 元素。使用该 API,我们可以定义一个自定义元素,并告知解析器如何正确构造该元素,以及该类的元素应如何响应更改。您是否曾经想要拥有自己的 HTML 元素,例如<my-cool-element>?现在,您可以!

影子 DOM

Shadow DOM为我们提供了一种封装组件样式和标记的方法。它是一棵附加到 DOM 元素的子 DOM 树,以确保我们的样式不会泄露,也不会被任何外部样式覆盖。这使得它非常适合模块化。

ES 模块

ES模块规范以基于标准、模块化、高性能的方式定义了 JS 文档的包含和重用。

HTML模板

HTML标签允许我们编写可复用的 DOM 块。在模板内部,脚本不会运行,图片不会加载,样式/标记也不会渲染。模板标签本身甚至不被视为文档的一部分,直到它被激活。HTML 模板非常棒<template>,因为对于我们元素的每个实例,只使用一个模板。

现在我们已经了解了 Web 组件所依赖的规范,接下来我们来看看自定义元素的生命周期。我知道,我们很快就能开始写代码了!

♻️ 组件的生命周期

让我们看一下自定义元素的生命周期。考虑以下元素:

class MyElement extends HTMLElement {
    constructor() {
        // always call super() first
        super(); 
        console.log('constructed!');
    }

    connectedCallback() {
        console.log('connected!');
    }

    disconnectedCallback() {
        console.log('disconnected!');
    }

    attributeChangedCallback(name, oldVal, newVal) {
        console.log(`Attribute: ${name} changed!`);
    }

    adoptedCallback() {
        console.log('adopted!');
    }
}

window.customElements.define('my-element', MyElement);
Enter fullscreen mode Exit fullscreen mode

构造函数()

constructor每当元素创建时,但在元素附加到文档之前,都会运行。我们将使用它constructor来设置一些初始状态、事件监听器以及创建影子 DOM。

已连接回调()

当元素插入到 DOM 时,会调用该方法connectedCallback。这里适合运行设置代码,例如获取数据或设置默认属性。

断开连接回调()

disconnectedCallback每当元素从 DOM 中移除时,都会调用 。清理时间到了!我们可以使用它disconnectedCallback移除任何事件监听器,或者取消间隔。

attributeChangedCallback(名称,旧值,新值)

attributeChangedCallback每当元素的观察属性发生变化时,都会调用该方法。我们可以通过实现静态 getter 来观察元素的属性observedAttributes,如下所示:

static get observedAttributes() {
    return ['my-attr'];
}
Enter fullscreen mode Exit fullscreen mode

在这种情况下,只要my-attr属性发生变化,attributeChangedCallback就会运行。我们将在后续博文中更深入地探讨这一点。

嘿!听着!

仅 getter 中列出的属性observedAttributes会受到影响attributeChangedCallback

adoptedCallback()

<iframe>每次将自定义元素移动到新文档时,都会调用 adoptedCallback。只有当您的页面中包含元素时,才会遇到此用例。

注册我们的元素

最后,虽然不是生命周期的一部分,但我们将元素注册到CustomElementRegistry类似的东西中:

window.customElements.define('my-element', MyElement);
Enter fullscreen mode Exit fullscreen mode

CustomElementRegistry一个接口,提供注册自定义元素和查询已注册元素的方法。registries 方法的第一个参数define是元素的名称,在本例中为 register <my-element>,第二个参数传递我们创建的类。

嘿!听着!

务必注意 Web 组件的命名方式。自定义元素名称必须始终包含连字符。例如:<my-element>is 正确,<myelement>is 错误。这样做是为了避免元素名称冲突,并区分自定义元素和常规元素。

自定义元素也不能自闭合,因为 HTML 只允许少数元素自闭合。这些元素被称为空元素,例如<br/><img/>,或者不允许有子节点的元素。

允许自闭合元素需要修改 HTML 解析器,而这会带来问题,因为 HTML 解析对安全敏感。HTML 生成器需要能够依赖特定 HTML 片段的解析方式,才能实现 XSS 安全的 HTML 生成。

⚒ 构建我们的待办事项应用

  • [x] 制作演示
  • [x] 无聊的东西
  • [ ] 设置属性
  • [ ] 设置属性
  • [ ] 将属性反映到特性
  • [ ] 事件
  • [ ] 总结

现在我们已经完成了所有无聊的事情,终于可以开始动手构建我们的待办事项应用程序了!点击此处查看最终结果。

让我们首先概述一下我们将要构建的内容。

  • 元素<to-do-app>

    • 包含待办事项数组作为属性
    • 添加待办事项
    • 删除待办事项
    • 切换待办事项
  • 元素<to-do-item>

    • 包含描述属性
    • 包含索引属性
    • 包含已检查的属性

太棒了!让我们开始为我们的待办事项应用奠定基础:

to-do-app.js

const template = document.createElement('template');
template.innerHTML = `
<style>
    :host {
    display: block;
    font-family: sans-serif;
    text-align: center;
    }

    button {
    border: none;
    cursor: pointer;
    }

    ul {
    list-style: none;
    padding: 0;
    }
</style>
<h1>To do</h1>

<input type="text" placeholder="Add a new to do"></input>
<button>✅</button>

<ul id="todos"></ul>
`;

class TodoApp extends HTMLElement {
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));
        this.$todoList = this._shadowRoot.querySelector('ul');
    }
}

window.customElements.define('to-do-app', TodoApp);
Enter fullscreen mode Exit fullscreen mode

我们将一步一步地进行。首先,我们<template>通过调用创建一个组件const template = document.createElement('template');,然后在其中设置一些 HTML。我们只在模板上设置一次innerHTML 。我们使用模板的原因是,克隆模板比调用.innerHTML组件的所有实例要便宜得多。

接下来,我们就可以开始定义元素了。我们将使用 来连接constructorshadowroot ,并将其设置为模式。然后,我们将模板克隆到 shadowroot。太棒了!现在我们已经使用了 2 个 Web 组件规范,并成功创建了一个封装的子 DOM 树。open

这意味着我们现在拥有了一个不会泄漏任何样式或被覆盖的 DOM 树。请考虑以下示例:

封装

我们有一个全局h1样式,可以将 light DOM 中的任何 h1 元素设置为红色。但由于我们的 h1 元素位于 shadow-root 中,因此它不会被全局样式覆盖。

请注意to-do-app,我们在组件中使用了:host伪类,这样我们就可以从内部为组件添加样式。需要注意的是,display始终设置为display: inline;,这意味着您无法设置元素的宽度或高度。因此,除非您更喜欢默认的 inline 样式,否则请务必设置:host显示样式(例如 block、inline-block、flex)。

嘿!听着!

Shadow DOM 可能有点令人困惑。请允许我稍微解释一下这些术语:

轻量级 DOM:

Light DOM 位于组件的 Shadow DOM 之外,基本上指任何不属于Shadow DOM 的内容。例如,<h1>Hello world</h1>上方的元素就位于 Light DOM 中。Light DOM 这一术语用于将其与 Shadow DOM 区分开来。使用 Light DOM 制作 Web 组件完全没问题,但您会错过 Shadow DOM 的强大功能。

打开影子 DOM:

从 Shadow DOM 规范的最新版本 (V1) 开始,我们现在可以使用openShadow closedDOM。Open Shadow DOM 允许我们在 Light DOM 旁边创建一个子 DOM 树,为组件提供封装。我们的 Shadow DOM 仍然可以被 JavaScript 穿透,如下所示:document.querySelector('our-element').shadowRoot。Shadow DOM 的缺点之一是 Web 组件相对年轻,许多外部库尚未考虑到这一点。

封闭的影子 DOM:

封闭式 Shadow root 不太适用,因为它会阻止任何外部 JavaScript 穿透 Shadowroot。封闭式 Shadow DOM 会降低组件对您和最终用户的灵活性,通常应避免使用。

一些使用封闭阴影 DOM 的元素示例是<video>元素。

📂 设置属性

太棒了!我们的第一个 Web 组件已经制作完成了,但目前为止,它完全没用。如果能给它传递一些数据,然后渲染一个待办事项列表就好了。

让我们实现一些 getter 和 setter。

to-do-app.js

class TodoApp extends HTMLElement {
    ...

    _renderTodoList() {
        this.$todoList.innerHTML = '';

        this._todos.forEach((todo, index) => {
            let $todoItem = document.createElement('div');
            $todoItem.innerHTML = todo.text; 
            this.$todoList.appendChild($todoItem);
        });
    }

    set todos(value) {
        this._todos = value;
        this._renderTodoList();
    }

    get todos() {
        return this._todos;
    }
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了一些 getter 和 setter,我们可以将一些丰富的数据传递给元素了!我们可以像这样查询组件并设置数据:

document.querySelector('to-do-app').todos = [
    {text: "Make a to-do list", checked: false}, 
    {text: "Finish blog post", checked: false}
];
Enter fullscreen mode Exit fullscreen mode

现在,我们已经成功地在组件上设置了一些属性,它现在看起来应该是这样的:

待办事项列表

太棒了!但它仍然没用,因为如果不使用控制台,我们就无法与任何事物交互。让我们快速实现一些功能,将新的待办事项添加到列表中。

class TodoApp extends HTMLElement {
    ...

    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));

        this.$todoList = this._shadowRoot.querySelector('ul');
        this.$input = this._shadowRoot.querySelector('input');

        this.$submitButton = this._shadowRoot.querySelector('button');
        this.$submitButton.addEventListener('click', this._addTodo.bind(this));
    }

    _addTodo() {
        if(this.$input.value.length > 0){
            this._todos.push({ text: this.$input.value, checked: false })
            this._renderTodoList();
            this.$input.value = '';
        }
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

这应该很容易理解,我们在中设置了querySelectors一些,并且在点击事件中,我们希望将输入推送到待办事项列表中,渲染它,然后再次清除输入。很简单👏。addEventListenersconstructor

添加

💅 设置属性

  • [x] 制作演示
  • [x] 无聊的东西
  • [x] 设置属性
  • [ ] 设置属性
  • [ ] 将属性反映到特性
  • [ ] 事件
  • [ ] 总结

事情到这里可能会有点混乱,因为我们将探索属性 (attributes)特性 (properties)之间的区别,并且还将把特性 (properties) 映射到属性 (attributes) 上。抓紧!

首先,让我们创建一个<to-do-item>元素。

to-do-item.js

const template = document.createElement('template');
template.innerHTML = `
<style>
    :host {
    display: block;
    font-family: sans-serif;
    }

    .completed {
    text-decoration: line-through;
    }

    button {
    border: none;
    cursor: pointer;
    }
</style>
<li class="item">
    <input type="checkbox">
    <label></label>
    <button>❌</button>
</li>
`;

class TodoItem extends HTMLElement {
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));

        this.$item = this._shadowRoot.querySelector('.item');
        this.$removeButton = this._shadowRoot.querySelector('button');
        this.$text = this._shadowRoot.querySelector('label');
        this.$checkbox = this._shadowRoot.querySelector('input');

        this.$removeButton.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index }));
        });

        this.$checkbox.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onToggle', { detail: this.index }));
        });
    }

    connectedCallback() {
        // We set a default attribute here; if our end user hasn't provided one,
        // our element will display a "placeholder" text instead.
        if(!this.hasAttribute('text')) {
            this.setAttribute('text', 'placeholder');
        }

        this._renderTodoItem();
    }

    _renderTodoItem() {
        if (this.hasAttribute('checked')) {
            this.$item.classList.add('completed');
            this.$checkbox.setAttribute('checked', '');
        } else {
            this.$item.classList.remove('completed');
            this.$checkbox.removeAttribute('checked');
        }

        this.$text.innerHTML = this._text;
    }

    static get observedAttributes() {
        return ['text'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        this._text = newValue;
    }
}
window.customElements.define('to-do-item', TodoItem);

Enter fullscreen mode Exit fullscreen mode

请注意,由于我们使用的是 ES 模块,因此我们可以const template = document.createElement('template');再次使用,而无需覆盖我们之前制作的模板。

让我们将_renderTodolist函数改成to-do-app.js这样:

class TodoApp extends HTMLElement {

        ...

        _renderTodoList() {
            this.$todoList.innerHTML = '';

            this._todos.forEach((todo, index) => {
                let $todoItem = document.createElement('to-do-item');
                $todoItem.setAttribute('text', todo.text);
                this.$todoList.appendChild($todoItem);
            });
        }

        ...

    }
Enter fullscreen mode Exit fullscreen mode

好吧,这里发生了很多不同的事情。让我们深入了解一下。之前,当将一些富数据(数组)传递给<to-do-app>组件时,我们将其设置如下:

document.querySelector('to-do-app').todos = [{ ... }];
Enter fullscreen mode Exit fullscreen mode

我们这样做是因为它是元素的todos一个属性。属性的处理方式不同,并且不允许使用富数据,事实上,由于 HTML 的限制,它们只允许使用字符串类型。属性则更灵活,可以处理对象或数组等复杂的数据类型。

区别在于,属性是在 HTML 元素上定义的。当浏览器解析 HTML 时,会创建一个相应的 DOM 节点。这个节点是一个对象,因此它具有属性。例如,当浏览器解析:<to-do-item index="1">时,会创建一个HTMLElement对象。这个对象已经包含了一些属性,例如childrenclientHeightclassList等,以及一些方法,例如appendChild()click()。我们也可以实现自己的属性,就像我们在to-do-app元素中那样,我们赋予了它一个todos属性。

下面是实际操作的一个例子。

<img src="myimg.png" alt="my image"/>
Enter fullscreen mode Exit fullscreen mode

浏览器将解析此<img>元素,创建一个DOM 元素对象src,并方便地为我们设置和的属性。需要注意的是,并非所有alt属性都具备属性反射功能。(例如:元素上的 属性不会反射。属性始终是 的当前文本内容,而属性将是初始文本内容。)我们稍后会更深入地了解如何将属性反射到属性。value<input>value <input><input>value

所以我们现在知道 alt 和 src属性是作为字符串类型处理的,如果我们想将待办事项数组传递给我们的<to-do-app>元素,如下所示:

<to-do-app todos="[{...}, {...}]"></to-do-app>
Enter fullscreen mode Exit fullscreen mode

我们不会得到期望的结果;我们期望一个数组,但实际上,该值只是一个看起来像数组的字符串。

嘿!听着!

  • 旨在仅接受丰富的数据(对象、数组)作为属性。
  • 不要将丰富的数据特性反映到属性中。

设置属性的方式也与设置属性的方式不同,请注意,我们没有实现任何 getter 或 setter 方法。我们将text属性添加到 getter 方法中static get observedAttributes,以便能够监听text属性的变化。并且,我们实现了attributesChangedCallback来响应这些变化。

此时,我们的应用程序应该是这样的:

待办事项

布尔属性

我们还没完成属性的学习。如果能在完成一些待办事项后勾选它们就好了,我们也会用到属性来实现这一点。不过,我们需要稍微改变一下布尔属性的处理方式。

元素上布尔属性的存在代表该True值,而属性的不存在代表该False值。

如果该属性存在,则其值必须是空字符串,或者是与该属性的规范名称不区分大小写的 ASCII 匹配的值,且没有前导或尾随空格。

布尔属性不允许使用“true”和“false”值。要表示 false 值,必须完全省略该属性。<div hidden="true">这是错误的。

这意味着只有以下示例才可以接受真实值:

<div hidden></div>
<div hidden=""></div>
<div hidden="hidden"></div>
Enter fullscreen mode Exit fullscreen mode

还有一个为假:

<div></div>
Enter fullscreen mode Exit fullscreen mode

因此让我们checked为我们的<to-do-item>元素实现属性!

将您的更改to-do-app.js为:

_renderTodoList() {
    this.$todoList.innerHTML = '';

    this._todos.forEach((todo, index) => {
        let $todoItem = document.createElement('to-do-item');
        $todoItem.setAttribute('text', todo.text);

    // if our to-do is checked, set the attribute, else; omit it.
        if(todo.checked) {
            $todoItem.setAttribute('checked', '');                
        }

        this.$todoList.appendChild($todoItem);
    });
}
Enter fullscreen mode Exit fullscreen mode

并将to-do-item其更改为:

 class TodoItem extends HTMLElement {

    ...

    static get observedAttributes() {
        return ['text', 'checked'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        switch(name){
            case 'text':
                this._text = newValue;
                break;
            case 'checked':
                this._checked = this.hasAttribute('checked');
                break;
        }
    }

    ...

}
Enter fullscreen mode Exit fullscreen mode

太棒了!我们的应用程序应该如下所示:

检查

♺ 将属性反映到特性

  • [x] 制作演示
  • [x] 无聊的东西
  • [x] 设置属性
  • [x] 设置属性
  • [ ] 将属性反映到特性
  • [ ] 事件
  • [ ] 总结

太棒了,我们的应用进展顺利。但如果最终用户能够查询组件的状态就更好了checkedto-do-item目前我们只将其设置为一个属性 (attribute),但我们希望它也能作为属性 (property ) 使用。这称为将属性 (property) 反射到属性 (attributes)

为此,我们要做的就是添加一些 getter 和 setter。将以下内容添加到您的to-do-item.js

get checked() {
    return this.hasAttribute('checked');
}

set checked(val) {
    if (val) {
        this.setAttribute('checked', '');
    } else {
        this.removeAttribute('checked');
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,每次我们更改属性或特性时,值总是会同步。

🎉 活动

  • [x] 制作演示
  • [x] 无聊的东西
  • [x] 设置属性
  • [x] 设置属性
  • [x] 将属性反映到特性
  • [ ] 事件
  • [ ] 总结

呼,既然我们已经解决了最难的部分,现在该开始做有趣的事情了。我们的应用程序目前已经按照我们想要的方式处理和公开数据,但它实际上还不能移除或切换待办事项。让我们来处理一下这个问题。

首先,我们需要追踪sindexto-do-item。让我们设置一个属性!

to-do-item.js

static get observedAttributes() {
    return ['text', 'checked', 'index'];
}

attributeChangedCallback(name, oldValue, newValue) {
    switch(name){
        case 'text':
            this._text = newValue;
            break;
        case 'checked':
            this._checked = this.hasAttribute('checked');
            break;
        case 'index':
            this._index = parseInt(newValue);
            break;
    }
}
Enter fullscreen mode Exit fullscreen mode

注意,我们在这里是如何将字符串类型的值解析为整数的,因为属性只允许字符串类型,但我们希望最终用户能够将索引属性作为整数获取。现在,我们也有一个很好的例子来说明如何处理字符串/数字/布尔类型的属性,以及如何将属性和属性按其实际类型处理。

因此让我们添加一些 getter 和 setter 到to-do-item.js

set index(val) {
    this.setAttribute('index', val);
}

get index() {
    return this._index;
}
Enter fullscreen mode Exit fullscreen mode

并将我们的_renderTodoList函数改为to-do-app.js

_renderTodoList() {
    this.$todoList.innerHTML = '';

    this._todos.forEach((todo, index) => {
        let $todoItem = document.createElement('to-do-item');
        $todoItem.setAttribute('text', todo.text);

        if(todo.checked) {
            $todoItem.setAttribute('checked', '');                
    }

        $todoItem.setAttribute('index', index);

        $todoItem.addEventListener('onRemove', this._removeTodo.bind(this));

        this.$todoList.appendChild($todoItem);
    });
}
Enter fullscreen mode Exit fullscreen mode

注意我们是如何设置的$todoItem.setAttribute('index', index);。现在我们有了一些状态来跟踪待办事项的索引。我们还设置了一个事件监听器来监听元素onRemove上的事件to-do-item

接下来,我们需要在点击删除按钮时触发constructor该事件。将of更改to-do-item.js为以下内容:

constructor() {
    super();
    this._shadowRoot = this.attachShadow({ 'mode': 'open' });
    this._shadowRoot.appendChild(template.content.cloneNode(true));

    this.$item = this._shadowRoot.querySelector('.item');
    this.$removeButton = this._shadowRoot.querySelector('button');
    this.$text = this._shadowRoot.querySelector('label');
    this.$checkbox = this._shadowRoot.querySelector('input');

    this.$removeButton.addEventListener('click', (e) => {
        this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index }));
    });
}
Enter fullscreen mode Exit fullscreen mode

嘿!听着!

我们可以设置{ detail: this.index, composed: true, bubbles: true }让事件从我们的组件影子 DOM 中冒泡出来。

并添加_removeTodo以下函数to-do-app.js

_removeTodo(e) {
    this._todos.splice(e.detail, 1);
    this._renderTodoList();
}
Enter fullscreen mode Exit fullscreen mode

太棒了!我们可以删除待办事项了:

消除

最后,让我们也创建一个切换功能。

to-do-app.js

class TodoApp extends HTMLElement {
    ...

    _toggleTodo(e) {
        const todo = this._todos[e.detail];
        this._todos[e.detail] = Object.assign({}, todo, {
            checked: !todo.checked
        });
        this._renderTodoList();
    }


    _renderTodoList() {
        this.$todoList.innerHTML = '';

        this._todos.forEach((todo, index) => {
            let $todoItem = document.createElement('to-do-item');
            $todoItem.setAttribute('text', todo.text);

            if(todo.checked) {
                $todoItem.setAttribute('checked', '');                
            }

            $todoItem.setAttribute('index', index);
            $todoItem.addEventListener('onRemove', this._removeTodo.bind(this));
            $todoItem.addEventListener('onToggle', this._toggleTodo.bind(this));

            this.$todoList.appendChild($todoItem);
        });
    }

    ...

}

Enter fullscreen mode Exit fullscreen mode

to-do-item.js

class TodoItem extends HTMLElement {

    ...

    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));

        this.$item = this._shadowRoot.querySelector('.item');
        this.$removeButton = this._shadowRoot.querySelector('button');
        this.$text = this._shadowRoot.querySelector('label');
        this.$checkbox = this._shadowRoot.querySelector('input');

        this.$removeButton.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index }));
        });

        this.$checkbox.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onToggle', { detail: this.index }));
        });
    }

    ...

}
Enter fullscreen mode Exit fullscreen mode

切换

成功!我们可以创建、删除和切换待办事项了!

👻 浏览器支持和 polyfill

  • [x] 制作演示
  • [x] 无聊的东西
  • [x] 设置属性
  • [x] 设置属性
  • [x] 将属性反映到特性
  • [x] 事件
  • [ ] 总结

在这篇博文中,我最后想讨论的是浏览器支持问题。在撰写本文时,Microsoft Edge 团队最近宣布他们将实现自定义元素以及影子 DOM,这意味着所有主流浏览器很快都将原生支持 Web 组件。

在此之前,您可以使用Google 维护的webcomponentsjs polyfill。只需导入 polyfill 即可:

<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.0.0/webcomponents-bundle.js"></script>
Enter fullscreen mode Exit fullscreen mode

为了简单起见,我使用了 unpkg,但您也可以使用 安装 webcomponentsjs NPM。为了确保 polyfill 已成功加载,我们可以等待事件WebComponentsReady触发,如下所示:

window.addEventListener('WebComponentsReady', function() {
    console.log('Web components ready!');
    // your web components here
});
Enter fullscreen mode Exit fullscreen mode

💫 总结

  • [x] 制作演示
  • [x] 无聊的东西
  • [x] 设置属性
  • [x] 设置属性
  • [x] 将属性反映到特性
  • [x] 事件
  • [x] 总结

完毕!

如果你已经完成了所有步骤,那么恭喜你!你已经了解了 Web 组件规范、(轻量/开放/封闭)阴影 DOM、模板、属性 (Attribute) 和特性 (Property) 之间的区别,以及如何将特性 (Property) 反射到属性 (Attribute)。

但你可能已经注意到,我们编写的很多代码可能感觉有点笨重,我们写了很多样板代码(getter、setter、查询选择器等等),而且很多事情都是命令式处理的。我们对待办事项列表的更新也不太高效。

Web 组件很简洁,但我不想花那么多时间编写样板代码和命令式设置东西,我想编写声明式代码! ”你哭喊道。

输入lit-html,我们将在下一篇博客文章中介绍。

文章来源:https://dev.to/thepassle/web-components-from-zero-to-hero-4n4m
PREV
欢迎主题 - v5
NEXT
如何提升你的开发体验