Table of Contents

Laborator 06

Utilizarea platformei Node.JS pentru dezvoltarea de aplicații web

Obiective

Cuvinte Cheie

Node.JS, V8 JavaScript Engine, I/O bound applications, data streaming applications, DIRT (Data Intensive Real-Time Applocations), event-driven applications, event emitter, callback, REPL terminal, NPM (Node.JS Package Manager), modules, buffers, streams, global objects, express framework, embedded javascript template (ejs)

Materiale Ajutătoare

TODO

Node.JS - aspecte generale

Node.JS este o platformă bazată pe JavaScript, construită peste motorul JavaScript V8 al Google Chrome, destinată dezvoltării de aplicații web rapide și scalabile. Node.JS folosește un model bazat pe evenimente în care operațiile de intrare/ieșire sunt gestionate asincron, astfel încât poate fi utilizat pentru aplicații de timp real care folosesc numeroase operații de intrare/ieșire și care rulează pe dispozitive distribuite, fără ca performanța acestora să fie afectată.

Este un produs open-source, gratuit, utilizat pe scară largă (eBay, General Electric, Microsoft, PayPal, Uber, Yahoo!) pentru dezvoltarea de aplicații la nivel de server sau care conțin componente de rețea. Aplicațiile Node.JS sunt scrise în JavaScript și pot fi rulate pe mai multe platforme (Linux, Windows, OS X). De asemenea, sunt disponibile mai multe librării prin care funcționalitatea oferită de Node.JS este extinsă și mai mult. Node Package Manager (npm), sistemul de gestiune al modulelor Node.JS, este considerat cel mai mare ecosistem de biblioteci open-source din lume în momentul de față.

Node.JS a fost dezvoltat de Ryan Dahl în 2009 și a ajuns în prezent la versiunea 4.2.2. Există și o versiune în care sunt incluse numeroase funcționalități noi, experimentale, versiunea 5.1.0.

Câteva dintre caracteristicile Node.JS sunt:

  1. toate API-urile din bibliotecile Node.JS sunt asincrone (non-blocante) și bazate pe evenimente astfel încât nu trebuie să se aștepte ca o anumită operație să furnizeze un rezultat; după ce o anumită metodă a fost invocată, se trece mai departe, urmând ca un mecanism de notificare să indice momentul la care operația a fost terminată și poate furniza date, apelând o metodă de callback în care informațiile obținute sunt procesate mai departe;
  2. fiind bazat pe motorul JavaScript V8 al Google Chrome, execuția codului sursă este foarte rapidă;
  3. deși utilizează un singur fir de execuție aplicațiile dezvoltate folosind Node.JS sunt foarte scalabile putând gestiona un număr mult mai mare de cereri concurente decât serverele HTTP tradiționale, datorită mecanismului de notificare prin intermediul evenimentelor; astfel timpul de răspuns este mic iar disponibilitatea serverului mare, de vreme ce așteptarea pentru date este eliminată complet;
  4. nu utilizează zone tampon de memorie, ci furnizează datele treptat.

Prin urmare, platforma Node.JS reprezintă o soluție pentru aplicațiile web care folosesc intensiv operații de intrare/ieșire (fluxuri multimedia), disponibile eventual în timp real, pentru aplicații web care utilizează API-uri bazate pe formatul JSON sau pentru aplicații web al căror conținut este inclus într-o singură pagină Internet. Nu este recomandat ca Node.JS să fie utilizat pentru aplicații web care realizează numeroase procesări.

Structura unei Aplicații Node.JS

O aplicație Node.JS conține următoarele componente:

  1. încărcarea modulelor pe care le va utiliza este realizată prin intermediul directivei require() care primește ca parametru denumirea modulului respectiv; prin intermediul acestei metode vor fi expuse toate metodele care sunt exportate în cadrul modulului respectiv;
  2. construirea unui server HTTP, folosind metoda createServer() care primește ca parametru o funcție de callback prin intermediul căreia vor fi gestionate cererea și răspunsul; indicarea unui port și a unei adrese prin intermediul căruia vor fi gestionate cererile și răspunsurile este realizată prin intermediul metodei listen();
  3. gestiunea cererii și a răspunsului:
    1. obiectul request are tipul http.IncomingMessage și prin intermediul său poate fi analizată cererea HTTP: close, headers, httpVersion, method, rawHeaders, rawTrailers, setTimeout, statusCode, statusMessage, socket, trailers, url.
    2. obiectul response are tipul http.ServerResponse și prin intermediul său poate fi construit răspunsul HTTP: addTrailers(), end(), finished, getHeader(), headersSent, removeHeader(), sendDate, setHeader(), setTimeout(), statusCode, write(), writeContinue(), writeHead().
main.js
var http = require('http');
 
http.createServer(function(request, response) {
  var url = request.url;
  var httpVersion = request.httpVersion;
  var method = request.method;
  var statusCode = request.statusCode;
  response.writeHead(200, {
    'Content-Type': 'text/html'
  });
  var content = '<html>\n' +
  '  <head>\n' +
  '    <title></title>\n' +
  '  </head>\n' +
  '  <body>\n' +
  '    URL: ' + url + '<br/>\n' +
  '    httpVersion: ' + httpVersion + '<br/>\n' +
  '    method: ' + method + '<br/>\n' +
  '    statusCode: ' + statusCode + '<br/>\n' +
  '  </body>\n' +
  '</html>';
  response.end(content);
}).listen(8080);
 
console.log('Server running at http://localhost:8080/');

Rularea serverului HTTP se face prin comanda node main.js.

Utilitare asociate Platformei Node.JS

Terminalul REPL

REPL (Read Eval Print Loop) reprezintă un utilitar integrat platformei Node.JS având funcționalitatea asemănătoare cu a unei console - utilizatorul specifică o anumită comandă pentru care se furnizează un rezultat, într-un mod interactiv:

Un astfel de utilitar este adecvat în momentul în care se dorește să se testeze o anumită secvență de cod sursă sau în momentul în care se face depanarea.

Terminalul REPL este pornit prin comanda node. Oprirea sa se face atunci când se apasă fie Ctrl + D (o dată), fie Ctrl + C (de două ori).

În cadrul acestui program pot fi introduse orice fel de comenzi. În cazul în care se detectează faptul că o instrucțiune nu este completă, se afișează indicatorul de continuare (șirul de caractere …) în mod automat, până la momentul în care comanda este terminată. În acest fel, se oferă suport inclusiv pentru comenzile multilinie.

De asemenea, se poate folosi operatorul _, care referă cel mai recent rezultat care a fost afișat în cadrul consolei.

Cele mai frecvent utilizate comenzi sunt:

COMANDA DESCRIERE
node determină pornirea consolei REPL
Ctrl + C determină terminarea comenzii curente
.break determină terminarea unei comenzi multilinie
.clear
tastele ↑ / ↓ afișează istoricul comenzilor, oferind posibilitatea ca acestea să fie modificate
tasta TAB completează automat comanda curentă, cu variantele disponibile
.help afișează lista tuturor comenzilor
.save <filename> stochează sesiunea curentă într-un fișier de pe discul local
.load <filename> încarcă fișierul de pe discul local în sesiunea curentă
Ctrl + C (x2) determină oprirea consolei REPL
Ctrl + D

Modulul de Gestiune a Pachetelor NPM

NPM (Node.JS Package Manager) este un program pentru gestiunea pachetelor Node.JS cu care această platformă este integrat și care oferă următoarele funcționalități:

  1. permite dezvoltarea unor aplicații Node.JS prin intermediul unui utilitar în linie de comandă care are rolul de a gestiona modulele dependente și de a realiza controlul versiunilor;
  2. stochează modulele Node.JS în depozite care pot fi accesate la distanță.

În situația în care versiunea npm nu este cea mai recentă (rezultat în urma verificării npm –version), aceasta poate fi actualizată, prin intermediul aceluiași utilitar:

student@aipi2015:~$ npm install npm -g
npm@3.5.0 /usr/local/lib/node_modules/npm

Operațiile pe care le pune la dispoziție utilitarul în linie de comandă sunt:

În momentul în care un modul este instalat, se afișează denumirea sa, versiunea curentă, precum și locația în care va fi accesibil.

Pentru o aplicație Node.JS, în care modulele de care depinde sunt specificate în fișierul package.json (din rădăcină), ca valori ale atributului dependencies, se poate folosi comanda npm install (fără parametri) în locația în care se află proiectul respectiv, iar modulele respective sunt descărcate automat. Pentru fiecare modul în parte, se poate specifica o versiune, aceasta avea următoarele formate:

...
"dependencies": {
  "express": "^4.10.6",
  "mysql": "^2.5.4",
  "ejs": "^1.0.0",
  "body-parser": "1.8",
  "serve-favicon": "2",
  "express-session": "^1.7.6"
}
...

