Este timpul să folosiți Nodul 8?
Publicat: 2022-03-11Nodul 8 este scos! De fapt, Node 8 a fost acum suficient de mult timp pentru a vedea o utilizare solidă în lumea reală. A venit cu un nou motor V8 rapid și cu funcții noi, inclusiv asincron/așteptare, HTTP/2 și cârlige asincrone. Dar este gata pentru proiectul tău? Să aflăm!
Nota editorului: probabil știți că nodul 10 (numit de cod Dubnium ) este și el. Alegem să ne concentrăm pe Nodul 8 ( Carbon ) din două motive: (1) Nodul 10 tocmai intră în faza de suport pe termen lung (LTS) și (2) Nodul 8 a marcat o iterație mai semnificativă decât a făcut-o Nodul 10 .
Performanță în Node 8 LTS
Vom începe prin a arunca o privire asupra îmbunătățirilor de performanță și a noilor funcții ale acestei lansări remarcabile. Un domeniu major de îmbunătățire este motorul JavaScript al Node.
Ce este exact un motor JavaScript, oricum?
Un motor JavaScript execută și optimizează codul. Ar putea fi un interpret standard sau un compilator just-in-time (JIT) care compilează JavaScript în bytecode. Motoarele JS utilizate de Node.js sunt toate compilatoare JIT, nu interpreți.
Motorul V8
Node.js a folosit motorul JavaScript Chrome V8 de la Google, sau pur și simplu V8 , încă de la început. Unele versiuni Node sunt folosite pentru a sincroniza cu o versiune mai nouă a V8. Dar aveți grijă să nu confundați V8 cu Node 8, deoarece comparăm versiunile V8 aici.
Acest lucru este ușor de împiedicat, deoarece în contexte software folosim adesea „v8” ca argo sau chiar forma scurtă oficială pentru „versiunea 8”, așa că unii ar putea combina „Node V8” sau „Node.js V8” cu „NodeJS 8”. ”, dar am evitat acest lucru pe parcursul acestui articol pentru a menține lucrurile clare: V8 va însemna întotdeauna motor, nu versiunea Node.
V8 versiunea 5
Nodul 6 folosește versiunea 5 a V8 ca motor JavaScript. (Primele versiuni punctuale ale Nodului 8 folosesc, de asemenea, versiunea V8 5, dar folosesc o ediție punctuală V8 mai nouă decât a făcut-o Node 6.)
Compilatoare
Versiunile V8 5 și anterioare au două compilatoare:
- Full-codegen este un compilator JIT simplu și rapid, dar produce cod de mașină lent.
- Arborele cotit este un compilator JIT complex care produce cod de mașină optimizat.
Fire
În profunzime, V8 folosește mai mult de un tip de fir:
- Firul principal preia codul, îl compilează, apoi îl execută.
- Firele secundare execută cod în timp ce firul principal optimizează codul.
- Firul profiler informează timpul de execuție despre metodele neperformante. Apoi, arborele cotit optimizează aceste metode.
- Alte fire gestionează colectarea gunoiului.
Procesul de compilare
În primul rând, compilatorul Full-codegen execută codul JavaScript. În timp ce codul este executat, firul de execuție de profiler adună date pentru a determina ce metode va optimiza motorul. Pe un alt fir, Crankshaft optimizează aceste metode.
Probleme
Abordarea menționată mai sus are două probleme principale. În primul rând, este complex din punct de vedere arhitectural. În al doilea rând, codul mașină compilat consumă mult mai multă memorie. Cantitatea de memorie consumată este independentă de numărul de executări ale codului. Chiar și codul care rulează o singură dată ocupă și o cantitate semnificativă de memorie.
V8 versiunea 6
Prima versiune Node care folosește motorul V8 versiunea 6 este Node 8.3.
În versiunea 6, echipa V8 a construit Ignition și TurboFan pentru a atenua aceste probleme. Ignition și TurboFan înlocuiesc Full-codegen și, respectiv, Crankshaft.
Noua arhitectură este mai simplă și consumă mai puțină memorie.
Ignition compilează codul JavaScript în bytecode în loc de codul mașinii, economisind multă memorie. Ulterior, TurboFan, compilatorul de optimizare, generează cod de mașină optimizat din acest bytecode.
Îmbunătățiri specifice de performanță
Să trecem prin zonele în care performanța în Node 8.3+ s-a schimbat în raport cu versiunile mai vechi de Node.
Crearea obiectelor
Crearea obiectelor este de aproximativ cinci ori mai rapidă în Node 8.3+ decât în Node 6.
Dimensiunea funcției
Motorul V8 decide dacă o funcție ar trebui optimizată pe baza mai multor factori. Un factor este dimensiunea funcției. Funcțiile mici sunt optimizate, în timp ce funcțiile lungi nu.
Cum se calculează dimensiunea funcției?
Arborele cotit din vechiul motor V8 folosește „numărul de caractere” pentru a determina dimensiunea funcției. Spațiile albe și comentariile dintr-o funcție reduc șansele ca aceasta să fie optimizată. Știu că s-ar putea să te surprindă, dar pe atunci, un comentariu ar putea reduce viteza cu aproximativ 10%.
În Node 8.3+, caracterele irelevante, cum ar fi spațiile albe și comentariile, nu dăunează performanței funcției. De ce nu?
Deoarece noul TurboFan nu numără caracterele pentru a determina dimensiunea funcției. În schimb, numără nodurile arborelui de sintaxă abstractă (AST), astfel încât în mod eficient ia în considerare doar instrucțiunile de funcție reale . Folosind Node 8.3+, puteți adăuga comentarii și spații albe cât doriți.
Argumente de definire a Array
Funcțiile obișnuite din JavaScript poartă un obiect argument
implicit de tip Array
.
Ce înseamnă Array
-like?
Obiectul arguments
acționează oarecum ca o matrice. Are proprietatea length
, dar nu are metodele încorporate ale Array
, cum ar fi forEach
și map
.
Iată cum funcționează obiectul arguments
:
function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c");
Deci, cum am putea converti obiectul arguments
într-o matrice? Prin utilizarea concisă Array.prototype.slice.call(arguments)
.
function test() { const r = Array.prototype.slice.call(arguments); console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output: [2, 4, 6]
Array.prototype.slice.call(arguments)
afectează performanța în toate versiunile Node. Prin urmare, copierea cheilor printr-o buclă for
are o performanță mai bună:
function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Bucla for
este puțin greoaie, nu-i așa? Am putea folosi operatorul de răspândire, dar este lent în Node 8.2 și în jos:
function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Situația s-a schimbat în Node 8.3+. Acum spread-ul se execută mult mai rapid, chiar mai rapid decât o buclă for.
Aplicare parțială (curring) și legare
Currying înseamnă defalcarea unei funcții care preia mai multe argumente într-o serie de funcții în care fiecare funcție nouă ia doar un argument.
Să presupunem că avem o funcție simplă de add
. Versiunea curry a acestei funcții are un argument, num1
. Returnează o funcție care ia un alt argument num2
și returnează suma num1
și num2
:
function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8
Metoda bind
returnează o funcție curry cu o sintaxă terser.
function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8
Deci bind
este incredibil, dar este lent în versiunile mai vechi de Node. În Node 8.3+, bind
este mult mai rapidă și îl puteți folosi fără să vă faceți griji cu privire la atingerile de performanță.
Experimente
Au fost efectuate mai multe experimente pentru a compara performanța Nodului 6 cu Nodul 8 la un nivel înalt. Rețineți că acestea au fost efectuate pe Node 8.0, așa că nu includ îmbunătățirile menționate mai sus care sunt specifice Nodului 8.3+ datorită upgrade-ului V8 ediția 6.
Timpul de randare a serverului în Nodul 8 a fost cu 25% mai mic decât în Nodul 6. În proiectele mari, numărul de instanțe de server ar putea fi redus de la 100 la 75. Acest lucru este uimitor. Testarea unei suită de 500 de teste în Nodul 8 a fost cu 10% mai rapidă. Compilările Webpack au fost cu 7% mai rapide. În general, rezultatele au arătat o creștere vizibilă a performanței în Nodul 8.
Caracteristicile nodului 8
Viteza nu a fost singura îmbunătățire în Node 8. De asemenea, a adus câteva funcții noi utile — poate cel mai important, asincron/așteptare .
Async/Await în Nodul 8
Apelurile și promisiunile sunt de obicei folosite pentru a gestiona codul asincron în JavaScript. Reapelurile sunt renumite pentru producerea de cod care nu poate fi întreținut. Au provocat haos (cunoscut în special sub numele de callback hell ) în comunitatea JavaScript. Promisele ne-au salvat mult timp din iadul callback, dar le lipsea totuși curățenia codului sincron. Async/wait este o abordare modernă care vă permite să scrieți cod asincron care arată ca un cod sincron.
Și în timp ce async/wait putea fi folosit în versiunile anterioare Node, a necesitat biblioteci și instrumente externe - de exemplu, preprocesare suplimentară prin Babel. Acum este disponibil nativ, din cutie.
Voi vorbi despre unele cazuri în care async/wait este superior promisiunilor convenționale.
Condiționale
Imaginați-vă că preluați date și veți determina dacă este necesar un nou apel API pe baza sarcinii utile . Aruncă o privire la codul de mai jos pentru a vedea cum se face acest lucru prin abordarea „promisiuni convenționale”.
const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.id).then(carData => { console.log(carData); return carData; }); } else { console.log(data); return data; } }); };
După cum puteți vedea, codul de mai sus pare deja dezordonat, doar dintr-un condițional suplimentar. Async/wait implică mai puțin imbricare:
const request = async () => { const data = await getData(); if (!data.car) { const carData = await fetchForCar(data); console.log(carData); return carData; } else { console.log(data); return data; } };
Eroare de manipulare
Async/wait vă oferă acces pentru a gestiona atât erorile sincrone, cât și asincrone în try/catch. Să presupunem că doriți să analizați JSON provenit dintr-un apel API asincron. O singură încercare/captură ar putea gestiona atât erorile de analiză, cât și erorile API.
const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };
Valori intermediare
Ce se întâmplă dacă o promisiune are nevoie de un argument care ar trebui rezolvat dintr-o altă promisiune? Aceasta înseamnă că apelurile asincrone trebuie să fie efectuate în serie.
Folosind promisiuni convenționale, s-ar putea să ajungeți cu cod ca acesta:
const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };
Async/wait strălucește în acest caz, în care sunt necesare apeluri asincrone înlănțuite:
const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };
Async în paralel
Ce se întâmplă dacă doriți să apelați mai mult de o funcție asincronă în paralel? În codul de mai jos, vom aștepta ca fetchHouseData
să se rezolve, apoi vom apela fetchCarData
. Deși fiecare dintre acestea este independent de celălalt, ele sunt procesate secvenţial. Veți aștepta două secunde pentru ca ambele API-uri să se rezolve. Acest lucru nu este bun.

function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();
O abordare mai bună este să procesați apelurile asincrone în paralel. Verificați codul de mai jos pentru a vă face o idee despre cum se realizează acest lucru în async/wait.
async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();
Procesarea acestor apeluri în paralel vă face să așteptați doar o secundă pentru ambele apeluri.
Noile funcții de bază ale bibliotecii
Nodul 8 aduce și câteva funcții de bază noi.
Copiați fișierele
Înainte de Node 8, pentru a copia fișiere, obișnuiam să creăm două fluxuri și să conductăm date de la unul la altul. Codul de mai jos arată modul în care fluxul de citire transmite datele către fluxul de scriere. După cum puteți vedea, codul este aglomerat pentru o acțiune atât de simplă precum copiarea unui fișier.
const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr);
În Nodul 8, fs.copyFile
și fs.copyFileSync
sunt abordări noi pentru copierea fișierelor cu mult mai puține bătăi de cap.
const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });
Promisify și Callbackify
util.promisify
convertește o funcție obișnuită într-o funcție asincronă. Rețineți că funcția introdusă ar trebui să urmeze stilul de apel invers comun Node.js. Ar trebui să ia un apel invers ca ultimul argument, adică (error, payload) => { ... }
.
const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err));
După cum puteți vedea, util.promisify
a convertit fs.readFile
într-o funcție asincronă.
Pe de altă parte, Node.js vine cu util.callbackify
. util.callbackify
este opusul util.promisify
: convertește o funcție asincronă într-o funcție de stil de apel invers Node.js.
destroy
Funcția pentru Readables și Writables
Funcția de destroy
din Nodul 8 este o modalitate documentată de a distruge/închide/abandona un flux care poate fi citit sau scris:
const fs = require('fs'); const file = fs.createWriteStream('./big.txt'); file.on('error', errors => { console.log(errors); }); file.write(`New text.\n`); file.destroy(['First Error', 'Second Error']);
Codul de mai sus are ca rezultat crearea unui fișier nou numit big.txt
(dacă nu există deja) cu textul New text.
.
Funcțiile Readable.destroy
și Writeable.destroy
din Nodul 8 emit un eveniment de close
și un eveniment de error
opțional — destroy
nu înseamnă neapărat că ceva a mers prost.
Operator Spread
Operatorul de răspândire (aka ...
) a lucrat în Nodul 6, dar numai cu matrice și alte iterabile:
const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]
În Nodul 8, obiectele pot folosi și operatorul de răspândire:
const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */
Caracteristici experimentale în Node 8 LTS
Caracteristicile experimentale nu sunt stabile, ar putea fi depreciate și ar putea fi actualizate în timp. Nu utilizați niciuna dintre aceste caracteristici în producție până când nu devin stabile.
Cârlige asincrone
Cârligele asincrone urmăresc durata de viață a resurselor asincrone create în interiorul Node printr-un API.
Asigurați-vă că înțelegeți bucla de evenimente înainte de a merge mai departe cu cârlige asincrone. Acest videoclip ar putea ajuta. Cârligele asincrone sunt utile pentru depanarea funcțiilor asincrone. Au mai multe aplicații; una dintre ele este urmele stivei de erori pentru funcțiile asincrone.
Aruncă o privire la codul de mai jos. Observați că console.log
este o funcție asincronă. Prin urmare, nu poate fi folosit în interiorul cârligelor asincrone. fs.writeSync
este folosit în schimb.
const asyncHooks = require('async_hooks'); const fs = require('fs'); const init = (asyncId, type, triggerId) => fs.writeSync(1, `${type} \n`); const asyncHook = asyncHooks.createHook({ init }); asyncHook.enable();
Urmăriți acest videoclip pentru a afla mai multe despre cârligele asincrone. În ceea ce privește în special ghidul Node.js, acest articol ajută la demistificarea cârligelor asincrone printr-o aplicație ilustrativă.
Modulele ES6 în Nodul 8
Nodul 8 acceptă acum module ES6, permițându-vă să utilizați această sintaxă:
import { UtilityService } from './utility_service';
Pentru a utiliza modulele ES6 în Nodul 8, trebuie să faceți următoarele.
- Adăugați
--experimental-modules
la linia de comandă - Redenumiți extensiile de fișiere din
.js
în.mjs
HTTP/2
HTTP/2 este cea mai recentă actualizare a protocolului HTTP care nu este adesea actualizat, iar Node 8.4+ îl acceptă nativ în modul experimental. Este mai rapid, mai sigur și mai eficient decât predecesorul său, HTTP/1.1. Și Google vă recomandă să îl utilizați. Dar ce altceva face?
Multiplexarea
În HTTP/1.1, serverul putea trimite doar un răspuns per conexiune la un moment dat. În HTTP/2, serverul poate trimite mai mult de un răspuns în paralel.
Server Push
Serverul poate împinge mai multe răspunsuri pentru o singură cerere client. De ce este acest lucru benefic? Luați ca exemplu o aplicație web. Convenţional,
- Clientul solicită un document HTML.
- Clientul descoperă resursele necesare din documentul HTML.
- Clientul trimite o cerere HTTP pentru fiecare resursă necesară. De exemplu, clientul trimite o cerere HTTP pentru fiecare resursă JS și CSS menționată în document.
Caracteristica server-push folosește faptul că serverul știe deja despre toate aceste resurse. Serverul împinge acele resurse către client. Deci, pentru exemplul aplicației web, serverul împinge toate resursele după ce clientul solicită documentul inițial. Acest lucru reduce latența.
Prioritizare
Clientul poate stabili o schemă de prioritizare pentru a determina cât de important este fiecare răspuns cerut. Serverul poate folosi apoi această schemă pentru a prioritiza alocarea memoriei, procesorului, lățimii de bandă și a altor resurse.
Renunțarea la vechile obiceiuri proaste
Deoarece HTTP/1.1 nu permitea multiplexarea, sunt folosite mai multe optimizări și soluții pentru a acoperi viteza lentă și încărcarea fișierelor. Din păcate, aceste tehnici provoacă o creștere a consumului de memorie RAM și randare întârziată:
- Împărțirea domeniului: au fost utilizate mai multe subdomenii, astfel încât conexiunile să fie dispersate și procesate în paralel.
- Combinarea fișierelor CSS și JavaScript pentru a reduce numărul de solicitări.
- Hărți sprite: combinarea fișierelor imagine pentru a reduce solicitările HTTP.
- Inline: CSS și JavaScript sunt plasate direct în HTML pentru a reduce numărul de conexiuni.
Acum, cu HTTP/2, puteți uita de aceste tehnici și vă puteți concentra asupra codului dvs.
Dar cum folosești HTTP/2?
Majoritatea browserelor acceptă HTTP/2 numai printr-o conexiune SSL securizată. Acest articol vă poate ajuta să configurați un certificat autosemnat. Adăugați fișierul .crt
generat și fișierul .key
într-un director numit ssl
. Apoi, adăugați codul de mai jos într-un fișier numit server.js
.
Nu uitați să utilizați --expose-http2
în linia de comandă pentru a activa această caracteristică. Adică comanda de rulare pentru exemplul nostru este node server.js --expose-http2
.
const http2 = require('http2'); const path = require('path'); const fs = require('fs'); const PORT = 3000; const secureServerOptions = { cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')) }; const server = http2.createSecureServer(secureServerOptions, (req, res) => { res.statusCode = 200; res.end('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );
Desigur, Nodul 8, Nodul 9, Nodul 10 etc. acceptă în continuare vechiul HTTP 1.1 — documentația oficială Node.js privind o tranzacție HTTP standard nu va fi învechită mult timp. Dar dacă doriți să utilizați HTTP/2, puteți aprofunda cu acest ghid Node.js.
Deci, ar trebui să folosesc Node.js 8 în final?
Nodul 8 a sosit cu îmbunătățiri de performanță și cu noi funcții precum async/wait, HTTP/2 și altele. Experimentele de la capăt la capăt au arătat că Nodul 8 este cu aproximativ 25% mai rapid decât Nodul 6. Acest lucru duce la economii substanțiale de costuri. Deci, pentru proiecte greenfield, absolut! Dar pentru proiectele existente, ar trebui să actualizați Node?
Depinde dacă ar trebui să modificați o mare parte din codul existent. Acest document enumeră toate modificările de la Node 8 dacă veniți de la Node 6. Nu uitați să evitați problemele comune reinstalând toate pachetele npm
ale proiectului dvs. folosind cea mai recentă versiune Node 8. De asemenea, utilizați întotdeauna aceeași versiune Node.js pe mașinile de dezvoltare ca și pe serverele de producție. Mult noroc!
- De ce naiba aș folosi Node.js? Un tutorial de la caz la caz
- Depanarea pierderilor de memorie în aplicațiile Node.js
- Crearea unui API REST securizat în Node.js
- Codarea Cabin Fever: Un tutorial back-end Node.js