Перейти до основного вмісту

Модулі

Одним з головних елементів архітектури Ditsmod - є його модулі. Але чим хороша саме модульна архітектура? - Модульність дозволяє компонувати різні автономні елементи і складати з них масштабований застосунок. Саме завдяки автономності модулів, великі проекти простіше розробляти, тестувати, деплоїти та обслуговувати.

Така архітектура дозволяє ізолювати в одному модулі декілька файлів коду, що можуть мати різні ролі, але спільну спеціалізацію. Модуль можна порівняти з оркестром, в якому є різні інструменти, але усі вони створюють спільну музику. З іншого боку, потреба в ізоляції різних модулів виникає через те, що вони можуть мати різну спеціалізацію і через це - можуть заважати один-одному. Продовжуючи аналогію з людьми, якщо в одому кабінеті розмістити поліцію та музикантів, або брокерів і перекладачів, швидше за все, вони заважатимуть один-одному. Саме тому для модуля важлива вузька спеціалізація.

Модулі є найбільшими будівельними блоками застосунку, а в їхніх метаданих декларуються такі складові модуля як:

  • контролери, що приймають HTTP-запити та відправляють HTTP-відповіді;
  • сервіси, де описується бізнес логіка застосунку;
  • інтерсептори та ґарди, що дозволяють автоматизувати обробку HTTP-запитів по типовим патернам;
  • декоратори та розширення, що дозволяють доповнювати застосунок новими правилами та новою поведінкою;
  • інші класи, інтерфейси, хелпери, типи даних, що призначаються для роботи поточного модуля.

Модулі є двох типів:

  1. Root module (кореневий модуль).
  2. Feature module (модуль фіч).

Кореневий модуль

До кореневого модуля підв'язуються інші модулі, він є єдиним на увесь застосунок, а його клас рекомендовано називати AppModule. TypeScript клас стає кореневим модулем Ditsmod завдяки одному з таких декораторів як rootModule, restRootModule, trpcRootModule і т.д., в залежності від модуля, який ви використовуєте. Наприклад, якщо ви використовуєте REST, кореневий модуль оголошується наступним чином:

import { restRootModule } from '@ditsmod/rest';

@restRootModule()
export class AppModule {}

Загалом, в декоратор restRootModule можна передавати об'єкт з такими властивостями:

import { restRootModule } from '@ditsmod/rest';

@restRootModule({
imports: [], // Імпорт модулів
appends: [], // Прикріплення модулів, що мають контролери
providersPerApp: [], // Провайдери на рівні застосунку
providersPerMod: [], // ...на рівні модуля
providersPerRou: [], // ...на рівні роуту
providersPerReq: [], // ...на рівні HTTP запиту
exports: [], // Експорт модулів та провайдерів з поточного модуля
extensions: [], // Розширення
extensionsMeta: {}, // Дані для роботи розширень
resolvedCollisionsPerApp: [], // Вирішення колізій імпортованих класів на рівні застосунку
resolvedCollisionsPerMod: [], // ...на рівні модуля
resolvedCollisionsPerRou: [], // ...на рівні роуту
resolvedCollisionsPerReq: [], // ...на рівні HTTP запиту
controllers: [], // Список контролерів в поточному модулі
})
export class AppModule {}

Модуль фіч

TypeScript клас стає feature модулем Ditsmod завдяки одному з таких декораторів як featureModule, restModule, trpcModule і т.д., в залежності від модуля, який ви використовуєте. Наприклад, якщо ви використовуєте REST, кореневий модуль оголошується наступним чином:

import { restModule } from '@ditsmod/rest';

@restModule()
export class SomeModule {}

Файли модулів рекомендується називати із закінченням *.module.ts, а назви їхніх класів - із закінченням *Module.

Він може містити точно такі самі метадані як і кореневі модулі, за виключенням властивості resolvedCollisionsPerApp. Окрім того, що feature module можна декларувати прямо у застосунку, його також можна публікувати на npmjs.com.

Передача провайдерів в реєстр DI

