Як Angular використовує ін’єкцію залежностей для розрішення сервісів за допомогою inject()
Зрозумійте, як Angular DI надає сервіси через inject(), providers та ієрархічні інжектори, а також як обмежувати діапазон інстанцій та уникати помилок через відсутність провайдерів.
Як тільки ви починаєте переміщувати логіку з компонентів 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, а не створює її сам.
Providerи: способи надання залежностей Angular
Запит на залежність може спрацювати лише тоді, коли Angular знає, як її створити. Ці знання знаходяться у providerі. Найпоширеніший спосіб оголошення providerа — це використання декоратора @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.