Паттерны для автономных API¶
Вместе с автономными компонентами команда Angular представила автономные API. Они позволяют настраивать библиотеки более легким способом. Примерами библиотек, предоставляющих Standalone API, на данный момент являются HttpClient
и Router
. Также ранним последователем этой идеи является NGRX.
В этой главе я представляю несколько паттернов для написания собственных Standalone API, взятых из вышеупомянутых библиотек. Для каждого паттерна обсуждаются следующие аспекты: намерения, лежащие в основе паттерна, описание, пример реализации, примеры, встречающиеся в упомянутых библиотеках, и вариации деталей реализации.
Большинство из этих паттернов особенно интересны для авторов библиотек. Они способны улучшить DX для потребителей библиотеки. С другой стороны, большинство из них могут оказаться излишними для приложений.
Исходный код
Пример для паттернов¶
Для представления выведенных закономерностей используется простая библиотека логгеров. Эта библиотека настолько проста, насколько это возможно, но настолько сложна, насколько это необходимо для демонстрации реализации паттернов.
Каждое сообщение журнала имеет LogLevel
, определяемый перечислением:
1 2 3 4 5 |
|
Для простоты мы ограничим нашу библиотеку Logger только тремя уровнями журнала.
Абстрактный LoggerConfig
определяет возможные параметры конфигурации:
1 2 3 4 5 |
|
Это абстрактный класс, так как интерфейсы не могут быть использованы в качестве маркеров для DI. Константа этого класса определяет значения по умолчанию для параметров конфигурации:
1 2 3 4 5 |
|
Форматтер LogFormatter
используется для форматирования сообщений журнала перед их публикацией через LogAppender
:
1 2 3 4 5 6 7 |
|
Как и LoggerConfiguration
, LogFormatter
— это абстрактный класс, используемый в качестве маркера. Потребитель библиотеки логгеров может настроить форматирование, предоставив свою собственную реализацию. В качестве альтернативы они могут использовать реализацию по умолчанию, предоставляемую библиотекой:
1 2 3 4 5 6 7 8 9 10 11 |
|
LogAppender
— это еще одна сменная концепция, отвечающая за добавление сообщения журнала в журнал:
1 2 3 4 5 6 7 |
|
Реализация по умолчанию записывает сообщение в консоль:
1 2 3 4 5 6 7 8 9 10 |
|
Хотя может быть только один LogFormatter
, библиотека поддерживает несколько LogAppender
. Например, первый LogAppender
может записывать сообщение в консоль, а второй — отправлять его на сервер.
Чтобы сделать это возможным, отдельные LogAppender
ы регистрируются через мультипровайдеров. Поэтому инжектор возвращает их все в массиве. Поскольку массив не может быть использован в качестве DI-токена, в примере вместо него используется InjectionToken
:
1 2 3 |
|
Сам LoggserService
получает LoggerConfig
, LogFormatter
и массив с LogAppenders
через DI и позволяет регистрировать сообщения для нескольких LogLevels
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Золотое правило¶
Прежде чем приступить к представлению выведенных паттернов, я хочу подчеркнуть то, что я называю золотым правилом предоставления сервисов:
Всегда, когда это возможно, используйте
@Injectable({providedIn: 'root'})
!
Особенно в коде приложений, но в некоторых ситуациях и в библиотеках, это то, что вы хотите иметь: Это легко, древовидно и даже работает с ленивой загрузкой. Последний аспект — заслуга не столько Angular, сколько лежащего в его основе бандлера: Все, что просто необходимо в ленивом бандле, помещено туда.
Паттерн: Фабрика провайдеров¶
Намерения
- Предоставление сервисов для многократно используемой библиотеки
- Конфигурирование многократно используемой библиотеки
- Обмен определенными деталями реализации
Описание
Фабрика провайдеров — это функция, возвращающая массив с провайдерами для заданной библиотеки. Этот массив преобразуется в тип Angular EnvironmentProviders
, чтобы убедиться, что провайдеры могут быть использованы только в области видимости окружения — в первую очередь, в корневой области видимости и области видимости, введенной с помощью конфигураций ленивой маршрутизации.
Angular и NGRX размещают такие функции в файлах под названием provider.ts
.
Пример
Следующая функция провайдера provideLogger
принимает частичную конфигурацию LoggerConfiguration
и использует ее для создания некоторых провайдеров:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Недостающие значения конфигурации берутся из конфигурации по умолчанию. Angular's makeEnvironmentProviders
оборачивает массив Provider
в экземпляр EnvironmentProviders
.
Эта функция позволяет потребляющему приложению настроить логгер во время загрузки, как и другие библиотеки, например, HttpClient
или Router
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Случаи и вариации
- Это обычный паттерн, используемый во всех рассмотренных библиотеках.
- Фабрики провайдеров для
Router
иHttpClient
имеют второй необязательный параметр, который принимает дополнительные возможности (см. паттерн Feature, ниже). - Вместо передачи конкретной реализации сервиса, например, LogFormatter, NGRX позволяет принимать либо токен, либо конкретный объект для редукторов.
- В
HttpClient
передается массив с функциональными перехватчиками через функциюwith
(см. паттерн Feature, ниже). Эти функции также регистрируются как сервисы.
Паттерн: Функция¶
Намерения
- Активация и настройка дополнительных функций
- Сделать эти функции изменяемыми в дереве
- Предоставление базовых сервисов через текущее окружение
Описание
Фабрика провайдеров принимает необязательный массив с объектом функции. Каждый объект функции имеет идентификатор kind
и массив providers
. Свойство kind
позволяет проверить комбинацию передаваемых функций. Например, могут быть взаимоисключающие функции, такие как настройка обработки токенов XSRF и отключение обработки токенов XSRF для HttpClient
.
Пример
В нашем примере используется функция цвета, которая позволяет отображать сообщения разных LoggerLevel
разными цветами:
Для категоризации функций используется перечисление:
1 2 3 4 5 |
|
Каждый признак представлен объектом LoggerFeature
:
1 2 3 4 |
|
Для предоставления цветовой характеристики вводится фабричная функция, следующая шаблону именования Feature:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Фабрика провайдеров принимает несколько функций через необязательный второй параметр, заданный в виде массива rest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Свойство kind
функции используется для проверки и подтверждения переданных функций. Если все в порядке, провайдеры, найденные в характеристике, помещаются в возвращаемый объект EnvironmentProviders
.
В результате инъекции зависимостей DefaultLogAppender
получает ColorService
, предоставляемый функцией цвета:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Поскольку функции являются необязательными, DefaultLogAppender
передает optional: true
в inject
. В противном случае мы получили бы исключение, если бы функция не была применена. Кроме того, DefaultLogAppender
должен проверять значения null
.
Случаи и вариации
- Маршрутизатор
Router
использует его, например, для настройки предварительной загрузки или для активации трассировки отладки. HttpClient
использует его, например, для предоставления перехватчиков, настройки JSONP и настройки/отключения обработки токенов XSRF.- И
Router
, иHttpClient
объединяют возможные возможности в союзный тип (например,export type AllowedFeatures = ThisFeature | ThatFeature
). Это помогает IDE предлагать встроенные функции. - Некоторые реализации инжектируют текущий
Injector
и используют его, чтобы узнать, какие функции были настроены. Это императивная альтернатива использованиюoptional: true
. - В реализациях функций Angular свойства
kind
иproviders
префиксируютсяɵ
и, следовательно, объявляются как внутренние свойства.
Паттерн: Фабрика поставщиков конфигурации¶
Намерения
- Конфигурирование существующих сервисов
- Предоставление дополнительных сервисов и их регистрация в существующих сервисах
- Расширение поведения сервиса из вложенного окружения.
Описание
Фабрики поставщиков конфигурации расширяют поведение существующей службы. Они могут предоставлять дополнительные сервисы и использовать ENVIRONMENT_INITIALIZER
для получения экземпляров как предоставляемых сервисов, так и существующих сервисов для расширения.
Пример
Предположим, что расширенная версия нашего LoggerService
позволяет определять дополнительный LogAppender
для каждой категории журналов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Чтобы сконфигурировать LogAppender
для категории, мы можем ввести еще одну фабрику провайдеров:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Эта фабрика создает провайдера для класса LogAppender
. Однако нам нужен не сам класс, а его экземпляр. Также нам нужен Injector
для разрешения зависимостей этого экземпляра. И то, и другое происходит при получении LogAppender
через инжектор.
Именно это и делает ENVIRONMENT_INITIALIZER
, который является мультипровайдером, привязанным к токену ENVIRONMENT_INITIALIZER
и указывающим на функцию. Она получает инжектированный LogAppender
, а также LoggerService
. Затем LogAppender
регистрируется в логгере.
Это позволяет расширить существующий LoggerService
, который может даже исходить из родительской области видимости. Например, в следующем примере предполагается, что LoggerService
находится в корневой области видимости, а дополнительная категория журнала устанавливается в области видимости ленивого маршрута:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Случаи и вариации
@ngrx/store
использует этот паттерн для регистрации фрагментов функций@ngrx/effects
использует этот паттерн для подключения эффектов, предоставляемых функцией.- Функция
withDebugTracing
использует этот паттерн, чтобы подписаться на наблюдаемуюevents
маршрутизатора.
Паттерн: NgModule Bridge¶
Намерения
- Не ломать существующий код, использующий
NgModules
при переходе на Standalone API. - Позволяет таким частям приложения устанавливать
EnvironmentProviders
, которые приходят из Provider Factory.
Замечания: Для нового кода этот паттерн кажется излишним, поскольку фабрика провайдеров может быть вызвана напрямую для потребляющих (унаследованных) NgModules.
Описание
Мост NgModule Bridge — это NgModule, получающий (некоторые) свои провайдеры через фабрику провайдеров (см. паттерн Фабрика провайдеров). Чтобы дать вызывающему модулю больше контроля над предоставляемыми услугами, можно использовать статические методы типа forRoot
. Эти методы могут принимать объект конфигурации.
Пример
Следующий NgModules
позволяет настроить логгер традиционным способом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Чтобы избежать повторной реализации фабрик провайдеров, методы модуля делегируются им. Поскольку использование таких методов является обычным делом при работе с NgModules, потребители могут использовать существующие знания и соглашения.
Случаи и вариации
- Все рассмотренные библиотеки используют этот паттерн для сохранения обратной совместимости
Паттерн: Цепочка сервисов¶
Намерения
- Делегирование сервиса другому своему экземпляру в родительской области видимости.
Описание
Когда один и тот же сервис размещается в нескольких вложенных инжекторах окружения, мы обычно получаем только экземпляр сервиса в текущей области видимости. Таким образом, вызов сервиса во вложенной области видимости не будет выполняться в родительской области видимости. Чтобы обойти эту проблему, сервис может найти экземпляр самого себя в родительской области видимости и делегировать его.
Пример
Предположим, что мы снова предоставляем библиотеку logger для ленивого маршрута:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Это устанавливает другой набор сервисов Logger в инжекторе окружения этого ленивого маршрута и его дочерних элементов. Эти сервисы являются тенью своих аналогов в корневой области видимости. Таким образом, когда компонент в ленивой области видимости вызывает LoggerService
, сервисы в корневой области видимости не срабатывают.
Чтобы предотвратить это, мы можем получить LoggerService
из родительской области видимости. Точнее, это не родительский scope, а "ближайший предковый scope", предоставляющий LoggerService
. После этого служба может делегировать полномочия своему родителю. Таким образом, сервисы связываются в цепочку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
При использовании inject для получения родительского LoggerService
, нам нужно передать optional: true
, чтобы избежать исключения, если не найдется ни одной области-предка с LoggerService
. Передача skipSelf: true
гарантирует, что поиск будет производиться только в областях-предшественниках. В противном случае Angular начнет поиск с текущего диапазона и получит вызывающий сервис самостоятельно.
Кроме того, пример, показанный здесь, позволяет активировать/деактивировать это поведение с помощью нового флага chaining
в LoggerConfiguration
.
Случаи и вариации
- В
HttpClient
этот паттерн используется также для запускаHttpInterceptors
в родительских диапазонах. Более подробно о цепочке HttpInterceptors можно прочитать здесь. Здесь поведение цепочки может быть активировано с помощью отдельной функции. Технически, эта функция регистрирует другой перехватчик, делегирующий функции в родительской области видимости.
Паттерн: Функциональный сервис¶
Намерения
- Сделать использование библиотек более легким за счет использования функций в качестве сервисов
- Сокращение непрямых связей за счет использования специальных функций.
Описание
Вместо того, чтобы заставлять потребителя реализовывать сервис на основе класса, следуя заданному интерфейсу, библиотека также принимает функции. Внутри библиотеки они могут быть зарегистрированы как сервис с помощью useValue
.
Пример
В этом примере потребитель может напрямую передать функцию, действующую как LogFormatter
, в provideLogger
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Для этого в логгере используется тип LogFormatFn
, определяющий сигнатуру функции:
1 2 3 4 5 |
|
Также, поскольку функции не могут использоваться в качестве токенов, вводится InjectionToken
:
1 2 3 |
|
Этот InjectionToken
поддерживает как основанные на классах LogFormatter
, так и функциональные. Это позволяет не ломать существующий код. Как следствие поддержки обоих вариантов, provideLogger
должен обрабатывать оба случая немного по-разному:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
В то время как сервисы, основанные на классах, регистрируются с помощью useClass
, для их функциональных аналогов подходит useValue
.
Кроме того, потребители LogFormatter
должны быть готовы как к функциональному, так и к классовому подходу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
Случаи и вариации
HttpClient
позволяет использовать функциональные перехватчики. Они регистрируются через функцию (см. паттерн Feature).- Маршрутизатор
Router
позволяет использовать функции для реализации охранников и резолверов.
Заключение¶
Фабрики провайдеров — это простые функции, возвращающие массив с провайдерами. Они используются для получения всех провайдеров, необходимых для настройки подсистемы или библиотеки. По условию, такие фабрики имеют шаблон именования privateXY
.
Фабрика провайдеров может принимать объект конфигурации и необязательные функции. Опциональная функция — это другая функция, возвращающая все провайдеры, необходимые для данной функции. Их имена следуют шаблону именования withXYZ
.
Для подключения сервисов можно использовать ENVIRONMENT_INITIALIZER
, а использование inject вместе с такими параметрами, как optional
и skipSelf
, позволяет создать цепочку с другим экземпляром того же сервиса в родительской области видимости.