Что такое Web Components?
СкопированоПредставьте, что вы создали сложный UI-компонент — например, интерактивную галерею или кастомный слайдер. Этот компонент работает отлично, но когда вы пытаетесь использовать его на другой странице или передать другому разработчику, начинаются проблемы: стили конфликтуют, скрипты ломаются, структура нарушается.
Именно для решения таких проблем были созданы Web Components — набор технологий, которые позволяют создавать переиспользуемые HTML-элементы с инкапсулированной функциональностью. Это как конструктор LEGO для веб-разработки: вы создаёте блоки, которые можно комбинировать в любом порядке, и они всегда работают одинаково.
Web Components состоят из трёх основных технологий:
- Custom Elements — API для создания собственных HTML-тегов
- Shadow DOM — инкапсуляция стилей и структуры
- HTML Templates — переиспользуемые шаблоны разметки
Как работают Web Components?
СкопированоОсновные принципы
СкопированоWeb Components следуют принципу инкапсуляции — каждый компонент изолирован от остального кода. Это означает, что:
- Стили компонента не влияют на внешние элементы
- Внутренняя структура скрыта от внешних скриптов
- Компонент работает одинаково в любом контексте
class MyButton extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> :host { display: inline-block; padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background 0.3s ease-in-out; } :host(:hover) { background: #0056b3; } </style> <slot></slot> `; }}customElements.define('my-button', MyButton);
class MyButton extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> :host { display: inline-block; padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background 0.3s ease-in-out; } :host(:hover) { background: #0056b3; } </style> <slot></slot> `; } } customElements.define('my-button', MyButton);
<my-button>Крутая кнопочка</my-button>
<my-button>Крутая кнопочка</my-button>
Жизненный цикл компонента
СкопированоКаждый Web Component проходит через определённые этапы жизни:
- Определение — компонент регистрируется в браузере
- Создание — экземпляр компонента создаётся в DOM
- Подключение — компонент добавляется на страницу
- Обновление — атрибуты компонента изменяются
- Отключение — компонент удаляется со страницы
class LifecycleExample extends HTMLElement { constructor() { super(); console.log('Конструктор вызван'); } connectedCallback() { console.log('Компонент подключен к DOM'); } disconnectedCallback() { console.log('Компонент отключен от DOM'); } attributeChangedCallback(name, oldValue, newValue) { console.log(`Атрибут ${name} изменился с ${oldValue} на ${newValue}`); }}
class LifecycleExample extends HTMLElement { constructor() { super(); console.log('Конструктор вызван'); } connectedCallback() { console.log('Компонент подключен к DOM'); } disconnectedCallback() { console.log('Компонент отключен от DOM'); } attributeChangedCallback(name, oldValue, newValue) { console.log(`Атрибут ${name} изменился с ${oldValue} на ${newValue}`); } }
Custom Elements
СкопированоАвтономные элементы
СкопированоСамый простой тип — создание полностью нового HTML-тега:
class MyCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> :host { display: block; border: 1px solid #ccc; border-radius: 8px; padding: 16px; max-width: 300px; } .title { font-weight: bold; margin-bottom: 8px; } .content { color: #666; } </style> <div class="title"> <slot name="title">Заголовок</slot> </div> <div class="content"> <slot>Содержимое карточки</slot> </div> `; }}customElements.define('my-card', MyCard);
class MyCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ` <style> :host { display: block; border: 1px solid #ccc; border-radius: 8px; padding: 16px; max-width: 300px; } .title { font-weight: bold; margin-bottom: 8px; } .content { color: #666; } </style> <div class="title"> <slot name="title">Заголовок</slot> </div> <div class="content"> <slot>Содержимое карточки</slot> </div> `; } } customElements.define('my-card', MyCard);
<my-card> <span slot="title">Моя карточка</span> <p>Это содержимое карточки</p></my-card>
<my-card> <span slot="title">Моя карточка</span> <p>Это содержимое карточки</p> </my-card>
Расширенные встроенные элементы
СкопированоМожно расширять существующие HTML-элементы:
class FancyButton extends HTMLButtonElement { constructor() { super(); this.addEventListener('click', () => { this.style.backgroundColor = '#' + Math.floor(Math.random()*16777215).toString(16); }); }}customElements.define('fancy-button', FancyButton, { extends: 'button' });
class FancyButton extends HTMLButtonElement { constructor() { super(); this.addEventListener('click', () => { this.style.backgroundColor = '#' + Math.floor(Math.random()*16777215).toString(16); }); } } customElements.define('fancy-button', FancyButton, { extends: 'button' });
<button is="fancy-button">Нажми меня!</button>
<button is="fancy-button">Нажми меня!</button>
Shadow DOM для инкапсуляции
СкопированоShadow DOM создаёт изолированное дерево DOM для компонента:
class EncapsulatedComponent extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> /* Эти стили не повлияют на внешние элементы */ .internal { color: red; padding: 10px; border: 2px solid blue; } /* :host позволяет стилизовать сам элемент */ :host { display: block; margin: 10px; } </style> <div class="internal"> <slot>Изолированное содержимое</slot> </div> `; }}
class EncapsulatedComponent extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> /* Эти стили не повлияют на внешние элементы */ .internal { color: red; padding: 10px; border: 2px solid blue; } /* :host позволяет стилизовать сам элемент */ :host { display: block; margin: 10px; } </style> <div class="internal"> <slot>Изолированное содержимое</slot> </div> `; } }
Режимы Shadow DOM
СкопированоОткрытый режим (mode
):
const shadow = element.attachShadow({ mode: 'open' });console.log(element.shadowRoot); // Доступен
const shadow = element.attachShadow({ mode: 'open' }); console.log(element.shadowRoot); // Доступен
Закрытый режим (mode
):
const shadow = element.attachShadow({ mode: 'closed' });console.log(element.shadowRoot); // null
const shadow = element.attachShadow({ mode: 'closed' }); console.log(element.shadowRoot); // null
HTML Templates и слоты
СкопированоИспользование <template>
СкопированоШаблоны позволяют создавать переиспользуемую разметку:
<template id="user-card"> <style> .card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; } .avatar { width: 50px; height: 50px; border-radius: 50%; } .name { font-weight: bold; margin: 8px 0; } </style> <div class="card"> <img class="avatar" src="" alt="Avatar"> <div class="name"></div> <div class="email"></div> <slot name="actions"></slot> </div></template>
<template id="user-card"> <style> .card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; } .avatar { width: 50px; height: 50px; border-radius: 50%; } .name { font-weight: bold; margin: 8px 0; } </style> <div class="card"> <img class="avatar" src="" alt="Avatar"> <div class="name"></div> <div class="email"></div> <slot name="actions"></slot> </div> </template>
class UserCard extends HTMLElement { constructor() { super(); const template = document.getElementById('user-card'); const shadow = this.attachShadow({ mode: 'open' }); shadow.appendChild(template.content.cloneNode(true)); // Заполняем данными const avatar = shadow.querySelector('.avatar'); const name = shadow.querySelector('.name'); const email = shadow.querySelector('.email'); avatar.src = this.getAttribute('avatar') || 'default-avatar.png'; name.textContent = this.getAttribute('name') || 'Имя не указано'; email.textContent = this.getAttribute('email') || 'email@example.com'; }}customElements.define('user-card', UserCard);
class UserCard extends HTMLElement { constructor() { super(); const template = document.getElementById('user-card'); const shadow = this.attachShadow({ mode: 'open' }); shadow.appendChild(template.content.cloneNode(true)); // Заполняем данными const avatar = shadow.querySelector('.avatar'); const name = shadow.querySelector('.name'); const email = shadow.querySelector('.email'); avatar.src = this.getAttribute('avatar') || 'default-avatar.png'; name.textContent = this.getAttribute('name') || 'Имя не указано'; email.textContent = this.getAttribute('email') || 'email@example.com'; } } customElements.define('user-card', UserCard);
Работа со слотами
СкопированоСлоты (<slot>
) позволяют вставлять внешний контент в компонент:
shadow.innerHTML = ` <div class="header"> <slot name="header">Заголовок по умолчанию</slot> </div> <div class="body"> <slot>Содержимое по умолчанию</slot> </div> <div class="footer"> <slot name="footer">Футер по умолчанию</slot> </div>`;
shadow.innerHTML = ` <div class="header"> <slot name="header">Заголовок по умолчанию</slot> </div> <div class="body"> <slot>Содержимое по умолчанию</slot> </div> <div class="footer"> <slot name="footer">Футер по умолчанию</slot> </div> `;
<my-component> <h1 slot="header">Мой заголовок</h1> <p>Моё содержимое</p> <button slot="footer">Действие</button></my-component>
<my-component> <h1 slot="header">Мой заголовок</h1> <p>Моё содержимое</p> <button slot="footer">Действие</button> </my-component>
События и взаимодействие
СкопированоСоздание кастомных событий
Скопированоclass EventComponent extends HTMLElement { constructor() { super(); this.addEventListener('click', () => { // Создаём кастомное событие const event = new CustomEvent('my-event', { detail: { message: 'Привет из компонента!' }, bubbles: true, composed: true }); this.dispatchEvent(event); }); }}
class EventComponent extends HTMLElement { constructor() { super(); this.addEventListener('click', () => { // Создаём кастомное событие const event = new CustomEvent('my-event', { detail: { message: 'Привет из компонента!' }, bubbles: true, composed: true }); this.dispatchEvent(event); }); } }
Слушание событий
Скопированоconst component = document.querySelector('event-component');component.addEventListener('my-event', (event) => { console.log(event.detail.message);});
const component = document.querySelector('event-component'); component.addEventListener('my-event', (event) => { console.log(event.detail.message); });
Что дальше?
СкопированоWeb Components — это мощная технология, которая меняет подход к созданию веб-интерфейсов. Она позволяет создавать действительно переиспользуемые компоненты, которые работают в любом контексте.
Когда использовать Web Components?
Скопировано🛠 Используйте Web Components когда:
- Создаёте библиотеку компонентов
- Нужна максимальная переиспользуемость
- Компонент должен работать в любом контексте
- Требуется полная инкапсуляция
НЕ используйте когда:
- Нужна максимальная гибкость стилизации
- Компонент должен адаптироваться к дизайн-системе
- Требуется простое решение без сложностей