Inizializzare un progetto in NodeJS

Nota: per saltare le spiegazioni e avere quanto necessario per inizializzare un progetto da zero (la pappa pronta) è possibile saltare direttamente al capitolo "Conclusioni".

Di recente un amico mi ha fatto riflettere su una cosa: ho scritto un pò di post che parlano di progetti NodeJS, e in ciascuno ho più o meno ripetuto i passi iniziali per la creazione del progetto stesso; ovviamente, in ciascun post l'ho spiegato in maniera diversa e con un diverso livello di dettaglio. Quindi, per amor di completezza, ho colto il suggerimento di dedicare un intero post a questo: come predisporre un nuovo progetto NodeJS?

Premessa: il post è dedicato, in particolare per quanto riguarda la prima parte, a utenti neofiti del mondo NodeJS. Per i più smaliziati non sarà particolarmente interessante, e i più pratici sicuramente troveranno innumerevoli inesattezze. Inoltre ho appena finito di rileggerlo ed è venuto fuori luuuungo... Portate pazienza :-)


Installazione tools

Per prima cosa occorre predisporre i tool di sviluppo sul proprio pc. Io ho Windows 10, ma i passaggi sono più o meno analoghi poichè i tool esistono anche per gli altri principali sistemi operativi.

Per lavorare come faccio io è necessario installare due strumenti, entrambi open source e gratuiti:

  • L'interprete Node, scaricabile da questo link. Conviene scaricare la versione LTS, che offre maggiori garanzie di stabilità. Il pacchetto contiene già anche il gestore di pacchetti npm.
  • Visual Studio Code, il mio IDE preferito per il mondo Javascript. Scaricabile a questo link.

Creazione e apertura nuova cartella

Una volta scaricato e installato il tutto dobbiamo creare una cartella vuota destinata a contenere il nostro nuovo progetto. La possiamo creare con la normale interfaccia di Windows: pulsante destro, "crea nuova cartella".

A questo punto possiamo fare click con il pulsante destro sulla nostra nuova cartella e selezionare "Apri con code". In questo modo si aprirà Visual Studio Code, con la nostra cartella come percorso di base.

L'interfaccia di VSCode è composta di due sezioni principali: la parte di sinistra, che mostra i file e le cartelle contenute nel nostro percorso di base, e la parte di destra che mostra il contenuto dei file che andremo a selezionare. Già che ci siamo possiamo chiudere la tab "Welcome".

Un aspetto particolarmente comodo di VSCode è la possibilità di agganciare all'interfaccia anche un terminale, in cui eseguire il codice che stiamo scrivendo per averne sott'occhio l'output. Il terminale può essere aperto selezionando da menu la voce "Terminal" -> "New Terminal".

A questo punto siamo pronti a iniziare a fare sul serio.


Inizializzazione progetto NodeJS

Per prima cosa: cos'è un progetto NodeJS? In realtà la dicitura è impropria, non esiste un concetto di "progetto" NodeJS. Una volta installato l'interprete (node) posso creare un semplice file di testo contenente del codice Javascript ed eseguirlo richiamando da terminale:

node nomefile.js

Tipicamente, però, una applicazione basata su NodeJS non è autosufficiente ma utilizza una serie di dipendenze, ovvero componenti aggiuntivi che forniscono funzionalità. Queste dipendenze sono, semplificando, dei file Javascript: potremmo scaricarli uno a uno, copiarli nella cartella del nostro progetto e il problema sarebbe risolto.

Questo modo di procedere però è lento, macchinoso, e non prevede un modo comodo per tenere aggiornati questi componenti aggiuntivi. Fortunatamente per noi esiste una cosa chiamata NPM (Node Package Manager) che si occupa di questo (e altri aspetti): non dobbiamo fare altro che dirgli il nome del componente, e lui per magia lo scarica e lo copia dentro la nostra cartella di progetto, pronto per essere usato.

Le informazioni circa le dipendenze del nostro applicativo sono scritte all'interno di un file, denominato "package.json": una cartella che contiene questo file può essere definita a tutti gli effetti un progetto npm, o per estensione NodeJS. Si tratta di un semplice file di testo in formato JSON. Potremmo crearlo a mano, ma npm prevede un comodo comando che lo crea per noi con delle impostazioni di default:

npm init --yes

