1. Знакомство с @Injectable()
В Angular сервис — это обычный TypeScript-класс, который мы хотим сделать доступным для внедрения через механизм Dependency Injection (DI). Но чтобы Angular понял, что наш класс — это сервис, который можно инжектировать, его нужно пометить специальным декоратором @Injectable().
Аналогия:
Представьте, что вы на складе, и у вас есть коробки с разными деталями. Только те коробки, на которых есть специальная наклейка «допущено к использованию», могут быть выданы на сборку. Вот эта наклейка — и есть наш @Injectable().
Если не украсить свой класс этим декоратором, Angular просто не узнает, что его можно использовать как сервис, и при попытке внедрить его в компоненте вы получите ошибку.
Минимальный пример сервиса с @Injectable()
Давайте создадим самый простой сервис, который будет считать, сколько раз к нему обратились. Начнем с чистого TypeScript-класса, а потом добавим декоратор:
// src/app/counter.service.ts
export class CounterService {
private count = 0;
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
Если мы попробуем внедрить такой сервис в компонент, Angular будет ругаться:
NullInjectorError: No provider for CounterService!
Чтобы Angular понял, что этот класс — сервис, который можно инжектировать, добавляем @Injectable():
import { Injectable } from '@angular/core';
@Injectable()
export class CounterService {
private count = 0;
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
Теперь Angular может работать с этим классом как с сервисом.
2. Где и как регистрировать сервис: providedIn vs providers
Декоратор @Injectable() сам по себе ещё не делает сервис доступным для всего приложения. Нужно зарегистрировать его в системе DI. Есть два основных способа:
Через providedIn
Это современный и рекомендуемый способ регистрации сервисов. В декоратор @Injectable() можно передать параметр providedIn, который указывает, где будет доступен сервис.
@Injectable({
providedIn: 'root'
})
export class CounterService { ... }
Пояснение:
- providedIn: 'root' — сервис становится синглтоном на всё приложение (один и тот же экземпляр для всех компонентов).
- Можно указать и другой модуль, если нужно ограничить область видимости (например, providedIn: SomeModule).
Через массив providers в компоненте или модуле
Более старый способ — вручную добавить сервис в массив providers компонента или модуля:
@Component({
selector: 'app-demo',
templateUrl: './demo.component.html',
providers: [CounterService]
})
export class DemoComponent { ... }
В этом случае сервис будет синглтоном только для этого компонента и его потомков.
В реальных проектах чаще всего используют providedIn: 'root' — это удобно и избавляет от необходимости таскать сервис везде вручную.
3. Инъекция сервиса в компонент: пример
Давайте используем наш сервис в компоненте. Пусть у нас есть простой компонент-счётчик:
import { Component } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
selector: 'app-counter',
template: `
<button (click)="inc()">+1</button>
<p>Счётчик: {{ value }}</p>
`
})
export class CounterComponent {
value = 0;
constructor(private counterService: CounterService) {}
inc() {
this.counterService.increment();
this.value = this.counterService.getCount();
}
}
Что происходит:
- В конструктор компонента Angular сам передаёт экземпляр CounterService.
- Мы используем методы сервиса для инкремента и получения значения.
- Если сервис зарегистрирован с providedIn: 'root', то даже если мы создадим несколько таких компонентов, они будут использовать один и тот же экземпляр сервиса.
4. Практика: сервис для списка задач (ToDo)
Давайте расширим наше учебное приложение и добавим сервис для управления списком задач. Это типичный кейс для сервисов.
Создаём сервис
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TodoService {
private todos: string[] = [];
getTodos(): string[] {
return [...this.todos]; // Возвращаем копию массива
}
addTodo(task: string) {
this.todos.push(task);
}
removeTodo(index: number) {
this.todos.splice(index, 1);
}
}
Используем сервис в компоненте
import { Component } from '@angular/core';
import { TodoService } from './todo.service';
@Component({
selector: 'app-todo',
template: `
<input [(ngModel)]="newTask" placeholder="Новая задача" />
<button (click)="add()">Добавить</button>
<ul>
<li *ngFor="let t of todos; let i = index">
{{ t }}
<button (click)="remove(i)">Удалить</button>
</li>
</ul>
`
})
export class TodoComponent {
newTask = '';
todos: string[] = [];
constructor(private todoService: TodoService) {
this.refresh();
}
add() {
if (this.newTask.trim()) {
this.todoService.addTodo(this.newTask.trim());
this.newTask = '';
this.refresh();
}
}
remove(index: number) {
this.todoService.removeTodo(index);
this.refresh();
}
refresh() {
this.todos = this.todoService.getTodos();
}
}
Обратите внимание:
Компонент не хранит задачи сам — он доверяет это сервису. Такой подход позволяет переиспользовать логику, легко тестировать её отдельно от UI и даже подключать другие компоненты к тому же сервису.
5. Полезные нюансы
Когда @Injectable() обязателен, а когда — нет?
Обязателен:
Если сервис или класс использует DI (например, в конструкторе есть другие сервисы), Angular требует декоратора @Injectable(), чтобы знать, как правильно создать экземпляр.
Желателен:
Даже если DI не используется, хорошей практикой считается всегда добавлять @Injectable() ко всем сервисам. Это делает код единообразным и защищает от будущих изменений (например, если вы потом захотите внедрить в сервис другой сервис).
Как работает Dependency Injection в Angular (простыми словами)
Когда вы пишете в конструкторе компонента:
constructor(private myService: MyService) { }
Angular ищет, где зарегистрирован MyService (например, через providedIn: 'root'), и создаёт экземпляр этого класса, если его ещё нет, или отдаёт уже созданный. Это избавляет нас от ручного создания сервисов через new и позволяет централизованно управлять зависимостями (например, легко подменять сервисы на тестовые моки).
Особенности и нюансы @Injectable()
- Сервис с providedIn: 'root' создаётся один раз на всё приложение (синглтон).
- Если добавить сервис в providers компонента, то каждый такой компонент получит свой отдельный экземпляр сервиса.
- Можно внедрять сервисы не только в компоненты, но и в другие сервисы — Angular разрулит все зависимости.
- @Injectable() можно использовать и для классов, которые не являются сервисами, но должны участвовать в DI (например, кастомные валидаторы, фабрики и т.д.)
6. Типичные ошибки при создании инжектируемого сервиса
Ошибка №1: забыли добавить @Injectable()
Если вы создали сервис, но не украсили его декоратором, Angular не сможет его инжектировать, и вы получите ошибку NullInjectorError. Даже если сервис не использует DI сам, лучше всегда добавлять этот декоратор.
Ошибка №2: не указали providedIn или не добавили в providers
Сервис не будет доступен, если его не зарегистрировать ни через providedIn, ни через providers. Angular не телепат, ему нужно явно сказать: «этот класс — сервис, его можно инжектировать».
Ошибка №3: добавили сервис в providers компонента, а ожидали синглтон
Если вы хотите, чтобы сервис был один на всё приложение, используйте providedIn: 'root'. Если добавить сервис в providers компонента, каждый экземпляр компонента получит свой сервис — иногда это неожиданно.
Ошибка №4: мутируют массивы или объекты напрямую
Если сервис возвращает ссылку на внутренний массив, и компонент его мутирует, данные могут «сломаться». Лучше возвращать копии массивов/объектов.
Ошибка №5: внедрение сервисов с циклической зависимостью
Если сервис A зависит от B, а B — от A, Angular не сможет создать их и выбросит ошибку. Стройте архитектуру так, чтобы не было циклов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