Сервисы и внедрение зависимостей
Сервис — это обычный класс с логикой, которая нужна нескольким компонентам: хранение списка задач, загрузка данных с сервера, работа с localStorage. Компонент не создаёт сервис сам через new — он просит его у Angular, и Angular выдаёт готовый объект. Такой механизм называется внедрением зависимостей (dependency injection, DI).
Создание сервиса
ng generate service todo-storeCLI создаст файл src/app/todo-store.ts с классом TodoStore (в версиях до 20 — todo-store.service.ts и класс TodoStoreService). Перенесём в него всё, что касается задач:
// src/app/todo-store.ts
import { Injectable, computed, signal } from '@angular/core';
import { Todo } from './todo';
@Injectable({ providedIn: 'root' })
export class TodoStore {
private nextId = 1;
readonly todos = signal<Todo[]>([]);
readonly left = computed(() => this.todos().filter((t) => !t.done).length);
add(title: string) {
const todo = { id: this.nextId++, title, done: false };
this.todos.update((list) => [...list, todo]);
}
toggle(id: number) {
this.todos.update((list) =>
list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
);
}
remove(id: number) {
this.todos.update((list) => list.filter((t) => t.id !== id));
}
}Декоратор @Injectable разрешает внедрять класс. Настройка providedIn: 'root' означает, что на всё приложение будет один объект сервиса, созданный при первом запросе.
Получение сервиса в компоненте
Функция inject() возвращает объект сервиса. Её вызывают при объявлении поля:
// src/app/todo-list/todo-list.ts
import { Component, inject } from '@angular/core';
import { TodoStore } from '../todo-store';
import { TodoItem } from '../todo-item/todo-item';
@Component({
selector: 'app-todo-list',
imports: [TodoItem],
templateUrl: './todo-list.html',
})
export class TodoList {
store = inject(TodoStore);
}<!-- src/app/todo-list/todo-list.html -->
<input #field (keyup.enter)="store.add(field.value)" />
<p>Осталось: {{ store.left() }}</p>
<ul>
@for (todo of store.todos(); track todo.id) {
<li>
<app-todo-item
[todo]="todo"
(toggle)="store.toggle($event)"
(remove)="store.remove(todo.id)"
/>
</li>
}
</ul>Список работает как раньше, но компонент стал тоньше: он только показывает данные и передаёт действия в сервис.
Один объект на всех
Любой другой компонент, который вызовет inject(TodoStore), получит тот же объект. Например, счётчик в шапке:
// src/app/header/header.ts
import { Component, inject } from '@angular/core';
import { TodoStore } from '../todo-store';
@Component({
selector: 'app-header',
template: `<header>Невыполненных задач: {{ store.left() }}</header>`,
})
export class Header {
store = inject(TodoStore);
}Добавьте задачу в списке — число в шапке изменится сразу, потому что оба компонента читают один и тот же сигнал.
Сервис для части приложения
Если указать сервис в providers компонента, у этого компонента и его детей будет свой отдельный объект:
// src/app/todo-list/todo-list.ts
@Component({
selector: 'app-todo-list',
providers: [TodoStore],
templateUrl: './todo-list.html',
})Так два списка дел на одной странице получат независимые задачи.
Частая ошибка
Вызывать inject() внутри обычного метода, например в обработчике щелчка. Angular выдаст ошибку NG0203: inject() must be called from an injection context. Вызывайте inject() только при объявлении полей или в конструкторе.