Как система внедрения зависимостей Angular решает проблему обращения к сервисам с помощью inject()
Понять, как Angular DI предоставляет сервисы с помощью функции inject(), элементов providers и иерархических инжекторов, а также как ограничивать область действия экземпляров и избегать ошибок, связанных с отсутствием провайдеров.
Источники: Angular 09: Dependency Injection in Angular
Как только вы начинаете перемещать логику из компонентов Angular в сервисы, возникает практический вопрос: когда компонент выполняет строку inject(UserService), откуда на самом деле берется этот объект, кто его создал и какой экземпляр получает компонент? Ответ — система внедрения зависимостей (DI) Angular. В этом руководстве рассматривается, что такое DI, как работают функции inject() и провайдеры, как иерархические инжекторы определяют экземпляр, который будет использован, и как избежать наиболее распространенных ошибок начинающих, чтобы вы могли уверенно делиться сервисами или ограничивать их доступ в собственных приложениях.
Вот строка, которая обычно вызывает этот вопрос:
userService = inject(UserService);
Ничто внутри компонента не создает объект UserService, однако поле в нем содержит готовый к использованию объект. Его предоставила система внедрения зависимостей Angular.
Почему компоненты не должны создавать собственные сервисы
Очевидной альтернативой является создание сервиса самостоятельно внутри компонента:
userService = new UserService();
Этот подход работает, но привязывает компонент к конкретному классу и определенному способу его создания. Теперь компонент сам решает, как будет создан сервис, что ему необходимо для существования и следует ли создавать новую копию каждый раз. Если позже сервису потребуются собственные зависимости, все места, где используется оператор new, тоже придется изменить.
Инженерия зависимостей меняет эту схему. Компонент лишь указывает, что ему нужен UserService. Angular находит соответствующего поставщика, создает экземпляр или использует уже существующий и передает его компоненту:
Component
│
│ "I need UserService"
↓
Angular DI System
│
↓
UserService
Два слова в названии точно описывают суть. Зависимость — это всё, что необходимо классу для выполнения его функций. Внедрение означает, что фреймворк предоставляет эту зависимость классу вместо того, чтобы класс сам её получал или создавал.
Первое внедрение «от начала до конца»
Начните с небольшого сервиса. Декоратор @Injectable обозначает класс как объект, который может быть создан с помощью механизма DI в Angular, а параметр providedIn: 'root' регистрирует его во всем приложении:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Компонент запрашивает сервис с помощью метода inject(), импортированного из модуля @angular/core вместе с классом Component:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
}
Когда Angular создает объект App, он оценивает этот выражение:
inject(UserService)
Angular интерпретирует это как «этому классу нужен UserService», решает проблему зависимости и присваивает полученный результат полю. Поскольку поле является публичным, шаблон может напрямую вызывать этот сервис:
<h1>{{ userService.getUserName() }}</h1>
Обратите внимание, чего не хватает в компоненте. Нигде нет вызова конструктора:
new UserService()
Создание и передача экземпляра — это полностью задача Angular.
Работа с inject()
inject() — это современный API для запроса зависимости из системы DI Angular. Вы передаете ему токен, чаще всего сам класс сервиса, и получаете экземпляр:
userService = inject(UserService);
Эта функция находится в основном пакете Angular:
import { Component, inject } from '@angular/core';
При присвоении значения поля в теле класса:
userService = inject(UserService);
любой метод в классе может обращаться к сервису через this:
this.userService
Например, метод, который считывает имя и записывает его в лог:
loadUser() {
const name = this.userService.getUserName();
console.log(name);
}
Одно правило, которое стоит запомнить: inject() работает только в контексте инъекции. На практике это означает инициализаторы полей, конструктор, а также некоторые места, управляемые фреймворком, такие как функции-фабрики и функциональные защитники. При вызове этого метода позже, например в обработчике клика или в колбэке setTimeout, возникает ошибка. Получите зависимость в поле, а затем используйте это поле там, где оно вам нужно.
inject() по сравнению с инъекцией через конструктор
В более старых учебниках и многих существующих кодовых базах зависимости задаются через параметры конструктора:
constructor(private userService: UserService) {}
Этот стиль, основанный на конструкторах, по-прежнему полностью поддерживается. Эквивалентный вариант, основанный на полях, выглядит так:
userService = inject(UserService);
Оба формата взаимодействуют с одной и той же системой DI и дают одинаковый результат. Для нового кода метод inject() обычно более краток, лучше сочетается с отдельными компонентами и другими современными API Angular, а также позволяет избежать длинных списков параметров конструктора, что особенно заметно при использовании наследования. Типичный класс выглядит примерно так:
export class App {
userService = inject(UserService);
}
Однако синтаксис — это самая менее важная часть. Важен принцип: компонент получает свою зависимость от Angular, а не создаёт её сам.
Провайдеры: указание Angular, как предоставить зависимость
Запрос зависимости возможен только в том случае, если Angular знает, как её создать. Эти знания хранятся в провайдере. Самый распространённый способ его объявления — с помощью декоратора @Injectable:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Ключевым параметром является эта одна строка:
providedIn: 'root'
Он регистрирует UserService в корневом инжекторе приложения. С этого момента любой запрос может быть обработан следующим образом:
inject(UserService)
Полезным способом представления поставщика является аналогия с рецептом, который хранится у Angular:
"When someone asks for UserService,
here is how you should provide it."
Регистрация в корне с использованием providedIn
Вы, возможно, уже видели такую конфигурацию при создании сервисов:
@Injectable({
providedIn: 'root'
})
При использовании providedIn: 'root' сервис принадлежит к корневому уровню инжектора. Angular создает один экземпляр в первый раз, когда появляется запрос на него, и использует тот же экземпляр для всех последующих запросов, что делает его синглтоном на уровне всего приложения. Полный код сервиса выглядит так:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
getUserName() {
return 'Rahul';
}
}
Любой компонент может запросить его, не указывая сервис в массиве providers:
userService = inject(UserService);
Для большинства сервисов, используемых во всем приложении, это самый простой и предпочтительный способ. У него также есть полезный побочный эффект: если никто не будет вставлять этот сервис, инструменты сборки могут исключить его из пакета, поскольку регистрация происходит непосредственно у самого сервиса, а не в модуле или компоненте.
Регистрация сервиса в компоненте
Провайдер также может быть привязан к конкретному компоненту через его массив providers:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
В этом случае UserService предоставляется собственным инжектором данного компонента, а не только корневым инжектором:
providedIn: 'root'
Разница между этими двумя способами регистрации служит отправной точкой для понимания иерархических инжекторов.
Иерархические инжекторы
Angular не использует единственный глобальный контейнер. Инжекторы образуют дерево, которое в целом соответствует дереву компонентов, причем корневой инжектор находится сверху:
Root Injector
│
├── App
│
├── Component A
│
│ └── Child Component
│
└── Component B
Служба, предоставленная на уровне корня, доступна практически везде. Однако компонент может зарегистрировать собственного поставщика для той же службы:
@Component({
selector: 'app-user',
providers: [UserService],
templateUrl: './user.html'
})
export class User {
}
Теперь Angular хранит поставщика, связанного с инжектором данного компонента. Когда компонент или любой из его потомков запрашивает UserService, Angular использует экземпляр этого уровня. Каждый экземпляр компонента получает собственную копию службы, созданную вместе с компонентом и уничтожаемую вместе с ним. Компоненты в других частях дерева продолжают видеть корневой экземпляр.
Зачем нужны несколько уровней
Рассмотрим SettingsService. Большинство экранов должны использовать один и тот же набор настроек, поэтому его предоставление на уровне корня имеет смысл:
Root
│
├── Home
├── Products
└── Profile
Теперь предположим, что административная область требует собственных настроек, которые не должны просачиваться в остальную часть приложения:
Root
│
├── Home
│
└── Admin
│
└── Admin Settings Service
Благодаря предоставлению сервиса в компоненте администрирования этот поддерево получает отдельную инстанцию, в то время как все остальные продолжают использовать общую. Состояние по функциям, состояние по диалогам и состояние по элементам списка — это распространённые примеры использования этого паттерна, и ни один из них невозможен, когда каждый компонент просто вызывает new.
Как Angular обрабатывает запросы
Когда Angular сталкивается с запросом вроде:
userService = inject(UserService);
он ищет провайдера, начиная с инжектора, находящегося ближе всего к запросующему компоненту, и двигаясь дальше:
Component requests UserService
↓
Check current injector
↓
Provider found?
↙ ↘
Yes No
↓ ↓
Provide it Look at parent
↓
Continue upward
Побеждает первый инжектор, у которого есть соответствующий поставщик. Если поиск доходит до верха без нахождения такого инжектора, Angular выдает ошибку DI, обычно представленную как NullInjectorError: No provider for UserService. Именно поэтому каждый сервис, который можно вставить, должен быть зарегистрирован там, где его может найти запросчик.
Вставка нескольких зависимостей
Компонент может использовать столько сервисов, сколько ему нужно. Здесь компонент запрашивает как сервис пользователя, так и сервис уведомлений:
import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
userService = inject(UserService);
notificationService = inject(NotificationService);
}
Затем метод может объединить их, например, для приветствия текущего пользователя:
showWelcomeMessage() {
const name = this.userService.getUserName();
this.notificationService.show(
`Welcome ${name}`
);
}
Ни один из сервисов не создается компонентом; Angular сам формирует их оба и вставляет в компонент.
Сервисы и механизм DI связаны, но являются разными вещами
Легко использовать эти два термина как синонимы, но они описывают разные вещи. Служба — это класс, содержащий логику, которую можно использовать повторно, или общее состояние. Внедрение зависимостей — это механизм, который предоставляет такую службу тем частям приложения, которые в ней нуждаются:
Service
↓
Provides functionality
Dependency Injection
↓
Provides the service where needed
Component
↓
Uses the service
Применение к конкретной службе:
UserService
↓
Angular DI
↓
User Component
↓
Uses UserService
Разделение ролей позволяет изменять способ или место предоставления службы без вмешательства в компоненты, которые её потребляют.
Сочетание DI с сигналами
DI естественным образом сочетается с сигналами для управления общим реактивным состоянием. Ниже приведена служба, владеющая сигналом-счётчиком и методом для его обновления:
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = signal(0);
increase() {
this.count.update(value => value + 1);
}
}
Компонент внедряет службу как и любую другую:
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
counterService = inject(CounterService);
}
Его шаблон считывает сигнал путём его вызова, а кнопка вызывает метод службы:
<h1>Count: {{ counterService.count() }}</h1>
<button (click)="counterService.increase()">
Increase
</button>
Здесь совместно действуют три концепции:
Dependency Injection
↓
CounterService
↓
Signal
↓
Component
↓
Template
Служба управляет состоянием, DI обеспечивает доступ к одному и тому же экземпляру службы для всех потребителей, а сигнал гарантирует обновление шаблона при изменении значения. Поскольку CounterService предоставляется на уровне корня, каждый компонент, который её встраивает, видит одинаковое значение счётчика. Если бы она предоставлялась непосредственно компоненту, у каждого поддерева был бы свой собственный независимый счётчик.
Что даёт DI
Преимущества усиливаются по мере роста приложения:
- Организация: компонентам не нужно знать, как создаются их зависимости или что им требуется.
- Повторное использование: одна служба может обслуживать множество компонентов.
Пример — приложение для покупок
Представьте онлайн-магазин с несколькими сервисами, каждый из которых выполняет одну функцию:
ProductService
↓
Gets products from API
↓
CartService
↓
Manages cart state
AuthService
↓
Manages authentication
NotificationService
↓
Displays notifications
Каждый компонент инжектирует только то, что ему нужно; например, панель навигации может требовать как авторизацию, так и информацию о корзине:
ProductComponent
↓
ProductService
↓
CartComponent
↓
CartService
Navbar
↓
AuthService
↓
CartService
Angular объединяет всё это воедино, и каждый компонент сосредотачивается на своем интерфейсе, а не на создании и управлении объектами, стоящими за ним.
Самая распространённая ошибка: сервис без провайдера
Новички часто пишут обычный класс:
export class UserService {
}
а затем пытаются его внедрить:
userService = inject(UserService);
Поскольку нигде не зарегистрирован провайдер, у Angular нет способа обработки этого класса, и происходит ошибка отсутствия провайдера, описанная ранее. Для сервиса, доступного во всём приложении, решение заключается в добавлении декоратора с корневой регистрацией:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Теперь корневой инжектор знает, как создать сервис. Альтернативно можно указать класс в массиве providers компонента, если нужен экземпляр с ограниченным диапазоном доступа.
Итог
В современном Angular класс получает зависимость с помощью inject():
userService = inject(UserService);
а сервис делает себя доступным во всём приложении с помощью:
@Injectable({
providedIn: 'root'
})
Параметр providedIn определяет место регистрации сервиса, а иерархическая система инжекторов позволяет делиться одним экземпляром во всем приложении или создавать отдельные экземпляры для компонента и его потомков. Полный процесс обработки запроса выглядит следующим образом:
Component
↓
Requests Dependency
↓
Angular DI
↓
Provider
↓
Service
А с добавлением сигналов для реактивного состояния:
Component
↓
inject()
↓
Service
↓
Signal
↓
Reactive UI
Основные выводы
- Компоненты запрашивают зависимости; они сами их не создают. Angular находит поставщика и предоставляет соответствующий экземпляр.
- Вызывайте
inject()только в контексте инжектора, например, в инициализаторе поля или конструкторе. providedIn: 'root'обеспечивает создание ленивого синглтона, который можно удалить при оптимизации кода, и является подходящим значением по умолчанию для сервисов, используемых во всем приложении.- Элемент
providersна уровне компонента создает отдельный экземпляр для этого компонента и его поддерева.
NullInjectorError.