I 10 errori più comuni commessi dagli sviluppatori di Node.js

Pubblicato: 2022-03-11

Dal momento in cui Node.js è stato presentato al mondo, ha visto una buona dose di elogi e critiche. Il dibattito continua e potrebbe non finire presto. Ciò che spesso trascuriamo in questi dibattiti è che ogni linguaggio di programmazione e piattaforma viene criticato in base a determinati problemi, che sono creati dal modo in cui utilizziamo la piattaforma. Indipendentemente dalla difficoltà con cui Node.js rende la scrittura di codice sicuro e da quanto facile renda la scrittura di codice altamente simultaneo, la piattaforma è in circolazione da un po' di tempo ed è stata utilizzata per creare un numero enorme di servizi Web robusti e sofisticati. Questi servizi Web si adattano bene e hanno dimostrato la loro stabilità grazie alla loro durata nel tempo su Internet.

Tuttavia, come qualsiasi altra piattaforma, Node.js è vulnerabile ai problemi e ai problemi degli sviluppatori. Alcuni di questi errori riducono le prestazioni, mentre altri fanno apparire Node.js inutilizzabile per qualsiasi cosa tu stia cercando di ottenere. In questo articolo, daremo un'occhiata a dieci errori comuni che fanno spesso gli sviluppatori che non conoscono Node.js e come possono essere evitati per diventare un professionista di Node.js.

Errori dello sviluppatore node.js

Errore n. 1: blocco del ciclo di eventi

JavaScript in Node.js (proprio come nel browser) fornisce un unico ambiente a thread. Ciò significa che non esistono due parti dell'applicazione eseguite in parallelo; invece, la concorrenza si ottiene attraverso la gestione asincrona delle operazioni legate all'I/O. Ad esempio, una richiesta da Node.js al motore di database per recuperare un documento è ciò che consente a Node.js di concentrarsi su un'altra parte dell'applicazione:

 // Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here }) 

ambiente a thread singolo node.js

Tuttavia, un pezzo di codice legato alla CPU in un'istanza Node.js con migliaia di client collegati è tutto ciò che serve per bloccare il ciclo di eventi, facendo attendere tutti i client. I codici associati alla CPU includono il tentativo di ordinare un array di grandi dimensioni, l'esecuzione di un ciclo estremamente lungo e così via. Per esempio:

 function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }

Invocare questa funzione "sortUsersByAge" potrebbe andare bene se eseguita su un piccolo array "users", ma con un array di grandi dimensioni, avrà un impatto orribile sulle prestazioni complessive. Se questo è qualcosa che deve essere assolutamente fatto e sei certo che non ci sarà nient'altro in attesa nel ciclo degli eventi (ad esempio, se faceva parte di uno strumento da riga di comando che stai creando con Node.js, e non avrebbe importanza se l'intera cosa funzionasse in modo sincrono), allora questo potrebbe non essere un problema. Tuttavia, in un'istanza del server Node.js che tenta di servire migliaia di utenti alla volta, un tale schema può rivelarsi fatale.

Se questa matrice di utenti venisse recuperata dal database, la soluzione ideale sarebbe recuperarla già ordinata direttamente dal database. Se il ciclo di eventi è stato bloccato da un ciclo scritto per calcolare la somma di una lunga cronologia dei dati delle transazioni finanziarie, potrebbe essere rinviato a un lavoratore esterno/impostazione della coda per evitare di monopolizzare il ciclo di eventi.

Come puoi vedere, non esiste una soluzione lampo d'argento a questo tipo di problema di Node.js, piuttosto ogni caso deve essere affrontato individualmente. L'idea fondamentale è di non eseguire un lavoro intensivo per la CPU all'interno delle istanze Node.js frontali, quelle a cui i client si connettono contemporaneamente.

Errore n. 2: invocare una richiamata più di una volta

