Top 10 cele mai frecvente greșeli pe care le fac dezvoltatorii Node.js

Publicat: 2022-03-11

Din momentul în care Node.js a fost dezvăluit lumii, a cunoscut o parte echitabilă atât de laude, cât și de critici. Dezbaterea continuă și s-ar putea să nu se termine prea curând. Ceea ce trecem adesea cu vederea în aceste dezbateri este că fiecare limbaj de programare și platformă este criticată pe baza anumitor probleme, care sunt create de modul în care folosim platforma. Indiferent de cât de dificilă face Node.js scrierea codului sigur și cât de ușoară face scrierea de cod extrem de concurent, platforma există de ceva timp și a fost folosită pentru a construi un număr mare de servicii web robuste și sofisticate. Aceste servicii web se scalează bine și și-au dovedit stabilitatea prin rezistența petrecută pe Internet.

Cu toate acestea, ca orice altă platformă, Node.js este vulnerabil la problemele și problemele dezvoltatorilor. Unele dintre aceste greșeli degradează performanța, în timp ce altele fac ca Node.js să pară direct inutilizabil pentru orice încercați să realizați. În acest articol, vom arunca o privire la zece greșeli frecvente pe care le fac adesea dezvoltatorii nou la Node.js și cum pot fi evitate pentru a deveni un profesionist Node.js.

greșelile dezvoltatorului node.js

Greșeala #1: Blocarea buclei de eveniment

JavaScript în Node.js (la fel ca și în browser) oferă un mediu cu un singur thread. Aceasta înseamnă că două părți ale aplicației dvs. nu rulează în paralel; în schimb, concurența se realizează prin gestionarea operațiunilor legate de I/O în mod asincron. De exemplu, o solicitare de la Node.js către motorul bazei de date pentru a prelua un document este ceea ce permite lui Node.js să se concentreze asupra unei alte părți a aplicației:

 // 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 }) 

mediu node.js cu un singur thread

Cu toate acestea, o bucată de cod legat de CPU într-o instanță Node.js cu mii de clienți conectați este tot ce este nevoie pentru a bloca bucla de evenimente, făcând toți clienții să aștepte. Codurile legate de CPU includ încercarea de a sorta o matrice mare, rularea unei bucle extrem de lungi și așa mai departe. De exemplu:

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

Invocarea acestei funcții „sortUsersByAge” poate fi bine dacă este rulată pe o matrice „utilizatori” mică, dar cu o matrice mare, va avea un impact oribil asupra performanței generale. Dacă acesta este ceva care trebuie făcut în mod absolut și sunteți sigur că nu va mai aștepta nimic în bucla de evenimente (de exemplu, dacă aceasta a făcut parte dintr-un instrument de linie de comandă pe care îl construiți cu Node.js și nu ar conta dacă întregul lucru a funcționat sincron), atunci aceasta poate să nu fie o problemă. Cu toate acestea, într-o instanță de server Node.js care încearcă să deservească mii de utilizatori simultan, un astfel de model se poate dovedi fatal.

Dacă această serie de utilizatori ar fi fost preluată din baza de date, soluția ideală ar fi să-l aduci deja sortați direct din baza de date. Dacă bucla de evenimente a fost blocată de o buclă scrisă pentru a calcula suma unui istoric lung de date privind tranzacțiile financiare, aceasta ar putea fi amânată la o configurație externă a lucrătorului/cozii pentru a evita blocarea buclei de evenimente.

După cum puteți vedea, nu există o soluție de vârf pentru acest tip de problemă Node.js, mai degrabă fiecare caz trebuie abordat individual. Ideea fundamentală este să nu faceți lucru intensiv CPU în instanțele Node.js frontale - cele la care clienții se conectează simultan.

Greșeala nr. 2: invocarea unui apel invers de mai multe ori

