Залежність Інжекції (Dependency Injection, DI) є одним із основних концептів, який робить Angular потужним інструментом для масштабованих і підтримуваних веб-додатків. Але що, якщо ви хочете зрозуміти основні принципи DI? А ще краще, створити свою власну легку систему DI в TypeScript? У цьому блозі ми глибоко зануримось у створення такої системи, додаючи трохи магії, схожої на Angular.
Подивіться моє відео на YouTube, де я пояснюю код крок за кроком і порівнюю його з DI системою Angular.
Чому важлива Залежність Інжекції?
Перш ніж зануритись у код, давайте повернемось до того, чому DI важливе. DI спрощує розробку додатків, відокремлюючи компоненти від їх залежностей. Замість того, щоб компоненти створювали свої власні залежності, фреймворки DI впроваджують необхідні екземпляри, сприяючи повторному використанню та тестуваності.
Давайте створимо мінімальну DI систему, яка імітує поведінку Angular, щоб краще зрозуміти концепцію.
Декоратор @Injectable
В Angular декоратор @Injectable
позначає клас як такий, що підходить для DI. Ми можемо відтворити цю функціональність за допомогою пакету TypeScript reflect-metadata
.
import 'reflect-metadata';
// Користувацький декоратор Injectable
function Injectable(): ClassDecorator {
return (target: Function) => {
Reflect.defineMetadata('injectable', true, target);
};
}
Декоратор @Injectable
позначає клас метаданими, які можна перевірити пізніше при реєстрації або вирішенні залежностей.
DI контейнер: Серце системи
Ми використаємо Map
для зберігання та вирішення залежностей:
// DI контейнер
const providers = new Map();
function provide(token: new (...args: any[]) => T, instance: T): void {
const isInjectable = Reflect.getMetadata('injectable', token);
if (!isInjectable) {
throw new Error(`${token.name} не позначено як @Injectable. Неможливо надати цей клас.`);
}
providers.set(token, instance);
}
function inject(token: new (...args: any[]) => T): T {
const instance = providers.get(token);
if (!instance) {
throw new Error(`Не знайдено провайдера для ${token.name}`);
}
return instance;
}
Цей DI контейнер перевіряє, чи позначено клас як injectable перед реєстрацією, забезпечуючи, щоб лише призначені класи брали участь у DI.
Створення Injectable сервісів
Давайте створимо кілька змодельованих сервісів — HttpClient
для отримання даних і UserService
для обробки логіки, пов'язаної з користувачами:
@Injectable()
class HttpClient {
get(url: string): T {
// Змодельоване отримання даних
return null as T;
}
}
@Injectable()
class UserService {
constructor(private httpClient: HttpClient) {}
getUsers(): void {
console.log('Отримання користувачів...');
}
}
// Реєстрація провайдерів
provide(HttpClient, new HttpClient());
provide(UserService, new UserService(inject(HttpClient)));
Зверніть увагу, як UserService
залежить від HttpClient
, і ця залежність вирішується нашою DI системою.
Вирішення залежностей в компонентах
Нарешті, давайте створимо компонент, який залежить від UserService
:
class UserComponent {
constructor(private userService: UserService) {}
render(): void {
this.userService.getUsers();
}
}
// Створення і використання компонента
const userComponent = new UserComponent(inject(UserService));
userComponent.render();
На відміну від Angular, наш компонент не використовує декоратор @Injectable
, оскільки система DI Angular по-іншому обробляє створення компонентів.
Покращення DI системи
Щоб зробити цю систему ще більш надійною, ви можете:
- Реалізувати обмежені провайдери (наприклад, сінглтони або транзієнтні).
- Додати підтримку ієрархічних інжекторів.
- Автоматично вирішувати залежності за допомогою метаданих параметрів конструктора.
Висновок
Створивши цю легку DI систему, ми глибше зрозуміли принципи, на яких базується DI система в Angular.
Хоча наша система є простою, основні концепти — декоратори (decorators), метадані (metadata) та контейнери (containers) — є фундаментальними для потужної DI системи Angular.
Перекладено з: Building a Simple Dependency Injection System in TypeScript: The Angular Way