Внедрение зависимостей: inject() и конструктор
Вы не создаёте зависимости сами — вы их запрашиваете, а Angular приносит готовые. Это и есть внедрение зависимостей.
«Не звоните нам — мы позвоним вам». Компонент не строит сервисы, он лишь объявляет, что они нужны».
Внедрение зависимостей (DI) — сердце Angular. Идея проста: вместо того чтобы компонент сам создавал нужные ему сервисы, он объявляет зависимости, а специальный инжектор подставляет готовые экземпляры. Это снижает связанность: компонент не знает, как именно собран сервис, и его легко подменить в тестах.
Современный способ запросить зависимость — функция inject():
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-buy-button',
standalone: true,
template: `
<button (click)="onBuy()">Купить</button>
<span>В корзине: {{ cart.total }}</span>
`,
})
export class BuyButtonComponent {
private cart = inject(CartService); // запросили — получили
onBuy() { this.cart.add('Кофемолка'); }
}
Альтернатива — классическое внедрение через конструктор, оно тоже работает:
export class BuyButtonComponent {
constructor(private cart: CartService) {}
}
Оба варианта эквивалентны: Angular видит тип CartService, находит его в инжекторе и подставляет тот самый общий экземпляр. inject() предпочтительнее в современном коде — он гибче и работает в большем числе контекстов.
Как работает под капотом
Инжектор — это, по сути, реестр: карта «токен → как создать экземпляр». Когда компонент просит CartService, инжектор смотрит, есть ли уже созданный экземпляр; если нет — создаёт, разрешая попутно его собственные зависимости, кеширует и отдаёт. Инжекторы образуют иерархию: запрос идёт вверх по дереву, пока не найдётся провайдер.
inject(CartService)
|
v
ИНЖЕКТОР: есть экземпляр? --да--> вернуть кешированный
|
нет
|
создать new CartService(...зависимости...) -> кеш -> вернуть
Запускаемая врезка: DI-контейнер на Map
Сделаем игрушечный инжектор, чтобы увидеть суть. «Попробуй сам ▶».
// мини DI-контейнер: реестр фабрик + кеш синглтонов
function createInjector() {
const factories = new Map();
const cache = new Map();
return {
provide(token, factory) { factories.set(token, factory); },
inject(token) {
if (cache.has(token)) return cache.get(token); // синглтон
const factory = factories.get(token);
if (!factory) throw new Error('Нет провайдера: ' + token);
const instance = factory(this); // фабрика может звать inject
cache.set(token, instance);
return instance;
},
};
}
const di = createInjector();
di.provide('CartService', () => ({ items: [], add(x){ this.items.push(x); } }));
const a = di.inject('CartService');
const b = di.inject('CartService');
a.add('Кофемолка');
console.log('один экземпляр?', a === b); // true
console.log('видно из b:', b.items); // ["Кофемолка"]
Частые ошибки
- Звать
inject()вне контекста. Его можно вызывать в инициализаторах полей и конструкторе, но не внутри произвольного метода. - Циклические зависимости. A зависит от B, B — от A: инжектор не сможет разрешить.
- Внедрять то, что не помечено
@Injectableи не имеет провайдера.
Best practices
- В новом коде предпочитайте
inject()— он лаконичнее и работает в функциях вроде гвардов и резолверов. - Делайте поля с зависимостями
private, если они не нужны в шаблоне. - Не злоупотребляйте локальными провайдерами — большинству сервисов хватает
providedIn: 'root'.
Итоги. DI избавляет компонент от создания зависимостей: он их объявляет, инжектор подставляет. inject(Service) — современный способ, конструктор — классический. Под капотом инжектор кеширует синглтоны и разрешает их по иерархии. Дальше — провайдеры и токены.
Закрепляем
Внедрение зависимостей переворачивает привычный порядок: вместо того чтобы компонент сам создавал нужные ему объекты, он лишь объявляет, что они нужны, а инжектор приносит готовые. Этот «принцип Голливуда» — «не звоните нам, мы позвоним вам» — снижает связанность кода. Компонент не знает и не должен знать, как именно собран сервис и какие у того свои зависимости; он просто получает рабочий экземпляр. А значит, в тестах сервис легко подменить на фейковый, не трогая компонент.
Запомните два равноценных способа запросить зависимость. Функция inject(Service) — современный, лаконичный и более гибкий вариант, который работает не только в классах, но и в функциях вроде гвардов маршрутов и резолверов. Внедрение через параметр конструктора — классический способ, знакомый по старому коду. Оба дают один и тот же экземпляр из инжектора, поэтому выбор между ними — вопрос стиля; в новом коде стоит предпочитать inject(). Под капотом же инжектор просто ведёт реестр и кеширует синглтоны, разрешая запросы по иерархии снизу вверх.
| Способ | Когда использовать |
|---|---|
| inject(Service) | Современный код, функции, гварды |
| constructor(private s: Service) | Классический стиль |
| Инжектор | Реестр и кеш экземпляров |