JavaScript s-a bazat pe apeluri inverse de totdeauna. În browserele web, evenimentele sunt gestionate prin transmiterea de referințe la funcții (adesea anonime) care acționează ca apeluri inverse. În Node.js, apelurile inverse erau singura modalitate prin care elementele asincrone ale codului dvs. comunicau între ele - până la introducerea promisiunilor. Reapelurile sunt încă în uz, iar dezvoltatorii de pachete își proiectează în continuare API-urile în jurul apelurilor. O problemă comună Node.js legată de utilizarea apelurilor inverse este apelarea acestora de mai multe ori. De obicei, o funcție furnizată de un pachet pentru a face ceva asincron este concepută pentru a aștepta o funcție ca ultimul argument, care este apelată atunci când sarcina asincronă a fost finalizată:

 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) }) }

Observați cum există o declarație return de fiecare dată când este apelat „terminat”, până la ultima dată. Acest lucru se datorează faptului că apelarea apelului invers nu încheie automat execuția funcției curente. Dacă primul „retur” a fost comentat, transmiterea unei parole fără șir acestei funcții va avea ca rezultat apelarea „computeHash”. În funcție de modul în care „computeHash” abordează un astfel de scenariu, „terminat” poate fi numit de mai multe ori. Oricine folosește această funcție din altă parte poate fi prins complet neprevăzut atunci când apelul invers pe care îl transmite este invocat de mai multe ori.

Este suficient să fii atent pentru a evita această eroare Node.js. Unii dezvoltatori Node.js adoptă obiceiul de a adăuga un cuvânt cheie return înainte de fiecare apelare inversă:

 if(err) { return done(err) }

În multe funcții asincrone, valoarea returnată aproape că nu are nicio semnificație, așa că această abordare facilitează evitarea unei astfel de probleme.

Greșeala nr. 3: Reapeluri de imbricare profundă

Apelurile cu imbricare profundă, denumite adesea „iad de apel invers”, nu reprezintă o problemă Node.js în sine. Cu toate acestea, acest lucru poate cauza probleme de a face codul să scape rapid de sub control:

 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') }) }) }) } 

Reapelare iadul

Cu cât sarcina este mai complexă, cu atât acest lucru poate deveni mai rău. Prin imbricarea apelurilor înapoi într-un astfel de mod, ajungem cu ușurință la un cod predispus la erori, greu de citit și greu de întreținut. O soluție este să declarați aceste sarcini ca funcții mici și apoi să le conectați. Deși, una dintre (probabil) cele mai curate soluții pentru aceasta este utilizarea unui pachet utilitar Node.js care se ocupă de modele JavaScript asincrone, cum ar fi 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() { // ... }) }

Similar cu „async.waterfall”, există o serie de alte funcții pe care Async.js le oferă pentru a face față diferitelor modele asincrone. Pentru concizie, am folosit aici exemple mai simple, dar realitatea este adesea mai proastă.

Greșeala nr. 4: așteptarea apelurilor inverse să ruleze sincron

Programarea asincronă cu apeluri inverse poate să nu fie ceva unic pentru JavaScript și Node.js, dar ele sunt responsabile pentru popularitatea sa. Cu alte limbaje de programare, suntem obișnuiți cu ordinea previzibilă de execuție în care două instrucțiuni se vor executa una după alta, cu excepția cazului în care există o instrucțiune specifică pentru a sări între instrucțiuni. Chiar și atunci, acestea sunt adesea limitate la instrucțiuni condiționate, instrucțiuni bucle și invocări de funcții.

Cu toate acestea, în JavaScript, cu apeluri inverse, o anumită funcție poate să nu ruleze bine până când sarcina pe care o așteaptă este terminată. Execuția funcției curente se va desfășura până la sfârșit fără nicio oprire:

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

După cum veți observa, apelarea funcției „testTimeout” va afișa mai întâi „Begin”, apoi „Waiting..” urmat de mesajul „Done!” după aproximativ o secundă.

Orice lucru care trebuie să se întâmple după declanșarea unui apel invers trebuie invocat din interiorul acestuia.

Greșeala #5: Atribuirea la „exporturi”, în loc de „module.exports”

Node.js tratează fiecare fișier ca pe un mic modul izolat. Dacă pachetul dvs. are două fișiere, poate „a.js” și „b.js”, atunci pentru ca „b.js” să acceseze funcționalitatea „a.js”, „a.js” trebuie să-l exporte adăugând proprietăți la obiectul exporturilor:

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

Când se face acest lucru, oricui solicită „a.js” va primi un obiect cu funcția de proprietate „verifyPassword”:

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

Totuși, ce se întâmplă dacă dorim să exportăm această funcție direct și nu ca proprietate a unui obiect? Putem suprascrie exporturile pentru a face acest lucru, dar nu trebuie să le tratăm ca pe o variabilă globală atunci:

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

Observați cum tratăm „exporturile” ca o proprietate a obiectului modul. Distincția dintre „module.exports” și „exports” este foarte importantă și este adesea o cauză de frustrare în rândul noilor dezvoltatori Node.js.

Greșeala #6: Aruncarea erorilor din apelurile interne

JavaScript are noțiunea de excepții. Imitând sintaxa aproape tuturor limbilor tradiționale cu suport pentru gestionarea excepțiilor, cum ar fi Java și C++, JavaScript poate „arunca” și prinde excepții în blocurile 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!') }

Cu toate acestea, try-catch nu se va comporta așa cum v-ați aștepta în situații asincrone. De exemplu, dacă doriți să protejați o bucată mare de cod cu multă activitate asincronă cu un singur bloc mare try-catch, nu ar funcționa neapărat:

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

Dacă apelul înapoi la „db.User.get” s-a declanșat asincron, domeniul de aplicare care conține blocul try-catch ar fi ieșit de mult timp în afara contextului, pentru ca acesta să poată încă prinde acele erori aruncate din interiorul callback-ului.

Acesta este modul în care erorile sunt tratate într-un mod diferit în Node.js, iar acest lucru face ca este esențial să urmați modelul (err, …) pe toate argumentele funcției de apel invers - se așteaptă ca primul argument al tuturor apelurilor inverse să fie o eroare dacă se întâmplă una .

Greșeala #7: Presupunând că numărul este un tip de date întreg

Numerele din JavaScript sunt virgule mobile - nu există un tip de date întreg. Nu v-ați aștepta ca aceasta să fie o problemă, deoarece numerele suficient de mari pentru a sublinia limitele de flotare nu sunt întâlnite des. Exact atunci se întâmplă greșelile legate de asta. Deoarece numerele cu virgulă mobilă pot conține doar reprezentări întregi până la o anumită valoare, depășirea acelei valori în orice calcul va începe imediat să o încurcă. Oricât de ciudat ar părea, următoarele evaluează drept adevărat în Node.js:

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

Din păcate, ciudateniile cu numerele în JavaScript nu se termină aici. Chiar dacă numerele sunt în virgulă mobilă, operatorii care lucrează pe tipuri de date întregi funcționează și aici:

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

Cu toate acestea, spre deosebire de operatorii aritmetici, operatorii pe biți și operatorii de deplasare funcționează numai pe ultimii 32 de biți ai unor numere „întregi” atât de mari. De exemplu, încercarea de a muta „Math.pow(2, 53)” cu 1 va fi întotdeauna evaluată la 0. Încercarea de a face un bit-sau de 1 cu același număr mare va evalua 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

Poate că rareori trebuie să vă ocupați de numere mari, dar dacă o faceți, există o mulțime de biblioteci întregi mari care implementează operațiunile matematice importante pe numere de precizie mare, cum ar fi node-bigint.

Greșeala #8: Ignorarea avantajelor API-urilor de streaming

Să presupunem că vrem să construim un server web mic, asemănător unui proxy, care să răspundă la solicitări prin preluarea conținutului de pe un alt server web. De exemplu, vom construi un mic server web care servește imagini 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)

