Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Сервіси та впровадження залежностей в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Сервіси та впровадження залежностей (DI) в Angular**: сервіс - це `@Injectable`-клас зі спільною логікою, а DI автоматично передає екземпляри через конструктор або `inject()`. ```typescript @Injectable({ providedIn: 'root' }) export class UserService { constructor(private http: HttpClient) {} getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users'); } } // У компоненті: constructor(private userService: UserService) {} ``` **Ключове:** ніякого `new UserService()` у компонентах - Angular резолвить залежності сам.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Сервіси та впровадження залежностей (dependency injection, DI) в Angular** працюють у парі: сервіс - це `@Injectable`-клас зі спільною логікою, а DI доставляє екземпляри компонентам автоматично через параметри конструктора або функцію `inject()`, без жодного ручного `new`. ## Теорія ### TL;DR - Сервіс (service) - звичайний клас з `@Injectable`, де живе логіка, спільна для кількох компонентів: HTTP-запити, стан, валідація - DI передає екземпляри автоматично: ніякого `new ServiceName()` у компонентах - `@Injectable({ providedIn: 'root' })` створює один синглтон на весь застосунок, tree-shakable за замовчуванням - Інжектори ієрархічні: кореневий, модульний, компонентний; дочірні успадковують від батьківського - Правило вибору: логіка потрібна в 2+ компонентах - виноси в сервіс; стан лише одного компонента - залишай у компоненті ### Швидкий приклад ```typescript // user.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) // один екземпляр на весь застосунок export class UserService { constructor(private http: HttpClient) {} // Angular сам передає HttpClient getUsers(): Observable<User[]> { return this.http.get<User[]>('/api/users'); } } // user-list.component.ts @Component({ selector: 'app-user-list', template: '...' }) export class UserListComponent implements OnInit { constructor(private userService: UserService) {} // Angular резолвить це ngOnInit() { this.userService.getUsers().subscribe(users => this.users = users); } } ``` `UserService` ніде не викликає `new HttpClient()`. Компонент ніколи не викликає `new UserService()`. Інжектор Angular читає типи параметрів конструктора під час виконання і передає готові екземпляри. Ось і вся ідея. ### Навіщо потрібен DI Без DI компонент, якому потрібні HTTP-запити, або жорстко прошивав би HTTP-налаштування всередині себе, або сам створював `UserService`. Замінити це в тесті означало б переписувати компонент. З DI достатньо одного рядка в `TestBed.configureTestingModule`: ```typescript providers: [{ provide: UserService, useValue: mockUserService }] ``` Компонент не чіпаємо. DI з'явився заради тестованості та гнучкості, а не заради магії. ### Як інжектор розв'язує залежності Інжектори Angular утворюють дерево. Вгорі - кореневий інжектор, що створюється при завантаженні застосунку. Нижче - модульні, потім компонентні. Коли компонент запитує сервіс, Angular піднімається деревом, поки не знайде відповідний провайдер. `@Injectable({ providedIn: 'root' })` реєструє сервіс на рівні кореня. Один екземпляр, спільний для всіх. Lazy-завантажені модулі отримують окрему гілку дерева: сервіси, оголошені в `providers: []` такого модуля, ізольовані в цій гілці. Провайдери на рівні компонента (`providers: [SomeService]` в `@Component`) створюють новий екземпляр для того компонента і його дочірніх, який знищується разом з компонентом. Прапор TypeScript `emitDecoratorMetadata` забезпечує всю цю механіку. На етапі компіляції декоратори записують метадані типів параметрів у клас. Під час виконання інжектор читає `design:paramtypes` і використовує сам конструктор класу як токен. Жодних рядкових ідентифікаторів. ### Три способи реєстрації сервісів ```typescript // 1. Кореневий синглтон - найпоширеніший, tree-shakable @Injectable({ providedIn: 'root' }) export class AuthService {} // 2. Компонентний скоп - новий екземпляр для піддерева компонента @Component({ selector: 'app-checkout', providers: [CartService] // ізольований, знищується разом з компонентом }) export class CheckoutComponent {} // 3. InjectionToken для значень, що не є класами export const API_URL = new InjectionToken<string>('API_URL'); // реєстрація providers: [{ provide: API_URL, useValue: 'https://api.example.com' }] // споживання private apiUrl = inject(API_URL); ``` ### Типи провайдерів | Провайдер | Що робить | Типове застосування | |---|---|---| | `useClass` | Підставляє інший клас | Замінити `ConsoleLogger` на `FileLogger` | | `useValue` | Надає статичне значення | Константи конфігурації, feature flags | | `useFactory` | Створює екземпляр через функцію | Потрібна конфігурація під час виконання | | `useExisting` | Псевдонім для іншого токена | Перейменування сервісу зі збереженням зворотної сумісності | ### Функція `inject()` Angular 14 додав `inject()` як альтернативу ін'єкції через конструктор. Вона працює в ініціалізаторах полів і добре підходить для standalone-компонентів. ```typescript import { inject } from '@angular/core'; @Component({ selector: 'app-dashboard', template: '...' }) export class DashboardComponent { private userService = inject(UserService); private apiUrl = inject(API_URL); // блок конструктора не потрібен } ``` Ін'єкція через конструктор повністю робоча і поширена в старіших кодових базах. Обидва підходи коректні. Команди зазвичай обирають один стиль на проект і дотримуються його. ### Типові помилки На практиці помилка з рівнем скопу зустрічається частіше за всі інші: сервіси, які мали б жити на рівні компонента, потрапляють у root, і стан починає витікати між представленнями, де він мав бути ізольованим. **Забули зареєструвати сервіс:** ```typescript @Injectable() // немає providedIn - ніде не зареєстровано export class DataService {} // NullInjectorError: No provider for DataService! ``` Виправлення: `@Injectable({ providedIn: 'root' })` для синглтона, або додати до `providers: []` потрібного модуля чи компонента. **Обійти DI через `new`:** ```typescript @Component({...}) export class MyComponent { private service = new DataService(/* що тут передавати? */); } ``` `DataService` потребує `HttpClient`. Довелося б самостійно його інстанціювати разом із усім HTTP-стеком. Нетестовано. Використовуй ін'єкцію через конструктор або `inject()`. **Циклічна залежність:** ```typescript // user.service.ts constructor(private auth: AuthService) {} // auth.service.ts constructor(private user: UserService) {} // A потребує B, B потребує A // Cannot instantiate cyclic dependency! ``` Виправлення: виноси спільну логіку в третій сервіс. Або роби ліниву ін'єкцію через `inject()` всередині методу, а не при ініціалізації класу. **Випадкове перевизначення провайдера:** якщо два провайдери реєструються для одного токена в одному інжекторі, останній виграє без жодного попередження. Корисно для навмисного мокування, але може дивувати при злитті модулів. ### Де зустрічається в реальних проектах - `HttpClient` - сам по собі DI-сервіс, надається через `provideHttpClient()` у standalone-застосунках - `MatDialog` з Angular Material - кореневий сервіс для відкриття модальних вікон - `Store` з NgRx ін'єктується в будь-який компонент або effect, що читає стан - `Firestore` з AngularFire використовує ту саму схему для роботи з базою даних - У NX monorepo-проектах генератор одразу додає `providedIn: 'root'` до кожного нового сервісу ### Питання на співбесіді **Q:** Яка різниця між `providedIn: 'root'` і `providers: []` на рівні модуля? **A:** `providedIn: 'root'` реєструє один екземпляр на весь застосунок і є tree-shakable: якщо сервіс ніхто не ін'єктує, він не потрапляє в бандл. `providers: []` на рівні модуля обмежує сервіс інжектором цього модуля; при lazy-завантаженні кожен модуль отримує свій екземпляр, якщо оголошує провайдер у себе. **Q:** Як Angular зіставляє параметри конструктора з провайдерами без рядкових ідентифікаторів? **A:** TypeScript з прапором `emitDecoratorMetadata: true` записує інформацію про типи параметрів у метадані `design:paramtypes` класу. Інжектор читає їх під час виконання і використовує сам конструктор класу як токен. **Q:** Як працює ієрархія інжекторів при lazy-завантаженні модуля? **A:** Lazy-модуль отримує дочірній інжектор, що успадковує від кореневого. Сервіси, оголошені в `providers: []` lazy-модуля, ізольовані в цій гілці. Сервіс з `providedIn: 'root'` завжди один і той самий екземпляр, незалежно від місця ін'єкції. **Q:** Як мокувати сервіс у юніт-тестах? **A:** Передай мок через `providers: [{ provide: UserService, useValue: { getUsers: () => of([]) } }]` у `TestBed.configureTestingModule`. Компонент отримає мок замість реального сервісу. Щоб отримати той самий екземпляр у тілі тесту, використовуй `TestBed.inject(UserService)`. **Q:** (Senior) Коли варто використовувати `useFactory` з `deps` замість `useClass`? **A:** Коли екземпляру сервісу потрібні значення, недоступні на рівні визначення класу. Наприклад, логер із префіксом, унікальним для кожного feature-модуля, або HTTP-інтерцептор, що читає конфігурацію, передану при bootstrap. Масив `deps` вказує Angular, які провайдери потрібно розв'язати і передати аргументами у фабричну функцію. ## Приклади ### Базовий: ін'єкція через конструктор з обробкою помилок ```typescript // user.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class UserService { private usersUrl = 'https://jsonplaceholder.typicode.com/users'; constructor(private http: HttpClient) {} getUsers(): Observable<User[]> { return this.http.get<User[]>(this.usersUrl).pipe( catchError((error: HttpErrorResponse) => throwError(() => new Error(`Помилка завантаження: ${error.message}`)) ) ); } } // user-list.component.ts @Component({ selector: 'app-user-list', template: ` <ul *ngIf="users.length"> <li *ngFor="let user of users">{{ user.name }}</li> </ul> <p *ngIf="error">{{ error }}</p> ` }) export class UserListComponent implements OnInit { users: User[] = []; error = ''; constructor(private userService: UserService) {} ngOnInit() { this.userService.getUsers().subscribe({ next: (users) => this.users = users, error: (err) => this.error = err.message }); } } // Завантажує з JSONPlaceholder API, показує імена або повідомлення про помилку ``` Сервіс обробляє HTTP-помилки в одному місці. Кожен компонент, що викликає `getUsers()`, отримує однакову обробку помилок без дублювання `catchError`. ### Середній рівень: компонентний скоп сервісу з `inject()` ```typescript // form-state.service.ts - скопований до піддерева checkout-компонента @Injectable() // без providedIn - буде надано на рівні компонента export class FormStateService { private formData: Partial<Order> = {}; update(data: Partial<Order>) { this.formData = { ...this.formData, ...data }; } get(): Partial<Order> { return this.formData; } } // checkout.component.ts @Component({ selector: 'app-checkout', template: '<app-address-form /><app-payment-form />', providers: [FormStateService] // новий екземпляр, знищується разом з компонентом }) export class CheckoutComponent { private state = inject(FormStateService); } // address-form.component.ts - дочірній компонент отримує той самий екземпляр @Component({ selector: 'app-address-form', template: '...' }) export class AddressFormComponent { private state = inject(FormStateService); // той самий екземпляр, що в CheckoutComponent } ``` `FormStateService` ділиться станом між `CheckoutComponent` і його дочірнім `AddressFormComponent`, але інша сесія оформлення замовлення в іншому місці застосунку отримає зовсім окремий екземпляр. Ось компонентний скоп інжектора на практиці. ### Просунутий рівень: factory provider з InjectionToken ```typescript // logger.service.ts @Injectable({ providedIn: 'root' }) export class LoggerService { prefix = ''; log(msg: string) { console.log(`${this.prefix}${msg}`); } } // feature.module.ts import { InjectionToken, NgModule } from '@angular/core'; import { LoggerService } from './logger.service'; export const FEATURE_LOGGER = new InjectionToken<LoggerService>('FeatureLogger'); @NgModule({ providers: [{ provide: FEATURE_LOGGER, useFactory: (rootLogger: LoggerService) => { const logger = new LoggerService(); logger.prefix = '[Feature] '; return logger; // окремий екземпляр з власним префіксом }, deps: [LoggerService] // бере кореневий LoggerService як аргумент фабрики }] }) export class FeatureModule {} // feature.component.ts @Component({ template: '<p>Перевір консоль</p>' }) export class FeatureComponent { constructor(@Inject(FEATURE_LOGGER) private logger: LoggerService) { this.logger.log('Завантажено'); // виводить "[Feature] Завантажено" } } // Кореневий LoggerService скрізь виводить без префікса ``` Фабрика створює окремий логер для feature-модуля. Вона отримує `LoggerService` з батьківського інжектора через `deps` і кастомізує власну копію. Кореневий логер не змінюється. Саме так Angular Material та інші бібліотеки адаптують поведінку для кожного lazy-модуля.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.