Injectable angular что это
Перейти к содержимому

Injectable angular что это

  • автор:

Dependency Injection в Angular: советы

Dependency Injection (DI) — одна из важнейших концепций в Angular. Это шаблон проектирования, который упрощает создание веб-приложений и ограничивает тесную связь.

Dependency Injection в Angular: советы

Что именно предусматривает DI:

  1. обмен функциональными возможностями между различными компонентами приложения;
  2. упрощение юнит-тестов;
  3. уменьшение потребности создавать экземпляры класса;
  4. облегчения понимания зависимостей определенного класса.

Кроме получения данных, механизм Dependency Injection в Angular позволяет уменьшить связанность компонентов в приложения. В этом материале мы разберемся, как он это делает.

Если вы новичок в Angular или не знаете о концепции Dependency Injection, ознакомьтесь с официальной документацией .

Создание переменных среды

Если вы уже имели дело с Angular, вы, вероятно, знакомы с файлами environment.ts. Из них можно получить информацию о среде, в которой запущено приложение.

Если приложение запущено в среде разработки, мы хотим, чтобы сервисы, которые отвечают за подгрузки данных в приложении, посылали запросы на http://localhost:3000/api. Если же приложение запущено на временном сервере для тестировщиков, мы направляем запросы https://qa.local.com/api. Все это управляется Angular во время процесса сборки с использованием различных файлов среды.

Например, у нас могут быть два файла environment.ts и environment.qa.ts в папке environments, а когда мы запускаем команду ng build — config qa , Angular CLI заменит файл environment.ts на environment.qa.ts, и приложение будет соответственно запущено в режиме для тестировщиков.

Но при чем здесь Dependency Injection?

Посмотрите на такой компонент:

import < Component, OnInit >from ‘@angular/core’;
import < environment >from ‘src/environments/environment’;

@Component( selector: ‘app-some’,
templateUrl: ‘./some.component.html’,
styleUrls: [‘./some.component.css’]
>)
export class SomeComponent implements OnInit

ngOnInit() if (!environment.production) console.log(‘In development environment’) <>
>
>

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

А если мы хотим заинжектить некоторые другие значения для переменных среды тестирования? К тому же у нас есть сервис, который также использует переменные среды:

import < environment >from ‘src/environments/environment’;

@Injectable()
export class SomeService

constructor(
private http: HttpClient,
)

getData(): Observable return this.http.get(environment.baseUrl + ‘/api/method’);
>

На первый взгляд все нормально, но на самом деле это не лучший способ использовать переменные среды.

Представьте ситуацию, что наш сервис является частью приложения, который состоит из нескольких Angular-проектов. Поэтому у нас есть папка projects, которая содержит различные приложения (веб-компоненты, например). Можем ли мы переиспользовать этот сервис в другом проектах?

Теоретически, можем: просто импортируем сервис в другой модуль Angular, используя массив providers . Однако получаем некоторые проблемы: различные проекты могут выполняться в различных средах. Мы не можем просто импортировать одну из них, но нам надо убедиться, что сервис может быть перевыполнен как можно большим количеством компонентов. Здесь нам на помощь приходит DI.

Существует много способов реализовать нужный нам функционал, мы начнем с самого простого — Injection Tokens.

Injection Tokens— концепция Angular, которая позволяет объявлять независимые уникальные токени, чтобы инжектить значение в другие классы по декоратором Inject. Более подробно по ссылке .

Нам нужно просто предсказать значение для нашей среды в модули:

export const ENV = new InjectionToken(‘ENV’);

@NgModule( declarations: [
AppComponent,
SomeComponent
],
imports: [
BrowserModule
],
providers: [

],
bootstrap: [
AppComponent
]
>)
export class AppModule

Как видите, мы определили значение для нашей переменной среды с помощью Injection Token. И теперь использую его в нашем сервисе:

import < Injectable, Inject >from ‘@angular/core’;
import < HttpClient >from ‘@angular/common/http’;
import < Observable >from ‘rxjs’;

import < ENV >from ‘../app.module’;

@Injectable()
export class SomeService

constructor(
private http: HttpClient,
@Inject(ENV) private environment,
)

getData(): Observable return this.http.get(this.environment.baseUrl + »);
>

Обратите внимание, что мы больше не импортируем файл environment.ts . Вместо этого мы берем токен ENVи позволяем приложению самостоятельно решить, какое значение передать в сервис, в зависимости от проекта, где он используется.

Но мы все еще не нашли лучшее решение. Мы не предусмотрели тип для частного поля environment .