În acest exemplu particular de problemă Node.js, preluăm imaginea de la Gravatar, o citim într-un buffer și apoi răspundem la cerere. Acesta nu este un lucru atât de rău de făcut, având în vedere că imaginile Gravatar nu sunt prea mari. Cu toate acestea, imaginați-vă dacă dimensiunea conținutului pe care îl transmitem prin proxy ar fi de mii de megaocteți. O abordare mult mai bună ar fi fost aceasta:

 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)

Aici, preluăm imaginea și pur și simplu transmitem răspunsul către client. În niciun moment nu trebuie să citim întregul conținut într-un buffer înainte de a-l servi.

Greșeala #9: Utilizarea Console.log în scopuri de depanare

În Node.js, „console.log” vă permite să imprimați aproape orice pe consolă. Transmiteți-i un obiect și îl va imprima ca un obiect JavaScript literal. Acceptă orice număr arbitrar de argumente și le tipărește pe toate bine separate prin spațiu. Există o serie de motive pentru care un dezvoltator se poate simți tentat să folosească acest lucru pentru a-și depana codul; cu toate acestea, este recomandat să evitați „console.log” în codul real. Ar trebui să evitați să scrieți „console.log” în tot codul pentru a-l depana și apoi să le comentați când nu mai sunt necesare. În schimb, utilizați una dintre bibliotecile uimitoare care sunt create doar pentru acest lucru, cum ar fi depanare.

Pachete ca acestea oferă modalități convenabile de a activa și dezactiva anumite linii de depanare atunci când porniți aplicația. De exemplu, cu depanare este posibil să preveniți tipărirea oricăror linii de depanare pe terminal prin nesetarea variabilei de mediu DEBUG. Folosirea lui este simplă:

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

Pentru a activa liniile de depanare, pur și simplu rulați acest cod cu variabila de mediu DEBUG setată la „app” sau „*”:

 DEBUG=app node app.js

Greșeala #10: Nu folosiți programe de supraveghere

Indiferent dacă codul dvs. Node.js rulează în producție sau în mediul dvs. de dezvoltare local, un monitor de program supervizor care vă poate orchestra programul este un lucru extrem de util. O practică recomandată adesea de dezvoltatorii care proiectează și implementează aplicații moderne recomandă ca codul să eșueze rapid. Dacă apare o eroare neașteptată, nu încercați să o gestionați, mai degrabă lăsați-vă programul să se blocheze și cereți un supervizor să îl repornească în câteva secunde. Beneficiile programelor de supraveghere nu se limitează doar la repornirea programelor blocate. Aceste instrumente vă permit să reporniți programul în caz de blocare, precum și să le reporniți atunci când unele fișiere se modifică. Acest lucru face ca dezvoltarea programelor Node.js să fie o experiență mult mai plăcută.

Există o multitudine de programe de supraveghere disponibile pentru Node.js. De exemplu:

  • pm2

  • pentru totdeauna

  • nodemon

  • supraveghetor

Toate aceste instrumente vin cu avantajele și dezavantajele lor. Unele dintre ele sunt bune pentru a gestiona mai multe aplicații pe aceeași mașină, în timp ce altele sunt mai bune la gestionarea jurnalelor. Cu toate acestea, dacă doriți să începeți cu un astfel de program, toate acestea sunt alegeri corecte.

Concluzie

După cum puteți spune, unele dintre aceste probleme Node.js pot avea efecte devastatoare asupra programului dvs. Unele pot fi cauza frustrării în timp ce încercați să implementați cele mai simple lucruri în Node.js. Deși Node.js a făcut ca noilor veniți să înceapă extrem de ușor, încă mai are zone în care este la fel de ușor să dați peste cap. Dezvoltatorii din alte limbaje de programare pot fi capabili să se raporteze la unele dintre aceste probleme, dar aceste greșeli sunt destul de comune în rândul dezvoltatorilor noi Node.js. Din fericire, sunt ușor de evitat. Sper că acest scurt ghid îi va ajuta pe începători să scrie cod mai bun în Node.js și să dezvolte un software stabil și eficient pentru noi toți.