JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /@Injectable(): создание инжектируемого сервиса

@Injectable(): создание инжектируемого сервиса

Модуль 4: Node.js, Next.js и Angular
15 уровень , 1 лекция
Открыта

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 не сможет создать их и выбросит ошибку. Стройте архитектуру так, чтобы не было циклов.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