Анатомия веб-компонента: Основы
Я изучаю веб-компоненты в рамках курса Роба Айзенберга "Разработка веб-компонентов" и решил, что мне стоит изложить свои знания в письменном виде. Итак, здесь представлен очень простой веб-компонент, демонстрирующий некоторые фундаментальные характеристики веб-компонентов, которые мы будем развивать в будущем (я прошел всего несколько уроков, а впереди еще очень много).
Здесь мы создадим элемент my-counter
, который на самом деле очень прост. Все, что он делает, - это выводит в DOM значение, содержащееся в свойстве count
. Так, что это:
<my-counter count="3"></my-counter>
будет выглядеть следующим образом:
3
Если не указать count
, то будет выведен 0
.
Для начала приведем полный код, а затем разберем каждую строку.
Сначала HTML:
<template id="my-counter">
<div id="count"></div>
</template>
<my-counter count="0"></my-counter>
А теперь JavaScript:
class MyCounter extends HTMLElement {
static observedAttributes = ["count"];
static #fragment = null;
#view = null;
#count;
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
if (this.#view === null) {
this.#view = this.#createView();
this.shadowRoot.appendChild(this.#view);
this.#count = this.shadowRoot.getElementById("count");
}
this.countChanged();
}
attributeChangedCallback() {
this.countChanged();
}
countChanged() {
if (this.#count) {
const value = this.getAttribute("count") ?? 0;
this.#count.innerText = value;
}
}
#createView() {
if (MyCounter.#fragment === null) {
const template = document.getElementById("my-counter");
MyCounter.#fragment = document.adoptNode(template.content);
}
return MyCounter.#fragment.cloneNode(true);
}
}
customElements.define("my-counter", MyCounter);
Объявление класса MyCounter
class MyCounter extends HTMLElement {
MyCounter
расширяет базовый HTMLElement
, превращая его в новый тип HTML-элемента. Теперь мы можем использовать этот класс MyCounter
для добавления собственных поведений, сохраняя при этом все свойства и методы обычного HTMLElement
.
Наблюдаемые атрибуты
static observedAttributes = ["count"];
Свойство static observedAttributes
определяет список атрибутов, которые компонент будет отслеживать. Когда атрибут count
изменяется, MyCounter
реагирует на это обновлением отображаемого значения компонента.
Эффективная работа с шаблонами
static #fragment = null;
Объявив свойство static #fragment
и сохранив в нем фрагмент шаблона при создании первого экземпляра HTMLElement MyCounter
, MyCounter
избегает избыточного принятия шаблонов для нескольких экземпляров. Этот общий фрагмент служит образцом для представления каждого экземпляра.
DOM для конкретного экземпляра
#view = null;
После того как шаблон принят, мы можем клонировать его для каждого экземпляра счетчика с помощью clone
. Свойство #view
содержит клонированный шаблон для экземпляра компонента. В отличие от общего фрагмента, это свойство является специфичным для каждого экземпляра, создавая уникальный вид для каждого счетчика на странице.
Ссылки на частные элементы
#count;
MyCounter
использует приватное поле #count
для хранения ссылки на конкретный элемент DOM, отображающий счетчик. Эта ссылка устанавливается в connectedCallback
, что обеспечивает независимое обновление отображения каждого экземпляра компонента.
Обратные вызовы жизненного цикла
Соединение компонентов
connectedCallback() {
// If the view has not yet been initialised, we need to
// create it by adopting the template into the DOM, then cloning.
if (this.#view === null) {
this.#view = this.#createView();
this.shadowRoot.appendChild(this.#view);
this.#count = this.shadowRoot.getElementById("count");
}
this.countChanged();
}
Когда экземпляр добавляется в DOM, срабатывает connectedCallback
. Этот обратный вызов отвечает за инициализацию представления экземпляра, если она еще не была выполнена, добавление его в теневой DOM и кэширование элемента count
.
Здесь также необходимо вызвать метод countChanged
, поскольку обратный вызов attributeChangedCallback
может еще не сработать (т.е. свойство count
может не иметь значения), и поэтому нам необходимо отобразить значение по умолчанию, равное 0.
Реагирование на изменение атрибутов
attributeChangedCallback() {
this.countChanged();
}
countChanged() {
// If the id is present it means the element has been connected to the DOM.
if (this.#count) {
const value = this.getAttribute("count") ?? 0;
this.#count.innerText = value;
}
}
Методы attributeChangedCallback
и countChanged
работают параллельно, обновляя отображение компонента при изменении атрибута count
. Это обеспечивает постоянную синхронизацию пользовательского интерфейса с состоянием компонента.
Усвоение и клонирование шаблонов
#createView() {
if (MyCounter.#fragment === null) {
const template = document.getElementById("my-counter");
MyCounter.#fragment = document.adoptNode(template.content);
}
return MyCounter.#fragment.cloneNode(true);
}
Метод #createView
проверяет, был ли шаблон принят классом. Если нет, то он принимает шаблон в документ. Затем он клонирует этот шаблон, чтобы создать представление для экземпляра. Этот процесс является важной оптимизацией производительности, позволяющей избежать лишних операций над DOM.
Регистрация компонентов
customElements.define("my-counter", MyCounter);
Наконец, MyCounter
регистрируется как пользовательский элемент в файле customElements.define
, что позволяет разработчикам использовать теги <my-counter>
непосредственно в HTML, как и любой другой стандартный элемент.
Заключение
MyCounter демонстрирует самые базовые характеристики создания и обновления пользовательского элемента на низком уровне.
Надеюсь, вам понравилось читать эту статью!