Инкапсуляция стилей

Haunted — это библиотека для создания веб-компонентов с использованием функционального подхода в JavaScript. Одним из ключевых преимуществ веб-компонентов является возможность инкапсуляции стилей, что обеспечивает независимость внешнего и внутреннего оформления элементов. В Haunted это реализуется через Shadow DOM, CSS-in-JS и специфические подходы к связыванию стилей с компонентами.


Shadow DOM и его роль

Shadow DOM создаёт изолированное дерево элементов внутри веб-компонента. Любые стили, объявленные внутри shadow root, применяются только к этому компоненту и не влияют на внешние элементы страницы.

Пример создания компонента с Shadow DOM в Haunted:

import { component, html } from 'haunted';

function MyButton({ label }) {
  return html`
    <style>
      button {
        background-color: #3498db;
        color: white;
        border: none;
        padding: 10px 20px;
        cursor: pointer;
        border-radius: 4px;
      }

      button:hover {
        background-color: #2980b9;
      }
    </style>
    <button>${label}</button>
  `;
}

customElements.define('my-button', component(MyButton));

В этом примере:

  • <style> помещается внутрь шаблона, что делает стили локальными.
  • Shadow DOM предотвращает “протекание” стилей наружу и блокирует воздействие глобальных CSS.

Инкапсуляция через adoptedStyleSheets

Современные браузеры поддерживают CSSStyleSheet API, что позволяет создавать и повторно использовать стили для нескольких компонентов без дублирования кода.

Пример с adoptedStyleSheets:

const buttonStyles = new CSSStyleSheet();
buttonStyles.replaceSync(`
  button {
    background-color: #2ecc71;
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 4px;
  }
  button:hover {
    background-color: #27ae60;
  }
`);

function StyledButton({ label }) {
  const ref = document.createElement('div').attachShadow({ mode: 'open' });
  ref.shadowRoot.adoptedStyleSheets = [buttonStyles];
  return html`
    <button>${label}</button>
  `;
}

customElements.define('styled-button', component(StyledButton));

Преимущества подхода:

  • Централизованное управление стилями.
  • Возможность повторного использования и кэширования CSS.
  • Более чистый код без дублирования <style> в каждом компоненте.

Scoped CSS через CSS-in-JS

Haunted позволяет использовать CSS внутри шаблонов через строки, что создаёт локальные scoped стили без прямого взаимодействия с shadow root:

import { component, html } from 'haunted';

function Card({ title, content }) {
  const cardStyles = `
    .card {
      border: 1px solid #ccc;
      padding: 16px;
      border-radius: 6px;
      background: #fff;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .card h2 {
      margin: 0 0 8px 0;
      font-size: 1.2em;
    }
    .card p {
      margin: 0;
      color: #555;
    }
  `;

  return html`
    <style>${cardStyles}</style>
    <div class="card">
      <h2>${title}</h2>
      <p>${content}</p>
    </div>
  `;
}

customElements.define('my-card', component(Card));

Этот метод удобен для быстрого внедрения локальных стилей, хотя и создаёт новый <style> при каждом рендере компонента.


Динамическая инкапсуляция стилей

Haunted поддерживает реактивные свойства, что позволяет динамически изменять стили в зависимости от состояния компонента.

Пример:

import { component, html, useState } from 'haunted';

function ToggleButton() {
  const [active, setActive] = useState(false);

  const styles = `
    button {
      padding: 10px 20px;
      border: none;
      border-radius: 4px;
      background-color: ${active ? '#e74c3c' : '#2ecc71'};
      color: white;
      cursor: pointer;
    }
  `;

  return html`
    <style>${styles}</style>
    <button @click=${() => setActive(!active)}>
      ${active ? 'Активен' : 'Не активен'}
    </button>
  `;
}

customElements.define('toggle-button', component(ToggleButton));

Особенности подхода:

  • Локальные стили реагируют на изменения состояния компонента.
  • Shadow DOM гарантирует изоляцию этих изменений от внешнего CSS.

Советы по организации стилей

  1. Разделять базовые и специфические стили. Базовые можно вынести в adoptedStyleSheets, а специфические оставлять внутри шаблонов.
  2. Минимизировать дублирование. Использовать переменные и CSS-переменные для динамических состояний.
  3. Следить за производительностью. Частая генерация <style> при рендере больших компонентов может негативно сказаться на производительности.
  4. Использовать CSS-переменные внутри Shadow DOM для простого обмена стилями между компонентами и их настройкой через атрибуты.

Ограничения и особенности

  • Shadow DOM не поддерживает все селекторы глобального CSS; псевдоклассы типа :host и :host-context помогают взаимодействовать с внешним контекстом.
  • adoptedStyleSheets поддерживается не во всех старых браузерах.
  • Динамическое внедрение стилей через <style> внутри шаблона создаёт новые элементы при каждом обновлении, что нужно учитывать при оптимизации.

Haunted сочетает реактивность функциональных компонентов с мощными средствами инкапсуляции стилей, предоставляя гибкость как для локальных, так и для повторно используемых CSS. Правильное применение Shadow DOM, adoptedStyleSheets и динамических <style> позволяет создавать масштабируемые, изолированные и управляемые компоненты с чистой архитектурой.