<spoiler|Fișierul package.json> Fișierul package.json se găsește în rădăcina oricărei aplicații Node.JS, fiind utilizat pentru a specifica proprietățile acesteia.

Acesta este un document JSON (NU un literal JavaScript) care are mai multe proprietăți:

Câmpurile obligatorii ale oricărei aplicații Node.JS sunt denumirea și versiunea. Fără aceste atribute specificate în fișierul package.json aplicația nu poate fi instalată, întrucât ele constituie un identificator unic al proiectului respectiv. </spoiler>

Concepte de Bază în Aplicațiile Node.JS

Funcții de Tip Callback

O funcție de tip callback este apelată în mod automat în momentul în care o operație este terminată. Toate metodele Node.JS oferă posibilitatea de a specifica o astfel de funcție ca parametru, atunci când sunt rulate (de regulă aceasta este ultimul parametru între argumente). Astfel, după invocarea metodei, fluxul aplicației poată continua, funcția de tip callback fiind apelată în mod automat doar în momentul în care datele / resursele pentru care ar fi fost necesară așteptarea (și, implicit, blocarea serverului) devin disponibile.

Acest mecanism face ca Node.JS să fie o platormă extrem de scalabilă, care poate gestiona mai multe accesări concomitent, folosind un singur fir de execuție.

Pentru o metodă, un apel sincron (blocant) ar putea fi:

var result = syncMethod(param);
console.log(result);
console.log('finished');

O astfel de abordare presupune faptul că aplicația așteaptă ca metoda syncMethod() să se termine înainte de a-și putea continua execuția. În situația în care sunt implicate procesări de intrare/ieșire intensive, comunicație prin rețea sau acces la anumite resurse partajate, o astfel de aplicație poate dura o perioadă de timp considerabilă, interval în care aplicația nu poate prelua alte solicitări, dar nici nu realizează alte procesări. Rezultatul execuției codului sursă va fi rezultatul furnizat de metoda sincronă, urmat de afișarea șirului de caractere de control. Această variantă presupune execuția secvențială a instrucțiunilor.

O variantă asincronă (non-blocantă) ar putea fi:

asyncMethod(param, function(error, result) {
  if (error) {
    console.log('An error has occurred: %s', error);
  }
  console.log(result);
});
console.log('finished');

O astfel de abordare presupune faptul că aplicația invocă metoda asyncMethod() după care își continuă execuția. Metoda primește ca parametru o funcție de tip callback (anonimă), care va fi apelată în mod automat în momentul în care rezultatul este disponibil. În acest fel, aplicația poate gestiona alte solicitări în timp ce se așteaptă ca rezultatul să fie furnizat. Rezultatul execuției codului sursă va fi afișarea șirului de caractere de control urmat de rezultatul furnizat de metoda sincronă. O astfel de variantă presupune execuția paralelă a instrucțiunilor.

În unele situații, poate fi necesar ca unele instrucțiuni să fie executate secvențial (din cauza unor dependențe de date, spre exemplu). În acest caz, codul sursă respectiv va fi imbricat în cadrul funcției de tip callback sau se generează un eveniment, atunci când informațiile necesare sunt disponibile, pe tratarea acestuia realizându-se procesările necesare.

Gestiunea Evenimentelor

Node.JS suportă concurența accesului (în ciuda faptului că utilizează un singur fir de execuție) prin implementarea unui mecanism de evenimente, utilizând șablonul de proiectare Observer.

Astfel, în momentul în care este lansată în execuție, o aplicație Node.JS invocă toate metodele asincrone din fluxul de execuție după care așteaptă ca acestea să se termine. Există o buclă principală în care se verifică dacă au fost generate evenimentele corespunzătoare terminării metodelor și în situația în care un astfel de obiect este detectat, se execută metoda de tip callback aferentă. Un astfel de comportament este implementat transparent, fără ca utilizatorul să cunoască denumirea evenimentelor sau să gestioneze operațiile legate de acesta.

Modulul events permite utilizarea de evenimente definite de utilizator. Un astfel de comportament este util mai ales în situația în care apelul mai multor metode trebuie sincronizat, pentru a se asigura faptul că acestea sunt executate secvențial. Pentru utilizarea funcționalității oferite de acest modul, este necesar ca acesta să fie importat, instanțiindu-se un obiect de tipul events.EventEmitter.

var events = require('events');
var eventEmitter = new events.EventEmitter();

Un eveniment este generat în momentul în care se apelează metoda emit(). Aceasta primește ca parametri denumirea evenimentului, precum și alte argumente care vor fi transmise funcției de tip callback, atașate ascultătorului specific evenimentului respectiv.

eventEmitter.emit('someEvent');

Metoda furnizează rezultatul true în situația în care au fost înregistrați în prealabil ascultători și false, în caz contrar. Această operație este realizată prin metodele on(), respectiv addListener() (echivalente), fiecare dintre acestea primind ca parametri denumirea evenimentului precum și funcția de tip callback.

var someEventHandler = function process()  {
  console.log('processing...');
  // do some processing
}
eventEmitter.on('someEvent', someEventHandler);
// or
eventEmitter.addListener('someEvent', someEventHandler);

În situația în care au fost apelate metodele on() sau addListener() pentru un tip de eveniment, funcțiile de tip callback definite de acestea vor fi apelate în mod automat în momentul în care se produce un astfel de eveniment, prin invocarea metodei emit() corespunzătoare.

Dacă nu mai este nevoie ca un eveniment să fie procesat, se poate înlătura ascultătorul respectiv prin intermediul metodei removeListener() care primește același tip de parametri. Similar, toți ascultătorii pot fi eliminați printr-un apel al metodei removeAllListeners().

În cazul în care este nevoie ca un eveniment să fie procesat o singură dată, se poate folosi metoda once(). Astfel, se apelează metoda addListener(), iar pe funcția de callback se apelează automat metoda removeListener().

Alte metode ale clasei events.EventEmitter sunt:

În clasa events.EventEmitter sunt definite și evenimente, generate în mod automat în momentul în care se adaugă, respectiv se șterge un ascultător:

Ambele evenimente au ca atribute denumirea evenimentului (event) și metoda de tip callback care este utilizată (listener). Generarea lor se face înainte de adăugare, respectiv după ștergere.

În situația în care trebuie procesate elementele unei liste (x1, x2, …, xn), iar procesarea fiecărui element trebuie realizată secvențial (x1 < x2 < … < xn), o abordare folosind un iterator clasic și o metodă asincronă ce operează pe elementul respectiv nu este viabilă (întrucât referința către acesta se poate schimba până la momentul în care este executată metoda asincronă).

for (var index = 1; index <= someList.length; index++) {
  someAsyncMethod(parameter, function(error, result) {
    // some processing involving the index value
  });
}

Un astfel de comportament poate fi simulat folosind evenimente:

  1. se implementează o funcție de tip callback, care va fi invocată pentru procesarea fiecărui element al listei, primind ca parametru indexul (sau elementul) respectiv:
    1. în momentul în care s-a terminat de parcurs lista, se generează un eveniment prin care se anunță acest lucru;
    2. se realizează procesarea, iar la terminarea acesteia, se invocă metoda pentru elementul următor (prin generarea evenimentului corespunzător);
  2. se adaugă un ascultător pentru evenimentul de procesare a unui element al listei;
  3. se adaugă un ascultător pentru evenimentul de terminare a procesării tuturor elementelor listei;
  4. se generează un eveniment de procesare a primului element al listei.
var someEventHandler = function someProcessing(index) {
  if (index > someList.length) {
    eventEmitter.emit('listProcessingEnd');
    return;
  }
  someAsyncMethod(parameter, function(error, result) {
    // some processing involving the index value
    eventEmitter.emit('listProcessingStart', index + 1);
  });
};
eventEmitter.addListener('listProcessingStart', someEventHandler);
eventEmiter.addListener('listProcessingEnd', function() {
  eventEmitter.removeAllListeners();
});
eventEmitter.emit('listProcessingStart', 1);

Fluxuri de Date

Un flux de date este un obiect care permite citirea, respectiv scrierea în / dintr-o sursă de date în mod continuu.

În Node.JS sunt definite următoarele tipuri de fluxuri de date:

  1. stream.Readable - abstractizare a unei surse de date din care se citește;
  2. stream.Writable - abstractizare a unei destinații de date în care se scrie;
  3. stream.Duplex - obiect care implementează ambele interfețe (Readable, respectiv Writable), putând fi folosite atât pentru citire cât și pentru scriere;
  4. stream.Transform - obiect care implementează interfața Duplex și în care ieșirea este determinată pe baza intrării.

Fiecare obiect de tip flux de date moștenește comportamentul clasei EventEmitter, astfel încât generează o serie de evenimente:

1. Clasa stream.Readable

