Галоўная / Артыкулы

Як Angular практыкуе ввод залежнасцей для адрэсавання службаў за дапамою inject()

З'ясавайце, як Angular DI апранае службы через метод inject(), элемент providers і іерархічных інжектароў, а таксама як контролюваць дыяпазон экзантраў і ухіліцца ад памылак з-за відсутнасці прадастальніка.

Выдавец: REACTAPP.TOP12 хв чытання2666 слоў

Крыніцы: Angular 09: Dependency Injection in Angular

Калі вы пачатаете пераносіць логіку з компонентаў Angular у сервісы, выклікаецца практычны вопыт: калі компонент запісвае inject(UserService), звядзе на самай працоўцы яны ёсць, хто яго стварыў і які экземпляр отрымае компонент? Адказ — цэе система внутрашняя інжектавання залежнасцей (DI) Angular. У гідзе паспакоўваецца, што такое DI, як працююць inject() і прадастальнікі, як іерархічныя інжектары выбіраюць экземпляр, які вы отрымаеце, і як ухіліцца ад найпашыльнейшых памылак пачатківаўца, ўжо тады, калі вы зможаце адважна дзеліцца сервісамі чыста ў своіх прыкладных програмах.

Ёсць рэч, якая зазвычай выклікае гэты вопыт:

userService = inject(UserService);

Нічога ў самым компоненте не стварае UserService, пры тым поле ўсё-такі мае готовы до вжывання об’ект. Яго запроставіла система DI Angular.

Чаму компоненты не должны ствараць своія сабе сервісы

Вочыяе альтэрнатыва — стварыць сама службу ўнутрэй компаненту:

userService = new UserService();

Гэта працюе, але такі падход прыкрепляе компанент да аднаго конкрэтнага класу і да аднаго спосабу яго стварэння. Тепер компанент сам вяршыць рашэнні пра тое, як ствараецца служба, што ёй патрэбна для функцыонавання і чы робіцца новая копія ў кожны раз. Якщо пазней службе знадобяцца савастныя залежнасці, кожна частка коду, якая выклекае new, таксама павінна быць зменена.

Дынамічна інжынерія залежнасцэй перакладае гэту адпаведальнасць. Компанент толькі заявляе, што ў яго патрэбен UserService. Angular знаходзіць адпаведны прадастальнік, стварае екземпляр або воспалоўвае існуючы і передае яго:

Component
    │
    │ "I need UserService"
    ↓
Angular DI System
    │
    ↓
UserService

Два слова ў назе точна апісваюць гэта. Залежнасць — это все, што класу патрэбна для выканання свайго завадзка. Ін’екцыя означае, што фрэймворк доставляе гэтую залежнасць класу, у працоўны час не заставляючы клас сам выкарыстоваць або стварыць яе.

Першая ін’екцыя, ад пачатку да канца

Пачніце з маленькага сервісу. Декоратар @Injectable пазначае клас як тое, што можа стварыць система DI Angular, а providedIn: 'root' рэгіструе яго ў всій аплікацыі:

import { Injectable } from '@angular/core';
@Injectable({
  providedIn: 'root'
})
export class UserService {
  getUserName() {
    return 'Rahul';
  }
}

Компонент запрашвае сервіс за дапамою inject(), які імпортуецца з @angular/core разам з класам Component:

import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {
  userService = inject(UserService);
}

Калі Angular стварае клас App, ён вырахоўвае гэты выраз:

inject(UserService)

Angular чытае гэта як «гэты клас патрабуе UserService», вырашвае залежнасць і прызначае рэзультат полю. Пакалі поле ў явнам доступе, шаблон можа безпасебна вызваць службу:

<h1>{{ userService.getUserName() }}</h1>

Зверніце ўвагу, чаго не хапяе ў компоненте. Нідзе нема вызову канструэйшну:

new UserService()

Стварэнне і передача экзанпляра — це абсалютна задача Angular.

