En introduktion til, hvordan JavaScript-pakkeforvaltere fungerer

Ashley Williams er en af ​​lederne af Node.js-samfundet. Hun tweetede om en ny pakkeforvalter.

Jeg forstod ikke rigtig, hvad hun mente, så jeg besluttede at grave dybere ned og læse om, hvordan pakkechefer fungerer.

Dette var rigtigt, da det nyeste barn på JavaScript-pakkehåndteringsblokken - Garn - netop var ankommet og genererede en masse brummer.

Så jeg brugte denne lejlighed til også at forstå, hvordan og hvorfor Garn gør tingene anderledes end npm.

Jeg havde så meget sjov med at undersøge dette. Jeg ville ønske, jeg havde gjort det for længe siden. Så jeg skrev denne enkle introduktion til npm og Garn for at dele det, jeg har lært.

Lad os starte med nogle definitioner:

Hvad er en pakke?

En pakke er et genanvendeligt stykke software, der kan downloades fra et globalt register til en udviklers lokale miljø. Hver pakke afhænger muligvis af andre pakker.

Hvad er en pakkehåndtering?

Kort sagt - en pakkehåndtering er et stykke software, der lader dig styre de afhængigheder (ekstern kode skrevet af dig eller en anden), som dit projekt har brug for for at fungere korrekt.

De fleste pakkeforvaltere jonglerer følgende stykker af dit projekt:

Projektkode

Dette er koden for dit projekt, som du har brug for til at styre forskellige afhængigheder. Typisk kontrolleres al denne kode i et versionskontrolsystem som Git.

Manifest fil

Dette er en fil, der holder styr på alle dine afhængigheder (pakkerne, der skal administreres). Den indeholder også andre metadata om dit projekt. I JavaScript-verdenen er denne fil dinpackage.json

Afhængighedskode

Denne kode udgør dine afhængigheder. Det bør ikke muteres i løbet af din applikations levetid og skal være tilgængeligt af din projektkode i hukommelsen, når det er nødvendigt.

Lås fil

Denne fil skrives automatisk af selve pakkehåndteringen. Den indeholder alle de oplysninger, der er nødvendige for at gengive det fulde afhængighedskildetræ. Den indeholder information om hvert af dit projekts afhængigheder sammen med deres respektive versioner.

Det er værd at påpege på dette tidspunkt, at garn bruger en låsfil, mens npm ikke gør det. Vi vil snakke lidt om konsekvenserne af denne sondring.

Nu hvor jeg har introduceret dig til delene af en pakkehåndtering, lad os selv diskutere afhængigheder.

Flad versus indlejret afhængighed

For at forstå forskellen mellem Flat versus Nested-afhængighedsordningerne, lad os prøve at visualisere en afhængighedsgraf over afhængigheder i dit projekt.

Det er vigtigt at huske på, at de afhængigheder, dit projekt afhænger af, måske har deres egne afhængigheder. Og disse afhængigheder kan til gengæld have nogle afhængigheder til fælles.

For at gøre dette klart, lad os sige, at vores applikation afhænger af afhængigheder A, B og C, og C afhænger af A.

Flade afhængigheder

Som vist på billedet har både appen og C A som deres afhængighed. For afhængighedsopløsning i et fladt afhængighedsskema er der kun ét lag afhængigheder, som din pakkehåndtering har brug for at krydse.

Lang historie kort - du kan kun have en version af en bestemt pakke i dit kildetræ, da der er et fælles navneområde for alle dine afhængigheder.

Antag, at pakke A er opgraderet til version 2.0. Hvis din app er kompatibel med version 2.0, men pakke C ikke er, har vi brug for to versioner af pakke A for at få vores app til at fungere korrekt. Dette er kendt som et afhængighedshelvede.

Indlejrede afhængigheder

En enkel løsning til at håndtere problemet med Dependency Hell er at have to forskellige versioner af pakke A - version 1.0 og version 2.0.

Det er her indlejrede afhængigheder spiller ind. I tilfælde af indlejrede afhængigheder kan enhver afhængighed isolere sine egne afhængigheder fra andre afhængigheder i et andet navneområde.

Pakkeadministratoren skal gennemgå flere niveauer for afhængighedsopløsning.