EVENIMENT DESCRIERE
close generat în momentul în care fluxul de intrare și resursele asociate au fost închise, astfel încât nu se vor mai realiza alte procesări
data generat în momentul în care datele sunt disponibile, acestea fiind furnizate prin intermediul atributului chunk
end generat în momentul în care nu mai există informații care să fie citite (când sursa de date a fost complet epuizată)
error generat în momentul în care se produce o eroare
readable generat în momentul în care datele sunt disponibile pentru a fi citite, prin invocarea metodei read()

2. Clasa stream.Writable

EVENIMENT DESCRIERE
drain generat în momentul în care fluxul de ieșire este disponibil pentru a fi scris (dacă anterior, metoda write() a furnizat rezultatul false)
error generat în momentul în care se produce o eroare
finish generat în momentul în care nu mai există informații care să fie scrise (când destinația de date primit toate datele)
pipe generat în momentul în care se apelează metoda pipe() pe un flux de intrare, la care fluxul de ieșire este atașat; furnizează în atributul src fluxul de intrare respectiv
unpipe generat în momentul în care se apelează metoda unpipe() pe un flux de intrare, la care fluxul de ieșire este detașat; furnizează în atributul src fluxul de intrare respectiv

Utilizarea unui flux de intrare implică definirea unui comportament pentru evenimentele data și end (eventual error).

var fs = require('fs');
var data = '';
 
var readerStream = fs.createReadStream(someResource);
readerStream.setEncoding('UTF8');
 
readerStream.on('data', function(chunk) {
  data += chunk;
});
 
readerStream.on('end',function() {
  console.log(data);
});
 
readerStream.on('error', function(error) {
  console.log(error.stack);
});
 
console.log('finished');

Alternativ, se poate utiliza evenimentul readable împreună cu metoda read() care primește ca parametru opțional un număr indicând dimensiunea (exprimată în octeți) care se așteaptă a fi citită (în situația în care nu este disponibilă cantitatea respectivă se întoarce null, cu excepția situației în care nu mai sunt alte date disponibile); în situația în care nu se furnizează nici un argument, se întoarce întreaga cantitate disponibilă.

var fs = require('fs');
var data = '';
 
var readerStream = fs.createReadStream(someResource);
readerStream.setEncoding('UTF8');
 
readerStream.on('readable', function() {
  var chunk;
  while (null !== (chunk = readerStream.read())) {
    data += chunk;
  }
});
 
readerStream.on('end', function() {
  console.log(data);
});
 
readerStream.on('error', function(error) {
  console.log(error.stack);
});
 
console.log('finished');

Utilizarea unui flux de ieșirea implică apelul metodelor write() respectiv end() precum și definirea unui comportament pentru evenimentul end (eventual error). Ambele metode primesc ca parametri datele care se doresc a fi scrise, tipul de codificare folosit precum și o funcție de tip callback care este apelată în momentul în care datele au fost transmise complet. Apelul metodei write() după ce anterior s-a invocat metoda end() nu este permis și va genera o eroare.

var fs = require('fs');
var data = 'Some Data';
 
var writerStream = fs.createWriteStream(someResource);
writerStream.write(data, 'UTF8');
writerStream.end();
 
writerStream.on('finish', function() {
  console.log('operation completed');
});
 
writerStream.on('error', function(error) {
  console.log(error.stack);
});
 
console.log('finished');

Un flux de intrare poate fi atașat unui flux de ieșire prin intermediul metodei pipe(). În acest mod, conținutul din sursa de date este transferată în mod automat în destinația de date. Metoda se apelează pe fluxul de intrare și primește ca parametru fluxul de ieșire, specificându-se eventual și un set de opțiuni (atributul end, având implicit valoarea true, indică faptul că fluxul de ieșire nu mai poate fi utilizat după ce datele din fluxul de intrare au fost transferate). O astfel de metodă este utilă pentru că adaptează transferul de date astfel încât să fie adecvate vitezelor de transfer ale fluxurilor de date de tip intrare, respectiv ieșire. Similar, se poate folosi metoda unpipe() care detașează fluxul de ieșire de la fluxul de intrare, în situația în care s-a produs o eroare sau a expirat un timp limită pentru transferul de date. Metoda se apelează pe fluxul de intrare și poate primi ca parametru opțional fluxul de ieșire (în caz contrar, fiind detașate toate fluxurile de ieșire).

var fs = require('fs');
 
var readerStream = fs.createReadStream(someResource);
var writerStream = fs.createWriteStream(someOtherResource);
 
readerStream.pipe(writerStream);
setTimeout(function() {
  readerStream.unpipe(writerStream);
  writerStream.end();
}, 1000);
 
console.log('finished');

Se pot crea lanțuri formate din mai multe fluxuri de intrare, respectiv fluxuri de ieșire, mai ales în situația în care este necesar să se realizeze procesări multiple pe setul de date respectiv. Un caz tipic este reprezentat de operația de compresie / decompresie a datelor.

var fs = require('fs');
var zlib = require('zlib');
 
var readerStream = fs.createReadStream(someResource);
var writerStream = fs.createWriteStream(someCompressedResource);
 
readerStream
  .pipe(zlib.createGzip())
  .pipe(writerStream);
 
console.log('file compressed');
var fs = require('fs');
var zlib = require('zlib');
 
var readerStream = fs.createReadStream(someCompressedResource);
var writerStream = fs.createWriteStream(someResource);
 
readerStream
  .pipe(zlib.createGunzip())
  .pipe(writerStream);
 
console.log('file uncompressed');

În cadrul operațiilor de intrare / ieșire, poate fi necesar să se lucreze cu fluxuri de octeți neinterpretați (date binare), lucru care poate fi realizat prin intermediul clasei Buffer. Aceasta este disponibilă global, fără a fi necesar să se încarce explicit un anumit modul. Un obiect de tip Buffer este alocat în afara spațiului de memorie al motorului JavaScript V8 și poate fi utilizat asemănător unui tablou de numere întregi.

Instanțierea unui astfel de obiect poate fi realizată în mai multe moduri:

Cele mai frecvent folosite metode pentru obiectele de tip Buffer sunt:

Sistemul de Fișiere

În Node.JS, operațiile de intrare / ieșire care implică sistemul de fișiere sunt implementate prin apeluri către funcții POSIX. Acestea sunt furnizate de modulul fs (File System) care trebuie încărcat explicit.

Fiecare metodă din modulul fs dispune de o variantă sincronă și de o variantă asincronă, care primește în plus ca parametru o funcție de tip callback care este apelată în momentul în care operația respectivă a fost terminată. Este de preferat să se utilizeze metode asincrone, atunci când specificațiile funcționale ale aplicației nu impun să se folosească metodele sincrone.

Modulul fs pune la dispoziție atât metode pentru fișiere cât și metode pentru directoare.

Metode pentru Fișiere

Deschiderea unui Fișier

Un fișier poate fi deschis asincron folosind metoda fs.open(path, flags[, mode], callback).

Parametrii pe care îi primește metoda sunt:

OPȚIUNE DESCRIERE
r deschide fișierul pentru citire, generându-se o eroare în situația în care acesta nu există
r+ deschide fișierul pentru citire și scriere, generându-se o eroare în situația în care acesta nu există
rs deschide fișierul pentru citire în mod sincron, indicând sistemului de operare să nu folosească o zonă de memorie tampon a sistemului local de fișiere
rs+ deschide fișierul pentru citire și scriere în mod sincron
w deschide fișierul pentru scriere, acesta fiind creat (dacă nu există) sau suprascris (dacă există)
wx deschide fișierul pentru scriere, generându-se o eroare în situația în care acesta există
w+ deschide fișierul pentru citire și scriere, acesta fiind creat (dacă nu există) sau suprascris (dacă există)
wx+ deschide fișierul pentru citire și scriere, acesta fiind creat (dacă nu există) sau generându-se o eroare în situația în care există
a deschide fișierul pentru scriere, acesta fiind creat (dacă nu există) sau completat (dacă există)
ax deschide fișierul pentru scriere, acesta fiind creat (dacă nu există) sau generându-se o eroare în situația în care există
a+ deschide fișierul pentru citire și scriere, acesta fiind creat (dacă nu există) sau completat (dacă există)
ax+ deschide fișierul pentru citire și scriere, acesta fiind creat (dacă nu există) sau generându-se o eroare în situația în care există
var fs = require('fs');
 
fs.open(someFile, 'r+', function(error, file_descriptor) {
  if (error) {
    return console.error(error);
  }
  console.log('file opened!');     
});

Există și varianta sincronă a metodei fs.openSync(path, flags[, mode]).

Închiderea unui Fișier

Un fișier poate fi închis asincron folosind metoda fs.close(fd, callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
 
// ...
 
fs.close(file_descriptor, function(error) {
  if (error) {
    return console.error(error);
  }
  console.log('file closed!');     
});

Există și varianta sincronă a metodei fs.closeSync(fd).

Obținerea de Informații cu privire la un Fișier

Obținerea de informații cu privire la un fișier poate fi realizată asincron folosind metoda fs.stat(path, callback).

Parametrii pe care îi primește metoda sunt:

OPȚIUNE DESCRIERE
isFile() întoarce true dacă resursa este un fișier obișnuit
isDirectory() întoarce true dacă resursa este un director
isBlockDevice() întoarce true dacă resursa este un dispozitiv de tip bloc
isCharacterDevice() întoarce true dacă resursa este un dispozitiv de tip caracter
isSymbolicLink() întoarce true dacă resursa este o legătură simbolică
isFIFO() întoarce true dacă tipul resursei este FIFO
isSocket() întoarce true dacă tipul resursei este un socket
var fs = require('fs');
 
fs.stat(someFile, function(error, stats) {
  if (error) {
    return console.error(error);
  }
  console.log('isFile: ' + stats.isFile());
  console.log('isDirectory: ' + stats.isDirectory());     
});

Există și varianta sincronă a metodei fs.statSync(path).

Citirea dintr-un Fișier

Un fișier poate fi citit în mod asincron folosind metoda fs.read(fd, buffer, offset, length, position, callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
var buffer = new Buffer(1024);
 
fs.open(someFile, 'r+', function(error, file_descriptor) {
  if (error) {
    return console.error(error);
  }
  fs.read(file_descriptor, buffer, 0, buffer.length, 0, function(error, bytes) {
    if (error) {
      console.log(error);
    }
    if (bytes > 0) {
      console.log(buffer.slice(0, bytes).toString());
    }
    console.log('file has been read!');
  });
});

Există și varianta sincronă a metodei fs.readSync(fd, buffer, offset, length, position).

Scrierea într-un Fișier

Un fișier poate fi scris în mod asincron folosind metoda fs.writeFile(file, data[, options], callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
 
fs.writeFile(someFile, someContent, function(error) {
  if (error) {
    return console.error(error);
  }
  console.log('file has been written!');
});

Există și varianta sincronă a metodei fs.writeFileSync(file, data[, options]).

Modificarea unui Fișier

Modificarea unui fișier implică reținerea primilor n octeți din cadrul său (funcționalitate de tip truncate), putând fi realizată în mod asincron folosind metoda fs.ftruncate(fd, len, callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
var buffer = new Buffer(1024);
 
fs.open(someFile, 'r+', function(error, file_descriptor) {
  if (error) {
    return console.error(error);
  }
 
  fs.ftruncate(file_descriptor, 512, function(error) {
    if (error) {
      console.log(error);
    } 
    fs.read(file_descriptor, buffer, 0, buffer.length, 0, function(error, bytes) {
      if (error) {
        console.log(error);
      }
 
      if(bytes > 0){
        console.log(buffer.slice(0, bytes).toString());
      }
 
      fs.close(file_descriptor, function(error) {
        if (error) {
          console.log(error);
        }
      });
    });
  });
});

Există și varianta sincronă a metodei fs.ftruncateSync(fd, len).

Ștergerea unui Fișier

Ștergerea unui fișier poate fi realizată în mod asincron folosind metoda fs.unlink(path, callback).

Parametrii pe care îî primește metoda sunt:

var fs = require('fs');
 
fs.unlink(someFile, function(error) {
  if (error) {
    return console.error(error);
  }
  console.log('file has been deleted!');
});

Există și varianta sincronă a metodei fs.unlinkSync(path).

Metode pentru Directoare

Crearea unui Director

Crearea unui director poate fi realizată în mod asincron folosind metoda fs.mkdir(path[, mode], callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
 
fs.mkdir(someDirectory, function(error) {
  if (error) {
    return console.error(error);
  }
  console.log('directory created!');
});

Există și varianta sincronă a metodei fs.mkdirSync(path[, mode]).

Ștergerea unui Director

Ștergerea unui director poate fi realizată în mod asincron folosind metoda fs.rmdir(path, callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
 
fs.rmdir(someDirectory, function(error) {
  if (error) {
    return console.error(error);
  }
  console.log('directory removed!');
});

Există și varianta sincronă a metodei fs.rmdirSync(path).

Obținerea de Informații cu privire la un Director

Obținerea de informații cu privire la un director (lista de fișiere pe care le conține) poate fi realizată în mod asincron folosind metoda fs.readdir(path, callback).

Parametrii pe care îi primește metoda sunt:

var fs = require('fs');
 
fs.readdir(someDirectory, function(error, files) {
  if (error) {
    return console.error(error);
  }
  files.forEach(function(file) {
    console.log(file);
  });
});

Există și varianta sincronă a metodei fs.readdirSync(path).

Obiecte Globale

În Node.JS, obiectele globale sunt disponibile în toate modulele, fără a fi necesar să se încare explicit funcționalități expuse de alte componente.

Accesul la aceste obiecte este realizat nemijlocit, prin referirea acestora.

OBIECT IMPLICIT DESCRIERE
__filename locația (cale absolută) la care se găsește fișierul care este executat în mod curent
__dirname locația (cale absolută) la care se găsește directorul din care face parte fișierul care este executat în mod curent
setTimeout(cb,ms) utilizată pentru a invoca o funcție de tip callback după o anumită durată; momentul la care va fi executată metoda propriu-zis depinde de modul în care este configurat sistemul de operare precum și de încărcarea mașinii pe care acesta este rulat
var timeout = setTimeout(function() {
  console.log('this is a message'_');
}, 1000);
clearTimeout(t) utilizată pentru a opri invocarea unei funcții de tip callback după o anumită durată;
clearTimeout(timeout);
setInterval(cb,ms) utilizată pentru a invoca o funcție de tip callback periodic, la un anumit interval; momentul la care va fi executată metoda propriu-zis depinde de modul în care este configurat sistemul de operare precum și de încărcarea mașinii pe care acesta este rulat
var interval = setInterval(function() {
  console.log('this is a message'_');
}, 1000);
clearInterval(t) utilizată pentru a opri invocarea unei funcții de tip callback periodic, la un anumit interval;
clearInterval(interval);

Alte obiecte globale sunt:

Module Utilitare

Node.JS pune la dispoziția utilizatorilor numeroase module utilitare ale căror funcționalități pot fi accesate pentru dezvoltarea de aplicații web:

Platforma Express

Express reprezintă o platformă minimală și flexibilă care oferă un set de funcționalități pentru dezvoltarea facilă și rapidă de aplicații web:

Platforma Express poate fi instalată global pentru a putea fi folosită împreună cu Node.JS în vederea dezvoltării de aplicații web.

student@aipi2015:~$ npm install express --save
Prin folosirea opțiunii –save, modulul express este stocat în fișierul package.json în lista de dependențe.

Alte module care trebuie instalate pentru a putea fi utilizate împreună cu Express sunt:

O aplicație web folosind platforma Express va realiza următoare operații:

  1. va încărca modulul express și va construi o instanță a acestuia;
  2. va preciza care sunt contextele care pot fi accesate precum și metodele prin care acestea pot fi vizualizate în navigator (metodele get, post, put, options, trace, delete primesc ca parametru calea precum și o funcție de tip callback care are drept argumente cererea și răspunsul);
  3. va lansa în execuție aplicația web printr-un apel al metodei listen() care primește ca parametri portul pe care aplicația este accesibilă precum și o funcție de tip callback invocată în momentul în care aplicația web rulează).
var express = require('express');
var app = express();
 
app.get('/', function (request, response) {
  res.send('Hello, Aipi2015!');
});
app.post('/', function (request, response) {
  res.send('Hello, Aipi2015!');
});
 
var server = app.listen(8080, function () {
  var host = server.address().address;
  var port = server.address().port;
  console.log('Application is available at http://%s:%s', host, port);
});

O astfel de aplicație nu va fi accesibilă decât în contextul rădăcină (http://localhost:8080/), prin metode de tip GET și POST, pentru toate celelalte contexte / metode furnizându-se codul 404.

Accesul la Resurse Statice

Accesul la resurse statice (imagini, foi de stil, cod sursă JavaScript) poate fi realizat prin intermediul middleware-ului express.static care este integrat cu platforma Express.

Astfel, metoda use() va primi ca parametru rezultatul funcției express.static() căreia i se va transmite ca argument locația la care se găsesc resursele care vor putea fi referite direct din navigator (întrucât referirea se face relativ la denumirea directorului static, acesta nu va fi inclus în URL atunci când se încearcă regăsirea informațiilor respective).

var express = require('express');
var app = express();
var path = require('path');
app.use(express.static(path.join(__dirname, 'public')));
În situația în care se folosesc mai multe directoare ce conțin resurse statice, acestea vor fi menționate secvențial, ceea ce va determina și ordinea de căutare în momentul în care va fi rezolvată o anumită solicitare.

Definirea Rutelor

O definiție a unei rute are structura app.METHOD(PATH, HANDLER) unde:

Metoda app.all() este folosită pentru a ignora tipul de cerere HTTP folosit, deservind resursa respectivă indiferent de aceasta.
Pentru fiecare metodă pot fi definite mai multe funcții de tip callback asociate (separate prin caracterul ,), însă este obligatoriu ca acestea să utilizeze și parametru next care trebuie invocat pentru a se realiza transferul din contextul unei funcții în contextul altei funcții.

Metoda app.route() permite definirea de mai multe rute (înlănțuite) în situația în care sunt folosite mai multe tipuri de cereri HTTP pentru aceeași resursă. O astfel de abordare permite modularizarea aplicației, reducând redundanța precum și erorile datorate unor greșeli de dactilografiere.

app.route('/')
  .get(function (request, response) {
    res.send('Hello, Aipi2015!');
  })
  .post(function (request, response) {
    res.send('Hello, Aipi2015!');
  });

De asemenea, un obiect de tipul express.Router permite definirea de route, care pot fi apoi exportate (devenind accesibile la nivelul întregii aplicații web, activarea acestora fiind realizată prin intermediul unui apel require() urmată de directiva use().

Gestiunea Obiectelor Cerere / Răspuns

Acțiunea asociată unei rute, pentru un anumit tip de cerere HTTP, este reprezentată printr-o metodă de tip callback, care primește ca argumente obiectele de tip cerere și răspuns.

Opțional, în situația în care se utilizează mai multe acțiuni (înlănțuite), metodele respective vor primi și parametrul next() prin intermediul căruia se realizează transferul către următoarea acțiune.

Obiectul de tip request are următoarea structură:

Obiectul de tip response are următoarea structură:

Utilizarea Motoarelor pentru Șabloane

Pentru ca platforma Express să poată reda fișiere de tip șablon, este necesar să se specifice anumite configurații:

var express = require('express');
var app = express();
var path = require('path');
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

Unul dintre cele mai utilizate motoare pentru redarea șabloanelor parametrizate este Embedded JavaScript Templates, având o sintaxă similară cu JavaServer Pages.

Un document EJS conține etichete HTML, codul sursă JavaScript care referă atributele transmise de la nivelul de logică a aplicației fiind încadrat între delimitatorii <% și %>.

Delimitatorii folosiți în cadrul documentelor EJS pentru a încadra codul sursă JavaScript sunt:

DELIMITATOR DESCRIERE
<% utilizat pentru controlul fluxului de execuție al programului ca delimitator de sfârșit; nu produce nici un fel de rezultat la nivelul paginii HTML care este generată pe baza sa
<%= utilizat pentru evaluarea rezultatului expresiei conținute și plasarea acestuia în șablonul obținut, înlocuind caracterele speciale HTML prin codul lor
<%- utilizat pentru evaluarea rezultatului expresiei conținute și plasarea acestuia în șablonul obținut, fără a înlocui caracterele speciale HTML prin codul lor
<%# utilizat pentru comentarii, codul sursă JavaScript nu este executat și nici nu produce vreun rezultat
<%% utilizat pentru a reda secvența de caractere <% la nivelul paginii HTML care este generată pe baza sa
%> utilizat pentru controlul fluxului de execuție al programului ca delimitator de sfârșit
-%> utilizat pentru eliminarea caracterelor '\n', în cazul în care acestea sunt conținute în codul sursă JavaScript

Există posibilitatea ca acești delimitatori să fie suprascriși, fiind definiți de utilizator. În acest scop, se stabilește valoarea proprietății ejs.delimiter la valoarea care se dorește a fi utilizată sau se transmite această valoare ca parametru metodei ejs.render():

var ejs = require('ejs');
ejs.delimiter = '?';
ejs.render('<? for (var index = 0; index < shoppingCart.length; index++) { ?>', { shoppingCart: request.session.shoppingCart });
var ejs = require('ejs');
ejs.render('<? for (var index = 0; index < shoppingCart.length; index++) { ?>', { shoppingCart: request.session.shoppingCart }, { delimiter: '?' });

În cazul în care pentru o aplicație a fost precizat un director views care conține șabloanele parametrizate folosind Embedded JavaScript Templates precum și proprietatea corespunzătoare asociată proprietății view engine, încărcarea automată a paginii respective se poate face prin metoda render() a obiectului http.ServerResponse, prin care se transmit și valorile parametrilor care vor fi referiți.

response.render('login', {
  error: ''
});

Alternativ, se pot folosi metodele compile() și template(), respectiv render() ale obiectului ejs care primesc ca parametri șirul de caractere care se dorește a fi interpretat, datele (parametri) care vor fi incluse precum și un set de opțiuni.

var ejs = require('ejs');
var template = ejs.compile(code, options);
template(data);
var ejs = require('ejs');
ejs.render(code, data, options);

O astfel de abordare este însă mai puțin întâlnită întrucât, de obicei, dimensiunea codului sursă JavaScript este suficient de voluminoasă spre a pleda pentru includerea sa într-un fișier distinct. Atât pentru date, cât și pentru opțiuni, se va utiliza un format de reprezentare JSON (JavaScript Object Notation).

Parametrii data și options pot fi incluși într-un singur argument al metodei render, situație în care aceștia vor putea fi referiți sub forma unor variabile locale.

Opțiunile care pot fi utilizate sunt:

În situația în care se dorește să se includă conținutul unei alte resurse, se folosește apelul metodei include() care primește ca parametru denumirea fișierului care urmează să fie redat în momentul în care șablonul parametrizat respectiv este lansat în execuție (acesta trebuie să fie specificat sub forma unei căi absolute sau a unei căi relative la locația fișierului curent). De regulă, aceasta trebuie să fie plasată între elemente de tipul <%- și %> astfel încât să se evite translatarea etichetelor HTML în codul corespunzător.

<%- include('header') -%>
<!-- the content goes here -->
<%- include('footer') -%>
Este permis ca în cadrul argumentului transmis metodei include() să fie utilizate și variabilele globale (transmise de la nivelul de logică a aplicației). Pentru ca o variabilă locală să poată fi utilizată, aceasta trebuie să fie declarată și inițializată înainte de aoelul în cauză.

În cazul în care se dorește ca funcțiile JavaScript intermediare să fie plasate într-o zonă de memorie tampon de unde să poată fi reutilizate, se poate folosi biblioteca lru-cache, indicându-se în proprietatea ejs.cache numărul de elemente care se doresc a fi stocate.

var ejs = require('ejs');
var lru = require('lru-cache');
ejs.cache = lru(1024);

Dacă se dorește să se elimine conținutul zonei de memorie tampon, se poate utiliza metoda clearCache() a obiectului ejs. De asemenea, în situația în care este necesar să se utilizeze o altă limită pentru zona de memorie tampon, se va atribui o nouă valoare atributului ejs.cache.

Pachetul MySQL

Cele mai multe aplicații web își generează conținutul dinamic pe baza informațiilor furnizate prin intermediul unei baze de date. Node.JS este o platformă compatibilă cu numeroase sisteme de gestiune pentru baze de date, existând biblioteci dedicate în acest sens.

Pachetul mysql este un driver Node.JS pentru MySQL, scris în JavaScript, care nu necesită compilare.

Pentru folosirea funcționalităților puse la dispoziție de acest pachet, este necesar să se creeze o conexiune la sistemul de gestiune pentru baze de date MySQL.

var mysql = require('mysql');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'StudentAipi2015',
  database: 'bookstore'
});

Parametrii care pot fi indicați pentru o conexiune sunt:

Metoda createConnection() poate primi ca parametru și un șir de caractere reprezentând URL-ul de conectare la baza de date:
var connection = mysql.createConnection('mysql://root:StudentAipi2015@localhost/bookstore');

Deschiderea unei conexiuni se face de regulă prin intermediul metodei connect() deși acest lucru nu este neapărat necesar, de vreme ce stabilirea legăturii este realizată în mod automat în momentul în care este realizată o interogare:

connection.connect(function(error) {
  if (error) {
    console.error('An error has occurred: ' + error.stack);
    return;
  }
  console.log('The connection connect() method was successful!');
});

Închiderea unei conexiuni poate fi realizată în două moduri:

În situația în care se dorește folosirea unui grup de conexiuni, se poate folosi metoda createPool() a obiectului mysql astfel încât numărul de conexiuni este specificat prin intermediul proprietății connectionLimit. Obținerea unei conexiuni se face ulterior prin apelul metodei getConnection(), iar în momentul în care aceasta nu mai este necesară se poate apela metoda release(). Un grup de conexiuni este terminat prin apelul metodei end().

Realizarea unei interogări este realizată de regulă prin intermediul metodei query() apelată pe obiectul de tip Connection, care suportă următoarele forme:

connection.query('SELECT * FROM book', function(error, result, fields) {
  if (error) {
    console.log('An error has occurred: %s', error);
    return;
  }
  for (var index = 0; index < result.length; index++) {
    var book = {
      "title": result[index].title,
      "subtitle": result[index].subtitle,
      "description": result[index].description,
      "edition": result[index].edition,
      "printing_year": result[index].printing_year,
      "collection_id": result[index].collection_id
    };
    books.push(book);
  }
});

În situația în care sunt incluși parametri direct în interogare, se recomandă să se utilizeze metoda escape() a obiectului connection, ce transformă caracterele care ar putea să aibă un impact asupra interogării în codul corespunzător, care poate fi procesat de interpretorul SQL:

connection.statement('SELECT * FROM book b WHERE b.id = ' + connection.escape(bookId), function(error, result, fields) {
  // ...
});
connection.query('SELECT * FROM book b WHERE b.id = ?', [bookId], function(error, result, fields) {
  // ...
});
connection.query({
  sql: 'SELECT * FROM books WHERE b.id = ?',
  values: [bookId],
  nestTables: true,
  typeCast: false, // !!! NOT recommended
  timeout: 10000
}, function (error, result, fields) {
  // ... 
});
connection.query({
  sql: 'SELECT * FROM books WHERE b.id = ?',
  nestTables: true,
  typeCast: false, // !!! NOT recommended
  timeout: 10000
}, [bookId], function (error, result, fields) {
  // ... 
});

În cazul interogărilor parametrizate, în situația în care pe post de parametrii se folosesc anumite câmpuri pentru care nu se aplică regulile de transformare obișnuite, se folosește șirul de caractere ??. De asemenea, șirul de caractere poate fi pregătit local, înainte de a fi transmis către sistemul de gestiune pentru baze de date MySQL, prin intermediul metodei format():

var unformatted_statement = 'SELECT * FROM ?? WHERE ?? = ?';
var parameters = ['book', 'id', bookId];
var formatted_statement = mysql.format(unformatted_statement, parameters);
// ...

Pentru operațiile de adăugare, modificare, ștergere, se pot determina informații legate de identificatorul valorii care este adăugată, respectiv de numărul de înregistrări care au fost modificate / șterse, ca proprietăți ale obiectului result transmis ca parametru al funcției de tip callback în momentul în care este invocată metoda query():

Activitate de Laborator

Structura aplicației web BookStore

Se doreşte implementarea unei interfeţe grafice cu utilizatorul (dezvoltată sub forma unei aplicaţii web, folosind platforma Node.JS) pentru gestiunea informaţiilor reţinute într-o bază de date, aceasta urmând a fi utilizată de un sistem ERP destinat unei librării care comercializează doar cărţi.

Sistemul informatic va putea fi accesat de:

Structura proiectului BookStore include următoarele componente:

Pentru fiecare funcționalitate există o pagină dedicată, gestionată de câte o rută distinctă, prin intermediul căreia este implementată logica aplicației (în directorul routes) și un document prin intermediul căruia sunt vizualizate informațiile (în directorul views).

Astfel, există o pagină de autentificare (login) de unde, în funcţie de rolul utilizatorului se trece la pagina de tip administrator (administrator) respectiv pagina de tip client (client). Pentru fiecare pagină, cererea primită din navigator este preluată de către modulul corespunzător, unde sunt realizate procesările ce țin de logica aplicației, inclusiv accesarea informațiilor din baza de date. Prezentarea informațiilor astfel obținute este delegată către documentul aferent, către care se transferă contextul. Datele care se doresc a fi vizualizate sunt transmise dinspre modul către document sub formă de atribute. În acest mod se construiește răspunsul care va fi trimis către navigator.

Atributele care sunt transmise între module și documente sunt:

  1. login.js → login.ejs
    • error - furnizează informații cu privire la rezultatul operației de autentificare a unui utilizator în cadrul aplicației web (succes, eșec);
  2. administrator.js → administrator.ejs
    • display - identificatorul utilizatorului autentificat în cadrul aplicației web (format din prenume și nume);
    • currentTable - denumirea tabelei vizualizată la un moment dat de timp;
    • databaseStructure - structura bazei de date, formată din lista tuturor tabelelor;
    • attributes - structura tabelei vizualizată la un moment dat de timp, conținând lista tuturor atributelor;
    • identifier - denumirea identificatorului în lista de atribute;
    • identifierNextValue - valoarea identificatorului unei înregistrări pentru adăugare;
    • tableContent - conținutul tabelei vizualizate la un moment dat de timp, conținând lista tuturor înregistrărilor, pentru fiecare înregistrare fiind reținute valorile tuturor atributelor;
    • identifierRecordToBeUpdated - valoarea identificatorului unei înregistrări selectate pentru modificare sau ștergere, în situația în care aceasta a fost selectată;
  3. client.js → client.ejs
    • display - identificatorul utilizatorului autentificat în cadrul aplicației web (format din prenume și nume);
    • recordsPerPageValue - numărul maxim de înregistrări care poate fi vizualizat pe o pagină la un moment dat;
    • previousRecordsPerPageValue - valoarea anterioară pentru numărul maxim de înregistrări care poate fi vizualizat pe o pagină la un moment dat; această informație este necesară pentru a se determina dacă este necesar ca valoarea paginii curente să fie reinițializată;
    • recordsPerPageValues - lista cu valorile posibile pentru numărul maxim de înregistrări care poate fi vizualizat pe o pagină la un moment dat;
    • pageValue - pagina vizualizată la un moment dat;
    • formatsList - lista cu toate formatele de prezentare disponibile;
    • formatsFilter - lista cu criteriile de sortare în funcție de formatul de prezentare;
    • languagesList - lista cu toate limbile disponibile;
    • languagesFilter - lista cu criteriile de sortare în funcție de limbă;
    • categoriesList - lista cu toate categoriile disponibile;
    • categoriesFilter - lista cu criteriile de sortare în funcție de categorie;
    • books - lista cărților care vor fi vizualizate, împreună cu toate atributele, în funcție de criteriile de filtrare care au fost precizate;
    • errorMessage - mesaj de eroare legat de procesarea coșului de cumpărături;
    • shoppingCart - conținutul coșului de cumpărături (identificatorul formatului de prezentare și cantitatea solicitată);

Pagina de tip autentificare din cadrul aplicaţiei BookStore

Pagina de tip administrator din cadrul aplicaţiei BookStore

Pagina de tip client din cadrul aplicaţiei BookStore

0. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la https://www.github.com/aipi2015/Laborator06. În urma acestei operații, directorul Laborator06 va trebui să conțină subdirectorul labtasks, fișierele README.md și LICENSE.

student@aipi2015:~$ git clone https://www.github.com/aipi2015/Laborator06.git

1. Să se ruleze, folosind MySQL Workbench (sau alt utilitar similar), scriptul Laborator06l.sql, localizat în directorul scripts. Acesta instalează baza de date bookstore.

În cazul în care baza de date bookstore este deja instalată, nu mai este necesar să se ruleze acest script.
Această operație poate dura o perioadă de timp îndelungată, în funcție de configurațiile mașinilor pe care se lucrează.

2. a) Să se instaleze platforma Node.JS, în situația în care această operație nu a fost realizată anterior. Se recomadă să se utilizeze versiunea 4.2.2. Se pot folosi instrucțiunile din acest tutorial.

b) Să se instaleze modulele necesare pentru rularea aplicației web, folosind comanda npm install. Modulele care vor fi instalate sunt serve-favicon, mysql, express-session, body-parser, express.

student@aipi2015:~/06-BookStore-NodeJS$ npm install
npm WARN package.json 06-BookStore-NodeJS@1.0.0 No description
npm WARN package.json 06-BookStore-NodeJS@1.0.0 No repository field.
npm WARN package.json 06-BookStore-NodeJS@1.0.0 No README data
npm WARN package.json 06-BookStore-NodeJS@1.0.0 No license field.
ejs@1.0.0 node_modules/ejs

serve-favicon@2.3.0 node_modules/serve-favicon
├── fresh@0.3.0
├── etag@1.7.0
├── ms@0.7.1
└── parseurl@1.3.0

mysql@2.9.0 node_modules/mysql
├── bignumber.js@2.0.7
└── readable-stream@1.1.13 (string_decoder@0.10.31, isarray@0.0.1, inherits@2.0.1, core-util-is@1.0.2)

express-session@1.12.1 node_modules/express-session
├── cookie-signature@1.0.6
├── utils-merge@1.0.0
├── on-headers@1.0.1
├── cookie@0.2.3
├── parseurl@1.3.0
├── depd@1.1.0
├── crc@3.3.0
├── debug@2.2.0 (ms@0.7.1)
└── uid-safe@2.0.0 (base64-url@1.2.1)

body-parser@1.8.4 node_modules/body-parser
├── media-typer@0.3.0
├── raw-body@1.3.0
├── bytes@1.0.0
├── depd@0.4.5
├── qs@2.2.4
├── iconv-lite@0.4.4
├── on-finished@2.1.0 (ee-first@1.0.5)
└── type-is@1.5.7 (mime-types@2.0.14)

express@4.13.3 node_modules/express
├── escape-html@1.0.2
├── merge-descriptors@1.0.0
├── cookie@0.1.3
├── array-flatten@1.1.1
├── cookie-signature@1.0.6
├── utils-merge@1.0.0
├── methods@1.1.1
├── content-type@1.0.1
├── vary@1.0.1
├── fresh@0.3.0
├── etag@1.7.0
├── range-parser@1.0.3
├── serve-static@1.10.0
├── path-to-regexp@0.1.7
├── content-disposition@0.5.0
├── parseurl@1.3.0
├── depd@1.0.1
├── qs@4.0.0
├── debug@2.2.0 (ms@0.7.1)
├── on-finished@2.3.0 (ee-first@1.1.1)
├── finalhandler@0.4.0 (unpipe@1.0.0)
├── proxy-addr@1.0.8 (forwarded@0.1.0, ipaddr.js@1.0.1)
├── send@0.13.0 (destroy@1.0.3, ms@0.7.1, statuses@1.2.1, mime@1.3.4, http-errors@1.3.1)
├── accepts@1.2.13 (negotiator@0.5.3, mime-types@2.1.7)
└── type-is@1.6.9 (media-typer@0.3.0, mime-types@2.1.7)

3. Modificați în fișierele login.js, administrator.js, client.js din directorul routes informațiile necesare obținerii drepturilor de acces la baza de date (nume de utilizator, parola).

4. Să se configureze mediul integrat de dezvoltare NetBeans 8.1 astfel încât acesta să fie interfațat cu platforma Node.JS.

Numai mediul integrat de dezvoltare NetBeans (începând cu versiunea 8.1) pune la dispoziție configurațiile necesare pentru dezvoltarea proiectelor Node.JS. Alternativ, se poate folosi orice editor de text. Rularea unei aplicații web dezvoltată folosind platforma Node.JS se face folosind utilitarul node care primește ca parametru denumirea fișierului care urmează a fi lansat în execuție (tipic, main.js).

Se accesează fereastra de configurare, din Tools → Options, accesându-se panoul HTML/JS → Node.JS. Utilizarea propriu-zisă a unei astfel de funcționalități poate fi accesată doar ulterior activării sale (se apasă butonul Activate).

În situația în care platforma Node.JS este deja instalată, locațiile la care este instalată aceasta precum și modulul de gestiune a pachetelor sunt completate în mod automat (Node Path, npm Path).

Pentru ca NetBeans 8.1 să poată gestiona proiecte Node.JS, este necesar să fie descărcat codul sursă (se apasă butonul Download…).

De asemenea, este necesar să se instaleze și platforma Express:

student@aipi2015:/usr/local/nodejs$ npm install express-generator -g
/usr/local/nodejs/bin/express -> /usr/local/nodejs/lib/node_modules/express-generator/bin/express
express-generator@4.13.1 /usr/local/nodejs/lib/node_modules/express-generator
├── sorted-object@1.0.0
├── commander@2.7.1 (graceful-readlink@1.0.1)
└── mkdirp@0.5.1 (minimist@0.0.8)

Calea către platforma Express trebuie să fie referită în cadrul câmpului Express Path.

5. Să se testeze aplicaţia prin accesarea adresei http://localhost:8080/.

Se poate consulta script-ul Laborator06l.sql există exemple de utilizatori care pot fi folosite pentru accesarea paginilor de administrator, respectiv de client.

6. Să se acceseze pagina de tip administrator şi să se testeze funcţionalităţile implementate în cadrul acesteia (adăugare, editare, ştergere).

7. Să se acceseze pagina de tip client. Să se implementeze funcționalitatea corespunzătoare filtrării în funcție de limbile în care sunt disponibile cărțile din librărie. Un utilizator poate adăuga o valoare din lista de limbi disponibile. Filtrarea se face în funcție de limbile selectate, pentru acele formate de prezentare a cărții care conțin cel puțin una dintre aceste valori. În situația în care nu s-a specificat nici o valoare, sunt incluse toate volumele comercializate. Un utilizator poate șterge una sau mai multe dintre limbile selectate anterior.

<spoiler|Indicații de Rezolvare> În documentul client.ejs (accesibil în directorul views), se afișează lista cu limbile în funcție de care se realizează filtrarea, pentru fiecare dintre acestea atașându-se un buton pentru eliminarea valorii respective.

Se iterează pe lista cu filtre în funcție de limbi (atributul languagesFilter, transmis din client.js), iar pentru fiecare element:

Elementele vor fi incluse ca linii în cadrul unui tabel (<tr><td>…</td></tr>).

Iterația pe elementele unei liste se face folosind instrucțiunea for, prin intermediul unui contor care ia valori de la 0 la dimensiunea tabloului respectiv (languagesFilter.length).

<% for (var index = 0; index < languagesFilter.length; index++) {
  // ...
} %>

Valoarea unui element al listei poate fi obținută prin evaluarea unei expresii, încadrată între elementele <%= … %>: <%= languagesFilter[index] %>.

În fișierul client.js localizat în directorul routes, se tratează acțiunile corespunzătoare evenimentelor de tip apăsare a butoanelor insert, respectiv delete corespunzătoare filtrului în funcție de limba în care a fost selectată cartea.

În cazul butonului de tip insert, se poate verifica direct faptul că acesta este inclus în corpul cererii întrucât acesta este unic:

if (request.body['insert_language.x']) {
  // ..
}

În cazul butonului de tip delete, se iterează asupra tuturor parametrilor incluși în corpul cererii, verificându-se dacă aceștia au formatul indicat, situație în care se parsează parametrul respectiv pentru a se determina denumirea limbii:

var delete_language;
for (var parameter in request.body) {
  if (parameter.startsWith('delete_language_') && parameter.endsWith('.x')) {
    delete_language = parameter.substring(parameter.lastIndexOf('_') + 1, parameter.indexOf('.x'));
  }
}

Verificarea faptului că un element aparține unui tablou se face prin intermediul metodei indexOf() prin care se determină poziția elementului în tablou, dacă este cazul. O valoare -1 indică faptul că elementul nu aparține tabloului.

var position = languagesFilter.indexOf(language);
if (position === -1) {
  // ...
}
if (position !== -1) {
  // ...
}

Adăugarea într-un tablou se face prin metoda push(), care primește ca parametru elementul respectiv.

Ștergerea dintr-un tablou se face prin metoda slice() care primește ca parametri poziția de la care se realizează operația precum și numărul de elemente care se doresc a fi eliminate.

Se poate utiliza ca model implementarea corespunzătoare filtrării în funcție de formatul de prezentare a cărții.

</spoiler>

8. În fișierul client.js localizat în directorul routes să se implementeze funcționalitatea pentru a crea conţinutul coşului de cumpărături, ţinând cont de situaţia în care pentru un produs deja existent să se poate actualiza cantitatea, astfel: dacă un produs există deja în coşul de cumpărături, cantitatea respectivă va fi suprascrisă, iar dacă aceasta este 0, produsul va fi şters din coşul de cumpărături.

<spoiler|Indicații de Rezolvare> În cazul în care se apasă un buton atașat unui câmp text pentru precizarea unei cantități pentru un anumit format de prezentare a unei cărți, este necesar să se realizeze următoarele operații:

a) butonul are denumirea insert_shoppingcart_{book_presentation_id} unde book_presentation_id reprezintă identificatorul formatului de prezentare a cărții (de exemplu, insert_shoppingcart_1 pentru formatul de identificare a cărții cu identificatorul 1); prin parsarea denumirii acestui parametru se obține identificatorul formatului de prezentare a cărții:

if (parameter.startsWith('insert_shoppingcart') && parameter.endsWith('.x')) {
  bookPresentationId = parameter.substring(parameter.lastIndexOf('_') + 1, parameter.indexOf('.x'));
}
Pentru un element de tip <input> având atributul type cu valoarea submit, se transmit ca parametrii coordonatele la care se găsește mouse-ul în momentul în care se produce evenimentul de tip apăsare, acestea având aceeași denumire, sufixată de .x, respectiv .y. Este important ca un singur parametru să fie procesat, în acest caz.

b) câmpul text ce conține cantitatea corespunzătoare are denumirea copies_shoppingcart_{book_presentation_id} unde book_presentation_id reprezintă identificatorul formatului de prezentare a cărții (de exemplu, copies_shoppingcart_1 pentru formatul de identificare a cărții cu identificatorul 1); valoarea corespunzătoare acestui parametru reprezintă cantitatea solicitată:

if (parameter.startsWith('insert_shoppingcart') && parameter.endsWith('.x')) {
  quantity = request.body['copies_shoppingcart_' + bookPresentationId];
}
Întrucât denumirea butonului corespunzător unui format de prezentare a unei cărți prin care se realizează plasarea sa în coșul de cumpărături nu este constantă (include identificatorul formatului de prezentare a cărții), este necesar să se itereze asupra tuturor parametrilor din corpul cererii. În situația în care un buton având formatul căutat este identificat, se completează informațiile în variabilele bookPresentationId, respectiv quantity prin care se indică faptul că trebuie realizată o procesare la nivelul coșului de cumpărături.
Vor fi transmiși un număr de parametrii egal cu numărul de câmpuri text afișate în cadrul paginii Internet, însă va fi luat în considerare doar cel asociat butonului care a fost apăsat, chiar dacă pot fi introduse valori și în cadrul acestora. Obținerea valorii transmise prin intermediul unui parametru se realizează prin request.body[parameter].

Obiectul shoppingCart, transmis prin intermediul sesiunii, conține următoarele proprietăți:

Aceste informații trebuie stocate în coșul de cumpărături pentru că în documentul .ejs acestea să fie afișate. Determinarea acestora se face prin intermediul unei interogări SQL folosind metoda connection.query() care primește ca parametri:

connection.query(statement, [parameters], function(error, result) {
  if (error) {
    console.log('An error has occurred: %s', error);
  }
  // ...
});

Interogarea care va fi folosită este:

SELECT bp.price AS price, b.title AS title, f.value AS value, l.name AS name
  FROM book_presentation bp, book b, format f, language 
 WHERE bp.id = ? AND b.id = bp.book_id AND f.id = bp.format_id AND l.id = bp.language_id

și va primi ca parametru identificatorul formatului de prezentare.

Obiectul care va fi plasat în coșul de cumpărături are următoarea formă:

var record = {
  bookPresentationId: bookPresentationId,
  quantity: quantity,
  price: result[0].price,
  title: result[0].title,
  format: result[0].value,
  language: result[0].name
};
Marcarea faptului că procesarea coșului de cumpărături a fost încheiată cu succes se face prin intermediul variabilei shoppingCartProcessing care va avea valoarea true. Este necesar să se apeleze și metoda renderClient() în situația în care procesarea asincronă a coșului de cumpărături este realizată după toate celelalte procesări, astfel încât ulterior acestui moment poate fi redat conținutul paginii în navigator, toate elementele care trebuie afișate fiind disponibile.

</spoiler>

9. În pagina client.ejs, să se implementeze funcționalitatea necesară pentru a se vizualiza coţinutul coşului de cumpărături. Se va afişa numărul de exemplare comandate, titlul cărţii, formatul de prezentare și limba, precum şi suma pentru fiecare produs în parte, respectiv preţul total pentru întregul coş de cumpărături.

<spoiler|Indicații de Rezolvare> Se verifică dacă lista corespunzătoare coșului de cumpărături, accesibilă în atributul shoppingCart, conține sau nu elemente. În acest scop se va folosi un instrucțiunea if:

<% if (shoppingCart.length !== 0) { %>
  <!-- display the shopping cart -->
<% } else { %>
  <tr>
    <td style="text-align: center;">The shopping cart is empty!</td>
  </tr>
<% } %>

Se iterează asupra conținutului coșului de cumpărături (obiectul shoppingCart), determinându-se, pentru fiecare element, cantitatea, prețul, titlul, valoarea, formatul de prezentare și limba. Este necesar să se calculeze și prețul total al coșului de cumpărături, într-o variabilă, prin însumarea tuturor valorilor corespunzătoare volumelor care se doresc a fi achiziționate.

<% for (var index = 0; index < shoppingCart.length; index++) {
  var currentBookValue = shoppingCart[index].quantity * shoppingCart[index].price; %>
  <!-- display the required information about the shopping cart element -->
<% shoppingCartValue += currentBookValue;
} %>

Prezentarea se va face în cadrul unui tabel (element de tip <table>), datele corespunzătoare unui obiect din coșul de cumpărături fiind incluse pe o linie a acestuia (<tr><td>…</td></tr>). </spoiler>

10. În pagina client.ejs, să se adauge două butoane prin care se poate anula, respectiv finaliza o comandă, în condițiile în care există produse în coșul de cumpărături.

<spoiler|Indicații de Rezolvare> Resursele grafice pentru butoane se regăsesc în directorul images, în subdirectorul user_interface, având denumirile:

Butoanele vor fi incluse în cadrul unui element de tip <input>, având atributul type cu valoarea image și denumirile cancelcommand, respectiv completecommand. Locațiile la care se găsesc resursele ce vor fi afișate vor fi precizate în cadrul atributului src.

<input type="image" name="cancelcommand" value="Cancel Command" src="./images/user_interface/remove_from_shopping_cart.png" />
&nbsp;&nbsp;
<input type="image" name="completecommand" value="Complete Command" src="./images/user_interface/shopping_cart_accept.png" />

</spoiler>

11. În fișierul client.js localizat în directorul routes, să se implementeze operaţiile de anulare, respectiv finalizare a unei comenzi.

<spoiler|Indicații de Rezolvare> În cazul anulării unei comenzi, se şterge conţinutul obiectului shoppingCart.

if (request.body['cancelcommand.x']) {
  shoppingCart = [];
}

În cazul finalizării unei comenzi, trebuie realizate următoarele operaţii:

var query_insert_invoice_header = 'INSERT INTO invoice_header (identification_number, issue_date, state, user_id) VALUES ('
    + '\'' + generateIdentificationNumber(3, 6) + '\', CURRENT_DATE(), \'issued\', \'' + request.session.user_id + '\')';
var currentBookPresentationId = shoppingCart[index].bookPresentationId;
var currentQuantity = shoppingCart[index].quantity;
var query_insert_invoice_line = 'INSERT INTO invoice_line (invoice_header_id, book_presentation_id, quantity) VALUES('
    + '\'' + invoiceHeaderId + '\', \'' + currentBookPresentationId + '\', \'' + currentQuantity + '\')';
Întrucât este important ca operațiile să fie realizate sincron, un pas al iterației va fi tratat de o metodă de callback a unui eveniment, parametrizată cu o variabilă care reține pasul iterației respective. Se lansează un eveniment cu pasul 0, acesta fiind incrementat după ce au fost completate procesările aferente și relansat. Când s-a parcurs întreaga listă, se lansează un alt eveniment pe care este golit coșul de cumpărături (dacă această operație nu ar fi fost realizată sincron, s-ar fi riscat ca ea să fie executată înainte de terminarea iterației pe întreg coșul de cumpărături) și se marchează terminarea tuturor operațiilor (variabila commandProcessing va avea valoarea true și se apelează metoda renderClient() pentru a se afișa pagina, în cazul în care toate celelalte procesări au fost încheiate).
var shoppingCartEvent = function(index) {
  if (index >= shoppingCart.length) {
    EventEmitter.emit('shoppingCartEnd');
    return;
  }
 
  // do some processing
  EventEmitter.emit('shoppingCartStart', index + 1);
};
EventEmitter.addListener('shoppingCartStart', shoppingCartEvent);
EventEmitter.addListener('shoppingCartEnd', function() {
  shoppingCart = [];
  commandProcessing = true;
  renderClient();
});
EventEmitter.emit('shoppingCartStart', 0);

</spoiler>

12. Să se implementeze operaţia de deautentificare printr-un buton plasat sub mesajul de întâmpinare pentru fiecare utilizator în pagina de tip client. În această situație, utilizatorul se va întoarce în pagina de autentificare.

<spoiler|Indicații de Rezolvare> În pagina client.ejs, se afișează un element de tip <input>, având atributul type cu valoarea image și denumirea signout. Resursa grafică se regăsește în directorul images, în subdirectorul user_interface, având denumirea signout.png.

În fișierul client.js, în cazul în care a fost apăsat butonul pentru operația de deautentificare (corpul cererii conține parametrul signout.x: if (request.body['signout.x'])):

Se poate utiliza ca model implementarea din cadrul paginii de administrator (fișierul administrator.js / pagina administrator.ejs).

</spoiler>

Resurse

Node.JS - Official Page
Node.JS - Tutorial
Node Package Manager (npm)
Pachetul express - Framework pentru Aplicații Web
Pachetul mysql - Driver MySQL pentru Node.JS
Embedded JavaScript Templates
Structura package.json

Soluții

Proiect NetBeans (este necesară versiunea 8.1)