Перейти к содержанию

Реактивные контроллеры

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

Вы можете использовать контроллеры для реализации функций, которые требуют собственного состояния и доступа к жизненному циклу компонента, например:

  • Обработка глобальных событий, таких как события мыши.
  • Управление асинхронными задачами, такими как получение данных по сети
  • Запуск анимации.

Реактивные контроллеры позволяют создавать компоненты путем компоновки более мелких частей, которые сами по себе не являются компонентами. Их можно рассматривать как многократно используемые частичные определения компонентов, обладающие собственной идентичностью и состоянием.

Реактивные контроллеры во многом похожи на миксины классов. Основное отличие заключается в том, что они имеют свою собственную идентичность и не добавляются к прототипу компонента, что помогает содержать их API и позволяет использовать несколько экземпляров контроллеров для одного компонента. Подробнее см. в разделе Контроллеры и миксины.

Использование контроллера

Каждый контроллер имеет свой собственный API для создания, но обычно вы создаете экземпляр и храните его вместе с компонентом:

1
2
3
class MyElement extends LitElement {
    private clock = new ClockController(this, 1000);
}

Компонент, связанный с экземпляром контроллера, называется хост-компонентом.

Экземпляр контроллера регистрируется для получения обратных вызовов жизненного цикла от хост-компонента и запускает обновление хоста, когда у контроллера появляются новые данные для отображения. Так в примере ClockController периодически отображается текущее время.

Контроллер обычно предоставляет некоторую функциональность для использования в методе render() хоста. Например, многие контроллеры имеют некоторое состояние, например, текущее значение:

1
2
3
4
5
  render() {
    return html`
      <div>Current time: ${this.clock.value}</div>
    `;
  }

Поскольку каждый контроллер имеет свой собственный API, обратитесь к документации по конкретным контроллерам, чтобы узнать, как их использовать.

Написание контроллера

Реактивный контроллер — это объект, связанный с компонентом хоста, который реализует один или несколько обратных вызовов жизненного цикла хоста или взаимодействует с ним. Он может быть реализован различными способами, но мы сосредоточимся на использовании классов JavaScript, с конструкторами для инициализации и методами для жизненных циклов.

Инициализация контроллера

Контроллер регистрируется в компоненте-хосте вызовом host.addController(this). Обычно контроллер хранит ссылку на свой компонент-хост, чтобы иметь возможность взаимодействовать с ним в дальнейшем.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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);
    }
}
1
2
3
4
5
6
7
8
class ClockController {
    constructor(host) {
        // Store a reference to the host
        this.host = host;
        // Register for lifecycle updates
        host.addController(this);
    }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class ClockController implements ReactiveController {
    private host: ReactiveControllerHost;
    timeout: number;

    constructor(
        host: ReactiveControllerHost,
        timeout: number,
    ) {
        this.host = host;
        this.timeout = timeout;
        host.addController(this);
    }
}
1
2
3
4
5
6
7
class ClockController {
    constructor(host, timeout) {
        this.host = host;
        this.timeout = timeout;
        host.addController(this);
    }
}

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

Жизненный цикл

Жизненный цикл реактивного контроллера, определенный в интерфейсе ReactiveController, является подмножеством цикла реактивного обновления. LitElement обращается к любым установленным контроллерам во время обратных вызовов жизненного цикла. Эти обратные вызовы являются необязательными.

  • hostConnected():
    • Вызывается, когда хост подключен.
    • Вызывается после создания renderRoot, поэтому в этот момент будет существовать теневой корень.
    • Полезно для настройки слушателей событий, наблюдателей и т.д.
  • 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 являются хостами контроллеров, но хостами могут быть и другие объекты, например, базовые классы из других библиотек веб-компонентов, компоненты из фреймворков или другие контроллеры.

Создание контроллеров из других контроллеров

Контроллеры также могут состоять из других контроллеров. Для этого создайте дочерний контроллер и передайте ему хост.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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;
    }
}

Контроллеры и директивы

Сочетание контроллеров с директивами может быть очень мощной техникой, особенно для директив, которые должны выполнять работу до или после рендеринга, например директивы анимации; или контроллеров, которым нужны ссылки на определенные элементы в шаблоне.

Существует две основные схемы использования контроллеров с директивами:

  • Директивы контроллера. Это директивы, которые сами являются контроллерами, чтобы подключиться к жизненному циклу хоста.
  • Контроллеры, владеющие директивами. Это контроллеры, которые создают одну или несколько директив для использования в шаблоне хоста.

Дополнительные сведения о написании директив см. в Custom directives.

Директивы контроллеров

Реактивные контроллеры не нужно хранить на хосте в виде полей экземпляра. Все, что добавляется к хосту с помощью addController(), является контроллером. В частности, директива также может быть контроллером. Это позволяет директиве подключаться к жизненному циклу хоста.

Контроллеры, владеющие директивами.

Директивы не обязательно должны быть отдельными функциями, они могут быть методами и других объектов, например контроллеров. Это может быть полезно в случаях, когда контроллеру нужна конкретная ссылка на элемент в шаблоне.

Например, представьте себе контроллер ResizeController, который позволяет наблюдать за размером элемента с помощью ResizeObserver. Для работы нам понадобится экземпляр ResizeController и директива, размещенная на элементе, который мы хотим наблюдать:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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>
        `;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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>
        `;
    }
}

Чтобы реализовать это, вы создаете директиву и вызываете ее из метода:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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. Вот несколько распространенных вариантов использования.

Внешние входы

Реактивные контроллеры можно использовать для подключения к внешним входам. Например, события клавиатуры и мыши, наблюдатели изменения размера или наблюдатели мутации. Контроллер может предоставить текущее значение входа для использования в рендеринге и запросить обновление хоста при изменении значения.

Пример: MouseMoveController

В этом примере показано, как контроллер может выполнять настройку и очистку при подключении и отключении хоста, а также запрашивать обновление при изменении входных данных:

Асинхронные задачи

Асинхронные задачи, такие как длительные вычисления или сетевой ввод-вывод, обычно имеют состояние, которое меняется со временем, и им необходимо уведомлять хост об изменении состояния задачи (завершение, ошибки и т. д.).

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

@lit/task содержит общий контроллер Task, который может получать входные данные от хоста, выполнять функцию задачи и выводить различные шаблоны в зависимости от состояния задачи.

Вы можете использовать Task для создания пользовательского контроллера с API, предназначенного для вашей конкретной задачи. Здесь мы обернули Task в NamesController, который может получить одно из заданного списка имен из демонстрационного REST API. NameController предоставляет свойство kind в качестве входных данных, а также метод render(), который может отображать один из четырех шаблонов в зависимости от состояния задачи. Логика задачи и то, как она обновляет хост, абстрагированы от компонента хоста.

См. также

Комментарии