Articolo originale: Modules in JavaScript – CommonJS and ESmodules Explained

Salve a tutti! In questo articolo daremo uno sguardo ai moduli in JavaScript.

Al giorno d'oggi i moduli sono una tecnica molto usata nella progettazione/architettura del software.

Per prima cosa impareremo cosa sono e quali diversi tipi di moduli esistono. Poi discuteremo del perché i moduli sono utili. Successivamente vedremo esempi e la sintassi base per i tipi di modulo maggiormente usati. Infine esamineremo il bundling (unione di moduli in un singolo modulo), perché è necessario e come farlo.

Basta chiacchiere, si inizia!

Sommario

Cosa sono i moduli e perché sono utili

Un modulo è semplicemente un pezzo di codice in un file che puoi chiamare per utilizzarlo in altri file. Un design modulare è l'opposto rispetto ad avere tutto il codice del tuo progetto in un singolo file.

Quando sviluppiamo un progetto di grandi dimensioni è molto utile dividere il nostro codice in moduli per le seguenti ragioni:

  • È efficace per la separazione delle responsabilità e funzionalità in file separati, il che aiuta la visualizzazione e l'organizzazione del codice.
  • Quando il codice è chiaramente organizzato è più facile da mantenere, ed è meno soggetto a errori e bug.
  • I moduli possono essere facilmente usati e riutilizzati in file e parti diverse del nostro progetto, senza bisogno di riscrivere lo stesso codice nuovamente.

Invece di avere tutti i componenti del nostro programma in un singolo file, possiamo dividerli in parti o moduli, e rendere ciascuno di essi responsabile per una singola funzionalità/responsabilità.

Se adesso questo concetto non è sufficientemente chiaro, non preoccuparti, lo sarà a breve, dopo aver visto alcuni esempi.

Tipi di Modulo

Come quasi tutto nella vita, specialmente in JavaScript, ci sono molti modi a nostra disposizione per implementare i moduli.

All'inizio, JavaScript fu creato per essere semplicemente un piccolo linguaggio di scripting per i siti web, e una funzionalità adatta a progetti di grandi dimensioni come i moduli non era supportata.

Mano a mano che il linguaggio e il suo ecosistema cresceva, gli sviluppatori iniziarono a vedere le necessità di questa funzionalità. Quindi diverse opzioni e librerie vennero sviluppate per aggiungere questa funzionalità a JavaScript.

Tra le molte disponibili, daremo uno sguardo solo ai moduli CommonJS ed ES (ESModule), che sono i più recenti e i più largamente usati.

Nota a margine: sai che  Javascript fu originariamente creato in soli 10 giorni di lavoro?

Quando si analizzano le complessità di JavaScript e si cerca di capire come il linguaggio si sia evoluto, penso sia importante tenere presente che il linguaggio non fu creato originariamente per fare quello che fa al giorno d'oggi. È la crescita dell'ecosistema JavaScript che ha incentivato molti dei cambiamenti che si sono verificati.

Moduli CommonJS

CommonJS è un insieme di standard usati per implementare i moduli in JavaScript. Il progetto fu lanciato nel 2009 dall'ingegnere di Mozilla, Kevin Dangoor.

CommonJS è principalmente usato per il lato server da app JS come Node, visto che i browser non supportano l'utilizzo di CommonJS.

Come nota a margine, Node un tempo supportava solo CommonJS per l'implementazione dei moduli, ma attualmente supporta anche i moduli ES, il che costituisce un approccio più moderno.

Vediamo come si presentano i moduli CommonJS in un effettivo esempio di codice.

Per implementare i moduli, devi prima avere un'app Node nel tuo computer, pertanto creane una eseguendo nel terminale npm init -y.

Prima creiamo un file main.js che contiene una semplice funzione.

const testFunction = () => {
    console.log('Sono la funzione principale')
}

testFunction()

Bene, diciamo ora che vogliamo avere un'altra funzione che chiamiamo dal nostro file main.js, ma non vogliamo che la funzione si trovi lì, in quanto non è parte della nostra funzionalità principale. Pertanto creiamo il file mod1.js e inseriamo il seguente codice:

const mod1Function = () => console.log('Mod1 è vivo!')
module.exports = mod1Function

module.exports è la parola chiave che usiamo per dichiarare tutto quello che vogliamo esportare dal file.

Per usare questa funzione nel file main.js possiamo fare quanto segue:

const mod1Function = require('./mod1.js')

const testFunction = () => {
    console.log('Sono la funzione principale')
    mod1Function()
}

testFunction()

