1. Внедрение зависимостей (Dependency Injection) в Angular
Если вы когда-нибудь собирали мебель из IKEA, то знаете: в инструкции всегда написано, какие детали и инструменты понадобятся для сборки. Примерно так же работает и Angular — только вместо шурупов и шестигранников у нас сервисы и другие зависимости.
Внедрение зависимостей (Dependency Injection, DI) — это паттерн, при котором объект (например, компонент) не сам создаёт свои зависимости, а получает их "снаружи". Это делает код гибким, тестируемым и избавляет от "жёсткой связки" между классами.
В Angular DI — один из краеугольных камней архитектуры. Когда вы хотите использовать сервис в компоненте, вы не делаете new MyService(). Вместо этого вы просите Angular: "Дай мне, пожалуйста, экземпляр этого сервиса". Angular сам решит, где его взять, когда создать и кому ещё его отдать.
Зачем внедрять сервисы через конструктор?
Можно было бы сделать так:
import { MyService } from './my.service';
@Component({ /* ... */ })
export class MyComponent {
myService = new MyService(); // Плохо!
}
Но это плохая практика. Почему?
- Тестируемость: Такой компонент невозможно подменить на тестовый сервис при юнит-тестах.
- Повторное использование: Если сервис зависит от других сервисов, их придётся создавать вручную.
- Жизненный цикл: Angular не сможет управлять созданием и уничтожением сервисов.
- Синглтоны: Каждый раз будет новый экземпляр, а не общий на всё приложение.
Вместо этого Angular предлагает внедрять зависимости через параметры конструктора компонента. Это и есть "внедрение через конструктор".
2. Как это выглядит в коде?
Давайте разберёмся на простом примере.
Создаём сервис
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(message: string) {
console.log('[LoggerService]', message);
}
}
Внедряем сервис в компонент через конструктор
import { Component } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-root',
template: `<h1>Привет, Angular!</h1>`
})
export class AppComponent {
// Внедряем сервис через конструктор
constructor(private logger: LoggerService) {
this.logger.log('AppComponent создан!');
}
}
Ключевые моменты:
- В конструкторе компонента указываем параметр с модификатором private (или public, или protected — на ваш вкус).
- Angular сам найдёт нужный сервис и передаст его в конструктор.
- Не нужно делать new LoggerService() — Angular всё сделает за вас.
Аналогия
Это как если бы вы пришли в кофейню и сказали: "Мне, пожалуйста, капучино". Бариста (Angular) сам решит, кто будет готовить кофе, из каких зёрен и с каким молоком. Вам не нужно идти на склад и варить кофе самому!
3. Как Angular находит и создаёт сервисы?
Когда вы просите Angular "внедрить" сервис, он ищет его в так называемом инжекторе. Это специальный объект, который знает, как создавать сервисы и где они объявлены.
Порядок поиска:
- Сначала Angular смотрит, не объявлен ли сервис в массиве providers текущего компонента.
- Если не найден — идёт выше по иерархии компонентов.
- Если нигде не найден — обращается к корневому инжектору (providedIn: 'root').
Результат:
- Если сервис объявлен в providedIn: 'root', экземпляр будет один на всё приложение (синглтон).
- Если сервис объявлен в providers компонента — будет отдельный экземпляр для этого компонента и его потомков.
4. Пример: внедрение нескольких сервисов
Допустим, у нас есть два сервиса: LoggerService и UserService.
@Injectable({ providedIn: 'root' })
export class UserService {
getUser() {
return { name: 'Вася', id: 123 };
}
}
В компоненте можно внедрить оба сервиса:
@Component({ /* ... */ })
export class ProfileComponent {
constructor(
private logger: LoggerService,
private userService: UserService
) {
const user = this.userService.getUser();
this.logger.log(`Пользователь: ${user.name}`);
}
}
Вопрос: Можно ли внедрять сколько угодно сервисов?
Ответ: Да! Angular не против. Главное — не переборщить, иначе конструктор станет похож на список покупок перед Новым годом.
5. Полезные нюансы
Внедрение сервисов только для чтения
Когда вы указываете параметр конструктора с модификатором private, Angular автоматически создаёт поле этого класса и присваивает ему сервис. Это удобно, если вы хотите использовать сервис внутри класса.
Если вы хотите использовать сервис только внутри конструктора (например, для инициализации), можете не указывать модификатор доступа:
constructor(logger: LoggerService) {
logger.log('Компонент создан');
}
В этом случае Angular всё равно передаст сервис в конструктор, но поле класса не будет создано.
Внедрение сервисов в других сервисах
Сервисы могут зависеть друг от друга. Это называется цепочка зависимостей.
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
constructor(private logger: LoggerService) {}
trackEvent(event: string) {
this.logger.log(`Событие: ${event}`);
}
}
Всё работает точно так же, как с компонентами. Angular сам разрулит, в каком порядке создавать сервисы.
6. Практика: добавляем сервис в наше учебное приложение
Допустим, мы разрабатываем ToDo-приложение (куда же без него!). У нас есть сервис для работы со списком задач:
@Injectable({ providedIn: 'root' })
export class TodoService {
private todos: string[] = [];
add(todo: string) {
this.todos.push(todo);
}
getAll() {
return this.todos;
}
}
В компоненте внедряем сервис:
@Component({
selector: 'app-todo-list',
template: `
<input [(ngModel)]="newTodo">
<button (click)="addTodo()">Добавить</button>
<ul>
<li *ngFor="let todo of todos">{{ todo }}</li>
</ul>
`
})
export class TodoListComponent {
newTodo = '';
todos: string[] = [];
constructor(private todoService: TodoService) {}
ngOnInit() {
this.todos = this.todoService.getAll();
}
addTodo() {
if (this.newTodo.trim()) {
this.todoService.add(this.newTodo);
this.todos = this.todoService.getAll();
this.newTodo = '';
}
}
}
Что здесь происходит:
- Сервис TodoService внедряется через конструктор.
- Вся логика работы со списком задач — в сервисе.
- Компонент только отображает данные и реагирует на действия пользователя.
7. Типичные ошибки при внедрении зависимостей через конструктор
Ошибка №1: Забыли указать сервис в декораторе @Injectable или не добавили его в providers
Если забыть декоратор @Injectable() или не указать сервис в массиве providers (если он не с providedIn: 'root'), Angular не сможет создать сервис и выдаст ошибку:
NullInjectorError: No provider for MyService!
Ошибка №2: Использование new вместо DI
Если вы пишете const s = new MyService(), Angular не сможет управлять этим экземпляром, и вы лишаетесь всех преимуществ DI: не будет синглтона, не будет цепочки зависимостей, не будет моков в тестах.
Ошибка №3: Несоответствие области видимости
Если вы случайно объявили сервис в providers компонента, а ожидали синглтон — получите отдельный экземпляр для этого компонента. Иногда это сюрприз, особенно при работе с состоянием.
Ошибка №4: Не используете модификатор доступа в конструкторе, но хотите обращаться к сервису через this
Если вы пишете constructor(logger: LoggerService), а потом в методах класса пишете this.logger.log(), получите ошибку: такого поля нет. Нужно писать private logger: LoggerService.
Ошибка №5: Внедряете сервис, который зависит от другого, но забыли указать зависимости
Если сервис A зависит от сервиса B, а B не инжектируется (нет в @Injectable или providers), получите ошибку при запуске приложения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