Angular: Dependency injection | Впровадження залежностей
Що таке Dependency injection?
Dependency injection — це патерн програмування, який дозволяє створювати об’єкти, використовуючи екземпляри інших об’єктів.
Підключення сервісу до компонента в Angular буде реалізовано саме за допомогою DI. Щоб створити сервіс, потрібно виконати 3 кроки:
- Створити клас (в деяких випадках сервіс може бути не класом)
- Оголосити за допомогою
@Injectable()
- Експортувати клас
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
getData(): string {
return 'data'
}
...
}
Компонента, яка використовує сервіс, починає пошук сервісу з самого нижнього injector-а і далі вгору по ієрархії, тобто спочатку перевіряється рівень самого компонента. Якщо сервіс було знайдено на одному з нижніх рівнів, то подальший пошук припиняється, а якщо ж пошук взагалі не дасть жодних результатів, то буде згенеровано помилку.
В прикладі @Injectable()
з providedIn
дозволяє зареєструвати сервіс на рутовому рівні так, що сервіс буде доступний у всіх компонентах. Далі ми детальніше розглянемо й інші способи реєстрації.
Реєстрація залежностей
Зареєструвати залежність можна на будь-якому з рівнів — на рутовому, рівні платформи, в модулі, компоненті або директиві. Кожен з цих методів має свої особливості.
Реєстрація @Injectable()
Як ми вже згадували, цей спосіб дає можливість залежності зареєструвати себе самій або за допомогою @Injectable()
, після чого залежність може бути використана в будь-якому компоненті. В такому випадку ми отримаємо Tree-shakable service.
Існує 4 методи як саме можна зареєструвати залежність:
- root — результатом стане Singelton сервіс, який буде доступний всьому додатку; життєвий цикл сервісу буде такий як життєвий цикл додатка;
- platform — Singelton-сервіс, який може бути доступний між додатками;
- any — часто використовується з lazyModules і буде створювати екземпляр для кожного модуля;
- вказавши конкретний тип реєстрації — компонент або модуль;
@Injectable({providedIn: 'root'})
export class DataService { }
@Injectable({ providedIn: 'any' })
export class DataService { }
@Injectable({ providedIn: DemoModule })
export class DataService { }
Реєстрація в @Component / @Derective
В такому випадку життєвий цикл сервісу такий самий як життєвий цикл компонента. Сервіс може бути Singelton, якщо екземпляр даного компонента лише один і не буде Singelton відповідно, якщо екземплярів декілька.
@Component({
selector: 'ang-universal',
template: '...',
providers: [DataService]
})
Реєстрація в @NgModule
Module scope — якщо не хочете, щоб DataService був доступний для програм, коли вони не імпортують ManagementModule, ви можете підключити провайдер в NgModule.
@NgModule({
declarations: [...],
providers: [DataService],
})
export class ManagementModule {}
Root scope — коли постачальник додається до кореневого модуля програми і він стає доступний для всієї програми. Основна відмінність від підключення з @Injectable() полягає в тому, що сервіс не буде Tree-shakable.
@NgModule({
imports: [...],
declarations: [...],
providers: [UserService], // Add UserService to providers array
bootstrap: [AppComponent]
})
export class AppModule {}
Якщо ми не підключаємо сервіс в lazyModule — сервіс буде Singleton, а якщо сервіс підключено у звичайний модуль і в lazyModule — буде створено новий екземпляр.
@NgModule({
imports: [BrowserModule],
declarations: [AppComponent],
providers: [UserService], // Add UserService to providers array
bootstrap: [AppComponent]
})
export class AppModule { }
Декоратори @Self, @SkipSelf, @Host та @Optional
Ми вже згадували про дефолтний порядок пошуку сервісів для компонентів, але час від часу виникає необхідність змінити стандартну поведінку пошуку. Для цього ми можемо використовувати спеціальні декоратори.
@Self
Декоратор @Self() обмежує пошук залежності поточним injector-ом.
@Component({...})
export class MenuComponent {
constructor(
@Self() private rootDataService: DataService
) {}
}
@SkipSelf
Виникають випадки, коли треба заігнорити сервіс, який підключено до даного компонента і використовувати глобальний. Тут нам на допомогу прийде декоратор @SkipSelf()
— якщо вказати його в конструкторі перед потрібним сервісом, локальний інжектор буде виключено з пошуку.
@Component({...})
export class MenuComponent {
constructor(
@SkipSelf() private rootDataService: DataService
) {}
}
@Optional
Якщо компонент не зможе знайти сервіс, який до нього підключено — буде згенеровано ексепшен, але ми цього можемо уникнути, використовуючи декоратор @Optional()
. В такому випадку, якщо сервіс не буде знайдено, помилки не виникне і в значення змінної буде просто записано null
. Єдине, що тут варто пам’ятати — це все ж додавати перевірку чи існує сервіс, перед тим як використовувати його методи в компоненті.
@Component({...})
export class MenuComponent {
constructor(
@Optional() private dataService: DataService
) {}
}
@Host
Якщо ми використовуємо декоратор @Host()
, пошук сервіса буде відбуватися від поточного елемента до host-компонента і на host-компмоненті пошук буде закінчено.
@Directive({ selector: '[ang-test]' })
export class TestDirective
{
constructor(@Host() data: DataService)
{}
}
Наприклад, якщо ми маємо директиву TestDirective, яка підключається в компонент TestComponent, в директиві буде прописано @Host()
, то пошук провайдера буде відбуватися спочатку в директиві, потім в компоненті TestComponent, яка для директиви TestDirective буде host-компонентою. Далі пошук відбуватися не буде.
Визначення провайдерів
providers: [Logger]
Такий спосіб реєстрації є скороченим записом, коли назва провайдера збігається з назвою класу. Окрім того, є й інші способи визначення провайдерів, котрі можуть бути корисні як під час розробки, так і для тестування. При реєстрації сервісу в компоненті або модулі, ми можемо вказувати його реалізацію для конкретного випадку.
useValue
За допомогою useValue ви можете вказати значення будь-якого типу, яке буде повертатися результатом виклику сервісу:
providers: [
{ provide: 'DataService', useValue: { id: 1, name: 'David' } },
]
useClass
В цьому варіанті для реалізації сервісу ви можете задати клас:
providers: [
{ provide: DataService, useClass: TestService },
]
useFactory
Ми можемо задати функцію, яка, в залежності від умов, буде повертати значення сервісу. Такий спосіб підключення може бути дуже зручний, коли в залежності від зовнішнього значення, ми використовуємо ServiceA або ServiceB.
@NgModule({
declarations: [...],
imports: [...],
providers: [
{
provide: DataService,
useFactory: ServiceFactory,
deps: [HttpClient],
},
],
exports: [...],
})
export class ManagementModule {}
В useFactory
передається функція, яка буде повертати значення сервісу, а в deps
ми можемо вказати залежності, які необхідні для роботи функції.
Тепер подивимось як би могла виглядати наша фабрика ServiceFactory:
export const DataService = new InjectionToken('TokenDataService');
export function ServiceFactory(
httpClient: HttpClient,
): any {
return environment.useMockService
? new MockService(httpClient)
: new ManagmentService(httpClient);
}
В результаті виконання фабрики ми отримаємо значення, прив’язане до токена DataService.
useExisting
Уявімо, що ми хочемо об’явити два провайдери, serviceA та serviceB, де обидва б використовували DataService.
Якщо ми зробимо так як в прикладі — ми отримаємо два екземпляри класів DataService:
providers: [
{ provide: 'serviceA', useClass: DataService },
{ provide: 'serviceB', useClass: DataService },
]
Тут нам на допомогу прийде useExisting
, який допоможе взяти вже зареєстровану залежність.
providers: [
{ provide: 'serviceA', useClass: DataService },
{ provide: 'serviceB', useExisting: 'serviceA' },
]
Реєстрація декількох залежностей з однаковим ключом
providers: [
{ provide: DataService, useClass: ServiceA },
{ provide: DataService, useClass: ServiceB },
]
Якщо ми напишемо такий спосіб реєстрації провайдерів, то другий запис виграє і в DataService буде екземпляр ServiceB.
Властивість multi дає можливість, щоб одна залежність не переписувала попередню, якщо ключ збігається, а накопичувала залежності в масиві.
providers: [
{ provide: DataService, useClass: ServiceA, multi: true },
{ provide: DataService, useClass: ServiceB, multi: true },
]
Тобто в такому випадку DataService буде не екземпляром сервісу, а масивом екземплярів.
Джерела: angular.io, medium.com, offering.solutions, habr.com