Компоненты в Lit проектируются как изолированные, переиспользуемые единицы. Их взаимодействие строится не на знании внутренней реализации, а на чётко определённых API-контрактах. Контракт описывает, какие данные компонент принимает, какие события генерирует, какие методы и слоты предоставляет, и какие гарантии даёт по поведению.
Отсутствие формализованных контрактов приводит к хрупкой системе: изменение одного компонента начинает ломать другие. В Lit контракты реализуются средствами стандарта Web Components и дополняются механизмами самого фреймворка.
Основной канал передачи данных в компонент — публичные свойства. В
Lit они объявляются через декоратор @property или
статическое поле properties.
@property({ type: String, reflect: true })
status = 'idle';
Контракт свойства включает:
Свойство считается частью контракта, если:
Внутренние состояния (@state) в контракт не входят.
Тип в декораторе — не просто подсказка, а часть контракта:
@property({ type: Boolean })
disabled = false;
Контракт фиксирует:
true | false);disabled, "",
"false" → false);Для сложных структур рекомендуется явно документировать форму объекта:
@property({ attribute: false })
config!: {
label: string;
value: number;
readonly?: boolean;
};
Lit-компоненты существуют в DOM, поэтому атрибуты — часть внешнего API.
<user-card status="active"></user-card>
Контракт атрибута определяет:
Важно различать:
reflect: true);attribute: false).Непредсказуемая синхронизация разрушает контракт и делает компонент трудноиспользуемым.
Компонент сообщает о действиях через CustomEvent. Это
основной способ передачи информации наружу.
this.dispatchEvent(new CustomEvent('item-selected', {
detail: { id },
bubbles: true,
composed: true
}));
Контракт события включает:
item-selected)detailbubbles,
composed)Событие — часть стабильного API. Изменение имени или структуры
detail считается breaking change.
Хороший контракт события:
item-selected, а не
select-item);Плохая практика — пробрасывать DOM-события напрямую как API контракта без явной спецификации.
Lit позволяет вызывать методы компонента напрямую:
const el = document.querySelector('modal-dialog');
el.open();
Контракт метода включает:
open(): Promise<void>
close(reason?: string): void
Методы не должны:
Метод — более жёсткий контракт, чем событие или свойство, и требует особой стабильности.
Слоты определяют, где и как внешний контент может быть встроен в компонент.
<card-layout>
<h3 slot="title">Заголовок</h3>
<p>Основной текст</p>
</card-layout>
Контракт слота включает:
<slot name="title"></slot>
<slot></slot>
Изменение или удаление слота — нарушение контракта.
Контракт должен учитывать:
<slot name="title">
<span class="default-title"></span>
</slot>
Контракт компонента подразумевает:
updated, render;Использование requestUpdate и ручного управления
обновлениями должно быть предсказуемо описано, если это влияет на
внешнее поведение.
К breaking изменениям относятся:
Безопасные изменения:
detail без изменения существующих
полей.Контракт должен рассматриваться как публичный интерфейс, а не как побочный эффект реализации.
Контракты определяют, что родитель может ожидать от потомка, не зная его реализации:
<filter-panel
.options=${options}
@change=${onChange}>
</filter-panel>
Родитель не должен:
Компонент не должен напрямую вызывать код родителя. Контракт реализуется через события и свойства обратной связи:
this.dispatchEvent(new CustomEvent('value-change', {
detail: { value }
}));
Контракты фиксируются не только в документации, но и в самом коде:
/**
* Текущий выбранный элемент.
* @fires item-selected { id: string }
*/
@property({ type: String })
selectedId?: string;
Контракт — основа компонентных тестов:
Тест не должен ломаться при рефакторинге, если контракт не изменён.
querySelector для доступа к внутренностям
компонента;Полноценный API-контракт компонента включает:
Lit предоставляет минималистичный, но строгий набор инструментов для реализации этих контрактов, опираясь на стандарты Web Components. Именно контракт, а не реализация, определяет качество и устойчивость компонентной системы.