Стоит все же заботиться о типах, чтобы наш код был менее уязвимым к неожиданным ошибкам. Angular может использовать типизацию, чтобы улучшить DI. Фактически, мы можем полностью избавиться декоратора Inject и InjectionToken. Для этого мы напишем класс-оболочку, чтобы описать интерфейс нашей переменной среды, а уже потом используем ее в коде. Пример такого класса:

export class Environment production: boolean;
baseUrl: string;
// some other fields maybe
> В этом классе мы описали нашу среду. Однако мы также можем использовать класс InjectionToken для ввода переменной среды. Просто используем useValue как провайдер:

@NgModule( declarations: [
AppComponent,
SomeComponent
],
imports: [
BrowserModule
],
providers: [

],
bootstrap: [
AppComponent
]
>)
export class AppModule

И в нашем сервисе:

@Injectable()
export class SomeService

constructor(
private http: HttpClient,
private environment: Environment,
)

getData(): Observable return this.http.get(this.environment.baseUrl + »);
>
>

Такое решение дает нам не просто более чистый и привычный механизм DI, но и статическую типизацию.

Однако до сих пор есть небольшая проблема. Рассмотрим такой фрагмент:

@Injectable()
export class SomeService

constructor(
private environment: Environment,
)

someMethod() this.environment.baseUrl = ‘something else’;
>

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

export class SomeComponent implements OnInit

constructor(
private someService: SomeService,
private environment: Environment,
)

ngOnInit() this.someService.someMethod();
console.log(this.environment.baseUrl);
>

Поэтому мы «случайно» изменили переменную среды. Исправить такую проблему достаточно просто — сделать поля нашего класса readonly :

export class Environment readonly production: boolean;
readonly baseUrl: string;
// some other fields maybe
>

Так мы получили защищен механизм DI.

Используем различные сервисы в зависимости от среды

Мы уже знаем, как использовать переменные среды через DI, но как насчет переключения между сервисами в различных средах?

Представим ситуацию: нам необходима некоторая статистика о сбоях / использовании нашего приложения. Однако механизм логирования отличается в зависимости от используемой среды: если эта среда разработки, нам надо логировать только ошибку или предупреждение в консоль; в случае среды тестирования, нам необходимо вызвать API, которое сгруппирует наши ошибки в Excel-файл; в продакшене мы хотели бы иметь отдельный файл с логами на бэкенд, поэтому мы вызываем другой API. Лучшим решением будет реализовать сервис Logger , который будет обрабатывать такой функционал. Такой вид наш сервис будет иметь в коде:

@Injectable()
export class LoggerService

constructor(
private environment: Environment,
private http: HttpClient,
)

logError(text: string): void switch (this.environment.name) case ‘development’: console.error(text);
break;
>
case ‘qa’: this.http.post(this.environment.baseUrl + ‘/api/reports’, )
.subscribe(/*handle http errors here */);
break;
>

case ‘production’: this.http.post(this.environment.baseUrl + ‘/api/logs/errors’, )
.subscribe(/* handle http errors here */);
>
>
>
>

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

  • Мы делаем проверки каждый раз при вызове метода logError. По сути, это лишено смысла, ведь после того, как приложение збилдилось, значение enviroment.name никогда не меняется. switch-выражение всегда будет работать одинаково — не важно, сколько раз мы вызвали метод.
  • Реализация самого метода достаточно неуклюжая: на первый взгляд не очень понятно, что происходит.
  • А если нам надо логировать больше различной информации? Для этого нужно делать проверки в каждом методе?

Какие есть альтернативы в нашей реализации? Мы могли бы написать отдельные LoggerServices для каждого сценария, а инжектить только один — в зависимости от условия с помощью factory:

export class LoggerService logError(text: string): void < >
// maybe other methods like logWarning, or info
>

@Injectable()
class DevelopLoggerService implements LoggerService logError(text: string) console.error(text);
>
>

@Injectable()
class QALoggerService implements LoggerService

constructor(
private http: HttpClient,
private environment: Environment,
) <>

logError(text: string) this.http.post(this.environment.baseUrl + ‘/api/reports’, )
>
>

@Injectable()
class ProdLoggerService implements LoggerService

constructor(
private http: HttpClient,
private environment: Environment,
) <>

logError(text: string) this.http.post(this.environment.baseUrl + ‘/api/logs/errors’, )
>
>

Пройдемся тем, что мы реализовали:

  • Мы оставляем от LoggerService только объявления его API, без реализации. Использовать его как injection token, а также, чтобы указать интерфейс для TS.
  • Создаем отдельные классы для каждой среды, убеждаемся, что реализовали LoggerService для каждого, чтобы у нас был одинаковый API.
  • Каждый класс имеет те же методы, однако с разной логикой. Нет нужды проверять среду.
  • Как теперь сообщить нашим компонентам, какую версию LoggerService мы используем? Здесь на помощь приходят factories.

