Композиция компонентов — базовый архитектурный принцип Stencil, позволяющий строить интерфейсы из независимых, переиспользуемых и слабо связанных элементов. Вместо монолитных компонентов применяется иерархия, где каждый уровень решает строго ограниченную задачу. Такой подход упрощает сопровождение, тестирование и развитие кода, а также делает веб-компоненты пригодными для использования вне зависимости от фреймворка.
Stencil опирается на стандарты Web Components, поэтому композиция реализуется через HTML-разметку, слоты, свойства, события и контекстное взаимодействие между компонентами.
Наиболее очевидная форма композиции — вложение одного компонента в другой. Любой компонент Stencil может использоваться как HTML-тег внутри JSX или шаблона другого компонента.
@Component({
tag: 'app-layout',
shadow: true,
})
export class AppLayout {
render() {
return (
<div class="layout">
<app-header></app-header>
<main>
<slot></slot>
</main>
<app-footer></app-footer>
</div>
);
}
}
В этом примере app-layout не знает деталей реализации
app-header и app-footer. Он лишь определяет
структуру. Такая декомпозиция позволяет изменять внутреннее устройство
вложенных компонентов без влияния на контейнер.
<slot> как механизма композицииСлоты — ключевой инструмент композиции в Stencil. Они позволяют передавать произвольное содержимое из родительского компонента во вложенный, не нарушая инкапсуляцию.
@Component({
tag: 'ui-card',
shadow: true,
})
export class UiCard {
render() {
return (
<div class="card">
<slot></slot>
</div>
);
}
}
Использование:
<ui-card>
<h2>Заголовок</h2>
<p>Произвольный контент</p>
</ui-card>
Компонент ui-card не зависит от структуры переданного
содержимого, что делает его универсальным контейнером.
Именованные слоты позволяют точнее контролировать размещение дочернего контента.
@Component({
tag: 'ui-modal',
shadow: true,
})
export class UiModal {
render() {
return (
<div class="modal">
<header>
<slot name="title"></slot>
</header>
<section>
<slot></slot>
</section>
<footer>
<slot name="actions"></slot>
</footer>
</div>
);
}
}
Использование:
<ui-modal>
<span slot="title">Подтверждение</span>
<p>Удалить элемент?</p>
<div slot="actions">
<button>Отмена</button>
<button>ОК</button>
</div>
</ui-modal>
Именованные слоты формируют контракт между компонентом-контейнером и его содержимым, сохраняя гибкость композиции.
Композиция компонентов редко ограничивается только разметкой. Для
передачи данных используется механизм @Prop.
@Component({
tag: 'user-avatar',
shadow: true,
})
export class UserAvatar {
@Prop() name: string;
@Prop() image: string;
render() {
return (
<div class="avatar">
<img src={this.image} alt={this.name} />
<span>{this.name}</span>
</div>
);
}
}
Родительский компонент управляет конфигурацией дочернего, не вмешиваясь в его логику:
<user-avatar
name="Иван"
image="/avatars/ivan.png">
</user-avatar>
Такой способ композиции подчеркивает однонаправленный поток данных сверху вниз.
Для обратной связи от вложенных компонентов применяется система
событий (@Event). Это позволяет дочерним элементам сообщать
о действиях, не зная, кто и как будет их обрабатывать.
@Component({
tag: 'ui-button',
shadow: true,
})
export class UiButton {
@Event() clicked: EventEmitter<void>;
private onCl ick = () => {
this.clicked.emit();
};
render() {
return <button onCl ick={this.onClick}><slot></slot></button>;
}
}
Использование в родительском компоненте:
<ui-button onClic ked={() => this.save()}>
Сохранить
</ui-button>
Событийная композиция исключает жёсткие зависимости между компонентами и делает их легко переиспользуемыми в разных контекстах.
В крупных проектах распространена модель разделения компонентов на контейнерные и презентационные.
Контейнерные компоненты:
Презентационные компоненты:
@Prop;Пример контейнера:
@Component({
tag: 'user-profile',
shadow: true,
})
export class UserProfile {
@State() user: User;
componentWillLoad() {
this.user = fetchUser();
}
render() {
return (
<profile-view user={this.user}></profile-view>
);
}
}
Пример презентационного компонента:
@Component({
tag: 'profile-view',
shadow: true,
})
export class ProfileView {
@Prop() user: User;
render() {
return (
<div>
<h1>{this.user.name}</h1>
<p>{this.user.email}</p>
</div>
);
}
}
Такая композиция улучшает читаемость и снижает связность.
Stencil-компоненты легко комбинируются в разных конфигурациях без изменения исходного кода.
<ui-card>
<user-avatar ...></user-avatar>
</ui-card>
<ui-modal>
<ui-card>
<user-avatar ...></user-avatar>
</ui-card>
</ui-modal>
Один и тот же компонент участвует в разных композициях, оставаясь неизменным. Это особенно важно при разработке дизайн-систем и библиотек компонентов.
Использование Shadow DOM влияет на композицию:
Stencil позволяет отключать Shadow DOM (shadow: false),
если требуется более тесная интеграция с глобальными стилями, но при
проектировании композиции предпочтительно учитывать ограничения Shadow
DOM и проектировать API компонентов через свойства, события и слоты.
Композиция в Stencil — не просто способ вложить один тег в другой, а полноценный механизм проектирования интерфейсов. Грамотно спроектированные компоненты:
Такой подход формирует устойчивую архитектуру, в которой рост проекта не приводит к усложнению связей между частями системы.