angular
March 9, 2024

Понимание принципов Dependency Injection в Angular

Dependency Injection, или DI, являются одной из фундаментальных концепций в Angular. DI встроен во фреймворк и позволяет классам с декораторам конфигурировать необходимые им зависимости (например, Components, Directives, Pipes, and Injectables).

В системе DI существуют две основные роли: потребитель зависимостей (consumer) и поставщик зависимостей (provider).

Angular облегчает взаимодействие между потребителями и поставщиками зависимостей с помощью абстракции, называемой Injector. Когда запрашивается зависимость, инжектор проверяет свой реестр, чтобы увидеть, есть ли уже там доступный экземпляр. Если нет, создается новый экземпляр и сохраняется в реестре. Angular создает Injector для всего приложения (также известный как root injector) во время инициализации приложения, а также любые другие инжекторы по мере необходимости (на уровне модулей или standalone-компонентов). В большинстве случаев вам не нужно вручную создавать инжекторы, но вы должны знать, что существует слой, который соединяет поставщиков и потребителей.

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

Объявление зависимости

Предположим, есть класс с именем HeroService, который должен выступать в качестве зависимости в компоненте. Первый шаг - добавить декоратор @Injectable, чтобы показать, что класс может быть внедрен.

@Injectable()
class HeroService {}

Далее необходимо сделать его доступным в DI, объявив его. Зависимость может быть предоставлена в нескольких местах:

  • На уровне компонента, используя поле providers декоратора @Component. В этом случае HeroService становится доступным для всех экземпляров этого компонента и других компонентов и директив, используемых в шаблоне. Например:
@Component({
  standalone: true,
  selector: 'hero-list',
  template: '...',
  providers: [HeroService]
})
class HeroListComponent {}

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

  • Используйте поле providers объекта ApplicationConfig, переданного функции bootstrapApplication, чтобы объявить службу или другой Injectable на уровне приложения. В этом случае HeroService доступен для всех компонентов, директив и каналов, объявленных в этом NgModule или другом NgModule, который находится в том же ModuleInjector, который применим для этого NgModule. При регистрации провайдера в ApplicationConfig тот же экземпляр сервиса доступен всем применимым компонентам, директивам и пайпам.
  • Для приложений на основе NgModule используйте поле providers декоратора @NgModule, чтобы объявить службу или другой Injectable, доступный на уровне приложения.
// main.ts
export const appConfig: ApplicationConfig = {
    providers: [
      { provide: HeroService },
    ]    
};

bootstrapApplication(AppComponent, appConfig)
  • В корне приложения, что позволит внедрить его в другие классы в приложении. Это можно сделать, добавив поле providedIn: 'root' к декоратору @Injectable:
@Injectable({
  providedIn: 'root'
})
class HeroService {}

Когда вы объявляете сервис в корневом инжекторе, Angular создает единственный, общий экземпляр HeroService и внедряет его в любой класс, который запрашивает его. Регистрация провайдера в метаданных @Injectable также позволяет Angular оптимизировать приложение, удаляя сервис из скомпилированного приложения, если он не используется (tree-shaking).

Подключение зависимости

Наиболее распространенным способом внедрения зависимости является объявление ее в конструкторе класса. Когда Angular создает новый экземпляр компонента, директивы или пайпа, он определяет, какие сервиса или зависимости нужны этому классу, просматривая типы параметров конструктора. Например, если для HeroListComponent требуется HeroService, конструктор может выглядеть следующим образом:

@Component({ … })
class HeroListComponent {
  constructor(private service: HeroService) {}
}

Или более современный вариант, с использованием функции Inject():

@Component({ … })
class HeroListComponent {
  private service = inject(HeroService);
}

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

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