Статьи

Компоненты и поставщики Angular 2: классы, фабрики и ценности

В предыдущей статье мы рассмотрели, как получать данные в компоненты и из них с помощью аннотаций @Input и @Output . В этой статье мы рассмотрим еще один фундаментальный аспект компонентов Angular 2 — их способность использовать провайдеров .

Возможно, вы видели «провайдеров» в списке свойств, которые вы можете использовать для настройки компонентов, и вы, возможно, поняли, что они позволяют вам определять набор инъецируемых объектов, которые будут доступны для компонента. Это хорошо, но, конечно, возникает вопрос: «Кто такой поставщик?»

Ответ на этот вопрос вовлекает нас в активное обсуждение системы Angular 2 Dependency Injection (DI). Мы можем специально рассказать о DI в будущем сообщении в блоге, но это хорошо освещено в серии статей Паскаля Прехта, начиная с: http://blog.thoughtram.io/angular/2015/05/18/dependency-injection-in -angular-2.html . Мы предполагаем, что вы знакомы с DI и системой DI Angular 2 в целом, как описано в статье Паскаля, но вкратце система DI отвечает за:

  • Регистрация класса, функции или значения. Эти элементы в контексте внедрения зависимостей называются «поставщиками», потому что они что-то приводят. Например, класс используется, чтобы предоставить или привести к экземпляру. (Подробнее о типах провайдеров см. Ниже).
  • Разрешение зависимостей между поставщиками — например, если один поставщик требует другого поставщика.
  • Сделать результат провайдера доступным в коде, когда мы его попросим. Этот процесс предоставления результата провайдера доступному блоку кода называется «инъекцией». Код, который внедряет результаты провайдера, достаточно логически называется «инжектором».
  • Поддержание иерархии инжекторов, чтобы, если компонент запрашивает результат поставщика из-за того, что поставщик недоступен в своем инжекторе, DI ищет иерархию инжекторов.

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

Иерархия компонентов с инжекторами и ресурсами

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

Из вышесказанного видно, что, хотя компоненты образуют направленный вниз граф, связанные с ними инжекторы имеют двустороннюю связь: родительские инжекторы создают дочерние элементы (вниз), а когда запрашивается поставщик, Angular 2 ищет родительский инжектор (вверх), если он не удается найти запрошенный провайдер в собственном инжекторе компонента. Это означает, что поставщик с тем же идентификатором на более низком уровне будет скрывать (скрывать) провайдера с таким же именем на более высоком уровне.

Кто такие провайдеры?

Итак, что это за «провайдеры», которых инжекторы регистрируют на каждом уровне? На самом деле все просто: провайдер — это ресурс или «вещь» JavaScript, которую Angular использует для предоставления (создания, создания) того, что мы хотим использовать:

  • Поставщик класса генерирует / предоставляет экземпляр класса.
  • Поставщик фабрики генерирует / предоставляет все, что возвращается, когда вы запускаете указанную функцию.
  • Провайдеру значений не нужно предпринимать действия для предоставления результата, как предыдущие два, он просто возвращает свое значение.

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

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

Простой класс с четырьмя свойствами

Рисунок 2: Простой класс с четырьмя свойствами. (Скриншоты кода взяты из кода Visual Studio)

Хорошо, это класс. Теперь давайте проинструктируем Angular использовать его для регистрации поставщика классов, чтобы мы могли попросить систему внедрения зависимостей дать нам экземпляр для использования в нашем коде. Мы создадим компонент ProvDemo_01.ts , который будет служить корневым компонентом для нашего приложения. Мы загружаем этот компонент и запускаем наше приложение в bootstrap.ts :

Файл bootstrap.ts нашего приложения, который создает экземпляр корневого компонента

Рисунок 3: Файл bootstrap.ts нашего приложения, который создает экземпляр корневого компонента.

Если вышеприведенное не имеет смысла, взгляните на наш предыдущий пост, в котором рассказывается о создании простого приложения Angular 2. Наш корневой компонент называется ProvDemo , и репозиторий содержит несколько его версий. Вы можете изменить отображаемую версию, обновив строку, которая импортирует ProvDemo выше. Наша первая версия корневого компонента выглядит так:

CompDemo с MyClass импортируется

Рисунок 4: CompDemo с MyClass импортирован, добавлен в массив провайдеров и используется в качестве типа в аргументах конструктора.

MyClass провайдера MyClass к этому компоненту просто:

  • Импортировать MyClass
  • Добавьте его в свойство поставщиков @Component
  • Добавьте аргумент типа «MyClass» в конструктор.

Под прикрытием, когда Angular создает компонент, система DI создает инжектор для компонента, который регистрирует провайдера MyClass . Затем Angular видит тип MyClass указанный в списке аргументов конструктора, и ищет вновь зарегистрированного поставщика MyClass и использует его для генерации экземпляра, который он назначает «myClass» (начальный маленький «m»).

Процесс поиска провайдера MyClass и генерации экземпляра для назначения «myClass» — все это Angular. Синтаксис TypeScript использует преимущества, чтобы узнать, какой тип искать, но инжектор Angular выполняет поиск и возвращает экземпляр MyClass .

Учитывая вышесказанное, вы можете заключить, что Angular берет список классов в массиве «provider» и создает простой реестр, используемый для извлечения класса. Но есть небольшой поворот, чтобы сделать вещи более гибкими. Основная причина, по которой требуется «поворот», заключается в том, чтобы помочь нам написать модульные тесты для наших компонентов, у которых есть поставщики, которых мы не хотим использовать в среде тестирования. В случае с MyClass нет особых причин не использовать реальную вещь, но если MyClass позвонит на сервер для получения данных, мы можем не захотеть или не сможем сделать это в тестовой среде. Чтобы обойти это, мы должны иметь возможность заменить в ProvDemo фиктивный MyClass , который не выполняет серверный вызов.

Как мы делаем замену? MyClassMock ли мы весь наш код и меняем каждую ссылку MyClass на MyClassMock ? Это неэффективно и плохо подходит для написания тестов.

Нам нужно поменять реализацию провайдера, не меняя ProvDemo компонента ProvDemo . Чтобы сделать это возможным, когда Angular регистрирует провайдера, он устанавливает карту, чтобы связать ключ (называемый «токеном») с фактическим провайдером. В нашем примере выше токен и поставщик — это одно и то же: MyClass . Добавление MyClass к свойству provider в декораторе @Component является сокращением для:

 providers: [ provide(MyClass, {useClass: MyClass} ] 

Это говорит: «зарегистрируйте провайдера, используя« MyClass »в качестве токена (ключа), чтобы найти провайдера и установить провайдер в MyClass поэтому, когда мы запрашиваем провайдера, система внедрения зависимостей возвращает экземпляр MyClass ». Большинство из нас привыкли думать ключей как числа или строки. Но в этом случае токен (ключ) — это сам класс. Мы могли бы также зарегистрировать провайдера, используя строку для токена следующим образом:

 providers: [ provide(“aStringNameForMyClass”, {useClass: MyClass} ] 

Итак, как это поможет нам с тестированием? Это означает, что в тестовой среде мы можем переопределить регистрацию провайдера, эффективно выполняя:

 provide(MyClass, {useClass: MyClassMock}) 

Это связывает токен (ключ) MyClass с поставщиком классов MyClassMock . Когда наш код попросил систему DI ввести MyClass при тестировании, мы получили экземпляр MyClassMock который может MyClassMock вызов данных. В результате весь наш код остается неизменным, и нам не нужно беспокоиться о том, будет ли модульный тест вызывать сервер, который может не существовать в тестовой среде.

Внедрение неклассных провайдеров

Выше мы ввели наш экземпляр поставщика классов в конструктор, написав:

 constructor( myClass: MyClass ) {...} 

TypeScript позволяет нам указать, что аргумент myClass должен иметь тип MyClass, а система DI выполняет всю работу, чтобы предоставить нам экземпляр MyClass.

Но как мы скажем Angular вводить результат нашего провайдера, если мы используем строковый токен вместо класса? Давайте отредактируем наш файл bootstrap.ts чтобы добавить нового поставщика значений и зарегистрировать его с помощью строкового токена. Помните, что провайдеры значения — это тип провайдера, который возвращает значение, связанное с токеном. В приведенном выше примере мы сказали Angular зарегистрировать провайдера, добавив его в свойство @Component provider, но мы также можем зарегистрировать провайдеров, передав их в функцию начальной загрузки следующим образом (то же самое можно добавить в свойство provider):

bootstrap.ts с добавленным провайдером

Рисунок 5: bootstrap.ts с добавленным поставщиком значений.

Здесь мы добавили провайдера, вызвав функцию provide и передав ему строковый токен («SECURITY_KEY») и объект, который указывает, что мы хотим создать провайдера значений и сам провайдер — в данном случае простое значение. Теперь мы хотели бы вставить значение, сгенерированное поставщиком значений, в наш конструктор, но это не сработает…

 constructor( SECKEY:SECURITY_KEY) {...} 

Это потому, что «SECURITY_KEY» не является типом. Чтобы сделать возможным ввод поставщиков с токенами, не относящимися к классу, Angular предоставляет нам декоратор параметров @Inject. Как и во всех других декораторах, нам нужно импортировать его, а затем мы используем его, чтобы сказать Angular вводить провайдера, связанного с нашим строковым токеном. Для этого мы настраиваем create ProvDemo\_02.ts :

Импорт декоратора «Впрыск» и использование его для внедрения провайдера значений

Рисунок 6: Импорт декоратора Inject и использование его для внедрения поставщика значений, идентифицированного с помощью строкового токена.

Мы могли бы использовать тот же синтаксис для внедрения провайдера MyClass :

 constructor( @Inject(MyClass) myClass, @Inject('SECURITY_KEY') SECKEY ) {...} 

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

Провайдеры и синглтоны

Как мы видели выше, поставщики несут ответственность за создание вещи, которая вводится. Поставщик класса генерирует экземпляр, и экземпляр внедряется. Но важно понимать, что вы не получаете новый экземпляр каждый раз, когда вводится результат поставщика класса. Вместо этого система DI генерирует экземпляр один раз, кэширует его, и каждая последующая инъекция получает один и тот же экземпляр, если вы используете один и тот же поставщик.

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

ChildComp с MyClass внедрен в конструктор

Рисунок 7: ChildComp с MyClass, внедренный в конструктор.

Обратите внимание, что мы импортируем MyClass и используем его для установки типа в списке аргументов конструктора. Важное ChildComp : Единственная цель, которую импортированный MyClass выполняет в ChildComp — это токен, используемый системой DI, для поиска зарегистрированного поставщика. Поскольку у ChildComp нет собственного провайдера, зарегистрированного с использованием этого токена, Angular ищет иерархию инжекторов, чтобы найти ее. Чтобы это работало, нам нужно добавить ChildComp в компонент ProvDemo :

ProvDemo с ChildComp добавлен в шаблон

Рисунок 8: ProvDemo с ChildComp, добавленным в шаблон.

Мы импортируем ChildComp , добавляем свойство директив в @Component, чтобы сообщить ProvDemo что мы собираемся использовать компонент ChildComp и добавить элемент ChildComp в шаблон. Когда приложение запускается, вывод консоли показывает, что и ProvDemo и ChildComp получают ProvDemo и тот же экземпляр MyClass :

 ProvDemomyClass 1453033148406 390 ChildCompmyClass 1453033148406 390 

Теперь давайте изменим ChildComp чтобы добавить провайдера MyClass в свой инжектор:

ParentComp с определенным поставщиком MyClass

Рисунок 9: ParentComp с определенным собственным провайдером MyClass.

Все, что мы изменили, — это добавление свойства провайдеров в аннотацию @Component. И, конечно же, мы видим, что созданы два разных экземпляра MyClass :

 ProvDemomyClass 1453033681877 263 ChildCompmyClass 1453033681881 761 

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

Заворачивать

В этой статье мы определили, что такое поставщик, и рассмотрели три различных типа поставщиков. Затем мы рассмотрели, как вы можете зарегистрировать провайдера для компонента и вставить результат, сгенерированный провайдером, в компонент. Мы также взглянули на то, как Angular использует иерархию инжекторов для поиска запрошенного поставщика. Angular дает вам дополнительный контроль над тем, как работает система внедрения зависимостей и где она ищет поставщиков, но вышеперечисленное должно помочь вам начать создавать и работать с поставщиками в ваших приложениях Angular 2.