Работа з inject()

inject() — это савэцкая API для запрашання залежнасці з системы DI Angular. Вы передаеце ёй токен, часта сам клас службы, і отрымляеце экзанпляр:

userService = inject(UserService);

Функцыя выйшла з основнага пакета Angular:

import { Component, inject } from '@angular/core';

Калі поле прызначаецца ў тэле класа:

userService = inject(UserService);

кожны метод у класе можа доступаць да службы через this:

this.userService

Напрыклад, метод, який чытае імя та фіксуе йое:

loadUser() {
  const name = this.userService.getUserName();
  console.log(name);
}

Адна правіла, якую трэба памятаць: inject() працюе толькі ў контэксте інжэкцыі. У практыцы гэта значыць ініцыялізаторы поль, канстрактар, а таксама калькі месца, каліраваных фрэймворкам, такія як функцыі-фабрыкі та функцыйнальныя захоўнікі. Яе вызов пазней, напрыклад у обробчыку кліка або кале-бэкфункцыі setTimeout, выклікае адказку. Захавайте залежнасць у полі, а потым выкарыстоўваеце гэта поле там, дзе яго трэба.

inject() у порываў з інжэкцыяй через канстрактар

Старэе навучальныя матэрыялы та багато існуючых кодавых базаў запрашваюць залежнасці через параметры канстрактара:

constructor(private userService: UserService) {}

Гэты стыль, базаваны на канстрактарах, яшчэ цяпер падтрымваны на 100%. Адпаведны варіянт, базаваны на полях, выглядае так:

userService = inject(UserService);

Оба формы взаімаюцься з той самым системай DI і даюць аднаковы рэзультат. Для новага коду inject() зазвычай ўсё простае, лепа пасваяецца да самастоятных компанентоў і іншых савэцкіх API Angular, а таксама дазволяе ухіліцца ад дужо дугіх спіса параметраў канстрактара, што стае пометным пад час викорыстання спадчынства. Тыповы клас у канцэ выглядае так:

export class App {
  userService = inject(UserService);
}

Аднак сынтаксіс — гэта найменш важлівая частка. Галоўнае — гэта прынцып: компанент отрымае сваю залежнасць ад Angular, а не стварае ёю сам.

Прадастальнікі: як паведаміць Angular, як запастыць залежнасць

Запрос залежнасці можа працаваць толькі тады, калі Angular знае, як ёю стварыць. Гэтыя ведамасці знаходзяцца ў прадастальніку. Найбольш распашчытаны спосаб прызначэння прадастальніка — за дапамогою декоратара @Injectable:

@Injectable({
  providedIn: 'root'
})
export class UserService {
}

Ключовы параметр — гэта ўсьмо тая адна лінія:

providedIn: 'root'

У яго рэгіструецца UserService у корневым інжектары працоўкі. З таго часу любы запит, падобны да наступнага, можа быць задоволены:

inject(UserService)

Прыемны спосаб прыкладзіць сэрвіс-прадаўцу – це як рэцепт, які Angular зберагае ў файле:

"When someone asks for UserService,
here is how you should provide it."

Рэгістрацыя ў коране за дапамогою providedIn

Вы вядома можаце бачыць такую настройку падчас стварэння сэрвісаў:

@Injectable({
  providedIn: 'root'
})

З providedIn: 'root' сэрвіс належыць да корневага інжектара. Angular стварае аднае экземпляры ў першы раз, калі хтось яго запрашвае, і практычна вяртае той самы экземпляр для кожнага наступнага запиту, што робіць яго сінглтонам у всій працоўкі. Полны відгляд сэрвіса выглядае так:

import { Injectable } from '@angular/core';
@Injectable({
  providedIn: 'root'
})
export class UserService {
  getUserName() {
    return 'Rahul';
  }
}

Потым любой компонент можа яго запрашваць, не паказваючы сэрвіс у масе providers:

userService = inject(UserService);

