module.exports è il valore che un file Node.js dà a qualsiasi altro file che lo carica con require(). All'inizio è un oggetto vuoto: aggiungigli proprietà per esportare più cose, oppure assegnagli un nuovo valore (una funzione, una classe) per esportarne una sola. exports è una variabile scorciatoia che punta allo stesso oggetto, quindi exports.add = ... funziona, ma exports = ... rompe il collegamento e non esporta niente.
Ha stampato 5 20 3.14159, poi [ 'add', 'multiply', 'PI' ]. Tutte e tre le righe di math.js hanno aggiunto proprietà allo stesso oggetto, sia passando da module.exports sia da exports, e require('./math') ha restituito quell'oggetto.
Esportare una funzione, una classe o un oggetto
Quando un modulo ha una sola cosa principale, assegnala direttamente a module.exports. Il chiamante riceve così la funzione stessa, non un oggetto che la contiene:
Ha stampato Hello, Ada e Grace (user). Se vedi TypeError: greet is not a function, il chiamante ha usato la forma sbagliata: scrivi const { greet } = require(...) per una proprietà di un oggetto e const greet = require(...) per un valore assegnato.
module.exports ed exports: la trappola della riassegnazione
Prima che il tuo file venga eseguito, Node.js imposta exports = module.exports. Entrambi i nomi puntano a un unico oggetto, e require restituisce module.exports. Assegnare un nuovo valore a exports cambia solo la variabile locale:
broken.js ha esportato {}, perché ora exports punta a un nuovo oggetto che nessuno restituisce. working.js ha esportato { version: 2 }.
| Scrivi | Esportato? |
|---|---|
module.exports.x = 1 | Sì |
exports.x = 1 | Sì, stesso oggetto |
module.exports = { x: 1 } | Sì, sostituisce l'oggetto |
exports = { x: 1 } | No, il valore va perso |
La regola più semplice: scrivi sempre module.exports. Funziona in tutti i casi, e il codice dice quello che fa.
require restituisce lo stesso oggetto in cache
Node.js esegue il codice di un modulo una volta sola, la prima volta che viene richiesto, e mette in cache module.exports. Ogni require successivo dello stesso file restituisce quello stesso oggetto, quindi lo stato dentro un modulo è condiviso da tutti i file che lo caricano:
counter.js is running è comparso una volta sola, b.next() ha restituito 3 e a === b è true, perché la chiave della cache è il path del file risolto e ./counter e ./counter.js puntano allo stesso file. Per questo una connessione al database o un oggetto di configurazione creato in un modulo è condiviso in tutta l'app.
module.exports e i moduli ES
I moduli ES usano invece export e import. Le forme corrispondono così:
// 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';
Un modulo ES può fare import di un file CommonJS: il suo module.exports arriva come import di default. Da Node.js 22.12, anche require() può caricare un modulo ES che non ha await al livello principale. Per capire quale scegliere, vedi CommonJS e moduli ES.
Errori comuni
Mescolare exports.x con module.exports = ... nello stesso file. L'assegnazione sostituisce l'oggetto, quindi ogni exports.x scritto prima sparisce. Scegli uno stile per file.
Esportare un valore che cambia dopo. module.exports = { count } copia il numero al momento del caricamento; quando il modulo aggiorna count, i chiamanti vedono ancora il vecchio valore. Esporta una funzione che lo restituisce, come fa counter.js qui sopra.
Dimenticare ./. require('math') cerca un pacchetto in node_modules, non math.js accanto al tuo file. I file locali iniziano sempre con ./ o ../.
Domande frequenti
Posso usare module.exports ed export nello stesso file?
No. Un file è CommonJS (require, module.exports) oppure un modulo ES (import, export). Node.js lo decide in base all'estensione (.cjs o .mjs) o al campo "type" in package.json.
Perché require restituisce un oggetto vuoto?
Di solito il modulo ha scritto exports = ... invece di module.exports = ..., oppure due moduli si richiedono a vicenda. In un require circolare uno dei due riceve il module.exports dell'altro prima che quel file abbia finito di essere eseguito.
Serve l'estensione .js in require?
No. require('./math') prova ./math, poi ./math.js, ./math.json e ./math.node, poi una cartella ./math con un index.js. In un import di un modulo ES l'estensione è obbligatoria.
Come ricarico un modulo che require ha già messo in cache?
Cancella la sua voce con delete require.cache[require.resolve('./math')] e richiedilo di nuovo. Serve soprattutto nei test e negli strumenti di sviluppo; il codice dell'applicazione non dovrebbe contarci.