Експорт, імпорт, прикріплення
Модуль, де ви декларуєте певні провайдери, називається модулем-хостом для цих провайдерів. А коли ви використовуєте дані провайдери у зовнішньому модулі, то цей зовнішній модуль називається модулем-споживачем даних провайдерів.
Для того, щоб модуль-споживач міг використовувати провайдери з модуля-хоста, спочатку необхідно експортувати відповідні токени провайдерів з модуля-хоста. Робиться це у метаданих, які передаються у декоратор featureModule
або rootModule
:
import { featureModule } from '@ditsmod/core';
import { FirstService } from './first.service.js';
import { SecondService } from './second.service.js';
import { ThirdService } from './third.service.js';
@featureModule({
providersPerMod: [FirstService, { token: SecondService, useClass: ThirdService }],
exports: [SecondService],
})
export class SomeModule {}
Беручи до уваги експортовані токени, Ditsmod буде експортувати відповідні провайдери з масивів:
providersPerMod
;providersPerRou
;providersPerReq
.
Експортувати провайдери, що передаються у providersPerApp
, не має сенсу, оскільки з цього масиву буде сформовано інжектор на рівні застосунку. Тобто провайдери з масиву providersPerApp
будуть доступними для будь-якого модуля, на будь-якому рівні, і без експорту.
Оскільки з модуля-хоста вам потрібно експортувати лише токени провайдерів, а не самі провайдери, у властивість exports
не можна безпосередньо передавати провайдери у формі об'єкта.
Майте на увазі, що з модуля-хоста потрібно експортувати лише ті провайдери, які безпосередньо будуть використовуватись у модулях-споживачах. У прикладі вище, SecondService
може залежати від FirstService
, але FirstService
не потрібно експортувати, якщо він безпосередньо не використовується у модулі-споживачу. Таким чином забезпечується інкапсуляція модулів.
Експортувати контролери не має сенсу, оскільки експорт стосується тільки провайдерів.
Експорт провайдерів з featureModule
Експортуючи токени з модуля-хоста у метаданих декоратора featureModule
, ви тим самим декларуєте, що відповідні провайдери можуть використовуватись у модулях-споживачах, якщо вони імпортуватимуть даний модуль-хост.
Експорт провайдерів з rootModule
Експорт провайдерів з кореневого модуля означає, що ці провайдери будуть автоматично додаватись до кожного модуля, що є в застосунку:
import { rootModule } from '@ditsmod/core';
import { SomeService } from './some.service.js';
import { OtherModule } from './other.module.js';
@rootModule({
imports: [OtherModule],
providersPerRou: [SomeService],
exports: [SomeService, OtherModule],
})
export class AppModule {}
В даному випадку, SomeService
буде додаватись до усіх модулів застосунку на рівні роуту. Як бачите, експортувати можна також і цілі модулі. В даному разі, усі провайдери, що експортуються з OtherModule
, також будуть додаватись до кожного модуля застосунку.
Імпорт модуля
Імпортувати окремий провайдер в модуль не можна, але можна імпортувати цілий модуль з усіма провайдерами та розширеннями, що експортуються з нього:
import { featureModule } from '@ditsmod/core';
import { FirstModule } from './first.module.js';
@featureModule({
imports: [
FirstModule
]
})
export class SecondModule {}
Якщо з FirstModule
експортується, наприклад, SomeService
, то тепер цей сервіс можна використовувати у SecondModule
. Разом з тим, якщо FirstModule
має контролери, у такій формі імпорту вони будуть ігноруватись. Щоб Ditsmod брав до уваги контролери з імпортованого модуля, цей модуль потрібно імпортувати з префіксом, що передається у path
:
// ...
@featureModule({
imports: [
{ path: '', module: FirstModule }
]
})
export class SecondModule {}
Хоча тут path
має порожній рядок, але для Ditsmod наявність path
означає:
- що потрібно брати до уваги також і контролери з імпортованого модуля;
- використовувати
path
у якості префіксу для усіх контролерів, що імпортуються зFirstModule
.
Як бачите, у попередньому прикладі імпортується на цей раз і не провайдер, і не модуль, а об'єкт. Цей об'єкт має наступний інтерфейс:
interface ModuleWithParams<M extends AnyObj = AnyObj, E extends AnyObj = AnyObj> {
id?: string;
module: ModuleType<M>;
path?: string;
guards?: GuardItem[];
/**
* List of modules, `ModuleWithParams` or tokens of providers exported by this
* module.
*/
exports?: any[];
providersPerApp?: Provider[];
providersPerMod?: Provider[];
providersPerRou?: Provider[];
providersPerReq?: Provider[];
/**
* This property allows you to pass any information to extensions.
*
* You must follow this rule: data for one extension - one key in `extensionsMeta` object.
*/
extensionsMeta?: E;
}
Зверніть увагу, що в даному інтерфейсі обов'язковим є лише властивість module
.
Щоб скоротити довжину запису при імпорті об'єкту з цим типом, інколи доцільно написати статичний метод у модулі, який імпортується. Щоб наочно побачити це, давайте візьмемо знову попередній приклад:
// ...
@featureModule({
imports: [
{ path: '', module: FirstModule }
]
})
export class SecondModule {}
Якщо б ви писали FirstModule
і знали, що цей модуль є сенс імпортувати багато разів в різні модулі з різними префіксами, в такому разі в даному класі можна написати статичний метод, що повертає об'єкт, спеціально призначений для імпорту:
// ...
export class FirstModule {
static withPrefix(path: string) {
return {
module: this,
path
}
}
}
Тепер об'єкт, що повертає цей метод, можна імпортувати наступним чином:
// ...
@featureModule({
imports: [
FirstModule.withPrefix('some-prefix')
]
})
export class SecondModule {}
В даному разі, скорочення запису майже не відбулось у порівнянні з попереднім прикладом, коли ми імпортували безпосередньо об'єкт, та ще й погіршилась читабельність. Тому коли пишете статичні методи для імпорту, зважуйте чи спрощують вони код.
Щоб TypeScript контролював, що саме повертає статичний метод для імпорту, рекомендується використовувати інтерфейс ModuleWithParams
:
import { ModuleWithParams } from '@ditsmod/core';
// ...
export class SomeModule {
static withParams(someParams: SomeParams): ModuleWithParams<SomeModule> {
return {
module: this,
// ...
}
}
}
Імпортуються класи чи інстанси класів?
Давайде розглянемо конкретну ситуацію. В наступному прикладі кожен із провайдерів є класом. Зверніть увагу, в які масиви передаються ці провайдери, і що саме експортується.
// ...
@featureModule({
providersPerMod: [Provider1],
providersPerRou: [Provider2],
providersPerReq: [Provider3],
exports: [Provider1, Provider2, Provider3],
})
export class Module1 {}
Припустимо ми цей модуль будемо імпортувати у Module2
, в якого своїх провайдерів немає:
// ...
@featureModule({
imports: [Module1]
// ...
})
export class Module2 {}
В результаті такого імпорту, Module2
тепер матиме три провайдери на тих самих рівнях, на яких вони були оголошені у Module1
. Під час роботи з цими провайдерами, їхні інстанси будуть створюватись окремо в обох модулях. Між модулями може бути спільним одинак, тільки якщо його провайдер оголошено на рівні застосунку. В нашому прикладі провайдери оголошені на рівні модуля, роута та запиту, тому у Module1
та Module2
інстанси класів не будуть спільними на жодному із рівнів.
Отже можна стверджувати, що імпортуються класи, а не їхні інстанси.
Імпорт та інкапсуляція
Давайте розглянемо ситуацію, при якій з Module1
експортується тільки Provider3
, оскільки тільки цей провайдер використовується у зовнішніх модулях безпосередньо:
// ...
@featureModule({
providersPerMod: [Provider1],
providersPerRou: [Provider2],
providersPerReq: [Provider3],
exports: [Provider3],
})
export class Module1 {}
Припустимо, що Provider3
має залежність від Provider1
та Provider2
. Як буде діяти Ditsmod при імпорті даного модуля в інші модулі? Ditsmod імпортуватиме усі три провайдери, оскільки Provider3
зележить від двох інших провайдерів.
Прикріплення модуля
Якщо вам не потрібно імпортувати провайдери та розширення в поточний модуль, а потрібно всього лиш прикріпити зовнішній модуль до path-префікса поточного модуля, можна скористатись масивом appends
:
import { featureModule } from '@ditsmod/core';
import { FirstModule } from './first.module.js';
@featureModule({
appends: [FirstModule]
})
export class SecondModule {}
В даному випадку, якщо SecondModule
має path-префікс, він буде використовуватись у якості префіксу для усіх маршрутів, що є у FirstModule
. Прикріплятись можуть лише ті модулі, що мають контролери.
Також можна закріпити додатковий path-префікс за FirstModule
:
// ...
@featureModule({
appends: [{ path: 'some-path', module: FirstModule }]
})
export class SecondModule {}
У даному прикладі був використаний об'єкт, в якому передається модуль для закріплення, він має наступний інтерфейс:
interface AppendsWithParams<T extends AnyObj = AnyObj> {
id?: string;
path: string;
module: ModuleType<T>;
guards?: GuardItem[];
}
Реекспорт модуля
Окрім імпорту певного модуля, цей же модуль можна одночасно й експортувати:
import { featureModule } from '@ditsmod/core';
import { FirstModule } from './first.module.js';
@featureModule({
imports: [FirstModule],
exports: [FirstModule],
})
export class SecondModule {}
Який у цьому сенс? - Тепер, якщо ви зробите імпорт SecondModule
у якийсь інший модуль, ви фактично матимете імпортованим ще й FirstModule
.
Зверніть увагу! Якщо під час реекспорту ви імпортуєте об'єкт з інтерфейсом ModuleWithParams
, цей же об'єкт потрібно й експортувати:
import { featureModule, ModuleWithParams } from '@ditsmod/core';
import { FirstModule } from './first.module.js';
const firstModuleWithParams: ModuleWithParams = { path: 'some-path', module: FirstModule };
@featureModule({
imports: [firstModuleWithParams],
exports: [firstModuleWithParams],
})
export class SecondModule {}