Для большасця служб, які працуюць у всій аплікацыі, гэты ўзельць яўляецца самым простым і праграмаваным спосабам. У яго таксама є корыстны пасляэфект: якщо ніхто не будзе інжектаваць гэтую службу, інструменты для кампіляцыі зможаць выключыць яе з пакета, таму што рэгістрацыя знаходзіцца на самай службе, а не ў модуле чы компоненте.

Рэгістрацыя службы ў компоненте

Прадастальнік таксама можа быць прыўязаны да конкрэтнага компонента через яго масів providers:

@Component({
  selector: 'app-user',
  providers: [UserService],
  templateUrl: './user.html'
})
export class User {
}

У гэтым случае UserService задаецца за дапамою сэрвісу інжектавання самага таго компонента, а не толькі за дапамою рэштовага інжектара:

providedIn: 'root'

Разлік между гэтымі двума спосабамі рэгістрацыі яўляецца вачоўкай для разумэння іерархічных інжектараў.

Іерархічныя інжектары

Angular не выкарыстоўвае аднаго глобальнага контэйнера. Інжектары утвараюць дрэва, якія прыблізна падобныя да дрэва компанентаў, пры чым інжектар-коран знаходзіцца на верху:

Root Injector
      │
      ├── App
      │
      ├── Component A
      │
      │    └── Child Component
      │
      └── Component B

Сэрвіс, які предстаўляецца на роўні корана, доступны практычна всюды. Аднак компанент можа зарэўнаваць свой сопраўднік для таго ж сэрвіса:

@Component({
  selector: 'app-user',
  providers: [UserService],
  templateUrl: './user.html'
})
export class User {
}

Тады Angular падтрымвае сопраўдніка, прычалёванага да інжектара таго компанента. Калі компанент або які-небудзь з його наследнікаў запрашае UserService, Angular выкарыстоўвае экзанпляр з таго роўня. Кожны экзанпляр компанента атрымвае свой сопраўднік сэрвіса, створаны разам з компанентам і знішчаны ўмоваў з яго. Компаненты, распакаваныя ў іншых частках дрэва, продовжаюць бачыць экзанпляр з роўня корана.

Чаму калькі роўняў ёсць корыстнымі

Разглядзім SettingsService. Большасц экранаў павінны дзеліцца адной і той жа настаўкай, таму ёё представленне на роўні корана мае сэнс:

Root
 │
 ├── Home
 ├── Products
 └── Profile

Тепер заўважкім, што адмінскія частыні програмы патрабуюць сэткаў, якія не должны прасачвацца да рэшты аплікацыі:

Root
 │
 ├── Home
 │
 └── Admin
       │
       └── Admin Settings Service

Заўданнем службы ў адмінскай складовай гэты паддрэво отрымае адзінэчную інстанцыю, тады калі весь іншы частыні продовжаюць выкарыстоўваць спяльную інстанцыю. Стан на адзіну функцыя, стан на адзін дыялог і стан на адзін элемент спіса — гэта распашчытныя прыклады выкарыстоўвання гэтага патэрану, і ні аднаго з іх не можна досягнуць, калі кожная складовая проста вызывае new.

Як Angular рашае запит

Калі Angular сталкнуцца з запитам такога типу:

userService = inject(UserService);

яна шукае прадастальніка, пачынаючы з інжектара, які знаходзіцца найбліжэй да таго, хто выканаў запит, і праходзячы да наступных:

Component requests UserService
             ↓
  Check current injector
             ↓
  Provider found?
       ↙           ↘
     Yes            No
      ↓              ↓
Provide it      Look at parent
                     ↓
              Continue upward

Першы вводзіч, у якога ёсць падходящы запрэдставлік, выграўвае. Якщо пошук дасягне верху без знаходжэння такога вводзіча, Angular выклеквае адказку пра недастатак інжектавання, якая зазвычай выказваецца як NullInjectorError: No provider for UserService. Самэй таго кожны сервіс, які можна інжектаваць, павінен быть зарэўнаваны там, дзе можа да яго дасягнуць той, хто яго запрашвае.