Normalmente npm ci chiederebbe una serie di informazioni circa il nostro progetto (nome, versione, eccetera); poichè per ora non ci interessano, l'opzione --yes serve a saltare questa fase e generare direttamente il file (possiamo sempre modificare il file a mano in un secondo tempo). Nel nostro esempio ha un aspetto del genere:

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

Gestione dipendenze

A questo punto possiamo iniziare a indicare le nostre dipendenze. Possiamo scriverle a mano in una sezione apposita nel suddetto file JSON, ma anche in questo caso il comando npm ci viene incontro.

Una piccola parentesi: in un progetto npm esistono due tipi di dipendenze:

  • quelle che servono per eseguire il programma.
  • quelle che servono solo per le fasi di sviluppo e test del programma, e che non saranno necessarie in produzione.

Per installare una dipendenza normale si usa il comando:

npm install nomedipendenza

Per installare una dipendenza di sviluppo si usa:

npm install --save-dev nomedipendenza

In entrambi i casi npm farà due cose:

  • Aggiungere il nome della dipendenza (all'ultima versione, se non altrimenti specificato) nel package.json.
  • Recuperare la dipendenza stessa e salvarla, insieme alle eventuali altre, in una cartella "node_modules" nella root del nostro progetto.

Quando eseguiamo il nostro programma, le dipendenze installate in node_modules saranno automaticamente disponibili. Volendo esiste anche la possibilità di inserire a mano il nome della dipendenza nel package.json e lanciare il comando:

npm install

In questo modo npn si preoccuperà di esaminare il package.json e scaricare tutte le dipendenze non ancora recuperate; questo è anche il modo corretto di procedere se scarichiamo un progetto, ad esempio da github. Come norma la cartella node_modules non è MAI backuppata o committata su git, appunto perchè può e deve essere ricostruita come visto sopra.


Le mie dipendenze preferite :-)