factory- чистая функция, которая получает зависимости в качестве аргументов и возвращает значение для токена. Посмотрим, как использовать наш LoggerService:

export function loggerFactory(environment: Environment, http: HttpClient): LoggerService switch (environment.name) case ‘develop’: return new DevelopLoggerService();
>
case ‘qa’: return new QALoggerService(http, environment);
>
case ‘prod’: return new ProdLoggerService(http, environment);
>
>
>

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

Конечно, на этом наша реализация не заканчивается: нам до сих пор нужно сообщить Angular об использовании factory и передать все необходимые зависимости через массив deps .

@NgModule( providers: [
provide: LoggerService,
useFactory: loggerFactory,
deps: [HttpClient, Environment], // we tell Angular to provide this dependencies to the factory as arguments
>,

],
// other metadata
>)
export class AppModule

С таким решением не нужно что-то менять в нашем приложении: все компоненты, которые использовали LoggerService, продолжат это делать, как и с предыдущей реализацией:

export class SomeComponent implements OnInit

constructor(
private logger: LoggerService,
)

ngOnInit() try // do something that may throw an error
> catch (error) this.logger.logError(error.message); // no need to change anything — works the same wy as previously
>
>

Создание глобальных одиночек (singletons)

Angular убеждается, что внутри заданного модуля все компоненты получают одинаковые экземпляры зависимостей. Например, если мы используем сервис в AppModule, затем объявляем SomeCompoоnent в модуле, а затем вводим туда SomeService, а также в AnotherComponent, объявленный в том же модуле, то SomeComponent и OtherComponent получат тот же экземпляр SomeService.

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

А если мы хотим тот же экземпляр для каждого компонента, сервиса или чего-либо, что использует наши зависимости? Тогда нам точно нужен singleton.

Мы можем реализовать singleton, использовав useFactory. Сначала нам надо будет реализовать статический метод getInstance для нашего класса, а затем вызвать его с factory.

Допустим, нам надо реализовать простое хранилище данных во время выполнения программы, вроде базового redux. Код реализации метода getInstance :

export class StoreService

// maybe store methods like dispatch, subscribe or others

private static instance: StoreService = null;
static getInstance(): StoreService if (!StoreService.instance) StoreService.instance = new StoreService();
>

>
Теперь мы должны сообщить Angular о нашем методе getInstance

export function storeFactory(): StoreService return StoreService.getInstance();
>

На этом все. Теперь мы можем просто инжектить StoreService в любой компонент и быть уверены, что имеем дело с тем же экземпляром. Мы можем передавать зависимости в factory, как обычно.

Общие советы по DI

  1. Всегда инжектите значение в ваш компонент — никогда не полагайтесь на глобальные переменные, переменные из других файлов и тому подобное. Стоит помнить, если метод вашего класса ссылается на свойства, которые не принадлежат этому классу, такое значение, скорее всего, инжектиться как зависимость (как мы делали с переменными среды).
  2. Никогда не используйте строчные токены для DI. В Angular есть возможность передать строку в декоратор Inject для поиска зависимостей. Однако вы всегда можете допустить ошибку, даже если у вас есть IntelliSense . Лучше использовать InjectionToken .
  3. Помните, что экземпляры сервисов распространяются между компонентами на уровне модуля. Если какие-либо свойства такого сервиса не будут меняться внешне, их можно обозначить как readonly.

Зарегистрируйтесь на Портале

и получите красивый адрес своей странички вида: senior.ua/sergey.ivanov

icon

senior.ua/ |

Потом все адреса будут Заняты 🙁

Injectable

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

Определяет, какие форсунки будут обеспечивать injectable.

See also

  • Введение в службы и DI
  • Руководство по внедрению зависимостей

Options

providedIn

Определяет, какие форсунки будут обеспечивать injectable.

providedIn?: Type | ‘root’ | ‘platform’ | ‘any’ | null

  • Type— связывает injectable с @NgModule или другим InjectorType . Этот вариант DEPRECATED.
  • «null»: эквивалентно undefined . injectable не предоставляется ни в какой области автоматически и должен быть добавлен в массив providers из @NgModule , @Component или @Directive .

Следующие параметры указывают, что этот injectable должен быть установлен в одной из следующих форсунок:

  • ‘root’ : Инжектор уровня приложения в большинстве приложений.
  • ‘platform’: специальный одноэлементный инжектор платформы, используемый всеми приложениями на странице.
  • ‘any’ : Предоставляет уникальный экземпляр в каждом лениво загружаемом модуле, в то время как все активно загружаемые модули используют один экземпляр. Этот вариант DEPRECATED.