Інжектаванне калькі залежнасцяў

Компонент можа завісіць ад столькіх сервісоў, сколькі яму трэба. Чырвоня ў прыкладзе компонент запрашвае як сервіс корыстніка, так і сервіс паведамленняў:

import { Component, inject } from '@angular/core';
import { UserService } from './user.service';
import { NotificationService } from './notification.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {
  userService = inject(UserService);
  notificationService = inject(NotificationService);
}

Пасля чаго метод можа ўжыць іх разам, напрыклад, каб прывітацца з нынешнім корыстнікам:

showWelcomeMessage() {
const name = this.userService.getUserName();
  this.notificationService.show(
    `Welcome ${name}`
  );
}

Ніякі з сервісоў не ствараецца компонентам; Angular сам стварае і інжектуе які-небудзь з іх.

Сервісы і прынцып інжектавання залежнасцяў ёсць супаўзаемыя, але разныя

Лёгкаа ўжыць гэтыя два тэрміны як заменныя, але яны описваюць разнае. Сэрвіс — это клас, які зберагае логіку, якая можа быць викорыстаная знову, або спільны стан. Впрыск залежнасцяў — это механізм, які доставляе гэты сэрвіс тым часткам працоўніку, якія яго патрабуюць:

Service
  ↓
Provides functionality

Dependency Injection
  ↓
Provides the service where needed
Component
  ↓
Uses the service

Застосаванае да конкрэтнага сэрвісу:

UserService
    ↓
Angular DI
    ↓
User Component
    ↓
Uses UserService

Раздзелэнне ролей дазволяе змяніць спосаб або месца запрэдставлення сэрвісу без вплыву на компаненты, якія яго выкарыстоўваюць.

Спалучэнне DI з сігналамі

DI натуральнаа супрацоўвае з сігналамі для спільнага рэакtyвнага стану. Нижчэйшы сэрвіс мае сігнал лічыльніка і метод, які яго апдэйтуе:

import { Injectable, signal } from '@angular/core';
@Injectable({
  providedIn: 'root'
})
export class CounterService {
  count = signal(0);
  increase() {
    this.count.update(value => value + 1);
  }
}

Компанент впрысквае сэрвіс як і любы іншы:

import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';
@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css'
})
export class App {
  counterService = inject(CounterService);
}

Його шаблон чытае сігнал, вызываючы яго, а кнапка вызывае метод сэрвісу:

<h1>Count: {{ counterService.count() }}</h1>
<button (click)="counterService.increase()">
  Increase
</button>

Хутчэй сюды саўместна працуюць тры канцэпцыі:

Dependency Injection
        ↓
    CounterService
        ↓
    Signal
        ↓
    Component
        ↓
    Template

Сервіс кантролюе стан, DI забезпечвае доступ да той самай інстанцыі сервіса для кожнага корыстувальца, а сігнал гарантуе апдэйт шаблона, калі змянюецца значэнне. Паколькі CounterService падаецца з корневага рэверсу, кожны компонент, які яго інжектуе, бачыць той самы лік. Якбы яго падавалі на конкрэтны компонент, кожна падшыя структура мела б свой незалежны лічылак.

Што дае вам DI