La scelta delle dipendenze da utilizzare dipende naturalmente dal progetto. Per quanto riguarda le dipendenze di sviluppo, invece, queste dipendono dal modo in cui si sviluppa e da qual'è il target del progetto. Nel caso di progetti NodeJS da eseguire da terminale, nei miei tipici progetto mi interessano due cose:

  • Babel: un sistema per tradurre il codice Javascript in una versione compatibile con uno standard più "base" del linguaggio. Javascript (per meglio dire ECMAScript, di cui Javascript è una implementazione) è un linguaggio interpretato; questo significa che deve essere eseguito da qualcuno (un browser, o l'interprete Node) che lo comprende. Problema: lo standard ECMAScript è in continua evoluzione; una istruzione funzionante su una versione nuova di Node potrebbe non funzionare su una versione di Node di 2 anni fa (e non fatemi parlare della compatibilità dei browser). Se posso garantire che installerò solo su una versione ben controllata di Node, allora la cosa non mi crea problemi; purtroppo però non è sempre possibile avere il pieno controllo dell'ambiente su cui il mio programma dovrà girare. Per rimediare a questo problema Babel traduce il mio Javascript in un altro file Javascript con le stesse identiche funzionalità ma con una versione del linguaggio più basilare; il risultato è orribile a vedersi (una istruzione moderna magari deve essere rimpiazzata da decine di comandi basilari), ma funzionerà anche con interpreti non aggiornatissimi.
  • Nodemon: scrivi un programma. Lo esegui. Scopri un problema. Lo termini. Lo modifichi. Salvi. Esegui ancora. Scopri un problema. Lo termini. Correggi. Riesegui. E così via. Nodemon aiuta a togliere un pò di passi nel normale processo di sviluppo: esegue il programma, e se si accorge che un file è stato modificato magicamente lo termina e lo riesegue, per darci modo di verificare subito le modifiche fatte. Sembra poco ma fa risparmiare una marea di tempo.

Per installare queste dipendenze, la cosa più semplice è usare la riga di comando:

npm install --save-dev nodemon @babel/cli @babel/preset-env @babel/node @babel/plugin-transform-runtime @babel/plugin-proposal-class-properties @babel/core

Osservazione: Babel è un pacchetto estremamente articolato e che utilizza decine di plugin, ciascuno dei quali gestisce uno specifico aspetto della traduzione del codice. Io qui ho installato i principali, quelli che mi sono accorto essere richiesti per i miei progetti, ma nulla toglie che ne debbano servire altri. Suggerisco di studiare la corposissima documentazione disponibile online al riguardo.

Per predisporre il sistema all'utilizzo di Babel manca un passaggio: la creazione della configurazione. Abbiamo predisposto Babel e i suoi moduli, ora dobbiamo dire al traduttore Babel di usarli e come. Questo si può fare predisponendo un file di configurazione ad hoc nella cartella oppure aggiungendo una sezione apposita nel package.json; la soluzione che preferisco è la seconda. Questa è la sezione che vado ad aggiungere al mio package.json:

  "babel": {
    "presets": [
      "@babel/preset-env"
    ],
    "plugins": [
      [
        "@babel/plugin-proposal-class-properties"
      ],
      [
        "@babel/plugin-transform-runtime",
        {
          "regenerator": true
        }
      ]
    ]
  },

A questo punto ci manca un solo argomento: gli script di npm.


Script di npm

Per eseguire un programma NodeJS occorre darlo in pasto all'interprete, ovverossia eseguire un comando del genere:

node file.js

Se vogliamo usare nodemon (che riavvia il programma ad ogni modifica) dobbiamo invece fare così:

nodemon file.js

Se vogliamo semplicemente eseguire il programma dopo averlo tradotto con babel:

babel-node file.js

Se vogliamo mettere assieme babel e nodemon la faccenda raggiunge livelli allarmanti:

nodemon --exec \"babel-node\" file.js

E questo solo per eseguire il programma in fase di sviluppo; dovremo ricordarci quantomeno anche come tradurre il programma con Babel (per produrre la versione di produzione) e come eseguire la versione di produzione. E non parliamo poi degli eventuali test.

Anche su questo per fortuna npm ci viene incontro con la funzionalità di esecuzione di script. Possiamo predisporre dei comandi arbitrari (tutti quelli che vogliamo) nella sezione "scripts" del nostro package.json, e a quel punto ci basterà chiamare lo script da riga di comando con:

npm run NOMESCRIPT

Per le funzionalità che ho descritto sopra possiamo predisporre una cosa del genere:

  "scripts": {
    "dev": "nodemon --exec \"babel-node\" ./src/index.js",
    "build": "babel src -d dist",
    "prod": "node ./dist/index.js"
  },

Il comando "dev" lancia il nostro programma (il cui file principale si suppone si chiami index.js e si trovi in /src) con nodemon e babel-node. Il comando "build" traspila i sorgenti con babel e produce la versione di produzione nella cartella /dist. Infine, il comando "prod" esegue la versione di produzione del nostro programma.


Conclusione

Per concludere, la predisposizione di un progetto NodeJS non è banale: i passi da seguire dipendono dal tipo di programma, dalla destinazione e dalle modalità di sviluppo. Ciascun programmatore ha la sua maniera di iniziare un progetto. Quella che ho presentato è una spiegazione molto semplicistica del procedimento, che però può essere un buon punto di partenza (almeno, lo è per me) per poi andare pian piano a scoprire gli aspetti più complicati.

Come bonus, un modello di package.json. Lo copiate un una cartella vuota, cambiate i parametri anagrafici (name, author, ecc). Dentro la cartella create una sottocartella "src" e dentro questa un file di testo "index.js". Poi fate "npm install". Ecco, siete pronti a lavorare al vostro progetto. Yeeh!

{
  "name": "my-happy-project",
  "version": "1.0.0",
  "description": "",
  "author": "mario.piccinelli@gmail.com",
  "license": "CC-BY-4.0",  
  "main": "src/index.js",
  
  "scripts": {
    "dev": "nodemon --exec \"babel-node\" ./src/index.js",
    "build": "babel src -d dist",
    "prod": "node ./dist/index.js"
  },
  
  "dependencies": {
  },
  
  "devDependencies": {
    "@babel/cli": "^7.11.6",
    "@babel/core": "^7.11.6",
    "@babel/preset-env": "^7.11.5",
    "@babel/node": "^7.10.5",
    "@babel/plugin-transform-runtime": "^7.11.5",
    "@babel/plugin-proposal-class-properties": "^7.12.1",
    "nodemon": "^2.0.4"
  },
  
  "babel": {
    "presets": [
      "@babel/preset-env"
    ],
    "plugins": [
      [
        "@babel/plugin-transform-runtime",
        {
          "regenerator": true
        }
      ],
      "@babel/plugin-proposal-class-properties"
    ]
  }
}

Ecco qui: