Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Kanon tajribasi: halqa va forwardRef
- 2.1 forwardRef siz
- 2.2 forwardRef bilan
- 2.3 Modul darajasidagi halqa
- 3. ESM tuzog'i: testda o'tadi, build'da yiqiladi
- 3.1 Tajriba
- 3.2 Sabab: metama'lumot qatori
- 3.3 JS'da TDZ'ni o'zingiz ko'ring
- 3.4 O'z loyihangizda qanday tekshirasiz
- 4. Eng yaxshi yechim — halqani buzish
- 5. ModuleRef: provider'ni ish vaqtida olish
- 5.1 Uch metod
- 5.2 Ishlangan misol: to'lov provayderini nom bo'yicha tanlash
- 5.3 Ehtiyot bo'ling
- 6. LazyModuleLoader: kerak bo'lganda yuklash
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): qaysi metod?
- 2-mashq (o'rta): halqani hodisa bilan buzish
- 3-mashq (qiyin): halqadagi hamma tugunlar
- 9. Nega loyihaga kirmadi
- 10. Real ishda
- Xulosa
- Manbalar
NestJS circular dependency: forwardRef, ModuleRef va LazyModuleLoader
Qisqacha: Aylanma bog'liqlik (circular dependency) —
AgaBkerak,Bga esaA. Nest buni ishga tushishda aniqlaydi va xato beradi.forwardRef(() => B)uni chetlab o'tadi: klassga murojaat keyinga suriladi. Lekin ESM'da fayllar bir-birini import qilsa,forwardRefham yetmasligi mumkin — kanon tajribasida Vitest'da o'tgan kodtscbuild'idan keyinReferenceErrorbilan yiqildi. Eng yaxshi yechim — aylanmani qayta loyihalash.ModuleRefprovider'ni ish vaqtida oladi (get,resolve,create),LazyModuleLoaderesa modulni birinchi kerak bo'lganda yuklaydi.
Bu darsda
- Aylanma bog'liqlik qanday paydo bo'lishini va Nest uni qanday xabar qilishini ko'rasiz.
forwardRefnima qilishini va qayerda yetmasligini (ESM, TDZ) tushuntirasiz.ModuleRef.get,resolvevacreatefarqini haqiqiy chiqish bilan bilasiz.LazyModuleLoaderbilan modulni kechiktirib yuklashni ko'rasiz.- Fayllar orasidagi aylanmani topadigan kichik vosita yozasiz.
Oldin bilishingiz kerak: Provider scope'lari, DI va IoC konteyner, Temporal Dead Zone (TDZ), CommonJS ichidan: circular require, ES Modules Node'da.
O'tgan darsda provider nusxasi qancha yashashini ko'rdik. Bu dars — konteynerning ikki "nozik" joyi: bog'liqliklar halqa hosil qilsa nima bo'ladi va provider'ni constructor'dan emas, ish vaqtida qanday olish mumkin. Bu ham tajriba dars: vazifalar-api ga kod qo'shilmaydi. Lekin Custom provider'lar darsida ombor-yarat.ts nega alohida fayl bo'lgani shu yerda to'liq ochiladi.
1. Nega bu kerak?
Loyiha o'sganda xizmatlar bir-biriga murojaat qila boshlaydi. Masalan: VazifalarService vazifa bajarilganda BildirishnomaService ni chaqiradi. BildirishnomaService esa xabar matniga vazifa nomini qo'yish uchun VazifalarService dan vazifani oladi. Ikki klass bir-birining constructor'ida.
Konteyner VazifalarService ni yaratmoqchi: avval unga BildirishnomaService kerak. Uni yaratmoqchi: avval unga VazifalarService kerak. Halqa. Bu — tuxum va tovuq muammosi: kim birinchi?
NestJS nima va nega darsining 3-mashqida o'z konteynerimizga aylanma tekshiruvini qo'shgandik: Aylanma bog'liqlik: A → B → A. Haqiqiy Nest ham shunday qiladi — buni ko'ramiz.
Express'da bunday halqa qanday ko'rinardi? 25-qismda xizmatlarni ilova.ts da qo'lda ulardingiz: const v = vazifaXizmati(ombor). Halqa bo'lsa, siz uni yozish paytidayoq sezardingiz — b ni yaratish uchun v kerak, v uchun esa b. Bunday kodni yozib bo'lmaydi. Nest'da esa ulashni konteyner qiladi, siz faqat constructor yozasiz. Halqa ikki xil faylda, ko'zga tashlanmay paydo bo'ladi va faqat ishga tushishda bilinadi. Yaxshi tomoni — Nest uni darhol, aniq xabar bilan aytadi.
2. Kanon tajribasi: halqa va forwardRef
2.1 forwardRef siz
@Injectable() class A1 {
constructor(@Inject("B1") readonly b: unknown) {}
}
@Injectable() class B1 {
constructor(readonly a: A1) {}
}
@Module({ providers: [A1, { provide: "B1", useClass: B1 }] })
class M1 {}A1 B1 ni so'raydi, B1 esa A1 ni — ikkalasi ham forwardRef siz. Natija (kanon tajribasi, Vitest muhitida). Xabarning oxirgi gapiga alohida e'tibor bering:
1) A ↔ B (forwardRef'siz): XATO
A circular dependency has been detected inside "A1". Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Note that circular relationships between custom providers (e.g., factories) are not supported since functions cannot be called more than once.Tarjimasi: "A1 ichida aylanma bog'liqlik aniqlandi. Ikki tomonlama munosabatning har ikki tomoni forwardRef() bilan belgilanganiga ishonch hosil qiling. Custom provider'lar (masalan, fabrikalar) orasidagi aylanma munosabat qo'llab-quvvatlanmaydi, chunki funksiyani bir martadan ortiq chaqirib bo'lmaydi."
Xabarning oxirgi gapi muhim: fabrika provider'lar (useFactory) halqada umuman ishlamaydi — forwardRef bilan ham. Fabrika natijani qaytaradi, uni oldindan "bo'sh qobiq" sifatida yaratib bo'lmaydi.
2.2 forwardRef bilan
@Injectable() class A2 {
constructor(
@Inject(forwardRef(() => B2)) readonly b: { nom: string },
) {}
nom = "A2";
}
@Injectable() class B2 {
constructor(@Inject(forwardRef(() => A2)) readonly a: A2) {}
nom = "B2";
}2) forwardRef bilan: OK — a.b.nom = B2, a.b.a === a: trueIshladi. a.b.a === a — true: halqa haqiqiy, ikkala obyekt bir-birini ko'radi. forwardRef(() => B2) — klassning o'zi emas, klassni qaytaradigan funksiya. Nest uni darhol chaqirmaydi. Avval ikkala klass uchun nusxa yaratiladi, bog'liqliklar esa keyin ulanadi.
Nima uchun funksiya? Chunki dekorator A2 e'lon qilinayotgan paytda ishlaydi — o'sha paytda B2 hali e'lon qilinmagan (u pastroqda). @Inject(B2) yozilsa, JavaScript B2 ni o'qishga urinadi va yiqiladi. Funksiya ichidagi B2 esa faqat funksiya chaqirilganda o'qiladi — u paytga kelib ikkala klass tayyor.
2.3 Modul darajasidagi halqa
Modullar ham bir-birini import qilishi mumkin:
@Module({ imports: [forwardRef(() => MY)] }) class MX {}
@Module({ imports: [MX] }) class MY {}5) modul X ↔ Y (Y tomonda forwardRef yo'q): OKNima bo'ldi? MX MY ni import qiladi, MY esa MX ni — modullar halqasi. Provider'lardagidek, Nest bu yerda ham halqani ko'radi. forwardRef(() => MY) unga "MY ni hozir o'qima, keyin so'ra" deydi. Bu holatda bitta tomondagi forwardRef yetdi. Lekin Nest hujjati ikkala tomonga ham yozishni tavsiya qiladi — qaysi modul birinchi baholanishiga bog'liq bo'lmasin.
3. ESM tuzog'i: testda o'tadi, build'da yiqiladi
3.1 Tajriba
Yuqoridagi misollarda ikkala klass bitta faylda edi. Real loyihada ular alohida fayllarda va bir-birini import qiladi. Kanon shu holatni sinadi: a.ts dagi A va b.ts dagi B — ikkalasi ham forwardRef bilan, fayllar bir-birini import qiladi (tajriba fayl nomlarini qisqartirdik). Ikkinchi juftlik — C va D — boshqacha yozilgan (pastda).
Avval testdagidek — Vitest muhitida (Vite/oxc o'giruvchisi). Har qator oxiridagi so'zga qarang — OK yoki XATO:
1) A ↔ B — oddiy import (tur metama'lumotga yoziladi) + forwardRef: OK — A → B
2) C ↔ D — import type (metama'lumotda Object) + namespace orqali forwardRef: OK — C → DIkkalasi o'tdi. Endi xuddi shu fayllarni tsc bilan kompilyatsiya qilib (nest build qiladigan ish), oddiy node bilan yurgizamiz. Kod bitta, o'zgarmadi — faqat uni JavaScript'ga o'girgan vosita boshqa. Qatorlarni yuqoridagisi bilan solishtiring:
--- node dist/esm.js:
1) A ↔ B — oddiy import (tur metama'lumotga yoziladi) + forwardRef: XATO — ReferenceError: Cannot access 'A' before initialization
2) C ↔ D — import type (metama'lumotda Object) + namespace orqali forwardRef: OK — C → DBirinchi juftlik yiqildi. Testlar yashil, prodakshn ishga tushmaydi. Bu — kanonning eng muhim Nest topilmalaridan biri.
3.2 Sabab: metama'lumot qatori
tsc A klassi uchun yozgan JavaScript'da shu qator bor:
dist/a.js:26: __metadata("design:paramtypes", [B])[B] — oddiy ifoda. U forwardRef ichida emas, klass e'lon qilinayotganda bajariladi. ESM'da fayllar aylanma import qilinsa, ulardan biri ikkinchisi tugamasidan baholanadi. Shu paytda B hali e'lon qilinmagan — Temporal Dead Zone (TDZ) ichida. class bilan e'lon qilingan nomni e'londan oldin o'qish — ReferenceError.
forwardRef(() => B) faqat @Inject ichidagi murojaatni kechiktiradi. emitDecoratorMetadata qo'shgan [B] esa kechikmaydi. Vite'ning o'giruvchisi esa bu qatorni boshqacha yozadi, shuning uchun testda xato ko'rinmadi.
Ikkinchi juftlik (C, D) nega o'tdi? U ikki usul bilan yozilgan:
import type { D } from "./d.ts";
import * as dModul from "./d.ts";
@Injectable()
export class C {
constructor(
@Inject(forwardRef(() => dModul.D)) readonly d: D,
) {}
}import type { D }— tur faqat TypeScript uchun; JavaScript'da o'chadi. Metama'lumotgaDemas,Functionyoziladi (kanon chiqishi:__metadata("design:paramtypes", [Function])). TDZ'ga tushadigan murojaat yo'q.import * as dModul— namespace import:dModul.Dfaqat funksiya chaqirilganda o'qiladi. ESM namespace "jonli" — u paytga kelibDtayyor.
3.3 JS'da TDZ'ni o'zingiz ko'ring
Mexanizm Nest'siz ham ko'rinadi. Bu blok brauzerda ishlaydi va ataylab yiqiladi:
// tsc yozgan qatorning soddalashgani:
// __metadata("design:paramtypes", [B])
const metama = { A: [B] };
class B {}
console.log(metama.A[0].name);Konsolda:
ReferenceError: Cannot access 'B' before initializationB ni e'londan oldin o'qishga urindik. Endi murojaatni funksiya ichiga olamiz — forwardRef g'oyasi:
const metama = { A: () => [B] }; // forwardRef(() => B) g'oyasi
class B {}
console.log(typeof metama.A); // function
console.log(metama.A()[0].name); // BFunksiya yaratilganda B o'qilmadi. Chaqirilganda — class B allaqachon e'lon qilingan edi. Muammo shundaki, tsc'ning __metadata qatori birinchi shaklda yoziladi, ikkinchisida emas.
3.4 O'z loyihangizda qanday tekshirasiz
Bu tuzoqdan qutulishning eng ishonchli yo'li — build qilingan ilovani haqiqatan ko'tarib ko'rish. vazifalar-api da bu ikki buyruq:
npm run build
npm startIlova ko'tarilsa va GET /v1/vazifalar javob bersa — ESM halqasi yo'q. ReferenceError chiqsa — xabardagi klass nomi (Cannot access 'A') qaysi fayldan boshlashni aytadi. Ikkinchi yo'l — import grafini oldindan tekshiradigan vosita: npx madge --circular --extensions ts src/ halqadagi fayllar ro'yxatini chiqaradi. U kodni ishga tushirmaydi, faqat import qatorlarini o'qiydi — pastda biz xuddi shunday kichik vosita yozamiz.
Diqqat: Aylanma importli Nest kodini faqat testlar bilan tekshirish yetmaydi. Kanon shundan keyin
npm run checkgabuildva ishga tushirishni ham qo'shdi. Siz ham CI'da build'dan keyin ilovani bir marta ko'taring.
Tekshirib ko'ring: Nega
Cconstructor'idagid: Dturi baribir to'g'ri ishlaydi, agar metama'lumotdaFunctionyozilgan bo'lsa?
Javob
Chunki Nest bu parametr uchun metama'lumotdagi turga qaramaydi: @Inject(forwardRef(() => dModul.D)) — token aniq berilgan va u design:paramtypes dan ustun (DI va IoC konteyner darsidagi self:paramtypes). d: D esa faqat TypeScript tekshiruvi uchun.
4. Eng yaxshi yechim — halqani buzish
forwardRef — chetlab o'tish, davo emas. Nest hujjati ham aylanmadan iloji boricha qochishni maslahat beradi. Uch yo'l:
- Umumiy qismni ajratish.
AvaBga kerak bo'lgan qism uchinchi klassga ko'chadi; ikkalasi unga tayanadi, bir-biriga emas. - Hodisalar.
VazifalarServiceBildirishnomaServiceni chaqirmaydi, "vazifa bajarildi" hodisasini chiqaradi; bildirishnoma uni tinglaydi. Bog'liqlik bir tomonga. Nest'da bu@nestjs/event-emitter— Hodisalar darsida, kanon aynan shu yo'lni tanlaydi. - Faylni bo'lish. Fayllar orasidagi aylanma importda — funksiyani alohida faylga ko'chirish.
Birinchi yo'lni «Nega bu kerak?» bo'limidagi misolda ko'raylik. BildirishnomaService ga VazifalarService ning hammasi kerak emas — unga faqat vazifani ID bo'yicha o'qish kerak. Bu ish aslida omborniki. Demak halqani shunday buzamiz:
| Oldin | Keyin | |
|---|---|---|
VazifalarService |
BildirishnomaService ni oladi |
o'zgarmaydi |
BildirishnomaService |
VazifalarService ni oladi |
VAZIFA_OMBORI ni oladi |
| Halqa | bor | yo'q: ikkalasi omborga tayanadi |
Bog'liqliklar endi bitta yo'nalishda: xizmatlar → ombor. Ombor hech kimni bilmaydi. Halqa ko'pincha shunday paydo bo'ladi: klass "butun xizmat" ni so'raydi, holbuki unga undan kichik bir qism kerak. Savol bering: "menga aynan nima kerak?" — javob ko'pincha halqani o'zi buzadi.
Uchinchi yo'lni vazifalar-api da allaqachon qildik. Custom provider'lar darsida fabrika omborYarat ni chaqirishi kerak edi. U ishga-tushir.ts da edi, va zanjir halqa hosil qilardi. Uni kichik vosita bilan tekshiramiz — fayllar import grafidan halqani topadigan funksiya (brauzerda ishlaydi):
function aylanmaTop(importlar) {
const holat = new Map(); // "kirildi" | "tugadi"
const yol = [];
function kir(m) {
if (holat.get(m) === "tugadi") return null;
if (holat.get(m) === "kirildi") {
return [...yol.slice(yol.indexOf(m)), m].join(" → ");
}
holat.set(m, "kirildi");
yol.push(m);
for (const b of importlar[m] ?? []) {
const t = kir(b);
if (t) return t;
}
yol.pop();
holat.set(m, "tugadi");
return null;
}
for (const m of Object.keys(importlar)) {
const t = kir(m);
if (t) return t;
}
return "aylanma yo'q";
}
console.log(aylanmaTop({
"ilova.ts": ["app.module.ts"],
"app.module.ts": ["ombor.module.ts"],
"ombor.module.ts": ["ishga-tushir.ts"],
"ishga-tushir.ts": ["ilova.ts"],
}));
console.log(aylanmaTop({
"ilova.ts": ["app.module.ts"],
"app.module.ts": ["ombor.module.ts"],
"ombor.module.ts": ["ombor-yarat.ts"],
"ishga-tushir.ts": ["ilova.ts", "ombor-yarat.ts"],
"ombor-yarat.ts": [],
}));Konsolda:
ilova.ts → app.module.ts → ombor.module.ts → ishga-tushir.ts → ilova.ts
aylanma yo'qBirinchi graf — omborYarat eski joyida qolgan variant. Ikkinchisi — kanon tanlovi: ombor-yarat.ts hech kimni import qilmaydi, halqa yo'q. Algoritm — chuqurlikka qidiruv (DFS) uch holat bilan: "kirildi" tugun qayta uchrasa — halqa topildi. Real loyihada bu ishni madge --circular kabi vositalar bajaradi.
flowchart LR
I["ilova.ts"] --> AM["app.module.ts"]
AM --> OM["ombor.module.ts"]
OM --> OY["ombor-yarat.ts"]
IT["ishga-tushir.ts"] --> I
IT --> OYDiagrammada strelka — import. Halqa yo'q: ombor-yarat.ts faqat "iste'mol qilinadi". Har strelkani barmog'ingiz bilan kuzatib chiqing: qaysi tugundan boshlamang, boshlang'ich tugunga qaytib kelolmaysiz. Halqasiz grafning ta'rifi aynan shu. ishga-tushir.ts va ombor.module.ts ikkalasi ham ombor-yarat.ts ga boradi — bu halqa emas, ikki yo'lning bitta nuqtada uchrashishi.
5. ModuleRef: provider'ni ish vaqtida olish
5.1 Uch metod
Ba'zan bog'liqlikni constructor'da emas, kerak paytda olish kerak: plagin nomi faqat so'rovda ma'lum bo'lganda yoki REQUEST/TRANSIENT provider'dan yangi nusxa kerak bo'lganda. Buning uchun ModuleRef — joriy modulning DI konteyneriga kirish:
@Injectable()
class Ishlatuvchi {
constructor(readonly moduleRef: ModuleRef) {}
}Kanon uch metodni sinadi. Har qatorning oxiriga qarang — true, xato xabari, ikki xil id va soat:
3) ModuleRef.get(Soat) === get(Soat): true
ModuleRef.get(TRANSIENT) → Hisobchi is marked as a scoped provider. Request and transient-scoped providers can't be used in combination with "get()" method. Please, use "resolve()" instead.
resolve(TRANSIENT) ikki marta: id 1 va 2
create(RoyxatdaYoq) — ro'yxatda yo'q klass, bog'liqligi ukol qilindi: soat| Metod | Nima qiladi | Qachon |
|---|---|---|
get(Token) |
tayyor singleton nusxani beradi (sinxron) | DEFAULT provider |
resolve(Token) |
scope'li provider'ning yangi nusxasi (async) | REQUEST, TRANSIENT |
create(Klass) |
ro'yxatda yo'q klassni bog'liqliklari bilan yaratadi | plagin, ish vaqtida tanlanadigan klass |
get ni TRANSIENT provider'ga ishlatish — xato: "Hisobchi scope'li provider deb belgilangan. REQUEST va TRANSIENT provider'lar get() bilan ishlatilmaydi. resolve() ishlating." resolve ikki marta chaqirilganda id 1 va 2 — har safar yangi nusxa. create esa konteynerda yo'q klassni yaratdi va uning Soat bog'liqligini to'ldirdi.
5.2 Ishlangan misol: to'lov provayderini nom bo'yicha tanlash
ModuleRef ning eng tushunarli ishlatilishi — strategiya tanlash. Buyurtma uchun to'lov provayderi so'rovda keladi: "click" yoki "payme". Ikkala provayder ham klass, ikkalasi ham konteynerda. Qaysi biri kerakligi esa faqat ish vaqtida ma'lum. Demo loyihada shunday yozdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0):
const PROVAYDERLAR = {
click: ClickTolov,
payme: PaymeTolov,
} as const;
@Injectable()
class BuyurtmaService {
constructor(private readonly moduleRef: ModuleRef) {}
tola(nom: string, summa: number): string {
if (!(nom in PROVAYDERLAR)) {
return `noma'lum provayder: ${nom}`;
}
const klass = PROVAYDERLAR[nom as keyof typeof PROVAYDERLAR];
const p: TolovProvayderi = this.moduleRef.get(klass, {
strict: false,
});
return p.tolov(summa);
}
}ClickTolov va PaymeTolov alohida TolovModule da yashaydi, BuyurtmaModule uni import qiladi. Uch xil nom bilan chaqirdik, oxirida esa strict: false siz get ni sinadik:
Click: 25000 so'm
Payme: 40000 so'm
noma'lum provayder: uzum
strict (sukut): Nest could not find ClickTolov element (this provider does not exist in the current context)Nima bo'ldi? Birinchi ikki qatorda ModuleRef nom bo'yicha to'g'ri klassni topdi. Uchinchisida nom ro'yxatda yo'q — biz uni get gacha to'xtatdik. Oxirgi qator — tuzoq. get sukut bo'yicha faqat joriy modulning o'z provider'larini qidiradi (strict: true). Import qilingan moduldagi provider uchun xabar: "Nest ClickTolov elementini topa olmadi (joriy kontekstda bunday provider yo'q)". { strict: false } qidiruvni butun ilovaga kengaytiradi.
E'tibor bering: nomlar jadvali (PROVAYDERLAR) baribir kodda turibdi. Yangi provayder qo'shilsa, jadvalga bitta qator qo'shiladi va BuyurtmaService o'zgarmaydi.
5.3 Ehtiyot bo'ling
ModuleRef qulay, lekin u bog'liqlikni yashiradi: constructor'ga qarab, klassga nima kerakligini bilib bo'lmaydi. Bu — Provider'lar darsidagi "yashirin bog'liqlik" muammosining qaytishi. "Service locator" deb ataladigan bu naqsh ko'pincha anti-naqsh hisoblanadi. Qoida: constructor bilan olish mumkin bo'lsa — constructor; ModuleRef — faqat nom yoki tur ish vaqtida ma'lum bo'lganda.
Tekshirib ko'ring: To'lov misolida ikkala provayderni
BuyurtmaServiceconstructor'iga to'g'ridan olsa bo'lmaydimi:constructor(click: ClickTolov, payme: PaymeTolov)?
Javob
Bo'ladi — va ikki provayder uchun bu hatto yaxshiroq. Bog'liqliklar ko'rinib turadi, testda ikkalasini soxta bilan almashtirish oson. ModuleRef provayderlar ko'p bo'lganda yoki ular plagin sifatida qo'shilganda foydali: constructor'ga o'nta parametr yozish o'rniga bitta jadval. Tanlov — "bog'liqlik ko'rinsin" va "ro'yxat kengaya olsin" o'rtasidagi muvozanat.
6. LazyModuleLoader: kerak bo'lganda yuklash
Ba'zi modullar og'ir (katta kutubxona, ko'p provider) va kam ishlatiladi — masalan, oyda bir marta ishlaydigan hisobot. Ularni ishga tushishda yuklamaslik mumkin:
@Injectable()
class Chaqiruvchi {
constructor(readonly lazy: LazyModuleLoader) {}
}
const m = await chaqiruvchi.lazy.load(() => OgirModul);
m.get(OgirXizmat).hisobla();4) LazyModuleLoader: bootstrap'dan keyin OgirXizmat yaratilgan: 0
load() 0.5 ms → hisobla() = 42, yaratilgan: 1
ikkinchi load() 0.16 ms (keshlangan), yaratilgan: 1Ishga tushishda OgirXizmat yaratilmagan (0). Birinchi load() — modul yuklandi va provider yaratildi (1). Ikkinchi load() — kesh, qayta yaratilmadi. Vaqtlar kichik, chunki tajriba moduli yengil; haqiqiy og'ir modulda farq sovuq startda seziladi — serverless muhitida foydali.
Cheklov: lazy yuklangan modulning controller'lari marshrutga qo'shilmaydi (Nest hujjati bo'yicha) — marshrutlar ishga tushishda ro'yxatga olinadi. Lazy modul — faqat provider'lar uchun.
Tekshirib ko'ring:
vazifalar-apidagi eksport (EksportModule) kam ishlatiladi. Uni lazy qilsak, nima yutamiz va nima buziladi?
Javob
Yutuq kichik: ilova bir necha millisekund tezroq ko'tariladi, chunki eksport provider'lari keyin yaratiladi. Buzilishi esa katta: EksportModule da controller bor (/eksportlar). Lazy modulning controller'lari marshrutga qo'shilmaydi — POST /eksportlar 404 qaytaradi. Uzoq ishlaydigan serverda bunday savdo foydasiz; lazy yuklash sovuq start muhim bo'lgan joyda — serverless'da — o'zini oqlaydi.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
A circular dependency has been detected inside "X" |
ikki provider bir-birini constructor'da so'raydi | halqani buzing yoki ikkala tomonda forwardRef |
Testlar o'tadi, node dist/main.js da Cannot access 'X' before initialization |
ESM aylanma import + emitDecoratorMetadata |
import type + namespace forwardRef yoki faylni bo'lish |
… can't be used in combination with "get()" |
scope'li provider get bilan |
resolve() |
Nest could not find X element |
get sukut bo'yicha faqat joriy modulda qidiradi |
get(X, { strict: false }) |
| Lazy moduldagi marshrut 404 | lazy modul controller'larini ro'yxatga olmaydi | modulni oddiy imports ga |
| Halqada fabrika provider | fabrika "bo'sh qobiq" bera olmaydi | klass provider'ga o'tkazing yoki halqani buzing |
8. Mashqlar
Mashqlar kurs/mashqlar/30/11-aylanma/ da.
1-mashq (oson): qaysi metod?
- REQUEST provider'dan yangi nusxa olish —
moduleRef.(X) - Singleton provider'ni ish vaqtida olish —
moduleRef.(X) - Ro'yxatda yo'q klassni bog'liqliklari bilan yaratish —
moduleRef.(X) forwardRefga beriladigan narsa — klassni qaytaradigantscbuild'idagi aylanma import xatosining nomi —
Yechim
- resolve — scope'li, async.
- get — tayyor nusxa, sinxron.
- create — konteynerda yo'q klass.
- funksiya —
() => B; murojaat chaqirilgunicha kechikadi. - ReferenceError —
Cannot access 'A' before initialization, TDZ.
2-mashq (o'rta): halqani hodisa bilan buzish
Quyidagi ikki "xizmat" bir-birini chaqiradi (JS modeli, Nest'siz):
class Vazifalar {
constructor(bildirishnoma) { this.b = bildirishnoma; }
bajar(nom) { this.b.yubor(nom); }
}
class Bildirishnoma {
constructor(vazifalar) { this.v = vazifalar; }
yubor(nom) { /* … */ }
}Halqani hodisa bilan buzing: Vazifalar faqat hodisalar obyektini olsin va "vazifa.bajarildi" chiqarsin; Bildirishnoma uni tinglasin. Ikkita vazifani bajarib, bildirishnoma xabarlarini chiqaring. Ishora: EventEmitter asoslari g'oyasi — on(nom, fn) va emit(nom, qiymat) ni Map bilan o'zingiz yozing.
Yechim
class Hodisalar {
#tinglovchilar = new Map();
on(nom, fn) {
const r = this.#tinglovchilar.get(nom) ?? [];
this.#tinglovchilar.set(nom, [...r, fn]);
}
emit(nom, qiymat) {
for (const fn of this.#tinglovchilar.get(nom) ?? []) fn(qiymat);
}
}
class Vazifalar {
constructor(hodisalar) { this.h = hodisalar; }
bajar(nom) { this.h.emit("vazifa.bajarildi", nom); }
}
class Bildirishnoma {
constructor(hodisalar) {
hodisalar.on("vazifa.bajarildi", (nom) => this.yubor(nom));
}
yubor(nom) { console.log(`Bildirishnoma: "${nom}" bajarildi`); }
}
const h = new Hodisalar();
new Bildirishnoma(h);
const v = new Vazifalar(h);
v.bajar("Non olish");
v.bajar("Choy damlash");Konsolda:
Bildirishnoma: "Non olish" bajarildi
Bildirishnoma: "Choy damlash" bajarildiEndi bog'liqlik bir tomonga: ikkala klass Hodisalar ga tayanadi, bir-biriga emas. Vazifalar bildirishnoma borligini ham bilmaydi — ertaga SMS yoki statistika tinglovchisi qo'shilsa, u o'zgarmaydi.
3-mashq (qiyin): halqadagi hamma tugunlar
Darsdagi aylanmaTop faqat birinchi halqani topadi. hammaHalqalar(importlar) yozing: grafdagi har modul uchun "u biror halqada turadimi?" degan savolga javob bersin va halqadagi modullar ro'yxatini (alifbo tartibida) qaytarsin. Sinov: a → b → c → a, c → d, d → e, e → d, f → a. Ishora: m halqada, agar m dan chiqib yana m ga qaytish mumkin bo'lsa — har modul uchun alohida qidiruv yetadi; toSorted (massiv saralash).
Yechim
function yetadimi(importlar, dan, ga) {
const korilgan = new Set();
const navbat = [...(importlar[dan] ?? [])];
while (navbat.length > 0) {
const m = navbat.pop();
if (m === ga) return true;
if (korilgan.has(m)) continue;
korilgan.add(m);
navbat.push(...(importlar[m] ?? []));
}
return false;
}
function hammaHalqalar(importlar) {
return Object.keys(importlar)
.filter((m) => yetadimi(importlar, m, m))
.toSorted();
}
console.log(hammaHalqalar({
a: ["b"], b: ["c"], c: ["a", "d"],
d: ["e"], e: ["d"], f: ["a"],
}));Konsolda:
[ 'a', 'b', 'c', 'd', 'e' ]f halqada emas: u a ga boradi, lekin unga hech kim qaytmaydi. Bu yerda ikki alohida halqa bor (a-b-c va d-e), funksiya ikkalasining a'zolarini topdi. Har modul uchun alohida qidiruv — oddiy, lekin katta grafda sekin; professional vositalar Tarjan algoritmi bilan buni bitta o'tishda qiladi.
9. Nega loyihaga kirmadi
vazifalar-api da aylanma bog'liqlik ham, ModuleRef ham, lazy modul ham yo'q — kanon qarori:
- Aylanma — oldini olish arzon, tuzatish qimmat.
ombor-yarat.tsajratilgani shuning uchun; xizmatlar orasidagi kelajakdagi bog'liqliklar hodisalar orqali. ModuleRef— hamma bog'liqlik constructor'da ko'rinib tursin.- Lazy modul —
vazifalar-apimodullari yengil; sovuq start tor joy emas (u uzoq ishlaydigan server).
Bu qarorlar "hech qachon ishlatmang" degani emas. 29-qismda SMS uchun ikki provayder (Eskiz va PlayMobile) bor edi. Ular Nest'ga ko'chganda ham tanlov config bo'yicha, ishga tushishda bir marta qilinadi — fabrika bilan (Custom provider'lar darsidagidek). ModuleRef faqat tanlov har so'rovda o'zgarganda kerak bo'ladi: masalan, to'lov usulini mijozning o'zi tanlasa.
10. Real ishda
- "Circular dependency" — Nest loyihalarida eng ko'p uchraydigan ishga tushish xatolaridan. Ko'pincha
UsersModule↔AuthModulejuftligida. - Kod review'da
forwardRefko'rinsa, "bu halqani buzib bo'lmaydimi?" degan savol beriladi. - CI'da
madge --circular src/kabi tekshiruv import halqalarini oldindan ushlaydi. - To'lov, SMS, xarita provayderlari kabi "bir xil vazifa, bir necha amalga oshirish" joylarida
ModuleRefbilan strategiya tanlash uchraydi. Ko'pincha uning o'rniga token ostida provayderlar jadvali (useFactorybilan) ham yoziladi — ikkalasi bir g'oya. - Intervyu: "Nest'da circular dependency qanday hal qilinadi?", "
forwardRefnega funksiya qabul qiladi?", "ModuleRef.getvaresolvefarqi?".
Xulosa
- Aylanma bog'liqlikni Nest ishga tushishda aniqlaydi; fabrika provider'lar halqada umuman ishlamaydi.
forwardRef(() => X)murojaatni kechiktiradi; provider'da ikkala tomonga yozing.- ESM'da fayllararo aylanma +
emitDecoratorMetadata— Vitest'da o'tadi,tscbuild'da TDZReferenceError; build'dan keyin ilovani ham sinang. - Eng yaxshi yechim — halqani buzish: umumiy qismni ajratish, hodisalar, faylni bo'lish.
ModuleRef:get— singleton,resolve— scope'li,create— ro'yxatda yo'q klass;LazyModuleLoader— provider'larni kechiktirib yuklash.getsukut bo'yicha faqat joriy modulda qidiradi (strict: true);ModuleRef— ish vaqtida tanlov uchun, oddiy bog'liqlik uchun emas.
Keyingi dars: Dynamic modullar — forRoot/forRootAsync, ConfigurableModuleBuilder; vazifalar-api ning OmborModule i ikki xil sozlanadigan modulga aylanadi.
Manbalar
- NestJS hujjati — Circular dependency: https://docs.nestjs.com/fundamentals/circular-dependency
- NestJS hujjati — Module reference (
get,resolve,create): https://docs.nestjs.com/fundamentals/module-ref - NestJS hujjati — Lazy-loading modules: https://docs.nestjs.com/fundamentals/lazy-loading-modules
- MDN — Temporal dead zone: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let#temporal_dead_zone_tdz
- Kanon tajribasi (IlmHamroh, 2026-10-27): aylanma provider va modul,
forwardRef,ModuleRef,LazyModuleLoader(Vitest muhitida) va ESM aylanma importningtsc+nodedagi natijasi; NestJS 12.2.0, Node 24.21.0.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!