1. Введение
Angular — это не только про красивые компоненты, но и про правильную архитектуру. Представьте себе кухню, где повар (компонент) не только готовит, но и сам ходит за продуктами, моет посуду, считает зарплаты и пишет отчёты. Смешно? Вот и в приложении не стоит заставлять компонент заниматься всем подряд.
Компонент — это, прежде всего, "виджет", который отвечает за внешний вид и взаимодействие с пользователем. Да, он может содержать немного логики, но если весь ваш бизнес-процесс (например, подсчёт стоимости заказа, работа с сервером, обработка корзины) живёт в компоненте — вас ждёт боль, слёзы и рефакторинг.
Сервис — это как отдельный работник на кухне: кто-то закупает продукты, кто-то моет посуду, кто-то ведёт бухгалтерию. Компоненту остаётся только красиво сервировать блюдо и общаться с клиентом.
Что такое сервис в Angular?
Сервис — это обычный TypeScript-класс, который предназначен для хранения и реализации бизнес-логики, данных и функционала, не связанного напрямую с отображением. Сервис создаётся отдельно, а затем "внедряется" (инжектируется) в компоненты, которые хотят им воспользоваться.
Классическая аналогия
- Компонент — это актёр на сцене, который показывает спектакль (UI).
- Сервис — это сценарист и гримёр за кулисами, который готовит всё необходимое, чтобы актёр выглядел и действовал правильно.
Для чего нужны сервисы?
- Хранение и управление данными приложения (например, список задач, корзина покупок, текущий пользователь).
- Вынесение бизнес-логики (например, подсчёт итоговой суммы, фильтрация, валидация).
- Общение с внешними источниками данных (API, сервер, LocalStorage).
- Реализация общих утилит (логирование, уведомления, форматирование дат).
2. Типичная проблема: "Жирные" компоненты
Давайте рассмотрим пример: у нас есть компонент списка задач (todo-list), который:
- Хранит массив задач.
- Добавляет/удаляет задачи.
- Сохраняет задачи в LocalStorage.
- Фильтрует задачи по статусу.
- Отправляет задачи на сервер.
Выглядит примерно так (упрощённо):
export class TodoListComponent {
todos: Todo[] = [];
constructor() {
this.loadTodos();
}
addTodo(text: string) {
this.todos.push({ text, done: false });
this.saveTodos();
}
removeTodo(index: number) {
this.todos.splice(index, 1);
this.saveTodos();
}
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
loadTodos() {
const saved = localStorage.getItem('todos');
this.todos = saved ? JSON.parse(saved) : [];
}
// ... и так далее ...
}
Что здесь не так? Компонент отвечает за всё сразу: и за отображение, и за бизнес-логику, и за работу с хранилищем. Если вы захотите использовать эти функции в другом компоненте — придётся копировать код. Если потребуется изменить способ хранения (например, перейти на сервер) — нужно менять компонент. Это не масштабируется!
3. Сервисы решают проблемы архитектуры
Преимущества сервисов
- Переиспользуемость: Один сервис — много компонентов. Логику можно использовать где угодно.
- Тестируемость: Тестировать сервисы проще, чем компоненты (нет зависимости от шаблонов и UI).
- Единое хранилище: Сервис может выступать как "Single Source of Truth" — все компоненты видят одни и те же данные.
- Разделение ответственности: Компонент становится "тонким" и простым, а вся тяжёлая работа уходит в сервис.
Схема взаимодействия
+-------------------+ +----------------------+
| Компонент 1 | <-----> | Сервис |
+-------------------+ +----------------------+
^
+-------------------+ /
| Компонент 2 | <------+
+-------------------+
4. Как создать сервис в Angular
Создать сервис очень просто (и даже проще, чем сварить кофе!):
1. Обычный TypeScript-класс
export class TodoService {
private todos: Todo[] = [];
getTodos() {
return this.todos;
}
// ...другие методы
}
2. @Injectable() — магия DI
Чтобы Angular мог "внедрять" сервис в компоненты, добавьте декоратор @Injectable():
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // сервис будет синглтоном на всё приложение
})
export class TodoService {
// ...
}
3. Использование в компоненте
В компоненте сервис "подключается" через конструктор:
import { TodoService } from './todo.service';
@Component({ /* ... */ })
export class TodoListComponent {
constructor(private todoService: TodoService) {}
get todos() {
return this.todoService.getTodos();
}
addTodo(text: string) {
this.todoService.addTodo(text);
}
}
Теперь компонент ничего не знает о том, как именно хранятся задачи — он просто вызывает методы сервиса.
5. Пример: Перенос логики из компонента в сервис
Давайте вынесем всю работу с задачами из компонента в сервис.
todo.service.ts
import { Injectable } from '@angular/core';
export interface Todo {
text: string;
done: boolean;
}
@Injectable({
providedIn: 'root'
})
export class TodoService {
private todos: Todo[] = [];
constructor() {
this.loadTodos();
}
getTodos(): Todo[] {
return this.todos;
}
addTodo(text: string) {
this.todos.push({ text, done: false });
this.saveTodos();
}
removeTodo(index: number) {
this.todos.splice(index, 1);
this.saveTodos();
}
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
loadTodos() {
const saved = localStorage.getItem('todos');
this.todos = saved ? JSON.parse(saved) : [];
}
}
todo-list.component.ts
import { Component } from '@angular/core';
import { TodoService, Todo } from './todo.service';
@Component({
selector: 'app-todo-list',
template: `
<ul>
<li *ngFor="let todo of todos; let i = index">
{{ todo.text }}
<button (click)="remove(i)">Удалить</button>
</li>
</ul>
<input [(ngModel)]="newTodo" placeholder="Новая задача" />
<button (click)="add()">Добавить</button>
`
})
export class TodoListComponent {
newTodo = '';
constructor(private todoService: TodoService) {}
get todos(): Todo[] {
return this.todoService.getTodos();
}
add() {
if (this.newTodo.trim()) {
this.todoService.addTodo(this.newTodo.trim());
this.newTodo = '';
}
}
remove(index: number) {
this.todoService.removeTodo(index);
}
}
Теперь компонент стал тонким и простым — он только отображает и делегирует действия сервису.
6. Полезные нюансы
Когда сервис обязателен, а когда — нет?
Обязательно используйте сервисы, если:
- Логику нужно переиспользовать в нескольких компонентах.
- Данные должны быть общими для нескольких частей приложения (например, корзина покупок, профиль пользователя).
- Работа с сервером (HTTP-запросы, WebSocket и т.д.).
- Хранение состояния между переходами по страницам (роутинг).
Можно обойтись без сервиса, если:
- Логика очень простая и специфична только для одного компонента.
- Нет необходимости делиться данными или функционалом с другими частями приложения.
- Вы пишете прототип или "игрушечный" пример (но даже тут сервисы часто полезны для тренировки правильной архитектуры).
Сервисы — это не только про данные
Сервисы часто используются не только для хранения данных, но и для:
- Работы с сервером: отдельный сервис для HTTP-запросов.
- Авторизации: сервис для хранения и проверки токена пользователя.
- Логирования: сервис для вывода сообщений в консоль или отправки на сервер.
- Валидации: сервис для проверки данных формы.
- Вспомогательных функций: форматирование дат, чисел, конвертация валют и т.д.
Как сервисы работают "под капотом": инъекция зависимостей
Angular использует механизм Dependency Injection (DI) — внедрение зависимостей. Это значит, что вы не создаёте сервис через new, а просто объявляете его в конструкторе. Angular сам создаёт экземпляр сервиса и "вкладывает" его в компонент. Это позволяет делать сервисы синглтонами (один экземпляр на всё приложение), а также удобно тестировать и подменять сервисы при необходимости.
Факт для любознательных:
Если вы укажете providedIn: 'root' в декораторе @Injectable(), сервис будет создан единожды на всё приложение (singleton). Если укажете в массиве providers компонента — сервис будет создан отдельно для каждого экземпляра компонента.
7. Типичные ошибки при работе с сервисами
Если часть бизнес-логики осталась в компоненте, а часть ушла в сервис — возникает путаница и дублирование. Лучше вынести всю "тяжёлую работу" в сервис, а компонент оставить только для отображения и вызова методов.
Сервис создаётся вручную через new. Это ломает систему DI! Никогда не делайте const s = new MyService(). Используйте только внедрение через конструктор.
Сервис не декорирован @Injectable() или не указан providedIn. Без этого Angular не сможет создать и внедрить сервис автоматически.
Дублирование сервисов в разных модулях/компонентах. Если вы случайно добавите сервис в providers разных компонентов, получите несколько экземпляров (и разные данные). Обычно сервисы должны быть синглтонами (providedIn: 'root').
Сервис хранит состояние, но вы ожидаете, что оно "сбрасывается" при переходе между страницами. Синглтон-сервисы живут всё время работы приложения. Если нужно сбрасывать состояние — делайте это явно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