Корысты праўяцца ў меру росту аплікацыі:

  • Арганізацыя: компонентам не трэба знать, як ствараюцца ўсе іх залежнасці чы і што яму трэба.
  • Повторна адналежнасць: адні хутчэй можа служыць для багато компонентов.
  • Можлівасць тэставання: пад час тэстаў можна выкорыстоваць фальшывага або спрабнога падаўжчыка замест насупрацьвеснай службы, напрыклад такога, який вяртае фіксаваныя данні замест таго, каб вызваць API.
  • Гнучкасць: іерархія інжектараў дазволяе выбраць межу адной спяльнай інстанцыі і інстанцый з абмежаным дыячым прасторам.
  • Слабая зв’язанасць: компонент завісіць ад таго, што прыносіць служба, а не ад таго, як яе стварыць.
  • Прыклад — аплікацыя для пакупак

    Уявіце сабе інтэрнет-магазін з калькама служб, кожная з яых мае адну задачу:

    ProductService
        ↓
    Gets products from API
        ↓
    CartService
        ↓
    Manages cart state
    AuthService
        ↓
    Manages authentication
    NotificationService
        ↓
    Displays notifications
    

    Кожны компонент тады інжектуе толькі тое, што яму патрэбна; напрыклад, паліця навігацыі можа патрабаваць як аутэнтыкацію, так і стан кошыка:

    ProductComponent
          ↓
    ProductService
          ↓
    CartComponent
          ↓
    CartService
    Navbar
          ↓
    AuthService
          ↓
    CartService
    

    Angular з’еднае ўсё гэта разам, і кожны компонент залишаецца сфокусаваным на свайм UI, а не на стварэнні і кераванні об’ектамі, якія стояць за ўсім гэтым.

    Найбольшая частая памяць: служба без прадстаўніка

    Пачатківцы часта пішу просты клас:

    export class UserService {
    }
    

    а потым прабуюць яго інжектаваць:

    userService = inject(UserService);
    

    Калі ніякі прадстаўнік не зарэгістраваны, Angular не мае спосабу створэння такога класа, і інжектаванне збіваецца з адной з вышэй описанных памяцей пра відсутнасць прадстаўніка. Для службы, якая павінна працаваць у всій аплікацыі, рашэнняя — дадаць декоратар з корановай рэгістрацыяй:

    @Injectable({
      providedIn: 'root'
    })
    export class UserService {
    }
    

    Тады корановы інжектар ведае, як створыць такую службу. Альтэрнатыўна, можна запісаць клас у масэвым элементе providers, якщо хочаце аб’екту з вялікім дыапазонам доступу.

    Падсумак

    У сучасным Angular клас отрымае залежнасць за дапамою inject():

    userService = inject(UserService);
    

    а служба робіць ся доступной у всій аплікацыі за дапамою:

    @Injectable({
      providedIn: 'root'
    })
    

    Налаштаванне providedIn вярнюе, дзе рэгіструецца сервіс, а іерархічная система інжектавання дазволяе аднародзіць адну інстанцыю ў всёй працоўны лініі аплікацыі або стварыць окольныя інстанцыі для компонента та яго наследнікаў. Полны прайсупутак запыту выглядае так:

    Component
        ↓
    Requests Dependency
        ↓
    Angular DI
        ↓
    Provider
        ↓
    Service
    

    А з даданымі сігналамі для рэакtyвнага стану:

    Component
        ↓
    inject()
        ↓
    Service
        ↓
    Signal
        ↓
    Reactive UI
    

    Галоўныя выводы

    • Компоненты запрашваюць залежнасці; яны іх не ствараюць. Angular знаходзіць прадастальніка і задае інстанцыю.
    • Выклікайте inject() толькі ў контэксте інжектавання, напрыклад у ініцыялайзеры поля або канстрактары.
    • providedIn: 'root' дае леніваа створаны, можна выключыць з дрэўа сінглтон і ёсць правы стандартны выбар для сервісаў у всій аплікацыі.
    • Энтрыя providers на рэвеніі компонента стварае окольную інстанцыю для таго компонента та яго паддрэва.
  • Рашыянне праходзіць па дрэве інжектара; якщо на будзь-яму з етапаў гэтага шляху не застаёцца прадастальнік, то выйшваецца NullInjectorError.
  • DI plus signals — гэта просты і эфектыўны патэрн для спільнага стану, а замена прадастальнікаў ёсць спосабом ізоляцыі компанентаў у тэстах.