Клавиша / esc

Web Components

Как создавать собственные HTML-элементы с инкапсулированной функциональностью и использовать их в веб-приложениях.

Время чтения: 10 мин

Что такое 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 проходит через определённые этапы жизни:

  1. Определение — компонент регистрируется в браузере
  2. Создание — экземпляр компонента создаётся в DOM
  3. Подключение — компонент добавляется на страницу
  4. Обновление — атрибуты компонента изменяются
  5. Отключение — компонент удаляется со страницы
        
          
          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: 'open'):

        
          
          const shadow = element.attachShadow({ mode: 'open' });console.log(element.shadowRoot); // Доступен
          const shadow = element.attachShadow({ mode: 'open' });
console.log(element.shadowRoot); // Доступен

        
        
          
        
      

Закрытый режим (mode: 'closed'):

        
          
          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 когда:

  • Создаёте библиотеку компонентов
  • Нужна максимальная переиспользуемость
  • Компонент должен работать в любом контексте
  • Требуется полная инкапсуляция

НЕ используйте когда:

  • Нужна максимальная гибкость стилизации
  • Компонент должен адаптироваться к дизайн-системе
  • Требуется простое решение без сложностей