Vi kan have flere kopier af en enkelt afhængighed i en sådan ordning.

Men som du måske har gættet, fører det også til et par problemer. Hvad hvis vi tilføjer en ny pakke - pakke D - og det afhænger også af version 1.0 af pakke A?

Så med denne ordning kan vi ende med duplikering af version 1.0 af pakke A. Dette kan forårsage forvirring og optager unødvendig diskplads.

En løsning på ovenstående problem er at have to versioner af pakke A, v1.0 og v2.0, men kun en kopi af v1.0 for at undgå unødvendig duplikering. Dette er den tilgang, som npm v3 tager, hvilket reducerer den tid, det tager at krydse afhængighedstræet betydeligt.

Som Ashley Williams forklarer, installerer npm v2 afhængigheder på en indlejret måde. Derfor er npm v3 betydeligt hurtigere ved sammenligning.

Determinisme vs ikke-determinisme

Et andet vigtigt koncept i pakkeforvaltere er determinisme. I forbindelse med JavaScript-økosystemet betyder determinisme, at alle computere med en given package.jsonfil alle vil have nøjagtigt det samme kildetræ af afhængigheder installeret på dem i deres node_modulesmappe.

Men med en ikke-deterministisk pakkehåndtering er dette ikke garanteret. Selvom du har nøjagtigt det samme package.jsonpå to forskellige computere, kan layoutet på din node_modulesvære forskellig mellem dem.

Determinisme er ønskelig. Det hjælper dig med at undgå problemer med "arbejdet på min maskine, men det brød, da vi implementerede det" , der opstår, når du har forskellige node_modulespå forskellige computere.

npm v3 har som standard ikke-deterministiske installationer og tilbyder en shrinkwrap-funktion, der gør installationer deterministiske. Dette skriver alle pakkerne på disken til en låsfil sammen med deres respektive versioner.

Garn tilbyder deterministiske installationer, fordi det bruger en låsfil til at låse alle afhængigheder rekursivt ned på applikationsniveau. Så hvis pakke A afhænger af v1.0 af pakke C, og pakke B afhænger af v2.0 af pakke A, vil begge blive skrevet separat til låsefilen.

Når du kender de nøjagtige versioner af de afhængigheder, du arbejder med, kan du nemt reproducere builds og derefter spore og isolere bugs.

”For at gøre det mere tydeligt package.jsonsiger din “ hvad jeg vil ” til projektet, mens din lockfile siger “ hvad jeg havde ” med hensyn til afhængigheder. - Dan Abramov

Så nu kan vi i første omgang vende tilbage til det oprindelige spørgsmål, der startede mig på denne læring: Hvorfor betragtes det som en god praksis at have låsefiler til applikationer, men ikke til biblioteker?

Hovedårsagen er, at du faktisk implementerer applikationer. Så du skal have deterministiske afhængigheder, der fører til reproducerbare bygninger i forskellige miljøer - test, iscenesættelse og produktion.

Men det samme gælder ikke for biblioteker. Biblioteker er ikke implementeret. De er vant til at opbygge andre biblioteker eller i anvendelse selv. Biblioteker skal være fleksible, så de kan maksimere kompatibiliteten.

Hvis vi havde en låsefil for hver afhængighed (bibliotek), som vi brugte i en applikation, og applikationen blev tvunget til at respektere disse låsefiler, ville det være umuligt at komme nogen steder tæt på en flad afhængighedsstruktur, vi talte om tidligere med den semantiske versionering fleksibilitet, som er det bedste tilfælde for afhængighedsopløsning.

Her er hvorfor: hvis din applikation skal rekursivt ære låsefilerne for alle dine afhængigheder, ville der være versionskonflikter overalt - selv i relativt små projekter. Dette ville medføre en stor mængde uundgåelig duplikering på grund af semantisk versionering.

Dette betyder ikke, at biblioteker ikke kan have låsefiler. Det kan de bestemt. Men den vigtigste afhentning er, at pakkeadministratorer som Garn og npm - som forbruger disse biblioteker - ikke respekterer disse låsefiler.

Tak for læsningen! Hvis du synes, dette indlæg var nyttigt, skal du trykke på “︎❤” for at hjælpe med at promovere dette stykke til andre.