Abbiamo dichiarato la variabile che vogliamo usare e l'abbiamo assegnata alla funzione require per richiedere al file indicato come parametro (nella fattispecie mod1.js)  quello che ci può fornire. Un gioco da ragazzi. ;)

Se avessimo voluto esportare più di una cosa da un singolo modulo, avremmo potuto fare in questo modo:

const mod1Function = () => console.log('Mod1 è vivo!')
const mod1Function2 = () => console.log('Mod1 sta girando, baby!')

module.exports = { mod1Function, mod1Function2 }

Nel file main.js avremmo potuto usare entrambe le funzioni come segue:

const { mod1Function, mod1Function2 } = require('./mod1.js')

const testFunction = () => {
    console.log('Sono la funzione principale')
    mod1Function()
    mod1Function2()
}

testFunction()

Questo è praticamente tutto. Piuttosto semplice, giusto? Semplice ma si tratta di uno strumento potente da usare. =)

Moduli ES

Esaminiamo ora i moduli ES (ESmodule). Si tratta di uno standard introdotto con ES6 (2015). L'idea era di uniformare il modo nel quale funzionano i moduli JS e implementare questa funzionalità nei browser (che in precedenza non supportavano i moduli).

I moduli ES costituiscono un approccio più moderno, attualmente supportato dai browser e da app lato server con Node.

Vediamo il codice. Ancora una volta iniziamo creando un'app Node con npm init -y.

Ora apriamo il file package.json e gli aggiungiamo "type": "module", in questo modo:

{
  "name": "modulestestapp",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module"
}

Se non lo facciamo e cerchiamo di implementare i moduli ES con Node otterremo un errore che ci avverte che per caricare un modulo ES occorre aggiungere "type":"module" al file package.json oppure occorre usare l'estensione ".mjs",  tipo questo:

(node:29568) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
...
SyntaxError: Cannot use import statement outside a module

Ora ripetiamo lo stesso identico esempio. Nel file  main.js scriviamo questo codice:

// main.js
import { mod1Function } from './mod1.js'

const testFunction = () => {
    console.log('Sono la funzione principale')
    mod1Function()
}

testFunction()

In mod1.js scriviamo questo:

// mod1.js
const mod1Function = () => console.log('Mod1 è vivo!')
export { mod1Function }

Nota che invece di require usiamo import e invece di module.exports usiamo export. La sintassi è un poco diversa ma il comportamento è molto simile.

Ancora una volta, se volessimo esportare più di una cosa dallo stesso file faremmo in questo modo:

// main.js
import { mod1Function, mod1Function2 } from './mod1.js'

const testFunction = () => {
    console.log('Sono la funzione principale')
    mod1Function()
    mod1Function2()
}

testFunction()
// mod1.js
const mod1Function = () => console.log('Mod1 è vivo!')
const mod1Function2 = () => console.log('Mod1 sta girando, baby!')

export { mod1Function, mod1Function2 }

Un'altra funzionalità disponibile nei moduli ES è la possibilità di rinominare un oggetto in fase di importazione, in questo modo:

// main.js
import { mod1Function as funct1, mod1Function2 as funct2 } from './mod1.js'

const testFunction = () => {
    console.log('Sono la funzione principale')
    funct1()
    funct2()
}

testFunction()

Nota che usiamo la parola chiave as dopo ogni funzione, seguita dal nuovo nome che vogliamo assegnare. Successivamente, nel nostro codice, possiamo usare quel nuovo nome invece del nome originale importato. ;)

Un'altra cosa che potresti fare è importare tutto insieme quanto esportabile da un modulo e inserirlo in un oggetto, in questo modo:

// main.js
import * as mod1 from './mod1.js' 

const testFunction = () => {
    console.log('Sono la funzione principale')
    mod1.mod1Function()
    mod1.mod1Function2()
}

testFunction()

Potrebbe essere utile nei casi in cui, nel nostro codice vogliamo rendere esplicito da dove proviene ogni importazione. Ora puoi vedere che quelle funzioni sono chiamate con mod1.mod1Function() e mod1.mod1Function2().

L'ultima cosa che vale la pena menzionare è la parola chiave default, con la quale possiamo designare un'esportazione predefinita per un dato modulo, così:

// mod1.js
const mod1Function = () => console.log('Mod1 è vivo!')
const mod1Function2 = () => console.log('Mod1 sta girando, baby!')

export default mod1Function
export { mod1Function2 }

Cosa vuol dire avere un esportazione predefinita? Significa che non dobbiamo destrutturarla in fase di importazione. Possiamo usarla semplicemente in questo modo:

// main.js
import mod1Function, { mod1Function2 } from './mod1.js' 

const testFunction = () => {
    console.log('Sono la funzione principale')
    mod1Function()
    mod1Function2()
}

testFunction()

Possiamo anche rinominare l'import a nostro piacere senza usare la parola chiave as visto che JavaScript "sa" che non stiamo destrutturando ma stiamo facendo riferimento all'import predefinito.

// main.js
import lalala, { mod1Function2 } from './mod1.js' 

const testFunction = () => {
    console.log('Im the main function')
    lalala()
    mod1Function2()
}

testFunction()

E questo praticamente riassume anche i moduli ES. Semplice, spero. =)

Usare i moduli

Ora che abbiamo chiarito quali sono i diversi tipi di modulo disponibili e come funzionano, vediamo come possiamo implementare i moduli in un sito web usando HTML e JS.

Creiamo un semplice file HTML con un'intestazione, due pulsanti e un tag script nel quale colleghiamo il nostro file main.js.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>I'm just a test...</h1>
    <button id="isAlive">Mod1 è vivo?</button>
    <button id="isRolling">Mod1 sta girando?</button>
    <script src="./main.js" type="module"></script>
</body>
</html>

Fai attenzione al fatto che sto dichiarando l'attributo di tipo type="module" nel tag script. È necessario farlo per potere usare la funzionalità di modulo di JS. Se non lo facciamo otterremo un errore nella console che ci avverte che non è possibile usare l'istruzione import al di fuori di un modulo, tipo questo:

Uncaught SyntaxError: Cannot use import statement outside a module

Se apri il file HTML dovresti vedere qualcosa come:

screenshot-2

Ora il nostro file main.js avrà questo codice:

// main.js
import { mod1Function, mod1Function2 } from './mod1.js'

const testFunction = () => console.log('Sono la funzione principale')

document.getElementById('isAlive').addEventListener('click', () => mod1Function())
document.getElementById('isRolling').addEventListener('click', () => mod1Function2())

testFunction()

Stiamo aggiungendo semplicemente un event listener per il click su ogni pulsante in modo che le funzioni che provengono dal file mod1.js vengano eseguite.

Ora possiamo servire il nostro file HTML e vedere se funziona. Dobbiamo utilizzare un server, non semplicemente aprire il file nel browser in quanto otterremmo un errore CORS come questo:

Access to script at ... from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, brave, chrome-untrusted, https.

Un modo veloce per attivare un server è usare l'estensione di VSCode Live Server, oppure creare un'app Node eseguendo npm init -y e poi eseguendo npx serve.

A ogni modo, una volta che il file è servito, possiamo fare click su ciascun bottone e verificare che le funzioni vengano correttamente eseguite. La nostra console dovrebbe avere quest'aspetto:

screenshot_1-1

C'è ancora un'altra cosa da considerare. Se nel browser apriamo la scheda "Rete" negli Strumenti per sviluppatori e filtriamo i file JS possiamo vedere che il sito web sta caricando due file, main.js e mod1.js:

screenshot_3

Naturalmente entrambi i file devono essere caricati, visto che usiamo il codice che si trova in ogni file, ma questa non è la cosa migliore da fare perchè il browser deve eseguire due richieste separate per caricare tutto il codice JS necessario.

Dovremmo sempre cercare di ridurre le richieste al minimo per migliorare le prestazioni dei nostri progetti. Quindi vediamo come possiamo farlo con l'aiuto di un module bundler (programma che unisce i moduli).

Nota a margine: se preferisci una spiegazione video, Kent C Dodds ne ha una molto valida (risorsa in inglese). Consiglio vivamente di seguirlo, in quanto è uno dei migliori insegnanti di JS in circolazione. Ecco un altro bel video da Fireship. ;)

Unire i moduli (bundling)

Come detto in precedenza, dividere il codice in moduli è una buona cosa in quanto il codebase sarà più organizzato e sarà più facile riutilizzare il nostro codice.

Questi vantaggi, tuttavia, sono solo per la fase di sviluppo del progetto. Quando siamo in produzione, i moduli non sono la cosa migliore, in quanto costringere il browser a eseguire una richiesta per ciascun modulo JS potrebbe influire sulla prestazione del tuo sito.

Questo problema può essere facilmente risolto con l'aiuto di un module bundler. In termini semplici, i module bundler sono programmi che ricevono i moduli JS in input e li combinano in un singolo file (molti module bundler hanno molte altre funzionalità, ma questo è il loro compito base).

Grazie a ciò, possiamo codificare il nostro progetto dividendolo in parti organizzate, quindi lanciare un module bundler per ottenere il codice finale da usare in produzione.

Questo passo di conversione del "codice di sviluppo" in "codice di produzione" viene normalmente identificato come "build" (costruzione).

Per fare questo ci sono molte opzioni (come Browserify, Parcel, Rollup.js, Snowpack...) ma quella maggiormente usata è Webpack. Ecco quindi un esempio usando Webpack.

  • Nota a margine 1: Se vuoi approfondire l'argomento module bundler e il loro funzionamento, questo stupendo video di Fireship (risorsa in inglese) potrebbe essere un buon punto di partenza.
  • Nota a margine 2: Webpack è uno strumento molto robusto e sofisticato che può fare molte altre cose a parte unire i file JS. Consulta la documentazione (risorsa in inglese) se vuoi saperne di più.

Grande, ora possiamo partire creando un'app Node (se non l'hai già fatto) eseguendo npm init -y. Poi dobbiamo installare Webpack e l'interfaccia da riga di comando Webpack CLI eseguendo npm i --save-dev webpack webpack-cli.

Successivamente creeremo un file webpack.config.js inserendo questo codice:

/* webpack.config.js */
const path = require('path');

module.exports = {
  entry: './main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
};
N.d.T.
Potrebbe essere necessario rinominare il file webpack.config.js con estensione .cjs e indicare la modalità di produzione con mode: 'production' all'interno dell'oggetto module.exports.

Questo file sarà responsabile della configurazione di Webpack e di come Webpack funzionerà nella tua applicazione.

Quello che stiamo facendo qui per prima cosa è impostare il file di partenza (entry: './main.js'). Webpack inizierà elaborando quel file, quindi analizzando tutte le dipendenze (i moduli importati da quel file). In altre parole il file di partenza è il tuo file JS principale dove vengono importati tutti gli altri moduli.

Quindi dichiariamo il nome del file di uscita, prima definiamo il percorso  (chiave path), poi nella chiave filename indichiamo il nome del file che conterrà il risultato dell'unione dei moduli.

output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
},

Super! Ora apriamo il file package.json e aggiungiamo una riga nella chiave scripts per configurare lo script che si occuperà della costruzione (l'unione dei moduli) , creando una chiave build alla quale associamo il nome dello script da eseguire, "webpack", in questo modo:

{
  "name": "testappv2",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2"
  }
}

Torniamo quindi al nostro terminale ed eseguiamo npm run build. Questo dovrebbe creare la directory il cui nome abbiamo a suo tempo indicato nella chiave path in webpack.config.js (una cartella dist nella cartella radice del progetto nel nostro esempio), all'interno della quale troveremo il file specificato con la chiave filename, vale a dire bundle.js.

Se provi ad aprire il file, vedrai questo codice al suo interno:

(()=>{"use strict";document.getElementById("isAlive").addEventListener("click",(()=>console.log("Mod1 è vivo!"))),document.getElementById("isRolling").addEventListener("click",(()=>console.log("Mod1 sta girando, baby!"))),console.log("Sono la funzione principale")})();

Si tratta praticamente dello stesso codice che avevamo distribuito nei nostri file, tutto raggruppato in un singolo file e minimizzato.

L'ultima cosa rimasta è modificare l'attributo src dell'elemento script nel file  index.html indicando il percorso e il nome del file JS raggruppato, in questo modo:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>I'm just a test...</h1>
    <button id="isAlive">Mod1 è vivo?</button>
    <button id="isRolling">Mod1 sta girando?</button>
    <script src="./dist/bundle.js" type="module"></script>
</body>
</html>

Ora possiamo servire nuovamente la pagina, verificando che il codice JS funzioni ancora correttamente; se apri la scheda "Rete" ancora una volta dovresti vedere caricato un singolo file! =D

screenshot_2-1

Spero che questo semplice esempio ti abbia aiutato a capire l'importanza dei module bundler e come possano aiutarci per combinare la buona esperienza di sviluppo con architettura modulare con delle buone prestazioni del sito.

Riepilogo

Bene, per oggi abbiamo finito. In questo articolo abbiamo visto cosa sono i moduli, perché sono validi, i diversi modi di implementare moduli in JavaScript e un esempio pratico di raggruppamento del nostro codice con Webpack.

Per una guida completa (risorsa in inglese) puoi dare uno sguardo a questo articolo.

Come sempre, spero tu abbia apprezzato l'articolo e imparato qualcosa di nuovo. Se ti fa piacere, puoi seguirmi su linkedin o twitter.

Saluti e alla prossima! =D

giphy