Usage notes

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

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

@Injectable() class UsefulService < >@Injectable() class NeedsService < constructor(public service: UsefulService) <> > const injector = Injector.create(< providers: [, ] >); expect(injector.get(NeedsService).service instanceof UsefulService).toBe(true);

Angular dependency injection

Определяется объектом ключ-значение и описывает как зависимость будет предоставлена.

Провайдер класса

Провайдер переменной или константы

Зависмость от сущности, не являющейся классом. Вместо «прибитой» константы:

 from '@angular/core'; . const sToken = new InjectionToken('блюдо');

Тогда запись зависимости выглядит следующим образом:

Провайдер «фабрика»

Используется для случая получения сущности по условию.

  < if (условие) < return new UserServiceA(); >else < return new UserServiceB(); >> >]

Множественные провайдеры

Для случая, когда вместо одной зависимости требуется массив используется multi: true . При этом значение не перезаписывают массив, а добавляют в него значения.

, < provide: sToken, useValue: 'Печенька', multi: true >] . export class AppComponent implements OnInit < constructor( @Inject(sToken) private _multiProvider, ) <>ngOnInit() < console.log(this._multiProvider); // выведет ["Пирожок", "Печенька"] >>

Injector

Механизм разрешающий зависимости и создающий сущности

Инжектор — объект в системе Angular, который может найти зависимость по ключу в своем кеше или создать зависимость с использованием настроенного провайдера (Provider). Инжекторы создаются для NgModules автоматически как часть процесса начальной загрузки и наследуются через иерархию компонентов.

Работа с инжектором вручную.

]); const cookie = injector.get(sToken); const _uService = injector.get(UserService);

Dependency

Обычно класс или константа. Тут всё просто — сущность, которую необходимо внедрить.

Зависимость Angular достаёт из декораторов @Component , @Pipe , @Directive , @Inject , @Injectable .

@Inject

Наиболее известна запись зависимости:

  • https://www.youtube.com/watch?v=fALKYP8voBQ
  • http://go.urish.org/trees
  • https://offering.solutions/blog/articles/2018/08/17/using-useclass-usefactory-usevalue-useexisting-with-treeshakable-providers-in-angular/#usefactory

Dependency injection

Вполне вероятна ситуация, когда мы захотим использовать один сервис в другом сервисе. Например, в прошлой теме был создан сервис для работы с данными. Что если нам необходимо логгировать все операции с данными. Для логгирования определим новый сервис. Для этого добавим в папку app новый файл log.service.ts со следующим содержимым:

export class LoggerService < public write(logMessage: string) < console.log(logMessage); > > 

Для логгирования в сервисе определен метод write , который выводит некоторое сообщение на консоль.

Теперь используем этот сервис. Для этого изменим код в файле data.service.ts :

@Injectable() export class DataService < private _data: Todo[] = [< title: "Todo 1" >, < title: "Todo 2" >, < title: "Todo 2" > ]; constructor(private logService: LogService)<> getData() < this.logService.write("Get Data"); return this._data; > addData(title: string) < this.logService.write("Add Data"); this.data.push(< title >); > > 

Чтобы указать, что сервис сам может использовать другие сервисы, к классу сервиса применяется декоратор @Injectable() . Если класс не будет иметь подобного декоратора, то встроенный механизм внедрения зависимостей не сможет создать объект этого класса и выдаст ошибку.

Существует общая рекомендации от разработчиков Angular применять декоратор @Injectable() к любому классу сервиса, хотя в некоторых ситуациях он не нужен.

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

И также как в случае с DataService, сервис LogService также надо зарегистрировать в списке провайдеров TodoListComponent :

providers: [DataService, LogService] 

Dependency injection — это способ предоставить новый экземпляр класса с полностью сформированными зависимостями, которые он требует. Большинство зависимостей — это сервисы. Angular использует dependency injection для обеспечения новых компонентов необходимыми сервисами.

Angular может определить, какие сервисы нужны компоненту, глядя на типы параметров его конструктора. Например, конструктору вашего TodoListComponent необходим сервис DataService :

constructor(private dataService: DataService)

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

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

Если injector не имеет нужного сервиса, как он знает, как его создать?

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

Мы можете регистрировать provider в компонентах (как делали это выше), то также и в модулях.

Регистрация на уровне компонента означает, что вы получаете новый экземпляр сервиса с каждым новым экземпляром этого компонента.

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

results matching » «

No results matching » «

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *