slik løser du ekkel sirkulære avhengighetsproblemer en gang for alle I JavaScript Og TypeScript
-
index.js
kreverAbstractNode.js
- modullasteren starter lasting
AbstractNode.js
og kjører modulkoden. Saken den først møter er en require (import) – setning tilLeaf
- , Slik at modullasteren begynner å laste
Leaf.js
– filen. Som i sin tur starter ved å kreveAbstractnode.js
. -
AbstractNode.js
lastes allerede inn, og returneres umiddelbart fra modulbufferen. Men siden den modulen ikke gikk utover den første linjen ennå (kravet tilLeaf
), er uttalelsene som introdusererAbstractNode
– klassen ennå ikke utført! - så prøver klassen
Leaf
å strekke seg fra verdienundefined
i stedet for en gyldig klasse. Som kaster runtime unntaket vist ovenfor. BANG!
Løs forsøk 1
så det viser seg at vår sirkulære avhengighet forårsaker et styggt problem. Men hvis vi ser nøye ut, er det ganske enkelt å avgjøre hva lasteordren skal være:
- Last inn
AbstractNode
– klassen først - Last inn
Node
ogLeaf
– klassen etter det.
Med andre ord, la oss definere klassen AbstractNode
først, og deretter kreve Leaf
og Node
. Det burde fungere, fordi Leaf
og Node
ikke må være kjent ennå når du definerer AbstractNode
– klassen. Så lenge de er definert før AbstractNode.from
kalles for første gang, bør vi ha det bra. Så la oss prøve følgende endring:
Viser seg, det er noen problemer med denne løsningen:
For Det Første er dette styggt og skalerer ikke. I en stor kodebase vil dette resultere i å flytte importen tilfeldig rundt til ting bare skjer for å fungere. Som ofte bare midlertidig, som en liten refactoring eller endring i import uttalelser i fremtiden kan subtilt justere modulen lasting rekkefølge, gjeninnføre problemet.
for det andre, om dette fungerer er svært avhengig av modulen bundler. For eksempel, i codesandbox, når bunting vår app Med Pakke (Eller Webpack eller Rollup), fungerer denne løsningen ikke. Men når du kjører dette lokalt med Node.js og commonJS moduler denne løsningen kan fungere helt fint.
Unngå problemet
så tilsynelatende kan dette problemet ikke løses enkelt. Kunne det vært unngått? Svaret er ja, det er flere måter å unngå problemet på. Først av alt, kunne vi ha holdt koden i en enkelt fil. Som vist i vårt første eksempel, kan vi på den måten løse problemet da det gir full kontroll over rekkefølgen der modulinitialiseringskoden kjører.
for det Andre vil noen mennesker bruke problemet ovenfor som argument for å komme med uttalelser som “Man bør ikke bruke klasser” eller “ikke bruk arv”. Dette er en overforenkling av problemet. Selv om jeg er enig i at programmerere ofte ty til arv for fort, for noen problemer er det bare perfekt og kan gi store fordeler når det gjelder kodestruktur, gjenbruk eller ytelse. Men viktigst er dette problemet ikke begrenset til klassearv. Nøyaktig det samme problemet kan innføres når du har sirkulære avhengigheter mellom modulvariabler og funksjoner som kjører under modulinitialisering!
vi kunne omorganisere koden vår på en slik måte at vi bryter opp AbstractNode
– klassen i mindre stykker, slik at AbstractNode
ikke har noen avhengigheter på Node
eller Leaf
. I denne sandkassen har from
– metoden blitt trukket ut AbstractNode
– klassen og satt inn i en egen fil. Dette løser problemet, men nå er vårt prosjekt og API strukturert annerledes. I store prosjekter kan det være svært vanskelig å finne ut hvordan å trekke dette trikset av, eller umulig! Tenk deg for eksempel hva som ville skje hvis print
– metoden var avhengig av Node
eller Leaf
i neste iterasjon av appen vår…
Bonus: et ekstra stygt triks jeg brukte før: returner baseklasser fra funksjoner og løftefunksjon for å få ting lastet i riktig rekkefølge. Jeg er ikke engang sikker på hvordan jeg skal forklare det riktig.
det interne modulmønsteret til redning!
jeg har kjempet med dette problemet ved flere anledninger på tvers av mange prosjekter Noen eksempler inkluderer mitt arbeid På Mendix, MobX, MobX-state-tree og flere personlige prosjekter. På et tidspunkt, for noen år siden jeg selv skrev et skript for å sette sammen alle kildefiler og slette alle import uttalelser. En fattig-mans modul bundler bare for å få tak i modulen lasting rekkefølge.
Men etter å ha løst dette problemet et par ganger, oppstod et mønster. En som gir full kontroll på modulen lasting rekkefølge, uten å måtte restrukturere prosjektet eller trekke rare hacks! Dette mønsteret fungerer perfekt med alle verktøykjedene jeg har prøvd det på (Rollup, Webpack, Parcel, Node).
kjernen i dette mønsteret er å introdusere en index.js
og internal.js
fil. Reglene i spillet er som følger:
- modulen
internal.js
importerer og eksporterer både alt fra hver lokal modul i prosjektet - Hver annen modul i prosjektet importerer bare fra filen
internal.js
, og aldri direkte fra andre filer i prosjektet. - filen
index.js
er hovedinngangspunktet og importerer og eksporterer alt frainternal.js
som du vil eksponere for omverdenen. Vær oppmerksom på at dette trinnet bare er relevant hvis du publiserer et bibliotek som brukes av andre. Så vi hoppet over dette trinnet i vårt eksempel.
Merk at reglene ovenfor bare gjelder for våre lokale avhengigheter. Ekstern modul import er igjen som den er. De er ikke involvert i våre sirkulære avhengighetsproblemer tross alt. Hvis vi bruker denne strategien til vår demo-applikasjon, vil koden vår se slik ut:
når du bruker dette mønsteret for første gang, kan det føles veldig contrived. Men det har noen svært viktige fordeler!
- Først av alt løste vi vårt problem! Som vist her vår app er lykkelig kjører igjen.
- grunnen til at dette løser vårt problem er: vi har nå full kontroll over modulen lasting rekkefølge. Uansett importordre i
internal.js
er, vil være vår modul lasting rekkefølge. (Du vil kanskje sjekke bildet nedenfor, eller lese modulordreforklaringen ovenfor for å se hvorfor dette er tilfelle) - Vi trenger ikke å bruke refactorings vi ikke vil ha. Vi er heller ikke tvunget til å bruke stygge triks, som å flytte krever uttalelser til bunnen av filen. Vi trenger ikke å kompromittere arkitekturen, API-en eller den semantiske strukturen i kodebasen vår.
- Bonus: import uttalelser vil bli mye mindre, som vi skal importere ting fra mindre filer. For eksempel
AbstractNode.js
har bare på import setning nå, hvor den hadde to før. - Bonus: med
index.js
har vi en enkelt kilde til sannhet, som gir finkornet kontroll over hva vi utsetter for omverdenen.