Angular: Dependency injection | Впровадження залежностей
Angular: Dependency injection | Впровадження залежностей

Angular: Dependency injection | Впровадження залежностей

Front-end 03.08.2022 4 min read

Що таке Dependency injection?

Dependency injection — це патерн програмування, який дозволяє створювати об’єкти, використовуючи екземпляри інших об’єктів.

Підключення сервісу до компонента в Angular буде реалізовано саме за допомогою DI. Щоб створити сервіс, потрібно виконати 3 кроки:

  1. Створити клас (в деяких випадках сервіс може бути не класом)
  2. Оголосити за допомогою @Injectable()
  3. Експортувати клас
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

Поширити

, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,

guest
0 коментарів
Міжтекстові Відгуки
Переглянути всі коментарі