Галоўная / Артыкулы / Дыягназаванне кашталоў «NestJS не можа развязваць залежнасці», па прычынам

Дыягназаванне кашталоў «NestJS не можа развязваць залежнасці», па прычынам

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

1046 слоў

Раней чым пазней кожны проект NestJS зупіняецца пад запускам з паведамленнем пра тое, што контейнер не можа знаходзіць параметр канстрактара. Фразаведчыня выглядае неяснаю, але ёй ўсё ж такі точная, і гэта майже завжды стаўкаецца на адну з пяці памылак у налаштаваннях. Шэрыкаваець паказвае, як чытаць гэта паведамленне і розбірае кожную прычыну па той порядку, які трэба ўжо пераканаліць.

Чытанне паведамлення пра адзінку

Тыповая адзінка выглядае так:

Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument at index [0] is available in the UsersModule context.

Клас, названы ў паведамленні (UsersService), — гэта той, які Nest намагаўся інстанцыяваць. У складках перыядоў паказваны кожны параметр канстрактара, а ? пазначае той, які не запрацаваў; index [0] падтверджвае гэту пазіцыю. У заканчальной частцы названы модуль, у яком велісь пошукі. Кожны з падаўжэнняў ёсць спосабом зробіць гэты параметр видным.

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

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

@Module({
  controllers: [UsersController],
  providers: [UsersService], // <-- missing? that's your error
})
export class UsersModule {}

Каманда CLI nest generate service сама апдэйтуе модуль. Службы, напісаныя вручную, часта застаюцца без такога крока.

Прычына 2: прадастальнік знаходзіцца ў іншым модуле, які не імпортуецца і не экспортуецца

Это найчастэйшы варіант міжмодульнага взаемадзейства. AuthService рэгіструецца ў AuthModule, і UsersService яго інжектуе, але UsersModule не мае імпорту, які б вялі да AuthModule:

@Module({
  imports: [AuthModule], // <-- without this, AuthService is invisible here
  providers: [UsersService],
})
export class UsersModule {}

Імпорт — толькі палова кантракту. Модуль, які володзіць прадастальнікам, таксама должен яго перыяднаваць пад элементам exports:

@Module({
  providers: [AuthService],
  exports: [AuthService], // <-- other modules can only use what you export
})
export class AuthModule {}

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

Прычына 3: два сервісы залежны адзін ад другога

Якщо UsersService патрэбуе OrdersService, а OrdersService патрэбуе UsersService, ніхто з іх не можа быць створаны першым. Nest пропонуе forwardRef() для адкладэння рашынення. Ён прыменяецца на рэвэлі модуля для імпорту:

// users.module.ts
@Module({
  imports: [forwardRef(() => OrdersModule)],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

і зноў у месцы інжэкцыі ў канстрактары сервіса:

// users.service.ts
constructor(
  @Inject(forwardRef(() => OrdersService))
  private ordersService: OrdersService,
) {}

На другай стороне патрэбна «дзеркальная» копія (OrdersModule імпортуе forwardRef(() => UsersModule)). Лічыце гэта тымчасовым рашэнням, а не стаўкам. Цыкл зазвычай означае, што спакульяваная логіка знаходзится не там, дзе трэба; яе перыявленне ў трэцім модулі, які можа імпортуваць яе абодвумя сторонамі, пазбавляе нас цыклу і восьбовай потрэбы ў forwardRef.

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

Прычына 4: токен інжэкціі не падходзіць

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

{
  provide: 'CONFIG_OPTIONS',
  useValue: configOptions,
}

Якщо параметр канструктара задаецца толькі з типам, яго не будзе можна знайсці, таму што сам тип не ўважаецца токенам. Неабяжна выкарыстоўваць @Inject() з той самым строкавым значэнням:

constructor(@Inject('CONFIG_OPTIONS') private config: ConfigOptions) {}

Тыпы ў TypeScript таксама зникаюць пад час выканання, таму інтэрфейс ніколі не можа служыць токенам сам по сабе.

Рэпазітары TypeORM таксама не працуюць правільна. Заданне параметра як класу рэпазітара не падходзіць да токена, зарэўнаванага Nest:

// Wrong
constructor(private repo: UserRepository) {}

У правільным варыянте выкарыстоўваецца @InjectRepository() з аб’ектам-ентытасом:

// Right
constructor(
  @InjectRepository(User)
  private repo: Repository<User>,
) {}

Ёнколі каб такі токен існаваў, модуль таксама павінен імпортуваць TypeOrmModule.forFeature([User]).

Прычына 5: модуль тэставання не мае прадастальнікаў або макаў

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

const module = await Test.createTestingModule({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository, // <-- every dependency needs one of these
    },
  ],
}).compile();

getRepositoryToken(User) стварае той самы токен, які шукае @InjectRepository(User). Якщо бяг выклікаецца толькі ў тэстах, значыць налагоджэнне практычнай прыграмы ў порядку, а налагоджэнне тэстаў непачаткова.

Спіс пераконтрацыяў пад час налагоджэння

Выпрацаваўце іх па порядку:

  1. Чы клас уключаны ў providers адпаведнага модуля?
  2. Чы модуль, які ўтримвае клас, імпортуўаны там, дзе викорыстоўваецца прадастальнік, і чы ён экспортуе гэты прадастальнік?
  • Чы гэта ціркулярная залежнасць, чы то між службамі, чы то між файламі? Выкарыстаце forwardRef для патчавання, а потым перерабоце код.
  • Для спецыяльных прадастчыкаў і рэпазітарыёў, чы токен уводу абоўсюдзе збігаецца з данымі рэўістрацыі?
  • Чы бяда выклікаецца толькі пад час тэстаў? Дадзіце нехватную прадастчыку або макаў.
  • Ключовыя выводы

    • Знак ? і індэкс у паведамленні точна вказываюць, які аргумент канстрактара нехватна і ў яком модулі.
    • Візыбельнасць у Nest ёсць явная: рэўістрацыя, экспорт, імпорт.
    • forwardRef маскіруе цыклы; выкарыстоўванне спяльнага модуля дапамагае іх выліквіць.
    • Для уводу пад час выканання викорыстоўваюцца токены, а не типы TypeScript.
    • Модулі тэстаў ёсць адзеленыя контейнеры і патрабуюць сваёй сабойчыной, полнай наладкі.

    Ёсць большае колькасць інфармацыі пра тое, як падтрымліваць здаровыя межы модуляў па мере росту кодавой базы; адзін раз прыгляніцеся шасць правілах DDD для структуравання домэнаў у прыкладах NestJS.