Moduli ECMA Script o CommonJS?

Principali differenze tra i moduli ECMA Script e CommonJS in Node.js

Davide D'Antonio
weBeetle
10 min readSep 14, 2021

--

In Node.js v14, è possibile creare due tipologie di moduli: ci sono moduli “vecchio stile” che utilizzano la specifica CommonJS (CJS) e gli script “nuovo stile” ESM che utilizzano la specifica ECMA Script. Gli script CJS usano require() e module.exports, mentre quelli ESM utilizzano import ed export. In questo articolo daremo uno sguardo al funzionamento di queste due specifiche descrivendone le principali differenze.

CJS !== ESM

Una cosa importante da chiarire prima di iniziare ad addentrarci nella discussione è che la specifica CommonJS è completamente diversa da quella ECMA Script. Possono risultare molto simili ma le loro implementazioni sono molto diverse.

Per comprendere ancora meglio quanto detto guardate l’immagine sottostante. In un primo momento i due animali raffigurati possono sembrare lo stesso ma in realtà il primo è un leone marino ed il secondo è una foca. Allo stesso modo non confondete i moduli CJS con quelli ESM. Si somigliano ma sono due specifiche completamente diverse!

Photo: belizar/Shutterstock; Bert van den Berg [GNU]/Wikimedia Commons

Perché i moduli?

Prima dell’avvento dei moduli ECMA Script, in Node.js l’unico modo di collegare i moduli fra di loro era mediante la funzione require(), un approccio semplice ma potente. Come probabilmente già saprete, Node.js utilizza il concetto di modulo come unità di misura per strutturare il codice di un’applicazione e soprattutto come meccanismo principale per l’occultamento delle informazioni, mantenendo private tutte le variabili che non sono esplicitamente esportate.

Uno dei maggiori problemi con JavaScript era rappresentato dall’assenza dello spazio dei nomi. Programmi che vengono eseguiti nell’ambito globale inquinandolo con dati e che provengono sia dal codice dell’applicazione interna che dalle dipendenze. Una tecnica popolare per risolvere questo problema, che probabilmente molti di voi conosceranno, è la seguente:

Questo pattern prende il nome di IIFE (Immediately Invoked Function Expression) -conosciuta anche come Funzione Anonima ad Esecuzione Automatica- e contiene due parti principali:

  • La prima è la funzione anonima con ambito lessicale racchiusa all’interno dell’operatore di raggruppamento (). Questa funzione impedisce l’accesso alle variabili all’interno dell’idioma IIFE e l’inquinamento dell’ambito globale;
  • La seconda parte crea la funzione immediatamente richiamata attraverso la quale il motore JavaScript interpreterà direttamente la funzione.

Vediamo ora un altro esempio:

Anche in questo caso sfruttiamo una funzione di richiamo automatico per creare un ambito privato, esportando solo le parti che dovrebbero essere pubbliche. Nel codice precedente, la variabile del modulo contiene solo l’API esportata, mentre il resto del contenuto del modulo è praticamente inaccessibile dall’esterno. Come vedremo tra poco, l’idea alla base di questo pattern viene utilizzata come base per il sistema di moduli Node.js.

La specifica CommonJS

CommonJS (CJS) è uno standard per strutturare e organizzare il codice JavaScript. CJS assiste nello sviluppo di app lato server e il suo formato ha fortemente influenzato la gestione dei moduli di NodeJS.

CommonJS avvolge ogni modulo in una funzione chiamata require e include un oggetto chiamato module.exports, che esporta il codice per la disponibilità richiesta da altri moduli. Tutto quello che bisogna fare è aggiungere tutto ciò che desideri accessibile ad altri file nell’oggetto exports e richiedere il modulo nel file dipendente. Prima che il codice di un modulo venga eseguito, Node.js lo avvolgerà con una funzione wrapper simile alla seguente:

In questo modo, Node.js ottiene le stesse cose che si propone di fare una IIFE:

  • Mantiene le variabili di primo livello (definite con var, const o let) nell’ambito del modulo anziché nell’oggetto globale;
  • Aiuta a fornire alcune variabili dall’aspetto globale che sono effettivamente specifiche del modulo, come ad esempio gli oggetti module ed exports che il programmatore può utilizzare per esportare i valori di un modulo, o le variabili filename e dirname che contengono rispettivamente il path assoluto del modulo e il percorso della directory che lo contiene.

Un piccolo esempio

