Сервисы: где жить логике
Сервис — это класс без шаблона, куда переезжает вся логика, не относящаяся к отрисовке: запросы, расчёты, общее состояние.
«Компонент должен заниматься видом. Как только он начинает считать налоги и ходить в сеть — пора звать сервис».
Компоненты отвечают за интерфейс. Но куда деть бизнес-логику — загрузку товаров, корзину, авторизацию? Если запихнуть всё в компонент, он разбухнет и станет неповторимо уникальным. Решение — сервисы: обычные TypeScript-классы, которые инкапсулируют логику и данные, и которые можно переиспользовать в любом компоненте.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartService {
private items: string[] = [];
add(product: string) { this.items.push(product); }
getItems() { return this.items; }
get total() { return this.items.length; }
}
Декоратор @Injectable помечает класс как доступный для внедрения зависимостей. Опция providedIn: 'root' говорит: создай один общий экземпляр на всё приложение. Именно «один экземпляр» делает сервис идеальным местом для общего состояния — корзина одна, и все компоненты видят одни и те же товары.
Как работает под капотом
providedIn: 'root' регистрирует сервис в корневом инжекторе. Angular создаёт экземпляр лениво — только когда его впервые попросят, — и кеширует. Все последующие запросы получают тот же объект (паттерн «синглтон»). Это же включает tree-shaking: если сервис нигде не используется, сборщик выкинет его из бандла.
корневой инжектор
|
первый запрос CartService
|
создан 1 экземпляр ----+
| |
компонент A ----+ | один и тот же
компонент B ----+------+ объект
компонент C ----+
Частые ошибки
- Хранить состояние в компоненте, а не в сервисе. Тогда при уничтожении компонента данные теряются.
- Создавать сервис через
new CartService(). Так вы получите отдельный экземпляр мимо DI — потеряете синглтон и зависимости. - Мешать вид и логику. HTTP-запросы и расчёты в шаблоне или обработчике клика — признак того, что нужен сервис.
Best practices
- По умолчанию используйте
providedIn: 'root'— это просто и поддерживает tree-shaking. - Один сервис — одна зона ответственности:
CartService,AuthService,ProductApiService. - Скрывайте внутренние данные через
private, отдавайте наружу методы — это защищает инварианты.
Итоги. Сервис — класс с логикой и состоянием, помеченный @Injectable. providedIn: 'root' даёт единый синглтон на приложение. Дальше разберём, как сервис попадает в компонент — через внедрение зависимостей.
Закрепляем
Главный принцип урока — разделение ответственности. Компоненты отвечают за то, что видит пользователь; сервисы — за то, как устроена логика и где живут данные. Как только в компоненте появляется бизнес-расчёт, запрос в сеть или состояние, которое должно пережить этот компонент, — это сигнал вынести логику в сервис. Так компоненты остаются тонкими и сменными, а ценная логика — переиспользуемой и тестируемой в одном месте.
Опция providedIn: 'root' делает сервис синглтоном — единственным экземпляром на всё приложение, который создаётся лениво при первом обращении и затем переиспользуется. Именно эта «единственность» превращает сервис в естественное хранилище общего состояния: одна корзина, один сервис авторизации, один кеш. Бонусом идёт tree-shaking: если сервис нигде не используется, сборщик просто выкинет его из финального бандла, и он не утяжелит приложение. Это сочетание удобства и эффективности и сделало providedIn: 'root' выбором по умолчанию.
| Элемент | Роль |
|---|---|
| @Injectable | Помечает класс как внедряемый |
| providedIn: 'root' | Синглтон на всё приложение |
| private поля | Скрытое состояние сервиса |
| публичные методы | Контролируемый доступ к логике |