Vai al contenuto

Node.js

Node.js

Node.js: che cos’è e come funziona

JavaScript nasce come linguaggio utilizzato principalmente all’interno del browser. Per anni il suo impiego è stato quindi legato soprattutto alla parte client delle applicazioni web: interazioni con le pagine, gestione degli eventi e modifica dinamica dei contenuti.

Per sviluppare la parte server era invece normale affiancare a JavaScript linguaggi come PHP, Java, Python o Ruby. Con Node.js lo scenario cambia: JavaScript può essere eseguito anche al di fuori del browser e utilizzato per creare applicazioni lato server, API, servizi web e altri tipi di software.

Node.js non sostituisce il browser e non modifica il funzionamento di JavaScript lato client. Fornisce un ambiente nel quale lo stesso linguaggio può essere eseguito direttamente sul sistema operativo e può quindi accedere, attraverso le API disponibili, a rete, file system, processi e altre risorse che normalmente non sono accessibili al JavaScript eseguito in una pagina web.

Node.js è disponibile sui Piani Professionali e Semidedicati di VHosting.

Node.js, le origini

JavaScript nasce nel 1995 per aggiungere comportamento dinamico alle pagine web. Già negli anni successivi vengono sviluppate soluzioni che permettono di utilizzarlo anche lato server. Tra queste c’è Netscape LiveWire, che però non raggiunge una diffusione paragonabile a quella che avrà Node.js.

Il passaggio decisivo arriva nel 2009, quando Ryan Dahl presenta Node.js. L’idea alla base del progetto è utilizzare JavaScript anche per applicazioni di rete, adottando un modello nel quale le operazioni di input/output possono essere gestite senza bloccare continuamente l’esecuzione in attesa di una risposta.

Negli anni Node.js viene sviluppato da una comunità molto più ampia e diventa una piattaforma utilizzata sia per piccoli strumenti sia per applicazioni server strutturate.

Che cos’è Node.js

Node.js è un ambiente di esecuzione JavaScript open source e multipiattaforma. In termini più semplici, permette di prendere un programma scritto in JavaScript ed eseguirlo senza bisogno di aprirlo all’interno di un browser.

È quindi più corretto parlare di runtime JavaScript e non di framework. Express, Fastify e NestJS sono invece esempi di framework utilizzabili insieme a Node.js per organizzare e semplificare lo sviluppo di applicazioni web e API.

Uno dei componenti alla base di Node.js è V8, il motore JavaScript open source utilizzato anche da Chromium e Google Chrome. V8 esegue il codice JavaScript, ma Node.js non è semplicemente una versione di V8 installata su un server. Intorno al motore sono presenti API e librerie che permettono al programma di interagire con il sistema operativo.

Un’applicazione Node.js può quindi aprire una porta di rete, leggere un file, interrogare un database, avviare un processo o ricevere una richiesta HTTP. Sono operazioni molto diverse da quelle normalmente associate al JavaScript eseguito dentro una pagina web.

Che cosa permette di fare Node.js

Uno degli utilizzi più comuni riguarda lo sviluppo back-end. Un’applicazione Node.js può ricevere una richiesta HTTP, elaborarla, recuperare informazioni da un database e restituire una risposta al client.

Con Node.js si possono, ad esempio:

Node.js non viene utilizzato esclusivamente sui server web. Lo stesso ambiente può essere alla base di strumenti per sviluppatori, utility da riga di comando e sistemi di automazione. Tecnologie costruite intorno a JavaScript e Node.js, come Electron, permettono inoltre di realizzare applicazioni desktop utilizzando tecnologie normalmente associate al Web.

Node.js

Come funziona Node.js

Per capire il comportamento di Node.js bisogna distinguere l’esecuzione del codice JavaScript dalla gestione delle operazioni di input/output.

Il codice JavaScript di un’applicazione Node.js viene normalmente eseguito su un thread principale. Questo però non significa che qualsiasi operazione debba essere completata prima di poter passare alla successiva.

Quando viene avviata un’operazione asincrona, come una richiesta di rete o alcune operazioni sul file system, Node.js può proseguire con altro lavoro. Al completamento dell’operazione, il relativo risultato viene gestito secondo il meccanismo previsto dall’applicazione.

Al centro di questo comportamento si trova l’event loop. L’event loop coordina l’esecuzione delle operazioni e delle callback che possono essere elaborate dal thread JavaScript.

Un altro componente importante è libuv, libreria multipiattaforma utilizzata da Node.js per l’event loop e per diverse funzionalità asincrone. A seconda dell’operazione, libuv può appoggiarsi direttamente alle funzionalità asincrone offerte dal sistema operativo oppure utilizzare un pool di thread. Dire semplicemente che “Node.js è single-thread” è quindi una semplificazione: il codice JavaScript viene normalmente eseguito sul thread principale, mentre dietro le quinte possono essere coinvolti altri thread e meccanismi del sistema operativo.

Questo modello funziona bene quando l’applicazione passa buona parte del proprio tempo in attesa di operazioni di input/output. Un server può, ad esempio, ricevere una richiesta, avviare un’interrogazione al database e nel frattempo continuare a occuparsi di altre connessioni.

Node.js e npm

Lo sviluppo con Node.js è strettamente legato all’utilizzo dei pacchetti. In questo contesto si incontra rapidamente npm, il package manager utilizzato comunemente per installare e gestire le dipendenze di un progetto.

Se un’applicazione deve utilizzare Express, ad esempio, il pacchetto può essere aggiunto al progetto con:

npm install express

Non è quindi necessario scrivere da zero ogni componente dell’applicazione. Si possono installare librerie dedicate all’accesso ai database, alla validazione dei dati, ai log, all’autenticazione e a molte altre funzioni.

Le informazioni principali del progetto vengono normalmente definite nel file package.json. Al suo interno possono essere indicati nome e versione dell’applicazione, script eseguibili, dipendenze e altri dati utilizzati dagli strumenti dell’ambiente Node.js.

Accanto a package.json si incontra normalmente anche un file di lock, come package-lock.json, che registra le versioni effettivamente risolte delle dipendenze e aiuta a ottenere installazioni riproducibili.

Un semplice server HTTP con Node.js

Per vedere cosa significa eseguire JavaScript lato server non serve necessariamente installare un framework. Node.js dispone già dei moduli necessari per creare un piccolo server HTTP.

const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end('Ciao da Node.js!');
});

server.listen(3000, () => {
    console.log('Server attivo sulla porta 3000');
});

Salvando il codice nel file server.js, si può avviare il programma dalla riga di comando:

node server.js

Il processo rimane in esecuzione e mette il server in ascolto sulla porta 3000. Quando arriva una richiesta HTTP, viene eseguita la funzione passata a http.createServer() e viene restituita la stringa Ciao da Node.js!.

L’esempio è volutamente ridotto. In un’applicazione reale bisogna gestire routing, errori, configurazione, sicurezza, log e accesso ai dati. Framework come Express vengono spesso utilizzati proprio per evitare di implementare manualmente tutta questa parte.

Quando conviene utilizzare Node.js

Node.js si presta bene alle applicazioni nelle quali vengono eseguite molte operazioni di input/output. API, servizi web, sistemi che interrogano database e applicazioni con molte connessioni contemporanee rientrano tra i casi tipici.

Un esempio semplice è una chat. Il server deve mantenere e gestire diverse connessioni, ricevere piccoli messaggi e inoltrarli rapidamente agli altri client. Gran parte del lavoro consiste nella gestione della rete e nell’attesa di nuovi dati, un tipo di carico coerente con il modello asincrono di Node.js.

Il discorso cambia quando il thread JavaScript deve eseguire per molto tempo un’elaborazione pesante. Un calcolo particolarmente costoso in termini di CPU può occupare l’event loop e ritardare la gestione delle altre richieste.

Questo non significa che con Node.js non si possano eseguire operazioni CPU-intensive. Significa che vanno progettate correttamente. Quando necessario si possono utilizzare Worker Threads, processi separati oppure servizi dedicati, evitando di bloccare il thread che deve continuare a gestire le richieste dell’applicazione. Node.js mette inoltre a disposizione meccanismi per utilizzare più processi e sfruttare più core della macchina.

Cosa serve per utilizzare Node.js su un hosting

Un’applicazione Node.js non funziona su un hosting soltanto perché il server è in grado di pubblicare pagine HTML o eseguire PHP. Il servizio deve prevedere esplicitamente l’esecuzione di applicazioni Node.js oppure consentire di gestire direttamente i processi necessari.

La configurazione dipende dal tipo di hosting. In genere bisogna poter scegliere la versione di Node.js, indicare la directory dell’applicazione e il relativo file di avvio, installare le dipendenze e configurare le variabili d’ambiente necessarie.

Un’applicazione può avere come punto di ingresso un file chiamato app.js, server.js o qualsiasi altro file previsto dal progetto. Non esiste un nome obbligatorio valido per tutte le applicazioni.

Va considerata anche la gestione del processo. Un server Node.js, a differenza di un normale file HTML, deve rimanere in esecuzione per poter ricevere le richieste. Su un servizio hosting questa parte viene normalmente gestita dall’infrastruttura messa a disposizione dal provider; su una VPS o su un server amministrato direttamente può invece essere necessario configurare un process manager o un servizio di sistema.

Anche CPU e RAM vanno valutate in base all’applicazione. Un’API che effettua poche operazioni e riceve un numero limitato di richieste ha esigenze diverse da un servizio che mantiene migliaia di connessioni, elabora file o interroga continuamente più database. Non esiste quindi una quantità di memoria o di CPU valida per qualsiasi progetto Node.js.

Prima della pubblicazione conviene verificare anche la versione del runtime richiesta dalle dipendenze. Un progetto sviluppato con una determinata versione di Node.js può utilizzare API o pacchetti non compatibili con versioni precedenti. Sul server deve quindi essere disponibile una versione adatta al progetto, preferibilmente appartenente a un ramo ancora supportato.

Nella pratica Node.js si incontra quindi sia nello sviluppo sia nell’hosting: il codice viene preparato con le relative dipendenze npm, caricato sul server e avviato tramite il runtime disponibile sul servizio. Da quel momento il processo Node.js riceve le richieste e svolge il lavoro previsto dall’applicazione, esattamente come nell’esempio del server HTTP, ma con la configurazione e le risorse necessarie a un ambiente di produzione.