Реактивные контроллеры
В Lit 2 появилась новая концепция повторного использования и компоновки кода, называемая реактивными контроллерами.
Реактивный контроллер — это объект, который может подключаться к циклу реактивного обновления компонента. Контроллеры могут объединять состояние и поведение, связанные с определённой функцией, чтобы её можно было повторно использовать в нескольких определениях компонентов.
С помощью контроллеров можно реализовать функции, которым требуется собственное состояние и доступ к жизненному циклу компонента, например:
- Обработка глобальных событий, таких как события мыши
- Управление асинхронными задачами, например получение данных по сети
- Запуск анимаций
Реактивные контроллеры позволяют создавать компоненты, объединяя небольшие части, которые сами по себе не являются компонентами. Их можно рассматривать как повторно используемые частичные определения компонентов с собственными идентификаторами и состоянием.
Во многом реактивные контроллеры похожи на примеси классов. Основное отличие состоит в том, что у них есть собственный идентификатор и они не добавляются в прототип компонента. Это позволяет ограничить их API и использовать несколько экземпляров контроллера для одного хост-компонента. Подробнее см. в разделе Контроллеры и примеси.
Использование контроллера
У каждого контроллера есть собственный API для создания, но обычно вы создаёте экземпляр и сохраняете его в компоненте:
class MyElement extends LitElement {
private clock = new ClockController(this, 1000);
}
Компонент, связанный с экземпляром контроллера, называется хост-компонентом.
Экземпляр контроллера регистрируется для получения обратных вызовов жизненного цикла от хост-компонента и инициирует обновление хоста, когда у контроллера появляются новые данные для отображения. Так пример ClockController периодически выводит текущее время.
Обычно контроллер предоставляет функциональность, которую можно использовать в методе render() хоста. Например, многие контроллеры имеют состояние, например текущее значение:
render() {
return html`
<div>Current time: ${this.clock.value}</div>
`;
}
Поскольку у каждого контроллера собственный API, обратитесь к документации конкретного контроллера, чтобы узнать, как его использовать.
Создание контроллера
Реактивный контроллер — это объект, связанный с хост-компонентом, который реализует один или несколько обратных вызовов жизненного цикла хоста либо взаимодействует с ним. Его можно реализовать разными способами, но мы сосредоточимся на использовании классов JavaScript с конструкторами для инициализации и методами для обработки событий жизненного цикла.
Инициализация контроллера
Контроллер регистрируется в хост-компоненте, вызывая host.addController(this). Обычно контроллер сохраняет ссылку на хост-компонент, чтобы взаимодействовать с ним позднее.
class ClockController implements ReactiveController {
private host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
// Store a reference to the host
this.host = host;
// Register for lifecycle updates
host.addController(this);
}
}
class ClockController {
constructor(host) {
// Store a reference to the host
this.host = host;
// Register for lifecycle updates
host.addController(this);
}
}Для настройки при создании можно добавить и другие параметры конструктора.
class ClockController implements ReactiveController {
private host: ReactiveControllerHost;
timeout: number
constructor(host: ReactiveControllerHost, timeout: number) {
this.host = host;
this.timeout = timeout;
host.addController(this);
}
class ClockController {
constructor(host, timeout) {
this.host = host;
this.timeout = timeout;
host.addController(this);
}После регистрации контроллера в хост-компоненте можно добавить в него обратные вызовы жизненного цикла, а также другие поля и методы класса для реализации нужного состояния и поведения.
Жизненный цикл
Жизненный цикл реактивного контроллера, определённый в интерфейсе ReactiveController, представляет собой подмножество цикла реактивного обновления. LitElement вызывает методы установленных контроллеров во время обратных вызовов своего жизненного цикла. Эти обратные вызовы необязательны.
-
hostConnected():- Вызывается, когда хост подключается.
- Вызывается после создания
renderRoot, поэтому к этому моменту корень теневого DOM уже существует. - Полезен для настройки прослушивателей событий, наблюдателей и т. д.
-
hostUpdate():- Вызывается перед методами
update()иrender()хоста. - Полезен для чтения DOM до его обновления (например, для анимации).
- Вызывается перед методами
-
hostUpdated():- Вызывается после обновлений, перед методом
updated()хоста. - Полезен для чтения DOM после его изменения (например, для анимации).
- Вызывается после обновлений, перед методом
-
hostDisconnected():- Вызывается, когда хост отключается.
- Полезен для очистки ресурсов, созданных в
hostConnected(), например прослушивателей событий и наблюдателей.
Дополнительную информацию см. в разделе Цикл реактивного обновления.
API хоста контроллера
Хост реактивного контроллера реализует небольшой API для добавления контроллеров и запроса обновлений, а также отвечает за вызов методов жизненного цикла контроллеров.
Минимальный API хоста контроллера:
addController(controller: ReactiveController)removeController(controller: ReactiveController)requestUpdate()updateComplete: Promise<boolean>
Можно также создавать контроллеры, предназначенные для HTMLElement, ReactiveElement, LitElement и требующие большего количества этих API; или даже контроллеры, связанные с классом определённого элемента или другим интерфейсом.
LitElement и ReactiveElement являются хостами контроллеров, но хостами могут быть и другие объекты, например базовые классы из других библиотек веб-компонентов, компоненты фреймворков или другие контроллеры.
Создание контроллеров на основе других контроллеров
Контроллеры также можно составлять из других контроллеров. Для этого создайте дочерний контроллер и передайте ему хост.
class DualClockController implements ReactiveController {
private clock1: ClockController;
private clock2: ClockController;
constructor(host: ReactiveControllerHost, delay1: number, delay2: number) {
this.clock1 = new ClockController(host, delay1);
this.clock2 = new ClockController(host, delay2);
}
get time1() { return this.clock1.value; }
get time2() { return this.clock2.value; }
}
class DualClockController {
constructor(host, delay1, delay2) {
this.clock1 = new ClockController(host, delay1);
this.clock2 = new ClockController(host, delay2);
}
get time1() { return this.clock1.value; }
get time2() { return this.clock2.value; }
}Контроллеры и директивы
Сочетание контроллеров и директив может быть очень эффективным приёмом, особенно для директив, которым нужно выполнять работу до или после отрисовки, например директив анимации; или для контроллеров, которым нужны ссылки на определённые элементы шаблона.
Существует два основных способа использования контроллеров с директивами:
- Директивы-контроллеры. Это директивы, которые сами являются контроллерами, чтобы подключаться к жизненному циклу хоста.
- Контроллеры, владеющие директивами. Это контроллеры, которые создают одну или несколько директив для использования в шаблоне хоста.
Дополнительную информацию о создании директив см. в разделе Пользовательские директивы.
Директивы-контроллеры
Реактивные контроллеры необязательно хранить в полях экземпляра хоста. Любой объект, добавленный в хост с помощью addController(), является контроллером. В частности, контроллером может быть и директива. Это позволяет директиве подключаться к жизненному циклу хоста.
Контроллеры, владеющие директивами
Директивы не обязательно должны быть самостоятельными функциями: они также могут быть методами других объектов, например контроллеров. Это может быть полезно, когда контроллеру нужна конкретная ссылка на элемент шаблона.
Например, представим ResizeController, который позволяет наблюдать за размером элемента с помощью ResizeObserver. Для этого нам понадобятся экземпляр ResizeController и директива, размещённая на элементе, за которым мы хотим наблюдать:
class MyElement extends LitElement {
private _textSize = new ResizeController(this);
render() {
return html`
<textarea ${this._textSize.observe()}></textarea>
<p>The width is ${this._textSize.contentRect?.width}</p>
`;
}
}
class MyElement extends LitElement {
_textSize = new ResizeController(this);
render() {
return html`
<textarea ${this._textSize.observe()}></textarea>
<p>The width is ${this._textSize.contentRect?.width}</p>
`;
}
}Чтобы реализовать это, создайте директиву и вызывайте её из метода:
class ResizeDirective {
/* ... */
}
const resizeDirective = directive(ResizeDirective);
export class ResizeController {
/* ... */
observe() {
// Pass a reference to the controller so the directive can
// notify the controller on size changes.
return resizeDirective(this);
}
}
ЗАДАЧА
- Проверить и доработать этот пример
Сценарии использования
Реактивные контроллеры — это универсальный механизм с широким спектром возможных сценариев использования. Они особенно хорошо подходят для подключения компонента к внешнему ресурсу, например к пользовательскому вводу, управлению состоянием или удалённым API. Ниже приведены несколько распространённых сценариев использования.
Внешние источники данных
Реактивные контроллеры можно использовать для подключения к внешним источникам данных. Например, к событиям клавиатуры и мыши, наблюдателям за изменениями размера или DOM. Контроллер может предоставлять текущее значение входных данных для отображения и запрашивать обновление хоста при изменении значения.
Пример: MouseMoveController
В этом примере показано, как контроллер может выполнять настройку и очистку при подключении и отключении хоста, а также запрашивать обновление при изменении входных данных:
Асинхронные задачи
Асинхронные задачи, например длительные вычисления или сетевой ввод-вывод, обычно имеют состояние, которое меняется со временем. Они должны уведомлять хост об изменении состояния задачи (завершении, ошибках и т. д.).
Контроллеры отлично подходят для объединения выполнения задачи и её состояния, что упрощает использование задачи внутри компонента. У задачи, реализованной как контроллер, обычно есть входные данные, которые может задавать хост, и выходные данные, которые хост может отображать.
@lit-labs/task содержит универсальный контроллер Task, который может получать входные данные от хоста, выполнять функцию задачи и отображать разные шаблоны в зависимости от состояния задачи.
С помощью Task можно создать пользовательский контроллер с API, настроенным под конкретную задачу. Здесь мы оборачиваем Task в NamesController, который может получать один из указанных имён из демонстрационного REST API. NameController предоставляет свойство kind в качестве входных данных и метод render(), который может отображать один из четырёх шаблонов в зависимости от состояния задачи. Логика задачи и способ обновления хоста абстрагированы от хост-компонента.
ЗАДАЧА
- Анимации
См. также
© Google LLC
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://lit.dev/docs/v2/composition/controllers/