API контракты между компонентами

Компоненты в Lit проектируются как изолированные, переиспользуемые единицы. Их взаимодействие строится не на знании внутренней реализации, а на чётко определённых API-контрактах. Контракт описывает, какие данные компонент принимает, какие события генерирует, какие методы и слоты предоставляет, и какие гарантии даёт по поведению.

Отсутствие формализованных контрактов приводит к хрупкой системе: изменение одного компонента начинает ломать другие. В Lit контракты реализуются средствами стандарта Web Components и дополняются механизмами самого фреймворка.


Свойства как основа входного API

Декларация публичных свойств

Основной канал передачи данных в компонент — публичные свойства. В Lit они объявляются через декоратор @property или статическое поле properties.

@property({ type: String, reflect: true })
status = 'idle';

Контракт свойства включает:

  • имя (часть публичного API)
  • тип (ожидаемый формат данных)
  • значение по умолчанию
  • правила отражения в атрибут

Свойство считается частью контракта, если:

  • используется извне;
  • влияет на рендер или поведение;
  • стабильно поддерживается между версиями.

Внутренние состояния (@state) в контракт не входят.


Типизация и ожидания

Тип в декораторе — не просто подсказка, а часть контракта:

@property({ type: Boolean })
disabled = false;

Контракт фиксирует:

  • допустимые значения (true | false);
  • преобразование из HTML (disabled, "", "false"false);
  • реактивность при изменении.

Для сложных структур рекомендуется явно документировать форму объекта:

@property({ attribute: false })
config!: {
  label: string;
  value: number;
  readonly?: boolean;
};

Атрибуты как HTML-контракт

Lit-компоненты существуют в DOM, поэтому атрибуты — часть внешнего API.

<user-card status="active"></user-card>

Контракт атрибута определяет:

  • строковое представление значения;
  • правила синхронизации с свойством;
  • допустимые значения и их семантику.

Важно различать:

  • отражаемые свойства (reflect: true);
  • только программные свойства (attribute: false).

Непредсказуемая синхронизация разрушает контракт и делает компонент трудноиспользуемым.


События как выходной API

Кастомные события

Компонент сообщает о действиях через CustomEvent. Это основной способ передачи информации наружу.

this.dispatchEvent(new CustomEvent('item-selected', {
  detail: { id },
  bubbles: true,
  composed: true
}));

Контракт события включает:

  • имя события (item-selected)
  • структуру detail
  • флаги распространения (bubbles, composed)
  • момент генерации

Событие — часть стабильного API. Изменение имени или структуры detail считается breaking change.


Именование и семантика

Хороший контракт события:

  • использует kebab-case;
  • описывает факт, а не действие (item-selected, а не select-item);
  • не зависит от внутренней реализации.

Плохая практика — пробрасывать DOM-события напрямую как API контракта без явной спецификации.


Методы как программный контракт

Публичные методы компонента

Lit позволяет вызывать методы компонента напрямую:

const el = document.querySelector('modal-dialog');
el.open();

Контракт метода включает:

  • назначение;
  • сигнатуру;
  • побочные эффекты;
  • допустимые состояния вызова.
open(): Promise<void>
close(reason?: string): void

Методы не должны:

  • напрямую менять DOM вне компонента;
  • требовать знания внутреннего состояния.

Метод — более жёсткий контракт, чем событие или свойство, и требует особой стабильности.


Слоты как структурный контракт

Явные точки расширения

Слоты определяют, где и как внешний контент может быть встроен в компонент.

<card-layout>
  <h3 slot="title">Заголовок</h3>
  <p>Основной текст</p>
</card-layout>

Контракт слота включает:

  • имя слота;
  • назначение;
  • допустимую структуру;
  • влияние на рендер.
<slot name="title"></slot>
<slot></slot>

Изменение или удаление слота — нарушение контракта.


Поведение при отсутствии контента

Контракт должен учитывать:

  • fallback-контент;
  • обязательность слота;
  • реакцию на пустой слот.
<slot name="title">
  <span class="default-title"></span>
</slot>

Контракты и жизненный цикл

Гарантии реактивности

Контракт компонента подразумевает:

  • изменение свойства → планирование обновления;
  • порядок вызовов updated, render;
  • консистентность DOM после апдейта.

Использование requestUpdate и ручного управления обновлениями должно быть предсказуемо описано, если это влияет на внешнее поведение.


Версионирование контрактов

Breaking vs non-breaking изменения

К breaking изменениям относятся:

  • переименование свойств;
  • изменение типа;
  • удаление событий или слотов;
  • изменение семантики без изменения сигнатуры.

Безопасные изменения:

  • добавление новых свойств;
  • добавление новых событий;
  • расширение detail без изменения существующих полей.

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


Контракты и композиция компонентов

Родитель → потомок

Контракты определяют, что родитель может ожидать от потомка, не зная его реализации:

  • какие свойства можно передать;
  • какие события будут сгенерированы;
  • какие слоты доступны.
<filter-panel
  .options=${options}
  @change=${onChange}>
</filter-panel>

Родитель не должен:

  • обращаться к shadow DOM потомка;
  • читать внутренние состояния;
  • полагаться на порядок рендера.

Потомок → родитель

Компонент не должен напрямую вызывать код родителя. Контракт реализуется через события и свойства обратной связи:

this.dispatchEvent(new CustomEvent('value-change', {
  detail: { value }
}));

Документирование контрактов в коде

Контракты фиксируются не только в документации, но и в самом коде:

  • JSDoc для свойств и событий;
  • строгая типизация;
  • явные имена;
  • отсутствие неявных побочных эффектов.
/**
 * Текущий выбранный элемент.
 * @fires item-selected { id: string }
 */
@property({ type: String })
selectedId?: string;

Контракты и тестирование

Контракт — основа компонентных тестов:

  • тестируются публичные свойства;
  • проверяются события;
  • игнорируется внутренняя реализация.

Тест не должен ломаться при рефакторинге, если контракт не изменён.


Антипаттерны API-контрактов

  • использование querySelector для доступа к внутренностям компонента;
  • прокидывание функций как свойств вместо событий;
  • неявные зависимости от порядка рендера;
  • смешивание публичных и приватных свойств.

Итоговая модель контракта компонента Lit

Полноценный API-контракт компонента включает:

  • свойства — входные данные;
  • атрибуты — HTML-интерфейс;
  • события — выходные сигналы;
  • методы — программное управление;
  • слоты — структурное расширение;
  • гарантии жизненного цикла.

Lit предоставляет минималистичный, но строгий набор инструментов для реализации этих контрактов, опираясь на стандарты Web Components. Именно контракт, а не реализация, определяет качество и устойчивость компонентной системы.