Du bør vide, at Javascript (bedre at sige EcmaScript) ikke specificerer nogen funktion til at læse og skrive filer.
Samtidig er Javascript blot det sprog, der bruges af mange miljøer (browseren eller NodeJS er eksempler på miljøer), der tilbyder flere objekter og funktioner at arbejde med.
Node var det første miljø, der tilbød en måde at organisere kode i moduler på ved hjælp af en særlig funktion kaldet require()
. Hvordan fungerer det? Lad os prøve at implementere den fra nul.
Her er et eksempel på require
på arbejde:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Lad os skrive denne require
-funktion.
Hvad skal en require() funktion gøre
En require
funktion forventes at:
- læse indholdet af en javascript-fil i en streng
- vurdere den kode
- gemme den eksporterede funktion/det eksporterede objekt i en cache til senere brug (kun læse filer én gang)
Disclaimer
Vi vil ikke genopbygge hele NodeJS i et enkelt indlæg. Faktisk vil jeg ikke implementere mange NodeJS-kontroller og giggles, vi er kun interesseret i at forstå, hvordan tingene fungerer.
Vi vil stadig have brug for den rigtige require
-funktion til at indlæse fs
-modulet. Jeg snyder ikke, det er bare det, at dette indlæg skal slutte før eller siden 🙂
myRequire() funktion
Her er koden:
//file setup.jsconst fs = require('fs');myRequire.cache = Object.create(null); //(1)function myRequire(name) { if (!(name in myRequire.cache)) { let code = fs.readFileSync(name, 'utf8'); //(2) let module = {exports: {}}; //(3) myRequire.cache = module; //(4) let wrapper = Function("require, exports, module", code); //(5) wrapper(myRequire, module.exports, module); //(6) } return myRequire.cache.exports; //(7)}...
Glemte du at deklarere myRequire-variablen?
Nej. I Javascript bliver funktioner, der er erklæret med nøgleordet function
, evalueret før al anden kode (funktioner er “hoisted”), så de kan refereres, selv før de er erklæret.
Og funktioner kan også have egenskaber (det er javascript!), så du kan tilføje cache
-egenskaben til myRequire
-funktionen (trin 1).
Endeligt opretter vi cache
-egenskaben med Object.create
. Med denne funktion kan vi angive objektprototypen, vi har valgt ikke at angive en prototype. Hvorfor? På denne måde roder vi ikke med andre funktioner eller egenskaber, der er deklareret af runtime. Her er en forklaring.
Lad os gå tilbage til myRequire
. Hvis den fil, vi importerer, ikke er i cache, læser vi filen fra disken (trin 2).
Derpå erklærer vi et tomt module
-objekt med kun én egenskab, exports
(trin 3).
Vi tilføjer dette tomme modul til cachen med filnavnet som nøgle, og så sker magien (trin 4).
Funktionskonstruktøren
I JS kan vi evaluere en streng js-kode på to måder. Den første måde er via eval()
funktion, der er en smule farlig (den ødelægger scope), så det frarådes stærkt at bruge den.
Den anden måde at evaluere kode, som vi har i en streng, er via Function
konstruktøren. Denne konstruktør tager en streng med argumenterne og en streng med koden. På denne måde har alting sit eget scope og ødelægger ikke tingene for andre.
Så i bund og grund opretter vi en ny funktion med disse variabler (trin 5): require
, exports
og module
. Lad os et øjeblik tænke på det første eksempel i dette indlæg, filen test.js
: den bliver til
function(require, exports, module) { module.exports = "Hello World" }
og den anden fil, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
Variabler, der virkede “globale” i filer, overføres faktisk som funktionsargumenter.
Sidste trin: eksekvering af funktionen
Vi har oprettet (trin 6) en wrapper
-variabel, der indeholder en funktion, men funktionen bliver aldrig eksekveret. Det gør vi på linjen:
wrapper(myRequire, module.exports, module);
Bemærk, at den anden variabel (der skulle være exports
) blot er et håndtag til module.exports
; NodeJS-skaberne mente, at dette kunne have hjulpet med at skrive mindre kode…
Når Node udfører funktionen, bliver alt, der blev “eksporteret” (dit offentlige API), knyttet til cachen.
(Kan du huske myRequire.cache = module;
-linjen? Da den først blev fundet af compileren, pegede den på et dummy { exports: {} }
-objekt; nu indeholder den dit modul.)
NOTE. Da vi overdrager
myRequire
til wrapper-funktionen, kan vi fra nu af brugerequire
i vores testfiler, men vores require bliver kaldt. Tilføj en console.log, hvis du ikke stoler på mig 😉
Endeligt… myRequire
returnerer de export
ede ting, som du deklarerede (trin 7), og som vi gemte i cachen, så vi ikke behøver at revurdere denne kode igen.
Slutovervejelser
Et eksempel på denne kode kan findes her, sammen med nogle konsollogs, der forklarer, hvad der foregår.
Ideen til denne artikel stammer fra forklaringen af denne funktion i kapitel 10 (Moduler). Bogen (Eloquent Javascript) er fremragende, men jeg havde lyst til at forstå bedre, og prøve med en debugger, hvad jeg ikke kunne forstå med min hjerne alene.
Du bør helt sikkert læse bogen, hvis du vil forstå javascript bedre.
Tags: javascript – nodejs