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

Контекст

Контекст — это способ сделать данные доступными для целых поддеревьев компонентов без необходимости вручную привязывать свойства к каждому компоненту. Данные доступны "контекстно", так что элементы-предки, находящиеся между поставщиком и потребителем данных, даже не знают о них.

Реализация контекста Lit доступна в пакете @lit/context:

1
npm i @lit/context

Context полезен для данных, которые должны потребляться большим количеством компонентов — например, хранилищем данных приложения, текущим пользователем, темой пользовательского интерфейса — или когда привязка данных невозможна, например, когда элемент должен предоставлять данные своим легким дочерним элементам DOM.

Context очень похож на React's Context или на системы инъекции зависимостей, такие как Angular's, с некоторыми важными отличиями, которые позволяют Context работать с динамической природой DOM и обеспечивают совместимость между различными библиотеками веб-компонентов, фреймворками и обычным JavaScript.

Пример

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

Определение контекста (logger-context.ts):

1
2
3
4
5
import { createContext } from '@lit/context';
import type { Logger } from 'my-logging-library';
export type { Logger } from 'my-logging-library';
export const loggerContext =
    createContext<Logger>('logger');

Провайдер:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { LitElement, property, html } from 'lit';
import { provide } from '@lit/context';

import { Logger } from 'my-logging-library';
import { loggerContext } from './logger-context.js';

@customElement('my-app')
class MyApp extends LitElement {
    @provide({ context: loggerContext })
    logger = new Logger();

    render() {
        return html`...`;
    }
}

Потребитель:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { LitElement, property } from 'lit';
import { consume } from '@lit/context';

import {
    type Logger,
    loggerContext,
} from './logger-context.js';

export class MyElement extends LitElement {
    @consume({ context: loggerContext })
    @property({ attribute: false })
    public logger?: Logger;

    private doThing() {
        this.logger?.log('A thing was done');
    }
}

Ключевые понятия

Протокол контекста

Контекст Lit основан на протоколе Context Community Protocol, разработанном группой W3C Web Components Community Group.

Этот протокол обеспечивает взаимодействие между элементами (или даже неэлементным кодом) независимо от того, как они были созданы. С помощью контекстного протокола элемент, основанный на Lit, может предоставлять данные потребителю, не построенному на Lit, или наоборот.

Контекстный протокол основан на событиях DOM. Потребитель запускает событие context-request, которое несет в себе нужный ему контекстный ключ, а любой элемент над ним может прослушать событие context-request и предоставить данные для этого контекстного ключа.

@lit/context реализует этот основанный на событиях протокол и делает его доступным с помощью нескольких реактивных контроллеров и декораторов.

Объекты контекста

Контексты идентифицируются контекстными объектами или контекстными ключами. Это объекты, которые представляют некоторые потенциальные данные, подлежащие совместному использованию идентичностью контекстного объекта. Вы можете думать о них как о ключах Map.

Провайдеры

Провайдеры обычно являются элементами (но могут быть и любыми обработчиками событий), которые предоставляют данные для определенных ключей контекста.

Потребители

Потребители запрашивают данные для определенных ключей контекста.

Подписки

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

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

Определение контекста

Каждое использование контекста должно иметь объект контекста для координации запроса данных. Этот объект контекста представляет личность и тип предоставляемых данных.

Объекты контекста создаются с помощью функции createContext():

1
2
3
export const myContext = createContext(
    Symbol('my-context'),
);

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

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

Функция createContext() принимает любое значение и возвращает его напрямую. В TypeScript значение приводится к типизированному объекту Context, который несет в себе тип контекста значение.

В случае ошибки, подобной этой:

1
2
3
4
5
6
const myContext = createContext<Logger>(Symbol('logger'));

class MyElement extends LitElement {
    @provide({ context: myContext })
    name: string;
}

TypeScript предупредит, что тип string не может быть присвоен типу Logger. Обратите внимание, что в настоящее время эта проверка выполняется только для публичных полей.

Равенство контекста

Объекты контекста используются провайдерами для сопоставления события контекстного запроса со значением. Контексты сравниваются на основе строгого равенства (===), поэтому провайдер будет обрабатывать контекстный запрос только в том случае, если его контекстный ключ равен контекстному ключу запроса.

Это означает, что существует два основных способа создания объекта контекста:

  1. С помощью глобально уникального значения, например, объекта ({}) или символа (Symbol())
  2. Со значением, которое не является глобально уникальным, так что оно может быть равным при строгом равенстве, как строка ('logger') или глобальный символ (Symbol.for('logger')).

Если вы хотите, чтобы два раздельных вызова createContext() ссылались на один и тот же контекст, то используйте ключ, который будет равен при строгом равенстве, например строку:

1
2
// true
createContext('my-context') === createContext('my-context');

Однако помните, что два модуля в вашем приложении могут использовать один и тот же контекстный ключ для ссылки на разные объекты. Чтобы избежать непреднамеренных коллизий, вы можете использовать относительно уникальную строку, например, 'console-logger' вместо 'logger'.

Обычно лучше всего использовать глобально уникальный контекстный объект. Символы — один из самых простых способов сделать это.

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

В @lit/context есть два способа предоставить значение контекста: контроллер ContextProvider и декоратор @provide().

@provide()

Декоратор @provide() — это самый простой способ предоставить значение, если вы используете декораторы. Он создает для вас контроллер ContextProvider.

Украсьте свойство с помощью @provide() и передайте ему ключ контекста:

1
2
3
4
5
6
7
8
9
import { LitElement, html } from 'lit';
import { property } from 'lit/decorators.js';
import { provide } from '@lit/context';
import { myContext, MyData } from './my-context.js';

class MyApp extends LitElement {
    @provide({ context: myContext })
    myData: MyData;
}

Вы можете сделать свойство также реактивным с помощью @property() или @state(), чтобы его установка обновляла элемент провайдера, а также потребителей контекста.

1
2
3
  @provide({context: myContext})
  @property({attribute: false})
  myData: MyData;

Свойства контекста часто должны быть приватными. Вы можете сделать приватные свойства реактивными с помощью @state():

1
2
3
  @provide({context: myContext})
  @state()
  private _myData: MyData;

Если сделать свойство контекста публичным, элемент может предоставить публичное поле своему дочернему дереву:

1
2
3
html`<my-provider-element
    .myData=${someData}
></my-provider-element>`;

ContextProvider

ContextProvider — это реактивный контроллер, который управляет обработчиками событий context-request за вас.

1
2
3
4
5
6
7
8
9
import { LitElement, html } from 'lit';
import { ContextProvider } from '@lit/context';
import { myContext } from './my-context.js';

export class MyApp extends LitElement {
    private _provider = new ContextProvider(this, {
        context: myContext,
    });
}

ContextProvider может принимать начальное значение в качестве опции в конструкторе:

1
private _provider = new ContextProvider(this, {context: myContext, initialValue: myData});

Или вы можете вызвать setValue():

1
this._provider.setValue(myData);

Потребление контекста

Декоратор @consume()

Декоратор @consume() — это самый простой способ потребления значения, если вы используете декораторы. Он создает для вас контроллер ContextConsumer.

Украсьте свойство с помощью @consume() и передайте ему ключ контекста:

1
2
3
4
5
6
7
8
import { LitElement, html } from 'lit';
import { consume } from '@lit/context';
import { myContext, MyData } from './my-context.js';

class MyElement extends LitElement {
    @consume({ context: myContext })
    myData: MyData;
}

Когда этот элемент подключается к документу, он автоматически запускает событие context-request, получает предоставленное значение, присваивает его свойству и запускает обновление элемента.

ContextConsumer

ContextConsumer — это реактивный контроллер, который управляет диспетчеризацией события context-request за вас. Контроллер будет заставлять элемент-хост обновляться при получении новых значений. Предоставленное значение будет доступно в свойстве .value контроллера.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { LitElement, property } from 'lit';
import { ContextConsumer } from '@lit/context';
import { myContext } from './my-context.js';

export class MyElement extends LitElement {
    private _myData = new ContextConsumer(this, {
        context: myContext,
    });

    render() {
        const myData = this._myData.value;
        return html`...`;
    }
}

Подписка на контексты

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

Подписаться можно с помощью декоратора @consume():

1
2
  @consume({context: myContext, subscribe: true})
  myData: MyData;

и контроллера ContextConsumer:

1
2
3
4
5
6
  private _myData = new ContextConsumer(this,
    {
      context: myContext,
      subscribe: true,
    }
  );

Примеры использования

Текущий пользователь, локаль и т. д.

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

Сервисы

Глобальные сервисы приложения, такие как логгеры, аналитика, хранилища данных, могут быть предоставлены контекстом. Преимущество контекста перед импортом из общего модуля — в позднем связывании и древовидном копировании, которые обеспечивает контекст. Тесты могут легко предоставлять имитируемые сервисы, или разные части страницы могут быть предоставлены различным экземплярам сервисов.

Темы

Темы — это наборы стилей, которые применяются ко всей странице или целым поддеревьям внутри страницы — именно такой охват данных, который обеспечивает контекст.

Одним из способов построения системы тем было бы определение типа Theme, который могут предоставлять контейнеры и который содержит именованные стили. Элементы, которые хотят применить тему, могут использовать объект темы и искать стили по имени. Пользовательские реактивные контроллеры тем могут обернуть ContextProvider и ContextConsumer, чтобы уменьшить количество шаблонов.

Плагины на основе HTML

Контекст можно использовать для передачи данных от родителя к его легким DOM-детям. Поскольку родитель обычно не создает легкие DOM-дочерние элементы, он не может использовать привязку данных на основе шаблона для передачи им данных, но он может слушать и отвечать на события context-request.

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

1
2
3
4
<code-editor>
    <code-editor-javascript-mode></code-editor-javascript-mode>
    <code-editor-python-mode></code-editor-python-mode>
</code-editor>

В этом случае <code-editor> будет предоставлять API для добавления языковых режимов через контекст, а подключаемые элементы будут использовать этот API и добавлять себя в редактор.

Форматировщики данных, генераторы ссылок и т. д.

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

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

API

Эта документация по API является кратким обзором до тех пор, пока не появятся готовые документы по API

createContext()

Создает типизированный объект Context

Импорт:

1
import { property } from '@lit/context';

Синтаксис:

1
2
3
function createContext<ValueType, K = unknown>(
    key: K,
): Context<K, ValueType>;

Контексты сравниваются с помощью строгого равенства.

Если вы хотите, чтобы два отдельных вызова createContext() ссылались на один и тот же контекст, то используйте ключ, который будет равен при строгом равенстве, как строка для Symbol.for():

1
2
3
4
5
// true
createContext('my-context') === createContext('my-context');
// true
createContext(Symbol.for('my-context')) ===
    createContext(Symbol.for('my-context'));

Если вы хотите, чтобы контекст был уникальным и гарантированно не сталкивался с другими контекстами, используйте ключ, который уникален при строгом равенстве, например Symbol() или object:

1
2
3
4
5
// false
createContext(Symbol('my-context')) ===
    createContext(Symbol('my-context'));
// false
createContext({}) === createContext({});

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

@provide()

Декоратор свойств, который добавляет контроллер ContextProvider в компонент, заставляя его реагировать на любые события context-request от дочерних потребителей.

Импорт:

1
import { provide } from '@lit/context';

Синтаксис:

1
@provide({context: Context})

@consume()

Декоратор свойств, добавляющий в компонент контроллер ContextConsumer, который будет получать значение свойства по протоколу Context.

Импорт:

1
import { consume } from '@lit/context';

Синтаксис:

1
@consume({context: Context, subscribe?: boolean})

По умолчанию значение subscribe равно false. Установите значение true, чтобы подписаться на обновления значения, предоставленного контекстом.

ContextProvider

ReactiveController, который добавляет поведение поставщика контекста в пользовательский элемент, слушая события context-request.

Импорт:

1
import { ContextProvider } from '@lit/context';

Конструктор:

1
2
3
4
5
6
7
ContextProvider(
  host: ReactiveElement,
  options: {
    context: T,
    initialValue?: ContextType<T>
  }
)

Члены:

  • setValue(v: T, force = false): void

    Устанавливает указанное значение и уведомляет всех подписанных потребителей о новом значении, если оно изменилось. force вызывает уведомление, даже если значение не изменилось, что может быть полезно при глубоком изменении свойств объекта.

ContextConsumer

Реактивный контроллер, который добавляет поведение потребления контекста к пользовательскому элементу, отправляя события context-request.

Импорт:

1
import { ContextConsumer } from '@lit/context';

Конструктор:

1
2
3
4
5
6
7
8
ContextConsumer(
  host: HostElement,
  options: {
    context: C,
    callback?: (value: ContextType<C>, dispose?: () => void) => void,
    subscribe?: boolean = false
  }
)

Члены:

  • value: ContextType<C>

    Текущее значение контекста.

Когда хост-элемент подключается к документу, он испускает событие context-request с ключом контекста. Когда запрос контекста будет удовлетворен, контроллер вызовет обратный вызов, если он присутствует, и запустит обновление хоста, чтобы он мог отреагировать на новое значение.

Он также вызовет метод dispose, предоставленный провайдером, когда элемент хоста будет отключен.

ContextRoot

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

Импорт:

1
import { ContextRoot } from '@lit/context';

Конструктор:

1
ContextRoot();

Члены:

  • attach(element: HTMLElement): void

    Прикрепляет ContextRoot к этому элементу и начинает прослушивать события context-request.

  • detach(element: HTMLElement): void

    Отсоединяет корень ContextRoot от этого элемента, перестает слушать события context-request.

ContextRequestEvent

Событие, запускаемое потребителями для запроса значения контекста. API и поведение этого события определены Context Protocol.

Импорт:

1
import { ContextRequestEvent } from '@lit/context';

context-request всплывает и композируется.

Члены:

  • readonly context: C

    Объект контекста, для которого это событие запрашивает значение

  • readonly callback: ContextCallback<ContextType<C>>

    Функция, которую нужно вызвать для получения значения контекста

  • readonly subscribe?: boolean

    Желает ли потребитель подписаться на новые значения контекста.

ContextCallback

Обратный вызов, который предоставляется запросчиком контекста и вызывается со значением, удовлетворяющим запросу.

Этот обратный вызов может быть вызван несколько раз поставщиками контекста по мере изменения запрашиваемого значения.

Импорт:

1
import { type ContextCallback } from '@lit/context';

Синтаксис:

1
2
3
4
type ContextCallback<ValueType> = (
    value: ValueType,
    unsubscribe?: () => void,
) => void;

Комментарии