module.exports to wartość, którą plik Node.js daje każdemu innemu plikowi, który wczyta go przez require(). Na początku to pusty obiekt: dodaj do niego właściwości, żeby wyeksportować kilka rzeczy, albo przypisz mu nową wartość (funkcję, klasę), żeby wyeksportować jedną. exports to skrótowa zmienna wskazująca ten sam obiekt, więc exports.add = ... działa, ale exports = ... zrywa powiązanie i niczego nie eksportuje.
Wypisało 5 20 3.14159, a potem [ 'add', 'multiply', 'PI' ]. Wszystkie trzy linie w math.js dodały właściwości do tego samego obiektu, niezależnie od tego, czy przez module.exports, czy przez exports, a require('./math') zwróciło ten obiekt.
Eksport funkcji, klasy albo obiektu
Gdy moduł ma jedną główną rzecz, przypisz ją bezpośrednio do module.exports. Wywołujący dostaje wtedy samą funkcję, a nie obiekt wokół niej:
Wypisało Hello, Ada i Grace (user). Jeśli widzisz TypeError: greet is not a function, wywołujący użył złego kształtu: pisz const { greet } = require(...) dla właściwości obiektu i const greet = require(...) dla przypisanej wartości.
module.exports vs exports: pułapka z przypisaniem
Zanim twój plik się wykona, Node.js ustawia exports = module.exports. Obie nazwy wskazują jeden obiekt, a require zwraca module.exports. Przypisanie nowej wartości do exports zmienia tylko lokalną zmienną:
broken.js wyeksportował {}, bo exports wskazuje teraz nowy obiekt, którego nikt nie zwraca. working.js wyeksportował { version: 2 }.
| Piszesz | Wyeksportowane? |
|---|---|
module.exports.x = 1 | Tak |
exports.x = 1 | Tak, ten sam obiekt |
module.exports = { x: 1 } | Tak, zastępuje obiekt |
exports = { x: 1 } | Nie, wartość przepada |
Najprostsza zasada: zawsze pisz module.exports. Działa w każdym przypadku, a kod mówi, co robi.
require zwraca ten sam obiekt z cache
Node.js wykonuje kod modułu raz, przy pierwszym require, i zapisuje w cache module.exports. Każde kolejne require tego samego pliku zwraca ten sam obiekt, więc stan wewnątrz modułu jest wspólny dla każdego pliku, który go wczytuje:
counter.js is running pojawiło się raz, b.next() zwróciło 3, a a === b to true, bo kluczem cache jest rozwiązana ścieżka pliku, a ./counter i ./counter.js wskazują ten sam plik. Dlatego połączenie z bazą danych albo obiekt konfiguracji utworzony w jednym module jest wspólny dla całej aplikacji.
module.exports i moduły ES
Moduły ES używają zamiast tego export i import. Formy odpowiadają sobie tak:
// CommonJS // ES modules
module.exports = greet; export default greet;
module.exports.add = add; export { add };
const greet = require('./greet'); import greet from './greet.js';
const { add } = require('./math'); import { add } from './math.js';
Moduł ES może zaimportować plik CommonJS przez import: jego module.exports przychodzi jako import domyślny. Od Node.js 22.12 require() potrafi też wczytać moduł ES bez await na najwyższym poziomie. Kiedy wybrać które, przeczytasz w CommonJS vs moduły ES.
Częste błędy
Mieszanie exports.x z module.exports = ... w jednym pliku. Przypisanie zastępuje obiekt, więc każde exports.x zapisane wcześniej przepada. Wybierz jeden styl na plik.
Eksportowanie wartości, która później się zmienia. module.exports = { count } kopiuje liczbę w chwili wczytania; gdy moduł zaktualizuje count, wywołujący nadal widzą starą wartość. Eksportuj funkcję, która ją zwraca, jak counter.js powyżej.
Pomijanie ./. require('math') szuka pakietu w node_modules, a nie pliku math.js obok twojego pliku. Lokalne pliki zawsze zaczynają się od ./ albo ../.
Najczęściej zadawane pytania
Czy mogę używać module.exports i export w tym samym pliku?
Nie. Plik to albo CommonJS (require, module.exports), albo moduł ES (import, export). Node.js decyduje na podstawie rozszerzenia (.cjs lub .mjs) albo pola "type" w package.json.
Dlaczego require zwraca pusty obiekt?
Zwykle moduł przypisał exports = ... zamiast module.exports = ... albo dwa moduły wzajemnie się wczytują. Przy cyklicznym require jeden z nich dostaje module.exports drugiego, zanim ten plik skończy się wykonywać.
Czy w require potrzebne jest rozszerzenie .js?
Nie. require('./math') próbuje ./math, potem ./math.js, ./math.json i ./math.node, a potem folderu ./math z plikiem index.js. W import w module ES rozszerzenie jest wymagane.
Jak przeładować moduł, który require już ma w cache?
Usuń jego wpis przez delete require.cache[require.resolve('./math')] i wczytaj go ponownie przez require. Przydaje się to głównie w testach i narzędziach deweloperskich; kod aplikacji nie powinien na tym polegać.