На одну залежність, в реєстр DI потрібно передавати один або декілька провайдерів. Частіше за все, провайдери передаються в реєстр DI через метадані модулів, хоча інколи вони передаються через метадані контролерів, або навіть напряму в інжектори. В наступному прикладі SomeService передається в масив providersPerMod:

import { restModule } from '@ditsmod/rest';

import { SomeService } from './some.service.js';
import { SomeController } from './some.controller.js';

@restModule({
controllers: [SomeController],
providersPerMod: [
SomeService
],
})
export class SomeModule {}

Після такої передачі, споживачі провайдерів можуть використовувати SomeService в межах SomeModule. І тепер давайте додатково з цим же токеном передамо інший провайдер, але на цей раз у метадані контролера:

import { controller } from '@ditsmod/rest';

import { SomeService } from './some.service.js';
import { OtherService } from './other.service.js';

@controller({
providersPerReq: [
{ token: SomeService, useClass: OtherService }
]
})
export class SomeController {
constructor(private someService: SomeService) {}
// ...
}

Зверніть увагу на виділений рядок. Таким чином ми говоримо DI: "Якщо даний контролер має залежність від провайдера з токеном SomeService, її потрібно підмінити інстансом класу OtherService". Ця підміна буде діяти тільки для даного контролера. Усі інші контролери в SomeModule по токену SomeService будуть отримувати інстанси класу SomeService.

Аналогічну підміну можна робити на рівні застосунку та на рівні модуля. Це інколи може знадобитись, наприклад коли ви хочете мати дефолтні значення конфігурації на рівні застосунку, але кастомні значення цієї конфігурації на рівні конкретного модуля. В такому разі передамо спочатку дефолтний конфіг в кореневому модулі:

import { rootModule } from '@ditsmod/core';
import { ConfigService } from './config.service.js';

@rootModule({
providersPerApp: [
ConfigService
],
})
export class AppModule {}

І вже у певному модулі підмінюємо ConfigService на довільне значення:

import { restModule } from '@ditsmod/rest';
import { ConfigService } from './config.service.js';

@restModule({
providersPerMod: [
{ token: ConfigService, useValue: { propery1: 'some value' } }
],
})
export class SomeModule {}

Повторне додавання провайдерів

Різні провайдери з одним і тим самим токеном можна додавати багато разів в метадані модуля чи контролера, але DI вибере той із провайдерів, що додано останнім (виключення з цього правила є, але це стосується лише мульти-провайдерів):

import { restModule } from '@ditsmod/rest';

@restModule({
providersPerMod: [
{ token: 'token1', useValue: 'value1' },
{ token: 'token1', useValue: 'value2' },
{ token: 'token1', useValue: 'value3' },
],
})
export class SomeModule {}

В даному разі, в межах SomeModule по token1 буде видаватись value3 на рівні модуля, роуту чи запиту.

Окрім цього, різні провайдери з одним і тим самим токеном можна передавати одночасно на декількох різних рівнях ієрархії, але DI завжди буде вибирати найближчі інжектори (тобто, якщо значення для провайдера запитується на рівні запиту, то спочатку буде проглядатись інжектор на рівні запиту, і лише якщо там немає потрібного провайдера, DI буде підніматись до батьківських інжекторів):

import { restModule } from '@ditsmod/rest';

@restModule({
providersPerMod: [{ token: 'token1', useValue: 'value1' }],
providersPerRou: [{ token: 'token1', useValue: 'value2' }],
providersPerReq: [{ token: 'token1', useValue: 'value3' }],
})
export class SomeModule {}

В даному разі, в межах SomeModule по token1 буде видаватись value3 на рівні запиту, value2 - на рівні роуту, і value1 - на рівні модуля.

Також, якщо ви імпортуєте певний провайдер із зовнішнього модуля, і у вас у поточному модулі є провайдер з таким же токеном, то локальний провайдер матиме вищій пріоритет, при умові, що вони передавались на однаковому рівні ієрархії інжекторів.