Facciamo un esempio. Creiamo un modulo geometry.js, utilizzando la specifica CJS, che esporta alcune semplici formule:

Il modulo geometry.js esporta le funzioni circleArea, circleCircumference, squarePerimeter e squareArea. Le funzioni e gli oggetti vengono aggiunti alla radice di un modulo, specificando proprietà aggiuntive sull’oggetto exports.

Le variabili locali al modulo saranno private, perché il modulo è racchiuso dalla funzione wrapper. In questo esempio, le variabili PI e pow saranno private. Alla proprietà module.exports può essere assegnato anche un valore o una classe. Per esempio:

Per poter utilizzare questo modulo creiamo il file calculator.js, che utilizza il modulo geometry:

Eseguendo lo script il risultato che otterremo sarà il seguente:

Scope di un modulo

In precedenza abbiamo detto che la specifica CJS avvolge ogni nostro modulo in un wrapper che ha la seguente forma:

Questa funzione crea uno scope per il nostro modulo, fornendogli alcune utility. Diamo ora uno sguardo a cosa rappresentano le diverse variabili messe a disposizione.

dirname e filename

Sono due variabili stringa il cui contenuto rappresenta:

  • __dirname: il nome della directory del modulo corrente;
  • __filename: il nome del file del modulo corrente. Questo è il percorso assoluto del file del modulo corrente con i collegamenti simbolici risolti. Per un programma principale questo non è necessariamente lo stesso del nome del file utilizzato nella riga di comando.

Ecco un piccolo script di esempio:

module.exports

In ogni modulo, la variabile module è un riferimento all’oggetto che rappresenta il modulo corrente. Per comodità, module.exports è accessibile anche tramite exports module-global. module non è in realtà un globale ma piuttosto locale per ogni modulo.

La variabile module contiene le seguenti informazioni:

  • module.children: gli oggetti modulo richiesti per la prima volta da questo;
  • module.exports: rappresenta l’istruzione che dice a Node. js quali bit di codice (funzioni, oggetti, stringhe, ecc.) da “esportare” da un determinato file in modo che altri file possano accedere al codice esportato;
  • module.filename: contiene il path assoluto del modulo. Equivale a __filename ;
  • module.id: l’identificatore del modulo. In genere questo è il nome del file completamente risolto;
  • module.isPreloading: è true se il modulo è in esecuzione durante la fase di pre caricamento di Node.js;
  • module.loaded: indica se il modulo ha terminato il caricamento o è in fase di caricamento;
  • module.paths: I percorsi di ricerca per i moduli.

exports

Questo è solo zucchero sintattico. Altro non è che un riferimento a module.exports che è più breve da digitare.

require

La funzione require() ci consente di includere moduli nella nostra applicazione. Puoi aggiungere moduli core di Node.js, moduli userland (node_modules) o moduli locali. È il modo più semplice per includere moduli che esistono in file separati. La funzionalità di base di require è che legge un file JavaScript, esegue il file e quindi procede alla restituzione dell’oggetto exports.

Il modulo principale

Quando un file viene eseguito direttamente da Node.js, require.main sarà uguale a module. Ciò significa che è possibile determinare se un file è stato eseguito direttamente testando require.main === module. Per un file main.js, questo sarà vero se eseguito dalla console con il comando node main.js, ma falso se eseguito da require('./main').

Se proviamo ad eseguire il seguente script:

Se ora creiamo un altro file che include main.js:

Eseguendo questo script il risultato che otterremo sarà:

Poiché module fornisce una proprietà filename (normalmente equivalente a __filename), il punto di ingresso dell’applicazione corrente può essere ottenuto controllando require.main.filename.

La specifica ECMAScript

I moduli ECMAScript rappresentano lo standard ufficiale per impacchettare il codice JavaScript per il riutilizzo. I moduli vengono definiti utilizzando una varietà di istruzioni di importazione ed esportazione. Node.js tratta il codice JavaScript come moduli CommonJS per impostazione predefinita. Ma possiamo dire a Node.js di trattare il codice JavaScript come moduli ECMAScript tramite l’estensione del file .mjs anziché js, o la proprietà "type" nel file package.json.

Esportare funzionalità

La prima cosa da fare per accedere alle funzionalità del modulo è esportarle. Questo viene fatto utilizzando export. Il modo più semplice per usarlo è posizionarlo davanti a tutti gli elementi che desideri esportare fuori dal modulo, ad esempio:

Come potete vedere è possibile esportare variabili, funzioni e classi esattamente come la specifica CJS. Un modo più conveniente per esportare tutti gli elementi che si desidera esportare consiste nell’utilizzare una singola istruzione export alla fine del modulo, seguita da un elenco separato da virgole delle funzionalità che si desidera esportare racchiuse tra parentesi graffe. Per esempio:

Questa tipologia di esportazione viene definita come esportazione nominale: ogni elemento (che si tratti di una funzione, const, ecc.) è stato indicato con il suo nome al momento dell’esportazione e quel nome, come vedremo di seguito, verrà utilizzato nel momento in cui effettueremo l’importazione.

Esiste anche un tipo di esportazione chiamato esportazione predefinita: è progettato per semplificare l’utilizzo di una funzione predefinita fornita da un modulo e aiuta anche i moduli JavaScript a interagire con i sistemi di moduli CommonJS. Torniamo alla classe Parallelogram vista in precedenza e vediamo come esportarla utilizzando questa tecnica

oppure

Importare moduli

Ora che sappiamo come esportare funzionalità da un modulo, è necessario importarle nello script per poterle utilizzare. Il modo più semplice per farlo è il seguente:

Un altro modo per importare tutte le funzionalità di un modulo è quella di utilizzare l’operatore * per poi accedere alle singole funzionalità con il punto (.):

Proviamo ora ad importare il modulo Parallelogram visto in precedenza. In questo caso è stato utilizzata un’esportazione predefinita, quindi importare il modulo diventa ancora più semplice:

Maggiori informazioni riguardo i moduli ECMAScript potete trovarle sulla documentazione ufficiale.

Ricreiamo il modulo geometry

Ora ricreiamo i moduli geometry e calculator utilizzando questo standard. Ecco il file geometry.mjs

Mentre calculator.mjs :

Differenze tra ESM e CJS

La specifica ECMA Script ha cambiato moltissime cose in JavaScript. Per esempio i moduli ECMA Script utilizzano la modalità rigorosa per impostazione predefinita ('use strict'), questo non si riferisce all’oggetto globale, l’ambito funziona in modo diverso, ecc. Ci sono anche altre differenze sostanziali tra i moduli ESM e CJS in Node.js. Le principali, a mio avviso, sono le seguenti:

  • require, exports o module.exports: Nella maggior parte dei casi, l’importazione del modulo ES può essere utilizzata per caricare i moduli CommonJS. Se necessario, è possibile costruire una funzione require all’interno di un modulo ES utilizzando module.createRequire().
  • __filename e __dirname: Queste due variabili non saranno disponibili nei modulie ES.

La lista completa delle differenze tra queste due specifiche potete trovarla sulla documentazione ufficiale di Node.js.

Sincrono o asincrono 🤔

Nella specifica CommonJS, require() è sincrona; non restituisce una promessa o una callback. require() legge dal disco (o dalla rete) ed esegue immediatamente lo script, che può eseguire a sua volta I/O o altro, e restituisce i valori esportati da module.exports.

Nella specifica ECMA Script invece, il loader attraversa alcune fasi asincrone. Nella prima fase, analizza lo script per rilevare le funzioni importate ed esportate senza però eseguirne il codice. Nella fase di analisi, il loader ESM può rilevare immediatamente un errore di battitura nelle importazioni denominate e generare un’eccezione senza mai eseguire effettivamente il codice di dipendenza.

Successivamente il loader del modulo ESM scarica e analizza in modo asincrono tutti gli script importati finché alla fine non trova uno script che non importa nulla. Man mano che carica i moduli costruisce un “grafico delle dipendenze”. Tutti gli script “fratelli” nel grafico del modulo ES vengono scaricati in parallelo, ma vengono eseguiti in ordine, garantito dalle specifiche del caricatore.

Creare moduli che supportano CJS e ESM

Se gestite un modulo che deve supportare CJS ed ESM, fate un favore ai vostri utenti e seguite queste linee guida per creare un “modulo doppio” che funzioni alla grande in CJS ed ESM.

Per concludere

In questo articolo è stata data una panoramica generale sia ai moduli CJS che ESM ed alle principali differenze tra i due. Voi quale usate quando scrivete le vostre applicazioni Node.js? Perché? Sarebbe bello sapere cosa ne pensate! Nel frattempo:

Bibliografia

--

--

Davide D'Antonio
weBeetle

👨‍🎓Degree in computer science 💑 Married with Milena 🤓 Huge Nerd! 💻 Code lover 👨🏻‍💻 Fullstack developer