JavaScript fa affidamento sui callback da sempre. Nei browser Web, gli eventi vengono gestiti passando i riferimenti a funzioni (spesso anonime) che agiscono come callback. In Node.js, i callback erano l'unico modo in cui gli elementi asincroni del codice comunicavano tra loro, fino a quando non venivano introdotte le promesse. I callback sono ancora in uso e gli sviluppatori di pacchetti progettano ancora le loro API attorno ai callback. Un problema comune di Node.js relativo all'utilizzo dei callback è chiamarli più di una volta. In genere, una funzione fornita da un pacchetto per eseguire qualcosa in modo asincrono è progettata per prevedere una funzione come ultimo argomento, che viene chiamata quando l'attività asincrona è stata completata:

 module.exports.verifyPassword = function(user, password, done) { if(typeof password !== 'string') { done(new Error('password should be a string')) return } computeHash(password, user.passwordHashOpts, function(err, hash) { if(err) { done(err) return } done(null, hash === user.passwordHash) }) }

Nota come c'è un'istruzione return ogni volta che viene chiamato "done", fino all'ultima volta. Questo perché la chiamata al callback non termina automaticamente l'esecuzione della funzione corrente. Se il primo "ritorno" è stato commentato, il passaggio di una password non stringa a questa funzione comporterà comunque la chiamata di "computeHash". A seconda di come "computeHash" gestisce un tale scenario, "done" può essere chiamato più volte. Chiunque utilizzi questa funzione da altrove può essere colto completamente alla sprovvista quando il callback che passa viene richiamato più volte.

Fare attenzione è tutto ciò che serve per evitare questo errore Node.js. Alcuni sviluppatori di Node.js adottano l'abitudine di aggiungere una parola chiave return prima di ogni chiamata di callback:

 if(err) { return done(err) }

In molte funzioni asincrone, il valore restituito non ha quasi alcun significato, quindi questo approccio spesso consente di evitare un problema del genere.

Errore n. 3: nidificazione profonda delle richiamate

I callback con annidamento profondo, spesso indicati come "callback hell", non sono di per sé un problema di Node.js. Tuttavia, ciò può causare problemi facendo girare il codice rapidamente fuori controllo:

 function handleLogin(..., done) { db.User.get(..., function(..., user) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) }) }) } 

Richiamata all'inferno

Più è complesso il compito, peggio può diventare. Annidando i callback in questo modo, finiamo facilmente con un codice soggetto a errori, difficile da leggere e difficile da mantenere. Una soluzione alternativa consiste nel dichiarare queste attività come piccole funzioni e quindi collegarle. Tuttavia, una delle soluzioni (probabilmente) più pulite a questo è utilizzare un pacchetto di utilità Node.js che si occupa di modelli JavaScript asincroni, come Async.js:

 function handleLogin(done) { async.waterfall([ function(done) { db.User.get(..., done) }, function(user, done) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { done(null, user, okay) }) }, function(user, okay, done) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) } ], function() { // ... }) }

Simile a "async.waterfall", ci sono una serie di altre funzioni che Async.js fornisce per gestire diversi modelli asincroni. Per brevità, abbiamo usato qui esempi più semplici, ma la realtà è spesso peggiore.

Errore n. 4: aspettarsi che i callback vengano eseguiti in modo sincrono

La programmazione asincrona con callback potrebbe non essere qualcosa di unico per JavaScript e Node.js, ma sono responsabili della sua popolarità. Con altri linguaggi di programmazione, siamo abituati all'ordine di esecuzione prevedibile in cui due istruzioni verranno eseguite una dopo l'altra, a meno che non ci sia un'istruzione specifica per saltare tra le istruzioni. Anche in questo caso, questi sono spesso limitati a istruzioni condizionali, istruzioni di ciclo e invocazioni di funzioni.

Tuttavia, in JavaScript, con i callback una particolare funzione potrebbe non funzionare correttamente fino al termine dell'attività in attesa. L'esecuzione della funzione corrente proseguirà fino alla fine senza interruzioni:

 function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }

Come noterai, chiamando la funzione “testTimeout” verrà prima stampato “Begin”, quindi “Waiting..” seguito dal messaggio “Done!” dopo circa un secondo.

Tutto ciò che deve accadere dopo l'attivazione di una richiamata deve essere richiamato dall'interno.

Errore n. 5: Assegnare a "esportazioni", invece di "modulo.esportazioni"

Node.js tratta ogni file come un piccolo modulo isolato. Se il tuo pacchetto ha due file, forse "a.js" e "b.js", quindi affinché "b.js" acceda alla funzionalità di "a.js", "a.js" deve esportarlo aggiungendo proprietà a l'oggetto di esportazione:

 // a.js exports.verifyPassword = function(user, password, done) { ... }

Al termine, a chiunque richieda "a.js" verrà assegnato un oggetto con la funzione di proprietà "verifyPassword":

 // b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }

Tuttavia, cosa succede se si desidera esportare questa funzione direttamente e non come proprietà di un oggetto? Possiamo sovrascrivere le esportazioni per farlo, ma non dobbiamo trattarlo come una variabile globale, quindi:

 // a.js module.exports = function(user, password, done) { ... }

Nota come stiamo trattando le "esportazioni" come una proprietà dell'oggetto modulo. La distinzione qui tra "module.exports" ed "exports" è molto importante ed è spesso motivo di frustrazione tra i nuovi sviluppatori di Node.js.

Errore n. 6: lanciare errori da callback interni

JavaScript ha la nozione di eccezioni. Imitando la sintassi di quasi tutti i linguaggi tradizionali con supporto per la gestione delle eccezioni, come Java e C++, JavaScript può "generare" e catturare eccezioni nei blocchi try-catch:

 function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') }

Tuttavia, try-catch non si comporterà come ci si potrebbe aspettare in situazioni asincrone. Ad esempio, se si desidera proteggere una grossa porzione di codice con molte attività asincrone con un grande blocco try-catch, non funzionerebbe necessariamente:

 try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }

Se il callback a "db.User.get" è stato attivato in modo asincrono, l'ambito contenente il blocco try-catch sarebbe andato fuori contesto da tempo per poter ancora rilevare quegli errori generati dall'interno del callback.

Questo è il modo in cui gli errori vengono gestiti in modo diverso in Node.js e ciò rende essenziale seguire il modello (err, ...) su tutti gli argomenti della funzione di callback: il primo argomento di tutti i callback dovrebbe essere un errore se ne accade uno .

Errore n. 7: presumere che il numero sia un tipo di dati intero

I numeri in JavaScript sono a virgola mobile: non esiste un tipo di dati intero. Non ti aspetteresti che questo sia un problema, poiché i numeri abbastanza grandi da sottolineare i limiti del float non si incontrano spesso. Questo è esattamente il momento in cui accadono errori relativi a questo. Poiché i numeri in virgola mobile possono contenere solo rappresentazioni intere fino a un certo valore, il superamento di quel valore in qualsiasi calcolo inizierà immediatamente a rovinarlo. Per quanto strano possa sembrare, quanto segue restituisce true in Node.js:

 Math.pow(2, 53)+1 === Math.pow(2, 53)

Sfortunatamente, le stranezze con i numeri in JavaScript non finiscono qui. Anche se i numeri sono a virgola mobile, anche gli operatori che funzionano su tipi di dati interi funzionano qui:

 5 % 2 === 1 // true 5 >> 1 === 2 // true

Tuttavia, a differenza degli operatori aritmetici, gli operatori bit per bit e gli operatori di spostamento funzionano solo sui 32 bit finali di numeri "interi" così grandi. Ad esempio, il tentativo di spostare "Math.pow(2, 53)" di 1 restituirà sempre 0. Se si tenta di eseguire un bit per bit o 1 con lo stesso numero elevato, verrà restituito 1.

 Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true

Potrebbe essere necessario occuparsi raramente di numeri grandi, ma se lo fai, ci sono molte grandi librerie di interi che implementano le importanti operazioni matematiche su numeri di grande precisione, come node-bigint.

Errore n. 8: ignorare i vantaggi delle API di streaming

Diciamo che vogliamo costruire un piccolo server web simile a un proxy che serva le risposte alle richieste recuperando il contenuto da un altro server web. Ad esempio, costruiremo un piccolo server web che serve immagini Gravatar:

 var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080)

In questo particolare esempio di problema con Node.js, stiamo recuperando l'immagine da Gravatar, leggendola in un Buffer e quindi rispondendo alla richiesta. Questa non è una brutta cosa da fare, dato che le immagini di Gravatar non sono troppo grandi. Tuttavia, immagina se la dimensione dei contenuti che stiamo inviando tramite proxy fosse di migliaia di megabyte. Un approccio molto migliore sarebbe stato questo:

 http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080)

Qui, prendiamo l'immagine e semplicemente convogliamo la risposta al cliente. In nessun momento è necessario leggere l'intero contenuto in un buffer prima di servirlo.

Errore n. 9: utilizzo di Console.log per scopi di debug

In Node.js, "console.log" ti consente di stampare quasi tutto sulla console. Passagli un oggetto e lo stamperà come letterale oggetto JavaScript. Accetta qualsiasi numero arbitrario di argomenti e li stampa tutti ordinatamente separati da uno spazio. Ci sono una serie di ragioni per cui uno sviluppatore potrebbe sentirsi tentato di usarlo per eseguire il debug del suo codice; tuttavia, si consiglia vivamente di evitare "console.log" nel codice reale. Dovresti evitare di scrivere "console.log" su tutto il codice per eseguire il debug e poi commentarli quando non sono più necessari. Invece, usa una delle straordinarie librerie create appositamente per questo, come debug.

Pacchetti come questi forniscono modi convenienti per abilitare e disabilitare determinate righe di debug all'avvio dell'applicazione. Ad esempio, con il debug è possibile impedire che eventuali righe di debug vengano stampate sul terminale non impostando la variabile d'ambiente DEBUG. Usarlo è semplice:

 // app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')

Per abilitare le righe di debug, esegui semplicemente questo codice con la variabile di ambiente DEBUG impostata su "app" o "*":

 DEBUG=app node app.js

Errore n. 10: non utilizzare i programmi del supervisore

Indipendentemente dal fatto che il codice Node.js sia in esecuzione in produzione o nell'ambiente di sviluppo locale, è estremamente utile disporre di un monitor del programma supervisore in grado di orchestrare il programma. Una pratica spesso consigliata dagli sviluppatori che progettano e implementano applicazioni moderne consiglia che il codice fallisca rapidamente. Se si verifica un errore imprevisto, non tentare di gestirlo, ma lascia che il tuo programma si arresti in modo anomalo e chiedi a un supervisore di riavviarlo in pochi secondi. I vantaggi dei programmi supervisore non si limitano solo al riavvio dei programmi bloccati. Questi strumenti consentono di riavviare il programma in caso di arresto anomalo, nonché di riavviarli quando alcuni file cambiano. Ciò rende lo sviluppo di programmi Node.js un'esperienza molto più piacevole.

C'è una pletora di programmi di supervisione disponibili per Node.js. Per esempio:

  • pm2

  • per sempre

  • nodomon

  • supervisore

Tutti questi strumenti hanno i loro pro e contro. Alcuni sono utili per gestire più applicazioni sulla stessa macchina, mentre altri sono migliori nella gestione dei registri. Tuttavia, se vuoi iniziare con un programma del genere, tutte queste sono scelte giuste.

Conclusione

Come puoi vedere, alcuni di questi problemi di Node.js possono avere effetti devastanti sul tuo programma. Alcuni potrebbero essere causa di frustrazione mentre stai cercando di implementare le cose più semplici in Node.js. Sebbene Node.js abbia reso estremamente facile per i nuovi arrivati ​​​​iniziare, ha ancora aree in cui è altrettanto facile sbagliare. Gli sviluppatori di altri linguaggi di programmazione potrebbero essere in grado di relazionarsi con alcuni di questi problemi, ma questi errori sono abbastanza comuni tra i nuovi sviluppatori di Node.js. Fortunatamente, sono facili da evitare. Spero che questa breve guida aiuti i principianti a scrivere codice migliore in Node.js ea sviluppare software stabile ed efficiente per tutti noi.