Dependency Injection
Підготовка
В наступних прикладах даного розділу припускається, що ви клонували репозиторій ditsmod/rest-starter. Це дасть вам змогу отримати базову конфігурацію для застосунку та експериментувати у теці src/app даного репозиторію.
Окрім цього, якщо ви ще не знаєте, що саме робить рефлектор і що таке "вирішення залежностей", рекомендуємо вам спочатку прочитати попередній розділ Декоратори та рефлектор.
Інжектор, токени та провайдери
Інжектор - це основний механізм, який реалізує патерн Dependency Injection в Ditsmod. Кінцева мета інжектора - видавати значення по певному ідентифікатору, який називається токеном. Іншими словами, інжектор працює дуже просто: приймає токен, а видає значення для цього токена. Очевидно, що для такої функціональності потрібні інструкції між тим, що запитують в інжектора, і тим що він видає. Такі інструкції забезпечуються так званими провайдерами.
Давайте розглянемо наступний приклад, який трохи розширює останній приклад з розділу Декоратори та рефлектор:
import { Injector, injectable } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
@injectable()
class Service3 {
constructor(service2: Service2) {}
}
const injector = Injector.resolveAndCreate([
Service1,
Service2,
Service3
]);
const service3 = injector.get(Service3); // Instance of Service3
service3 === injector.get(Service3); // true
Як бачит е, метод Injector.resolveAndCreate() на вході приймає масив класів, а на виході видає інжектор, який вміє створювати інстанс кожного переданого класу за допомогою методу injector.get(), з урахуванням усього ланцюжка залежностей (Service3 -> Service2 -> Service1).
Отже, які задачі стоять перед інжектором, і що робить його метод injector.get():
- Під час створення інжектора, йому передається масив провайдерів - тобто масив інструкцій між тим, що у нього запитують (токеном), та тим, що він повинен видавати (значенням). В даному разі у якості провайдерів виступають класи у масиві
[Service1, Service2, Service3]. Але де тут згадані вище "інструкції"? Справа в тому, що під капотом DI, масив даних класів потім перетворяться на масив ось таких інструкцій:[{ token: Service1, useClass: Service1 }, { token: Service2, useClass: Service2 }, { token: Service3, useClass: Service3 }]. Цей етап є дуже важливим для подальшого функціонування інжектора. Якщо ви не передасте усіх необхідних провайдерів, інжектор не матиме відповідних інструкцій, коли ви запитуєте певний токен. - Після створення інжектора, коли у нього запитують токен
Service3, він проглядає конструктор цього класу, бачить залежність відService2. - Потім проглядає конструктор у
Service2, бачить залежність відService1. - Потім проглядає конструктор у
Service1, не знаходить там залежності, і тому першим створює інстансService1. - Потім створює інстанс
Service2використовуючи інстансService1. - І останнім створює інстанс
Service3використовуючи інстансService2. - Якщо пізніше будуть запитувати повторно інстанс
Service3, методinjector.get()буде повертати раніше створений інстансService3з кешу даного інжектора.
Давайте тепер порушимо пункт 1, і спробуємо під час створення інжектора передати йому пустий масив. В такому разі виклик injector.get() кидатиме помилку:
const injector = Injector.resolveAndCreate([]);
const service3 = injector.get(Service3); // Error: No provider for Service3!
Як і варто було очікувати, коли ми передаємо пустий масив замість масиву провайдерів, а потім запитуємо в інжектора токен Service3, інжектор кидає помилку вимагаючи провайдера для даного токена.
Щоб краще зрозуміти якими можуть бути провайдери, давайте передамо інжектору масив провайдерів в наступній формі:
import { Injector } from '@ditsmod/core';
class Service1 {}
class Service2 {}
class Service3 {}
class Service4 {}
const injector = Injector.resolveAndCreate([
{ token: Service1, useValue: 'value for Service1' },
{ token: Service2, useClass: Service2 },
{ token: Service3, useFactory: () => 'value for Service3' },
{ token: Service4, useToken: Service3 },
]);
injector.get(Service1); // value for Service1
injector.get(Service2); // instance of Service2
injector.get(Service3); // value for Service3
injector.get(Service4); // value for Service3
Зверніть увагу, що у цьому прикладі не використовується декоратор injectable, оскільки кожен представлений тут клас не має конструктора, де б можна було вказати залежності.
Як бачите, тепер під час створення інжектора, замість класів ми передали масив з об'єктами. Ці об'єкти також називаються провайдерами. Кожен провайдер представляє собою інструкцію для DI:
- Якщо запитується токен
Service1, потрібно видавати текстvalue for Service1. - Якщо запитується токен
Service2, треба спочатку створити інстансService2, а потім видати його. - Якщо запитується токен
Service3, треба запустити надану функцію, яка видає текстvalue for Service3. - Якщо запитується токен
Service4, треба видати значення для токенуService3, тобто треба видати текстvalue for Service3.
Тепер, коли ми передали до інжектора провайдери у вигляді інструкцій, стає більш зрозуміло, що інжектору нео бхідні інструкції для мапінгу між тим, що у нього запитують (токеном), та тим що він видає (значенням). В документації такий мапінг також може називатись реєстром інжектора або реєстром провайдерів. Для інжектора токен - це ідентифікатор для пошуку значення у його реєстрі.
Коротка та довга форма передачі токенів у методах класу
Якщо у якості типу параметра конструктора використовується клас, його одночасно можна використовувати у якості токена:
import { injectable } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {} // Коротка форма вказання залежності
}
Дуже важливо розуміти, що сам механізм використання токенів потрібний для JavaScript-runtime, тому у якості токенів не можна використовувати такі типи, які у TypeScript-коді ви оголошуєте з ключовими словами interface, type, enum, declare і т.п., бо їх не існує у JavaScript-коді. Окрім цього, токени не можна імпортувати з ключовим словом type, оскільки у JavaScript-коді такого імпорту не буде.
На відміну від класу, масив не може одночасно використовуватись і у якості TypeScript-типу, і у якості токену. З іншого боку, токен може мати зовсім нерелевантний тип даних відносно залежності, з якою він асоціюється, тому, наприклад, рядковий тип токена може асоціюватись із залежністю, що має будь-який TypeScript-тип, включаючи масиви, інтерфейси, enum і т.д.
Передати токен можна у короткій або довгій формі вказання залежності. В останньому прикладі використовується коротка форма вказання залежності, вона має суттєві обмеження, бо таким чином можна вказати залежність лише від певного класу.
А ще існує довга форма вказання залежності за допомогою декоратора inject, вона дозволяє використовувати альтернативний токен:
import { injectable, inject } from '@ditsmod/core';
interface InterfaceOfItem {
one: string;
two: number;
}
@injectable()
export class Service1 {
constructor(@inject('some-string') private someArray: InterfaceOfItem[]) {} // Довга форма вказання залежності
// ...
}
Коли використовується inject, DI бере до уваги лише переданий в нього токен. В даному разі DI ігнорує тип змінної - InterfaceOfItem[], використовуючи в якості токена текстовий рядок some-string. Іншими словами, DI використовує some-string як ключ для пошуку відповідного значення для залежності, і для DI взагалі ніякого значення не має тип для цього параметра - тобто InterfaceOfItem[]. Таким чином, DI дає можливість розділяти токен та тип змінної, тому в конструкторі можна отримати будь-який тип залежності, включаючи різні типи масивів чи enum.
Токеном може бути референс на клас, об'єкт чи функцію, також у якості токену можна використовувати текстові, числові значення, та символи. Для довгої форми вказання залежностей, у якості токена рекомендуємо використовувати інстанс класу InjectionToken<T>, оскільки клас InjectionToken<T> має параметризований тип T, за допомогою якого можна вказати тип даних, який асоціюється з даним токеном:
// tokens.ts
import { InjectionToken } from '@ditsmod/core';
import { InterfaceOfItem } from './types.js';
const SOME_TOKEN = new InjectionToken<InterfaceOfItem[]>('SOME_TOKEN');
// second-service.ts
import { injectable, inject } from '@ditsmod/core';
import { InterfaceOfItem } from './types.js';
import { SOME_TOKEN } from './tokens.js';
@injectable()
export class Service1 {
constructor(@inject(SOME_TOKEN) private someArray: InterfaceOfItem[]) {}
// ...
}
Провайдер
Формально, тип провайдера представляє собою таку декларацію:
import { Class } from '@ditsmod/core';
type Provider = Class<any> |
{ token: any, useValue?: any, multi?: boolean } |
{ token: any, useClass: Class<any>, multi?: boolean } |
{ token?: any, useFactory: [Class<any>, Class<any>.prototype.methodName], multi?: boolean } |
{ token?: any, useFactory: (...args: any[]) => any, deps: any[], multi?: boolean } |
{ token: any, useToken: any, multi?: boolean }
*зверніть увагу, що токен для провайдера з властивістю useFactory є опціональним, оскільки DI може використати функцію чи метод вказаного класу у якості токена.
Якщо провайдер представлено у вигляді об'єкта, його типи можна імпортувати з @ditsmod/core:
import { ValueProvider, ClassProvider, FactoryProvider, TokenProvider } from '@ditsmod/core';
Більш детально про кожен із цих типів:
-
ValueProvider - цей тип провайдера має властивість
useValue, в яку передається будь-яке значення, окрімundefined, DI його вида ватиме без змін. Приклад такого провайдера:{ token: 'token1', useValue: 'some value' } -
ClassProvider - цей тип провайдера має властивість
useClass, в яку передається клас, чий інстанс буде використано як значення цього провайдера. Приклад такого провайдера:{ token: 'token2', useClass: SomeService } -
FactoryProvider - цей тип провайдера має властивість
useFactory, в яку можна передавати аргументи двох типів:-
ClassFactoryProvider (рекомендовано, через свою кращу інкапсуляцію) передбачає, що до
useFactoryпередається tuple, де на першому місці повинен бути клас, а на другому місці - метод цього класу, який повинен повернути якесь значення для вказаного токена. Наприклад, якщо клас буде таким:import { factoryMethod } from '@ditsmod/core';
export class ClassWithFactory {
@factoryMethod()
method1(dependecy1: Dependecy1, dependecy2: Dependecy2) {
// ...
return '...';
}
}В такому разі провайдер потрібно передавати до реєстру DI в наступному форматі:
{ token: 'token3', useFactory: [ClassWithFactory, ClassWithFactory.prototype.method1] }Спочатку DI створить інстанс цього класу, потім викличе його метод та отримає результат, який вже і буде асоціюватись з указаним токеном. Метод указаного класу може повертати будь-яке значення, окрім
undefined. -
FunctionFactoryProvider передбачає, що до
useFactoryможна передавати функцію, яка може мати параметри - тобто може мати залежність. Цю залежність необхідно додатково вручну вказувати у властивостіdepsу вигляді масиву токенів, причому порядок передачі токенів важливий:function fn(service1: Service1, service2: Service2) {
// ...
return 'some value';
}
{ token: 'token3', deps: [Service1, Service2], useFactory: fn }Зверніть увагу, що у властивість
depsпередаються саме токени провайдерів, і DI їх сприймає саме як токени, а не як провайдери. Тобто для цих токенів до реєстру DI ще треба буде передати відповідні провайдери. Також зверніть увагу, що уdepsне передаються декоратори для параметрів (наприкладoptional,skipSelfі т.д.). Якщо для вашої фабрики необхідні декоратори параметрів - вам потрібно скористатисьClassFactoryProvider.
-
-
TokenProvider - цей тип провайдера має властивість
useToken, в яку передається інший токен. Якщо ви записуєте таке:{ token: Service2, useToken: Service1 }Таким чином ви говорите DI: "Коли споживачі провайдерів запитують токен
Service2, потрібно використати значення для токенаService1". Іншими словами, ця директива робить аліасService2, який вказує наService1. Отже,TokenProviderне є самодостатнім, на відміну від інших типів провайдерів, і в кінцевому підсумку він завжди повинен вказувати на інші типи провайдерів - наTypeProvider,ValueProvider,ClassProviderчиFactoryProvider:import { Injector } from '@ditsmod/core';
const injector = Injector.resolveAndCreate([
{ token: 'token1', useValue: 'some value for token1' }, // <-- non TokenProvider
{ token: 'token2', useToken: 'token1' },
]);
console.log(injector.get('token1')); // some value for token1
console.log(injector.get('token2')); // some value for token1Тут під час створення інжектора передається
TokenProvider, який вказує наValueProvider, тому цей код працюватиме. Якщо ж ви цього не зробите, то DI кидатиме помилку:import { Injector } from '@ditsmod/core';
const injector = Injector.resolveAndCreate([
{ token: 'token1', useToken: 'token2' },
]);
injector.get('token1'); // Error! No provider for token2! (token1 -> token2)
// OR
injector.get('token2'); // Error! No provider for token2!Це стається через те, що ви говорите DI: "Якщо у тебе будуть запитувати
token1, то використовуй значення дляtoken2", але ви не передаєте значення дляtoken2.З іншого боку, ваш
TokenProviderможе вказувати на той же тип -TokenProvider- у якості проміжного значення, але в кінцевому підсумкуTokenProviderзавжди повинен вказувати на провайдер іншого типу:import { Injector } from '@ditsmod/core';
const injector = Injector.resolveAndCreate([
{ token: 'token1', useValue: 'some value for token1' }, // <-- non TokenProvider
{ token: 'token2', useToken: 'token1' },
{ token: 'token3', useToken: 'token2' },
{ token: 'token4', useToken: 'token3' },
]);
console.log(injector.get('token4')); // some value for token1
Тепер, коли ви вже ознайомились з поняттям провайдер, можна уточнити, що під залежністю розуміють залежність саме від значення провайдера. Таку залежність мають споживачі значень провайдерів або в конструкторах сервісів, або в конструкторах чи методах контролерів, або в методі get() інжекторів.
Ієрархія та інкапсуляція інжекторів
DI надає можливість створювати ще й ієрархію та інкапсуляцію інжекторів, в я кій беруть участь батьківські та дочірні інжектори. Саме завдяки ієрархії та інкапсуляції - і будується структура та модульність застосунку. З іншого боку, якщо є інкапсуляція, існують правила, які треба вивчити, щоб орієнтуватись, коли один сервіс може отримати доступ до певного провайдера, а коли - ні.
Давайте розглянемо наступну ситуацію. Уявіть, що вам треба створити дефолтну конфігурацію для логера, яка буде використовуватись для усього застосунку. Але для певного модуля рівень виводу повідомлень треба підвищити, наприклад щоб робити дебаг цього конкретного модуля. Це означає, що на рівні даного модуля ви будете змінювати конфігурацію, і вам треба, щоб вона не впливала на дефолтне значення та інші модулі. Саме для цього і призначається ієрархія інжекторів. Самий вищій в ієрархії - інжектор на рівні застосунку, а від нього вже відгалуджуються інжектори для кожного модуля. Причому важливою особливістю є те, що вищій в ієрархії інжектор не має доступу до нижчих в ієрархії інжекторів. А ось нижчі в ієрархії інжектори можуть мати доступ до вищих в ієрархії інжекторів. Саме тому інжектори на рівні модуля можуть отримувати, наприклад конфігурацію логера від інжектора на рівні застосунку, якщо на рівня модуля вони не отримали змінену конфігурацію.
Давайте розглянемо наступний приклад. Для спрощення, тут взагалі не використовуються декоратори, оскільки жоден клас не має залежностей:
import { Injector } from '@ditsmod/core';
class Service1 {}
class Service2 {}
class Service3 {}
class Service4 {}
const parent = Injector.resolveAndCreate([Service1, Service2]); // Батьківський інжектор
const child = parent.resolveAndCreateChild([Service2, Service3]); // Дочірній інжектор
child.get(Service1); // ОК
parent.get(Service1); // ОК
parent.get(Service1) === child.get(Service1); // true
child.get(Service2); // ОК
parent.get(Service2); // ОК
parent.get(Service2) === child.get(Service2); // false
child.get(Service3); // ОК
parent.get(Service3); // Error - No provider for Service3!
child.get(Service4); // Error - No provider for Service4!
parent.get(Service4); // Error - No provider for Service4!
Як бачите, при створенні дочірнього інжектора, йому не передали Service1, тому при запиті інстансу цього класу він звернеться до батька. До речі, тут є один неочевидний, але дуже важливий момент: через метод get() дочірні інжектори тільки запитують у батьківських інжекторів певні інстанси класів, а самостійно вони їх не створюють. Саме тому цей вираз повертає true:
parent.get(Service1) === child.get(Service1); // true
Оскільки це дуже важлива особливість в ієрархії інжекторів, давайте ще раз опишемо її: значення певного провайдера зберігається саме у тому інжекторі, в який передається відповідний провайдер. Тобто, якщо під час створення дочірнього інжектора йому не передавали Service1, то child.get(Service1) може видати інстанс Service1, але він буде створюватись у батьківському інжекторі. І вже після того, як інстанс Service1 створено у батьківському інжекторі, цей самий інстанс буде видаватись (з кешу) при повторному запиті або через child.get(Service1), або через parent.get(Service1). Це теж дуже важлива особливість, бо вона визначає де саме буде зберігатись стан конкретного провайдера.
Коли ж ми поглянемо на поведінку інжекторів при запиті у них Service2, то тут вони будуть поводитись по-іншому, оскільки під час їх створення їм обом передали провайдер Service2, тому кожен із них створить свою локальну версію цього сервіса, і саме через це даний вираз повертає false:
parent.get(Service2) === child.get(Service2); // false
Коли ми запитуємо у батьківського інжектора Service3, він не може створити інстансу класу Service3 через те, що він не має зв'язку з дочірнім інжектором, в якому є Service3.
Ну і обидва інжектори не можуть видати інстансу Service4, бо їм не передали цього класу при їхньому створенні.
Ієрархія інжекторів в застосунку Ditsmod
Пізніше в документації ви зустрічатимете наступні властивості об'єкта, які передаються через метадані модуля:
providersPerApp- провайдери на рівні застосунку;providersPerMod- провайдери на рівні модуля;providersPerRou- провайдери на рівні роута;providersPerReq- провайдери на рівні HTTP-запиту.
Використовуючи ці масиви, Ditsmod формує різні інжектори, що пов'язані між собою ієрархічним зв'язком. Таку ієрархію можна зімітувати наступним чином:
import { Injector } from '@ditsmod/core';
const providersPerApp = [];
const providersPerMod = [];
const providersPerRou = [];
const providersPerReq = [];
const injectorPerApp = Injector.resolveAndCreate(providersPerApp);
const injectorPerMod = injectorPerApp.resolveAndCreateChild(providersPerMod);
const injectorPerRou = injectorPerMod.resolveAndCreateChild(providersPerRou);
const injectorPerReq = injectorPerRou.resolveAndCreateChild(providersPerReq);
Під капотом, Ditsmod робить аналогічну процедуру багато разів для різних модулів, роутів та запитів. Наприклад, якщо застосунок Ditsmod має два модулі, і десять роутів, відповідно буде створено од ин інжектор на рівні застосунку, по одному інжектору для кожного модуля (2 шт.), по одному інжектору для кожного роуту (10 шт.), і по одному інжектору на кожен запит. Інжектори на рівні запиту видаляються автоматично кожен раз після завершення обробки запиту.
Нагадаємо, що вищі в ієрархії інжектори не мають доступу до нижчих в ієрархії інжекторів. Це означає, що при передачі класу у певний інжектор, потрібно знати найнижчий рівень ієрархії його залежностей.
Наприклад, якщо ви напишете клас, що має залежність від HTTP-запиту, ви зможете його передати тільки у масив providersPerReq, бо тільки з цього масиву формується інжектор, до якого Ditsmod буде автоматично додавати провайдер з об'єктом HTTP-запиту. З іншого боку, інстанс цього класу матиме доступ до усіх своїх батьківських інжекторів: на рівні роуту, модуля, та застосунку. Тому клас, що передається в масив providersPerReq може залежати від провайдерів на будь-якому рівні.
Також ви можете написати певний клас і передати його в масив providersPerMod, в такому разі він може залежати тільки від провайдерів на рівні модуля, або на рівні застосунку. Я кщо він буде залежати від провайдерів, які ви передали в масив providersPerRou чи providersPerReq, ви отримаєте помилку про те, що ці провайдери не знайдені.
Метод injector.pull()
Цей метод є сенс використовувати лише у дочірньому інжекторі, коли у нього бракує певного провайдера, який є у батьківському інжекторі, причому цей провайдер повинен залежати від іншого провайдера, який є у дочірньому інжекторі.
Наприклад, коли Service залежить від Config, причому Service є тільки у батьківському інжекторі, а Config є як у батьківському, так і у дочірньому інжекторі:
import { injectable, Injector } from '@ditsmod/core';
class Config {
one: any;
two: any;
}
@injectable()
class Service {
constructor(public config: Config) {}
}
const parent = Injector.resolveAndCreate([Service, { token: Config, useValue: { one: 1, two: 2 } }]);
const child = parent.resolveAndCreateChild([{ token: Config, useValue: { one: 11, two: 22 } }]);
child.get(Service).config; // returns from parent injector: { one: 1, two: 2 }
child.pull(Service).config; // pulls Service in current injector: { one: 11, two: 22 }
Як бачите, якщо в даному випадку використовувати child.get(Service), то Service створиться з тим Config, який є у батьківському інжекторі. Якщо ж використовувати child.pull(Service), він спочатку зтягне потрібний провайдер у дочірній інжектор, а потім створить його значення в контексті дочірнього інжектора не додаючи його значення у кеш інжектора (тобто child.pull(Service) повертатиме кожен раз новий інстанс).
Але якщо запитуваний провайдер є у дочірньому інжекторі, то вираз child.pull(Service) буде працювати ідентично до виразу child.get(Service) (з додаванням значення провайдера у кеш інжектора):
import { injectable, Injector } from '@ditsmod/core';
class Config {
one: any;
two: any;
}
@injectable()
class Service {
constructor(public config: Config) {}
}
const parent = Injector.resolveAndCreate([]);
const child = parent.resolveAndCreateChild([Service, { token: Config, useValue: { one: 11, two: 22 } }]);
child.get(Service).config; // { one: 11, two: 22 }
Поточний інжектор
Безпосередньо сам інжектор сервіса чи контролера вам рідко може знадобиться, але ви його можете отримати у конструкторі як і значення будь-якого іншого провайдера:
import { injectable, Injector } from '@ditsmod/core';
import { FirstService } from './first.service.js';
@injectable()
export class SecondService {
constructor(private injector: Injector) {}
someMethod() {
const firstService = this.injector.get(FirstService); // Lazy loading of dependency
}
}
Майте на увазі, що ви таким чином отримуєте інжектор, що створив інстанс даного сервіса. Рівень ієрархії цього інжектора залежить тільки від того, в реєстр якого інжектора передали SecondService.
Мульти-провайдери
Цей вид провайдерів існує тільки у вигляді об'єкта, і він відрізняється від звичайних DI-провайдерів наявністю властивості multi: true. Такі провайдери доцільно використовувати, коли є потреба у передачі до DI зразу декількох провайдерів з однаковим токеном, щоб DI повернув таку саму кількість значень для цих провайдерів в одному масиві:
import { Injector } from '@ditsmod/core';
import { LOCAL } from './tokens.js';
const injector = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk', multi: true },
{ token: LOCAL, useValue: 'en', multi: true },
]);
const locals = injector.get(LOCAL); // ['uk', 'en']
По-суті, мульти-провайдери дозволяють створювати групи провайдерів, що мають спільний токен. Ця можливість зокрема використовується для створення групи HTTP_INTERCEPTORS.
Не допускається щоб в одному інжекторі однаковий токен мали і звичайні, і мульти-провайдери:
import { Injector } from '@ditsmod/core';
import { LOCAL } from './tokens.js';
const injector = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk' },
{ token: LOCAL, useValue: 'en', multi: true },
]);
const locals = injector.get(LOCAL); // Error: Cannot mix multi providers and regular providers
Дочірні інжектори можуть повертати значення мульти-провайдерів батьківського інжектора, лише якщо при їх створенні їм не передавались провайдери з такими самими токенами:
import { InjectionToken, Injector } from '@ditsmod/core';
const LOCAL = new InjectionToken('LOCAL');
const parent = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk', multi: true },
{ token: LOCAL, useValue: 'en', multi: true },
]);
const child = parent.resolveAndCreateChild([]);
const locals = child.get(LOCAL); // ['uk', 'en']
Якщо ж і в дочірнього, і в батьківського інжектора є мульти-провайдери з однаковим токеном, дочірній інжектор повертатиме значення лише зі свого масиву:
import { Injector } from '@ditsmod/core';
import { LOCAL } from './tokens.js';
const parent = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk', multi: true },
{ token: LOCAL, useValue: 'en', multi: true },
]);
const child = parent.resolveAndCreateChild([
{ token: LOCAL, useValue: 'аа', multi: true }
]);
const locals = child.get(LOCAL); // ['аа']
Підміна мультипровайдерів
Щоб стала можливою підміна конкретного мультипровайдера, можна зробити так:
- передайте певний клас в об'єкт мультипровайдера використовуючи властивість
useToken; - потім даний клас передайте у якості
ClassProviderчиTypeProvider; - наступним в масив провайдерів потрібно передати провайдер для підміни даного класу.
import { Injector, HTTP_INTERCEPTORS } from '@ditsmod/core';
import { DefaultInterceptor } from './default.interceptor.js';
import { MyInterceptor } from './my.interceptor.js';
const injector = Injector.resolveAndCreate([
{ token: HTTP_INTERCEPTORS, useToken: DefaultInterceptor, multi: true },
DefaultInterceptor,
{ token: DefaultInterceptor, useClass: MyInterceptor }
]);
const locals = injector.get(HTTP_INTERCEPTORS); // [MyInterceptor]
Така конструкція має сенс, наприклад, якщо перші два пункти виконуються десь у зовнішньому модулі, до якого у вас немає доступу на редагування, а третій пункт виконує вже користувач поточного модуля.
Редагування значень в реєстрі DI
Як вже було сказано раніше, в реєстр DI передаються провайдери, з яких потім формуються значення, щоб в кінцевому результаті мати мапінг між токеном та його значенням:
token1 -> value15
token2 -> value100
...
Окрім цього, існує можливість редагування готових значень реєстра DI:
import { Injector } from '@ditsmod/core';
const injector = Injector.resolveAndCreate([{ token: 'token1', useValue: undefined }]);
injector.setByToken('token1', 'value1');
injector.get('token1'); // value1
Зверніть увагу, що в даному разі до реєстру спочатку передається провайдер з token1, який має значення undefined, і лише потім ми змінюємо значення для даного токена. Якщо ви спробуєте редагувати значення для токена, якого у реєстрі немає, DI кине приблизно таку помилку:
DiError: Setting value by token failed: cannot find token in register: "token1". Try adding a provider with the same token to the current injector via module or controller metadata.
У більшості випадків, редагування значень використовують інтерсептори або гарди, оскільки вони таким чином передають результат своєї роботи до реєстру:
У якості альтернативи для методу injector.setByToken(), можна використовувати еквівалентний вираз:
import { KeyRegistry } from '@ditsmod/core';
// ...
const { id } = KeyRegistry.get('token1');
injector.setById(id, 'value1');
// ...
Переваги використання методу injector.setById() в тому, що він швидший за метод injector.setByToken(), але лише при умові, якщо ви один раз отримуєте ID із KeyRegistry, а потім багато разів використовуєте injector.setById().
Декоратори optional, fromSelf та skipSelf
Ці декоратори використовуються для управління поведінкою інжектора під час пошуку значень для певного токена.
optional
Інколи вам може знадобитись вказати опціональну (необов'язкову) залежність в конструкторі. Давайте розглянемо наступний приклад, де після властивості firstService поставлено знак питання, і таким чином вказано для TypeScript що ця властивість є опціональною:
import { injectable } from '@ditsmod/core';
import { FirstService } from './first.service.js';
@injectable()
export class SecondService {
constructor(private firstService?: FirstService) {}
// ...
}
Але DI проігнорує цю опціональність і видасть помилку у разі відсутності можливості для створення FirstService. Щоб даний код працював, необхідно скористатись декоратором optional:
import { injectable, optional } from '@ditsmod/core';
import { FirstService } from './first.service.js';
@injectable()
export class SecondService {
constructor(@optional() private firstService?: FirstService) {}
// ...
}
fromSelf
Д екоратори fromSelf та skipSelf мають сенс у випадку, коли існує певна ієрархія інжекторів. Декоратор fromSelf використовується дуже рідко.
import { injectable, fromSelf, Injector } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(@fromSelf() public service1: Service1) {}
}
const parent = Injector.resolveAndCreate([Service1, Service2]);
const child = parent.resolveAndCreateChild([Service2]);
const service2 = parent.get(Service2) as Service2;
service2.service1 instanceof Service1; // true
child.get(Service2); // Error - Service1 not found
Як бачите, Service2 залежить від Service1, причому декоратор fromSelf вказує DI: "При створенні інстансу Service1 використовувати тільки той самий інжектор, який створить інстанс Service2, а до батьківського інжектора не потрібно звертатись". Коли створюється батьківський інжектор, йому передають обидва необхідні сервіси, тому при запиті токену Service2 він успішно вирішить залежність та видасть інстанс цього класу.
А ось при створенні дочірнього інжектора, йому не передали Service1, тому при запиті токену Service2 він не зможе вирішити залежність цього сервісу. Якщо прибрати декоратор fromSelf з конструктора, то дочірній іжектор успішно вирішить залежність Service2.
skipSelf
Декоратор skipSelf використовується частіше, ніж fromSelf, але також рідко.
import { injectable, skipSelf, Injector } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(@skipSelf() public service1: Service1) {}
}
const parent = Injector.resolveAndCreate([Service1, Service2]);
const child = parent.resolveAndCreateChild([Service2]);
parent.get(Service2); // Error - Service1 not found
const service2 = child.get(Service2) as Service2;
service2.service1 instanceof Service1; // true
Як бачите, Service2 залежить від Service1, причому декоратор skipSelf вказує DI: "При створенні інстансу Service1 пропустити той інжектор, який створить інстанс Service2, і зразу звертатись до батьківського інжектора". Коли створюється батьківський інжектор, йому передають обидва необхідні сервіси, але через skipSelf він не може використати значення для Service1 з власного реєстру, тому він не зможе вирішити вказану залежність.
А при створенні дочірнього інжектора, йому не передали Service1, зате він може звернутись до батьківського інжектора за ним. Тому дочірній інжектор успішно вирішить залежність Service2.
Коли DI не може знайти потрібного провайдера
Пам'ятайте, що коли DI не може знайти потрібного провайдера, існує всього три можливі причини:
- ви не передали потрібний провайдер до DI в метадані модуля чи контролера (ну або у випадку тестування - у
Injector.resolveAndCreate()); - ви не імпортували модуль, де передається потрібний вам провайдер, або ж цей провайдер не експортується;
- ви запитуєте у батьківському інжекторі провайдер з дочірнього інжектора.