hur man fixar otäcka cirkulära beroendeproblem en gång för alla i JavaScript & TypeScript
-
index.js
kräverAbstractNode.js
- modulladdaren börjar ladda
AbstractNode.js
och kör modulkoden. Det som det först möter är ett krav (import) uttalande tillLeaf
- så modulladdaren börjar ladda filen
Leaf.js
. Vilket i sin tur börjar med att krävaAbstractnode.js
. -
AbstractNode.js
laddas redan och returneras omedelbart från modulens cache. Eftersom den modulen inte gick utöver den första raden ännu (kravet påLeaf
), har uttalandena som introducerar klassenAbstractNode
ännu inte utförts! - så försöker klassen
Leaf
att sträcka sig från värdetundefined
snarare än en giltig klass. Som kastar runtime undantag som visas ovan. BOM!
Fix försök 1
så det visar sig att vårt cirkulära beroende orsakar ett otäckt problem. Men om vi tittar noga är det ganska lätt att bestämma vad laddningsordern ska vara:
- ladda
AbstractNode
klassen först - ladda
Node
ochLeaf
klassen efter det.
med andra ord, låt oss definiera klassen AbstractNode
först och sedan kräva Leaf
och Node
. Det borde fungera, för Leaf
och Node
behöver inte vara kända ännu när man definierar klassen AbstractNode
. Så länge de definieras innan AbstractNode.from
kallas för första gången borde vi ha det bra. Så låt oss försöka följande förändring:
visas, det finns några problem med den här lösningen:
för det första är detta fult och skalar inte. I en stor kodbas kommer detta att resultera i att importen flyttas slumpmässigt tills saker bara råkar fungera. Vilket ofta bara är tillfälligt, eftersom en liten refactoring eller förändring av importdeklarationer i framtiden kan subtilt justera modulens laddningsorder och återinföra problemet.
för det andra, om detta fungerar är mycket beroende av modulen bundler. Till exempel, i codesandbox, när du kombinerar vår app med paket (eller Webpack eller Rollup), fungerar den här lösningen inte. Men när du kör detta lokalt med nod.js och commonJS moduler denna lösning kan fungera alldeles utmärkt.
Undvik problemet
så uppenbarligen kan detta problem inte åtgärdas enkelt. Kunde det ha undvikits? Svaret är ja, det finns flera sätt att undvika problemet. Först och främst kunde vi ha behållit koden i en enda fil. Som visas i vårt första exempel kan vi på så sätt lösa problemet eftersom det ger full kontroll över den ordning i vilken modulinitieringskoden körs.
för det andra kommer vissa människor att använda ovanstående problem som argument för att göra uttalanden som “man bör inte använda klasser” eller “använd inte arv”. Men det är en förenkling av problemet. Även om jag håller med om att programmerare ofta tillgriper arv för snabbt, för vissa problem är det bara perfekt och kan ge stora fördelar när det gäller kodstruktur, återanvändning eller prestanda. Men viktigast av allt är detta problem inte begränsat till klassarv. Exakt samma problem kan introduceras när man har cirkulära beroenden mellan modulvariabler och funktioner som körs under modulinitiering!
vi kan omorganisera vår kod på ett sådant sätt att vi bryter upp klassen AbstractNode
i mindre bitar, så att AbstractNode
inte har några beroenden på Node
eller Leaf
. I denna sandlåda har from
– metoden dragits ut AbstractNode
– klassen och placerats i en separat fil. Detta löser problemet, men nu är vårt projekt och API strukturerat annorlunda. I stora projekt kan det vara mycket svårt att avgöra hur man kan dra detta trick off, eller till och med omöjligt! Föreställ dig till exempel vad som skulle hända om print
– metoden berodde på Node
eller Leaf
i nästa iteration av vår app…
Bonus: ett extra fult trick jag använde tidigare: returnera basklasser från funktioner och hävstångsfunktionslyftning för att få saker laddade i rätt ordning. Jag är inte ens säker på hur jag ska förklara det ordentligt.
det interna modulmönstret till undsättning!
jag har kämpat med detta problem vid flera tillfällen i många projekt några exempel inkluderar mitt arbete på Mendix, MobX, MobX-state-tree och flera personliga projekt. Vid något tillfälle, för några år sedan skrev jag även ett skript för att sammanfoga alla källfiler och radera alla import uttalanden. En dålig mans modul bundler bara för att få ett grepp om modulen lastning ordning.
men efter att ha löst detta problem några gånger uppträdde ett mönster. En som ger full kontroll på modulen lastning ordning, utan att behöva omstrukturera projektet eller dra konstiga hacka! Detta mönster fungerar perfekt med alla verktygskedjor jag har provat det på (Rollup, Webpack, Parcel, Node).
kärnan i detta mönster är att införa en index.js
och internal.js
fil. Spelets regler är följande:
- modulen
internal.js
både importerar och exporterar allt från varje lokal modul i projektet - varannan modul i projektet importerar endast från filen
internal.js
och aldrig direkt från andra filer i projektet. -
index.js
– filen är den viktigaste ingångspunkten och importerar och exporterar allt fråninternal.js
som du vill exponera för omvärlden. Observera att detta steg endast är relevant om du publicerar ett bibliotek som konsumeras av andra. Så vi hoppade över detta steg i vårt exempel.
Observera att ovanstående regler endast gäller för våra lokala beroenden. Extern modul import lämnas som är. De är inte inblandade i våra cirkulära beroendeproblem trots allt. Om vi tillämpar denna strategi på vår demoapplikation kommer vår kod att se ut så här:
när du applicerar detta mönster för första gången kan det kännas väldigt konstruerat. Men det har några mycket viktiga fördelar!
- först och främst löste vi vårt problem! Som visat här körs vår app glatt igen.
- anledningen till att detta löser vårt problem är: vi har nu full kontroll över modulens laddningsorder. Oavsett import order i
internal.js
är, kommer att vara vår modul lastning ordning. (Du kanske vill kolla bilden nedan, eller läs om modulbeställningsförklaringen ovan för att se varför så är fallet) - vi behöver inte tillämpa refactorings som vi inte vill ha. Vi tvingas inte heller använda fula knep, som att flytta kräver uttalanden längst ner i filen. Vi behöver inte kompromissa med arkitekturen, API eller semantiska strukturen i vår kodbas.
- Bonus: import uttalanden kommer att bli mycket mindre, eftersom vi kommer att importera saker från mindre filer. Till exempel
AbstractNode.js
har bara på import uttalande nu, där det hade två tidigare. - Bonus: med
index.js
har vi en enda källa till sanning, vilket ger finkornig kontroll över vad vi utsätter för omvärlden.