Powinieneś wiedzieć, że Javascript (lepiej powiedzieć EcmaScript) nie określa żadnej funkcji do odczytu i zapisu plików.
W rzeczywistości Javascript jest tylko językiem używanym przez wiele środowisk (przeglądarka, lub NodeJS, są przykładami środowisk), które oferują więcej obiektów i funkcji do pracy.
Node był pierwszym środowiskiem, które oferowało sposób na organizowanie kodu w moduły za pomocą specjalnej funkcji o nazwie require()
. Jak to działa? Spróbujmy zaimplementować ją od zera.
Oto przykład require
w pracy:
//test.jsmodule.exports = "Hello World";
//main.jsconst test = require("./test.js"); console.log(test)
Zapiszmy tę funkcję require
.
Co powinna robić funkcja require()
Oczekuje się, że funkcja require
będzie:
- odczytać zawartość pliku javascript w postaci ciągu znaków
- ocenić ten kod
- zapisać wyeksportowaną funkcję/obiekt w pamięci podręcznej do późniejszego użycia (tylko raz odczytaj pliki)
Zrzeczenie się odpowiedzialności
Nie przebudujemy całego NodeJS w jednym poście. W rzeczywistości nie będę implementował wielu kontroli NodeJS i chichotów, jesteśmy tylko zainteresowani zrozumieniem, jak rzeczy działają.
Będziemy nadal potrzebować prawdziwej funkcji require
, aby załadować moduł fs
. Nie oszukuję, po prostu ten post musi się skończyć prędzej czy później 🙂
funkcja myRequire()
oto kod:
//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)}...
Czy zapomniałeś zadeklarować zmienną myRequire?
Nie. W języku JavaScript funkcje zadeklarowane za pomocą słowa kluczowego function
są oceniane przed jakimkolwiek innym kodem (funkcje są „podnoszone”), więc można się do nich odwoływać nawet zanim zostaną zadeklarowane.
Ponadto funkcje mogą mieć właściwości (to jest javascript!), więc możesz dodać właściwość cache
do funkcji myRequire
(krok 1).
W końcu tworzymy właściwość cache
za pomocą Object.create
. Za pomocą tej funkcji możemy określić prototyp obiektu, my wybraliśmy opcję nie określania prototypu. Dlaczego? W ten sposób nie mieszamy z innymi funkcjami lub właściwościami zadeklarowanymi przez runtime. Oto wyjaśnienie.
Powróćmy do myRequire
. Jeśli plik, który importujemy, nie znajduje się w pamięci podręcznej, odczytujemy go z dysku (krok 2).
Następnie deklarujemy pusty obiekt module
z tylko jedną właściwością, exports
(krok 3).
Dodajemy ten pusty moduł do pamięci podręcznej, używając nazwy pliku jako klucza, i wtedy dzieje się magia (krok 4).
Konstruktor funkcji
W JS możemy oceniać kod js string na dwa sposoby. Pierwszym sposobem jest funkcja eval()
, która jest trochę niebezpieczna (psuje zakres), więc jest wysoce odradzana do użycia.
Drugim sposobem na ocenę kodu, który mamy w łańcuchu jest konstruktor Function
. Konstruktor ten pobiera ciąg znaków z argumentami i ciąg znaków z kodem. W ten sposób wszystko ma swój własny zakres i nie psuje rzeczy dla innych.
Więc, w zasadzie tworzymy nową funkcję z tymi zmiennymi (krok 5): require
, exports
, oraz module
. Zastanówmy się przez chwilę nad pierwszym przykładem w tym poście, plikiem test.js
: staje się on
function(require, exports, module) { module.exports = "Hello World" }
i drugim plikiem, main.js
:
function(require, exports, module) { const test = require("./test.js"); console.log(test) }
Zmienne, które wydawały się „globalne” w plikach są rzeczywiście przekazywane jako argumenty funkcji.
Ostatni krok: wykonanie funkcji
Utworzyliśmy (krok 6) zmienną wrapper
, która przechowuje funkcję, ale funkcja nigdy nie jest wykonywana. Robimy to w linii:
wrapper(myRequire, module.exports, module);
Zauważ, że druga zmienna (która powinna być exports
) jest tylko uchwytem do module.exports
; twórcy NodeJS pomyśleli, że mogłoby to pomóc w pisaniu mniejszego kodu…
Gdy Node wykonuje funkcję, wszystko, co zostało „wyeksportowane” (twoje publiczne API) zostaje połączone z cache.
(Pamiętasz linię myRequire.cache = module;
? Kiedy została po raz pierwszy znaleziona przez kompilator, wskazywała na atrapę { exports: {} }
obiektu; teraz zawiera twój moduł.)
UWAGA. Ponieważ przekazujemy
myRequire
do funkcji wrappera, możemy od teraz używaćrequire
w naszych plikach testowych, ale nasz require zostanie wywołany. Dodaj console.log, jeśli mi nie ufasz 😉
Wreszcie… myRequire
zwraca export
zadeklarowane rzeczy (krok 7), które zapisaliśmy do pamięci podręcznej, więc nie będziemy musieli ponownie analizować tego kodu.
Uwagi końcowe
Przykład tego kodu można znaleźć tutaj, wraz z kilkoma logami konsoli, które wyjaśniają, co się dzieje.
Pomysł tego artykułu pochodzi z wyjaśnienia tej funkcji w rozdziale 10 (Moduły). Książka (Eloquent Javascript) jest doskonała, ale miałem potrzebę lepszego zrozumienia i wypróbowania za pomocą debuggera tego, czego nie mogłem zrozumieć za pomocą samego umysłu.
Powinieneś zdecydowanie przeczytać książkę, jeśli chcesz lepiej zrozumieć javascript.
Tagi: javascript – nodejs
.