back to top

Node.js: moduli, module.exports e funzione require

Dopo aver parlato dell’Event Loop e dell’architettura di Node.js, passiamo ad un altro argomento fondamentale. Prima dell’avvento dei moduli in ES2015, in Node.js era possibile organizzare il codice in moduli usando una sintassi semplice ed intuitiva. (viene utilizzata la sintassi dei moduli CommonJS) I due elementi chiave di questo sistema sono:

  • La funzione require() che usiamo per importare uno o più moduli da usare in quello corrente.
  • L’oggetto module che è una rappresentazione del modulo corrente e che permette di rendere visibili agli altri moduli, tramite module.exports, funzioni, oggetti o variabili dichiarati nel modulo corrente.

Cerchiamo di capire meglio il tutto partendo da un esempio. Creiamo quindi un file index.js all’interno del quale inseriamo una sola istruzione con cui stampiamo all’interno della shell un messaggio. Eseguiamo quindi lo script col comando node index.js.

console.log(arguments);

Otterremo nella shell un output simile a quello mostrato in basso.

{ '0': {},
  '1':
    { [Function: require]
      resolve: [Function: resolve],
      main:
       Module {
         id: '.',
         exports: {},
         parent: null,
         filename: '/Users/claudio/workspace/modules/example_01/index.js',
         loaded: false,
         children: [],
         paths: [Array] },
      extensions: { '.js': [Function], '.json': [Function], '.node': [Function] },
      cache: { '/Users/claudio/workspace/modules/example_01/index.js': [Object] } },
  '2':
    Module {
      id: '.',
      exports: {},
      parent: null,
      filename: '/Users/claudio/workspace/modules/example_01/index.js',
      loaded: false,
      children: [],
      paths:
       [ '/Users/claudio/workspace/modules/node_modules',
         '/Users/claudio/workspace/node_modules',
         '/Users/claudio/node_modules',
         '/Users/node_modules',
         '/node_modules' ] },
  '3': '/Users/claudio/workspace/modules/example_01/index.js',
  '4': '/Users/claudio/workspace/modules/example_01' 
}

L’oggetto arguments contiene cinque proprietà che andremo ad analizzare nel resto della lezione.

arguments modulo node js

Però, la domanda, a cui dobbiamo dare prima una risposta, è da dove salta fuori arguments. La risposta è che Node.js, prima di eseguire il codice all’interno di un file, lo circonda con una funzione simile a quella riportata sotto.

(function(exports, require, module, __filename, __dirname) {
  // Codice del modulo 
  console.log(arguments);
});

In questo modo, tutto ciò che viene dichiarato all’interno di un modulo mantiene visibilità locale e non va ad ‘affollare’ l’oggetto globale global. In più, ogni modulo ha la possibilità di importarne altri grazie alla funzione require() e decidere cosa rendere visibile all’esterno attraverso module.exports.

Le variabili __filename e __dirname

Le variabili __filename e __dirname contengono rispettivamente i percorsi assoluti del modulo e della cartella che lo contiene.

La funzione require()

La funzione require() serve principalmente per importare altri moduli in quello corrente. È possibile importare moduli nativi, scritti da noi o aggiunti al progetto attraverso NPM. (In quest’ultimo caso i moduli verranno inseriti nella cartella node_modules nella directory base del progetto)

Un’altra funzionalità interessante di require è rappresentata dalla possibilità di testare se un determinato file è stato eseguito direttamente con il comando node o importato da un altro modulo. Per far ciò possiamo usare la proprietà require.main che nel primo caso sarà uguale al valore della variabile module. Vediamo subito un esempio per chiarire meglio quanto appena detto. Consideriamo due file che chiameremo index.js e somma.js. Nel primo file importeremo la funzione esportata dal secondo. All’interno di somma.js useremo require.main per testare se è stato lanciato direttamente col comando node somma.js addendo1 addendo2. In questo caso stamperemo il risultato della somma, in caso contrario useremo module.exports per rendere disponibile la funzione somma() all’interno di altri moduli.

// file somma.js

function isNumber(number) {
  return typeof(number) === 'number';
}

function somma(addendo1, addendo2) {
  return addendo1 + addendo2;
}

if (require.main === module) {
  // file eseguito con il comando 'node somma.js'
  const args = process.argv.slice(2);
  const [
    addendo1, 
    addendo2
  ] = args.map(value => parseInt(value) || 'Invalid argument');

  if (isNumber(addendo1) && isNumber(addendo2)) {
     console.log( somma(addendo1, addendo2) );
  } else {
    console.log(`
      Errore: 
        addendo1 -> ${addendo1}
        addendo2 -> ${addendo2}

      *************************************************

      Uso: node somma.js addendo1 addendo2

      Esempio: node somma.js 1 2 // esegue la somma 1+2
      `
    );
  }
} else {
  /* gli altri moduli potranno usare la funzione somma() 
  *  ma non potranno invocare direttamente la funzione isNumber()
  */ 
  module.exports = somma;
}

All’interno del file somma.js abbiamo dichiarato due funzioni: isNumber() che testa se il valore passato è un numero o meno e somma() che restituisce il risultato della somma di due addendi. Abbiamo poi testato se il modulo è stato eseguito direttamente o importato da un altro modulo. Nel primo caso, usiamo l’array process.argv che contiene tutti gli argomenti passati quando eseguiamo il file somma.js attraverso il comando node somma.js addendo1 addendo2. In particolare argv[0] avrà sempre valore pari alla stringa ‘node’ e argv[1] conterrà il nome del file (somma.js nel nostro caso). Proprio per questo motivo usiamo la funzione Array.prototype.slice() per ricavare un array contenente il valore dei due addendi. Dal momento che il valore dei due addendi sarà di tipo stringa (es. ‘1’) cerchiamo di convertirlo in valore numerico e in caso di successo restituiamo la somma dei due valori. In caso contrario stampiamo un messaggio di errore con le istruzioni su come eseguire lo script. Se eseguiamo il seguente comando:

node somma.js 36 37

Otteniamo come risultato il valore 73. Se invece passiamo uno o entrambi gli addendi in maniera errata, verrà mostrato un messaggio di errore.

node somma.js 1 ciao
    
Errore:
addendo1 -> 1
addendo2 -> Invalid argument
    
*************************************************
    
Uso: node somma.js addendo1 addendo2
    
Esempio: node somma.js 1 2 // esegue la somma 1+2

All’interno del file index.js importiamo il modulo somma. (Non è necessario specificare l’estensione ‘.js’. Node.js è abbastanza ‘intelligente’ e individuerà il file corretto) La costante somma conterrà quindi un riferimento all’omonima funzione dichiarata nel file somma.js. (La funzione require() restituirà infatti il valore che abbiamo assegnato a module.exports all’interno di somma.js)

// file index.js
    
const somma = require('./somma');
    
const addendo1 = 13;
const addendo2 = 17;
const result = somma(addendo1, addendo2);
    
console.log( `${addendo1} + ${addendo2} =  ${result}` );

Lanciando node index.js visualizzeremo il seguente risultato.

13 + 17 =  30

Ovviamente, se all’interno del file index.js avessimo provato a invocare direttamente la funzione isNumber(), dichiarata nel file somma.js, avremmo visualizzato un errore in quanto tale funzione non è accessibile all’esterno del modulo in cui è stata dichiarata.

ReferenceError: isNumber is not defined

Nell’esempio appena visto, alla funzione require(‘./somma’) abbiamo passato come argomento un percorso relativo (‘.’ indica la directory corrente, ovvero la cartella in cui si trova il file index.js). Tuttavia, nella prima lezione di questa guida avevamo usato il modulo fs importandolo nel seguente modo.

// notate che abbiamo espresso solo il nome del modulo 
// e non un percorso
const fs = require('fs');
    
// ...

Come fa la funzione require() a localizzare un modulo

Ci si può chiedere a questo punto che algoritmo usa Node.js per recuperare un modulo con la funzione require(). Possiamo sintetizzarlo in pochi semplici punti.

Per importare i moduli nativi (es. const fs = require(‘fs’);) e quelli installati con l’ausilio di NPM, specificheremo solo il nome e non il percorso. Quando passiamo solo un nome alla funzione require(), Node.js proverà prima a importare un modulo nativo, se non trova nessun modulo con quel nome, comincerà a cercarlo all’interno di una cartella denominata node_modules all’interno della cartella corrente. In caso di esito negativo, navigherà a ritroso fino alla cartella radice e ogni volta verificherà se esiste una cartella chiamata node_modules che contiene il file richiesto.

L’array module.paths presenta al suo interno tutti i percorsi che verranno esplorati da Node.js alla ricerca di un modulo che è stato richiesto passando alla funzione require() soltanto un nome.

console.log(module.paths)
    
[ 
  '/Users/claudio/workspace/modules/node_modules',
  '/Users/claudio/workspace/node_modules',
  '/Users/claudio/node_modules',
  '/Users/node_modules',
  '/node_modules' 
]

In alternativa possiamo specificare il percorso del modulo da importare. Solitamente useremo un percorso relativo per recuperare un modulo che abbiamo creato all’interno del nostro progetto. (Esattamente come abbiamo fatto nell’esempio visto in precedenza in cui a partire dal file index.js importavamo un altro modulo usando require(‘./somma’))

La funzione require() e i file.json

La funzione require() permette di importare anche i file.json di cui effettuerà anche il parsing come potete vedere nell’esempio sottostante in cui all’interno della stessa cartella abbiamo creato due file: data.json e app.js. All’interno di quest’ultimo importeremo i dati contenuti nel primo con l’ausilio della funzione require().

// data.json
{ 
  "name":"Homer", 
  "age":39, 
  "city":"Springfield",
  "occupation": "Safety Inspector at the Springfield Nuclear Power Plant"
}
// app.js
const data = require('./data.json');
    
console.log(data);

All’interno della shell verrà stampato l’oggetto javascript data ottenuto dal parsing del file data.json

La funzione require() e le cartelle

Come argomento della funzione require() possiamo specificare anche il nome o il percorso di una cartella. In tal caso, Node.js cercherà di importare il modulo contenuto in un file index.js all’interno di quella cartella. In alternativa, con l’ausilio di un file package.json, possiamo indicare quale file deve essere importato. Dovremo soltanto specificare il nome di tale file come valore del campo ‘main’. Vediamo un esempio in cui creeremo in una directory un file index.js e una cartella lib che conterrà al suo interno i file moltiplica.js e package.json.

// lib/package.json
    
{
  "name": "simple_lib",
  "version": "1.0.0",
  "main": "moltiplica.js"
}
// lib/moltiplica.js
    
module.exports = function moltiplica (a, b) {
  return a * b;
}
// index.js
    
const moltiplica = require('./lib');
    
console.log(moltiplica(3, 2)); // 6

Node.js importerà, come indicato dal file package.json, la funzione dichiarata all’interno del file moltiplica.js presente nella directory lib. In assenza del file package.json, Node.js avrebbe cercato un file index.js dentro la cartella lib e non trovandolo avrebbe mostrato un messaggio di errore.

Efficienza della funzione require()

Quando invochiamo la funzione require() per la prima volta, Node.js aggiungerà il modulo importato all’interno di una cache (require.cache). Si tratta di un semplice oggetto a cui verrà aggiunta una proprietà corrispondente al percorso di ogni modulo importato. Consideriamo un altro esempio per capire meglio di cosa si tratta. Creiamo un file index.js e un file moduleA.js contenenti i seguenti frammmenti di codice.

// file moduleA.js
      
console.log('ModuleA');
// file index.js
    
require('./moduleA'); // ModuleA
    
require('./moduleA');

Nel file index.js importiamo il file moduleA.js che verrà eseguito e stamperà semplicemente il nome del modulo nella shell. Verrà stampato ‘ModuleA’ soltanto una volta in corrispondenza della prima esecuzione della funzione require().

L’oggetto require.cache, dopo la prima esecuzione di require(‘./moduleA’), sarà simile a quello riportato in basso. Si tratta di un oggetto contenente due proprietà aventi come nomi i percorsi dei file index.js e moduleA.js.

// require.cache
{ 
  '/Users/claudio/guida_node/moduli/index.js':
    Module {
      id: '.',
      exports: {},
      parent: null,
      filename: '/Users/claudio/guida_node/moduli/index.js',
      loaded: false,
      children: [ [Object] ],
      paths:
      [ '/Users/claudio/guida_node/moduli/node_modules',
        '/Users/claudio/guida_node/node_modules',
        '/Users/claudio/node_modules',
        '/Users/node_modules',
        '/node_modules' ] 
    },
  '/Users/claudio/guida_node/moduli/moduleA.js':
    Module {
      id: '/Users/claudio/guida_node/moduli/moduleA.js',
      exports: {},
      parent:
      Module {
        id: '.',
        exports: {},
        parent: null,
        filename: '/Users/claudio/guida_node/moduli/index.js',
        loaded: false,
        children: [Array],
        paths: [Array] },
      filename: '/Users/claudio/guida_node/moduli/moduleA.js',
      loaded: true,
      children: [],
      paths:
      [ '/Users/claudio/guida_node/moduli/node_modules',
        '/Users/claudio/guida_node/node_modules',
        '/Users/claudio/node_modules',
        '/Users/node_modules',
        '/node_modules' ] 
    } 
}

Modifichiamo ora il file index.js come segue. Eliminiamo cioè dalla cache la proprietà relativa a moduleA.js.

require('./moduleA'); // moduleA
    
delete require.cache['/Users/claudio/guida_node/moduli/moduleA.js'];
    
console.log('Ora verrà eseguita di nuovo la funzione console.log('ModuleA')...');
require('./moduleA'); // moduleA

In questo caso verrà stampato due volte il messaggio ‘ModuleA’ dal momento che, dopo aver importato moduleA, cancelliamo l’elemento corrispondente dalla cache. Nonostante quanto visto in questo esempio, la soluzione ottimale resta comunque quella di dichiarare una funzione all’interno di un modulo ed eseguirla il numero di volte desiderato all’interno del file che importa il modulo.

// file moduleA.js
    
module.exports = function log() {
  console.log('ModuleA');
}
// file index.js
    
const log = require('./moduleA');
    
log(); // moduleA
log(); // moduleA

Module.exports e exports

Per concludere questa lezione, parliamo dell’ultimo pezzo del puzzle che ci permette di indicare quali funzioni, variabili e oggetti saranno visibili al di fuori del modulo corrente. Parliamo in particolare di module.exports e di exports. La prima cosa da evidenziare è che quest’ultimo è semplicemente un riferimento al primo. Sarà comunque l’oggetto module.exports ciò che sarà visibile negli altri moduli.

exports = module.exports
module.exports e exports

Ciò significa che dobbiamo porre particolare attenzione all’uso di exports e non assegnare mai direttamente un nuovo valore a tale variabile perché sarebbe del tutto inutile. Spieghiamo il motivo con un esempio. Supponiamo di avere due file module.js e index.js.

// file module.js
    
function foo() {
  console.log('foo');
}
    
exports = foo;
    
/*  A questo punto module.exports è ancora un oggetto vuoto e
*   exports non punta più a module.exports
*/
// file index.js
    
const oggettoVuoto = require('./module');
    
console.log(oggettoVuoto); // {}

Come potete vedere da questo esempio, abbiamo sovrascritto il valore di exports che quindi non punta più a module.exports. Il risultato è che, se cerchiamo di importare module.js in index.js, ci verrà restituito un oggetto vuoto.

module.exports e exports

Per semplicità possiamo usare exports quando vogliamo esportare più funzioni, oggetti o variabili di un determinato modulo.

function foo() {
  console.log('foo');
}
    
function baz() {
  console.log('baz');
}
    
exports.foo = foo;
exports.baz = baz;
    
/*  
*   exports = module.exports = { foo: [Function: foo], baz: [Function: baz] }
*/

Infine vediamo alcuni esempi che mostrano come poter usare module.exports.

// module1.js
    
function foo() {};
    
module.exports = exports = foo;
    
// module2.js
    
class Persona {};
    
module.exports = exports = Persona;
    
// module3.js
    
function foo() {};
    
function bar() {};
    
function baz() {};
    
 module.exports = exports = {
  foo,
  bar2: bar
}
    
// module4.js
    
const TOKEN = 1234;
    
module.exports.TOKEN = TOKEN;

Conclusioni

Nella prossima lezione riprenderemo il discorso già iniziato nel precedente sugli Event Emitter. Vedremo alcuni esempi e come implementare una versione semplificata di Event Emitter.

Pubblicitร