Что такое сервисы и для чего они нужны?

Сервисы в Angular — это классы, предназначенные для инкапсуляции и переиспользования логики, которая не связана напрямую с отображением. Они позволяют разделить бизнес-логику приложения и представление (компоненты), обеспечивая повторное использование, тестируемость и слабую связанность между частями приложения. Сервисы — важный элемент архитектуры Angular, особенно в больших и модульных проектах.

1. Назначение сервисов

Сервисы решают задачи, которые не должны обрабатываться компонентами:

  • Получение данных с сервера (HTTP-запросы);

  • Работа с кешем, localStorage, cookies;

  • Обмен данными между компонентами;

  • Управление состоянием;

  • Авторизация и аутентификация;

  • Логирование, уведомления, модальные окна;

  • Обработка бизнес-логики;

  • Вспомогательные утилиты (форматирование, преобразования и пр.).

2. Создание сервиса

Сервис — обычный TypeScript-класс с декоратором @Injectable(). Он регистрируется в системе внедрения зависимостей (DI) Angular.

Пример:

import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class LoggerService {
log(message: string) {
console.log('LOG:', message);
}
}

Декоратор @Injectable() сообщает Angular, что этот класс может иметь зависимости, и его можно инжектировать в другие классы.

3. Регистрация сервиса: providedIn

Существует два способа зарегистрировать сервис:

3.1. В декораторе

@Injectable({ providedIn: 'root' })

Angular создаст singleton-экземпляр и сделает его доступным во всём приложении. Это предпочтительный и современный способ.

3.2. В модуле

Альтернативный способ — вручную зарегистрировать сервис в providers:

@NgModule({
providers: \[MyService\]
})
export class AppModule {}

Можно также ограничить область действия, зарегистрировав сервис только в определённом модуле или компоненте.

4. Внедрение сервиса (Dependency Injection)

Для использования сервиса в компоненте (или в другом сервисе), его нужно инжектировать через конструктор:

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-root',
template: \`<h1>Пример</h1>\`
})
export class AppComponent {
constructor(private logger: LoggerService) {
this.logger.log('Приложение запущено');
}
}

Angular сам создаёт экземпляр сервиса и передаёт его в компонент. Это называется внедрением зависимостей (DI).

5. Область действия сервиса

Сервис по умолчанию — singleton во всей области действия, в которой он зарегистрирован:

  • providedIn: 'root' — глобальный singleton;

  • providedIn: SomeModule — один экземпляр на модуль;

  • Регистрация в providers компонента — создаёт новый экземпляр для каждого экземпляра компонента.

Пример локального сервиса:

@Component({
providers: \[MyService\]
})
export class SomeComponent { }

6. Работа с HTTP-запросами в сервисах

Один из главных кейсов использования сервисов — работа с REST API через HttpClient.

import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get<User\[\]>('/api/users');
}
createUser(user: User) {
return this.http.post('/api/users', user);
}
}

Компонент:

constructor(private api: ApiService) {}
ngOnInit() {
this.api.getUsers().subscribe(users => this.users = users);
}

7. Реактивность в сервисах: Subject / BehaviorSubject

Сервисы могут использовать RxJS для управления состоянием и обмена данными между компонентами.

@Injectable({ providedIn: 'root' })
export class MessageService {
private message$ = new BehaviorSubject<string>('Начальное сообщение');
get messageStream$() {
return this.message$.asObservable();
}
updateMessage(msg: string) {
this.message$.next(msg);
}
}

Использование:

this.messageService.updateMessage('Новое сообщение');
this.messageService.messageStream$.subscribe(msg => {
console.log(msg);
});

8. Использование сервисов внутри сервисов

Сервисы могут зависеть друг от друга:

@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private logger: LoggerService) {}
login() {
this.logger.log('Пользователь вошёл');
}
}

9. Интерфейсы и типизация в сервисах

Хорошая практика — использовать типы и интерфейсы для описания данных:

export interface User {
id: number;
name: string;
}
getUser(id: number): Observable<User> {
return this.http.get<User>(\`/api/users/${id}\`);
}

10. Примеры типов сервисов

  • ApiService — обёртка над HttpClient;

  • AuthService — аутентификация, хранение токенов, логин/логаут;

  • UserService — работа с пользователями;

  • CartService — корзина покупок;

  • StateService — управление состоянием;

  • ModalService — управление модальными окнами;

  • NotificationService — отображение уведомлений (тосты, алерты);

  • LoggerService — логирование ошибок и действий.

11. Тестирование сервисов

Сервисы легко тестируются отдельно от компонентов.

Пример:

describe('LoggerService', () => {
let service: LoggerService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LoggerService);
});
it('должен логировать сообщение', () => {
spyOn(console, 'log');
service.log('Test');
expect(console.log).toHaveBeenCalledWith('LOG:', 'Test');
});
});

12. Отличие от компонентов

Компонент Сервис
Имеет шаблон Да Нет
--- --- ---
Отображает UI Да Нет
--- --- ---
Логика Только UI-логика Бизнес-логика, данные
--- --- ---
Внедрение Через @Component Через @Injectable()
--- --- ---
Жизненный цикл Зависит от шаблона Singleton (по умолчанию)
--- --- ---

13. Использование сервисов с InjectionToken

Для создания абстракций или подмены сервисов можно использовать InjectionToken.

export const API_URL = new InjectionToken<string>('API_URL');
@NgModule({
providers: \[
{ provide: API_URL, useValue: 'https://api.example.com' }
\]
})

14. Lazy-loading и сервисы

Если сервис используется в ленивом модуле и зарегистрирован в providers этого модуля, он будет иметь свой экземпляр. Это позволяет реализовать scoped-сервисы.

15. Жизненный цикл сервисов

Сервисы живут столько, сколько живёт область, в которой они зарегистрированы:

  • providedIn: 'root' — пока работает всё приложение;

  • В providers модуля — пока работает модуль;

  • В providers компонента — пока существует компонент.

Это позволяет контролировать расход памяти и количество инстансов.