Table of Contents
Laborator 06
Utilizarea platformei Node.JS pentru dezvoltarea de aplicații web
Obiective
- descrierea caracteristicilor pe care le oferă platforma Node.JS în comparație cu alte tehnologii similare;
- familiarizarea cu mecanismul de funcționare a platformei Node.JS și a modului în care este asigurată eficiența și scalabilitatea în condițiile gestiunii unui număr de operații de intrare/ieșire considerabil;
- înțelegerea conceptului de metodă asincronă pe baza modelului bazat pe evenimente și utilizarea funcțiilor de callback;
- proiectarea și dezvoltarea unei aplicații web folosind platforma Node.JS precum și a unor biblioteci disponibile prin intermediul utilitarului
npm
.
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:
- 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;
- fiind bazat pe motorul JavaScript V8 al Google Chrome, execuția codului sursă este foarte rapidă;
- 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;
- 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:
- î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;
- 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();
- gestiunea cererii și a răspunsului:
- 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
. - 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:
- se citește comanda transmisă de utilizator, este parsată pentru a putea fi stocată în structuri de date specifice Java Script după care este stocată în memorie;
- se evaluează structura de date;
- se tipărește rezultatul;
- se repetă ciclul atât timp cât programul rulează.
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:
- 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;
- 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:
- vizualizarea listei tuturor modulelor disponibile se face prin intermediul comenzii
npm ls
(cu opțiunea-g
dacă se dorește să se afișeze modulele instalate global); - instalarea unui modul este realizată prin intermediul comenzii
npm install <module_name>
și suportă două moduri:- local (implicit) - modulul va fi instalat în directorul din care a fost rulată comanda, în locația
node_modules
, fiind accesibil prin intermediul directiveirequire
- global - modulul va fi instalat într-un director de sistem (spre exemplu,
/usr/lib/node_modules
), funcțiile pe care le expune fiind accesibile prin interfața în linie de comandă (CLI - Command Line Interface) dar nu și în cadrul unor anumite aplicații; pentru ca un modul să fie instalat global, se folosește opțiunea-g
Î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:
versiune
: se va folosi exact versiunea menționată;>versiune
,>=versiune
: se va folosi o versiune mai mare (sau egală) cu versiunea menționată;<versiune
,<=versiune
: se va folosi o versiune mai mică (sau egală) cu versiunea menționată;~versiune
: se va folosi o versiune (aproximativ) echivalentă (permite schimbări la nivel de patch dacă este specificată versiunea minor, altfel permite schimbări la nivel de minor);^versiune
: se va folosi o versiune compatibilă (permite schimbări care nu modifică prima cifră nenulă din stânga);http://...
: se va folosi o versiune de la o adresă specificată;*
sau“”
(sirul vid): se poate folosi orice versiune;versiune1 - versiune2
: echivalent cu>=versiune1
și<=versiune2
;git …
: se va folosi o versiune de la un depozit pentru care se folosește un sistem de versionare a codului;utilizator/depozit
: se va folosi o versiune de la un depozit pentru care se folosește un sistem de versionare a codului;eticheta
: se va folosi o versiune pentru care s-a utilizat o anumită etichetă;cale
: se va folosi o versiune disponibilă la o locație în sistemul local de fișiere.
... "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:
name
(obligatoriu): reprezintă denumirea aplicației și trebuie să respecte următoarele constrângeri:- să aibă o lungime de până în 214 caractere (trebuie să fie totuși descriptivă, de preferat să nu se utilizeze denumirea modulului principal);
- să nu fie prefixat de . sau _; de asemenea, ar trebui evitată extensia
.js
saunode
(subînțeleasă); - să nu existe deja (se recomandă să se verifice registrul de nume npm anterior)
- să nu utilizeze caractere care nu pot fi folosite într-un URL (acest atribut putând fi folosit în acest scop).
version
(obligatoriu): reprezintă versiunea aplicației, în formatulmajor.minor.patch
, toate acestea având valori numerice; o astfel de valoare trebuie să poată fi parsată de utilitarul semver;description
: un șir de caractere prin intermediul căruia pachetul poate fi regăsit;keywords
: un tablou, conținând mai multe cuvinte-cheie (șiruri de caractere) prin intermediul cărora pachetul poate fi regăsit;homepage
: URL-ul proiectului;bugs
: pagina Internet / adresa de poștă electronică la care pot fi raportate defecte ale aplicației (pot fi specificate oricare dintre valori);license
: o licență prin intermediul căreia se indică permisiunile de utilizare ale pacjetului respectiv;author
,contributors
: autorul / lista de contributori, pentru fiecare putându-se specifica atributelename
,email
,url
;files
: lista de fișiere care vor fi incluse în proiect (automat sunt inclusepackage.json
,README
,CHANGELOG
,LICENSE
/LICENCE
); fișierul.npmignore
indică fișierele care vor fi ignorate (implicit.git
,CVS
,.svn
,.hg
,.lock-wscript
,wafpickle-N
,*.swp
,.DS_Store
,._*
,npm-debug.log
);main
: modulul principal al aplicației;bin
: executabilele care vor fi incluse înPATH
- pentru fiecare se va indica o denumire (alias) sub care vor fi utilizate precum și locația fișierului care va fi executat;man
: listă cu fișiere care vor fi accesibile pentru utilitarulman
;directories
: indică structura pachetului - suportă proprietățilelib
,bin
,man
,doc
,example
;repository
: depozitul la distanță de unde codul poate fi accesat, o astfel de funcționalitate fiind utilă pentru persoanele care doresc să contribuie; suportă atributeletype
șiurl
;scripts
: locația la care pot fi găsite script-uri care for fi rulate în diferite momente din ciclul de viață al aplicației;config
: conține un set de configurații care sunt valabile întotdeauna (nu se modifică între diferite versiuni);dependencies
: lista cu modulele care trebuie să existe pentru ca aplicația să poată fi rulată; are forma unei liste de perechi în care atributul este denumirea modulului iar valoarea este dată de o gamă de versiuni;devDependencies
: lista cu modulele folosite în scop de dezvoltare și care nu ar trebui descărcate în mod curent;peerDependencies
: lista cu modulele cu care aplicația este compatibilă, dar care nu sunt necesare neapărat pentru ca aceasta să funcționeze;bundledDependencies
: lista cu modulele cu care aplicația este integrată în mod implicit;optionalDependencies
: lista cu modulele care nu sunt neapărat necesare pentru ca aplicația să funcționeze;engines
: versiunile denode
/npm
cu care aplicația este compatibilă;os
: sistemul de operare pe care aplicația poate să ruleze;cpu
: arhitectura mașinii pe care aplicația poate să ruleze;preferGlobal
: specifică dacă aplicația ar trebui să fie instalată global sau nu;private
: indică dacă aplicația ar trebui să fie publicată sau nu;publishConfig
: listă de valori de configurare care vor fi utilizate doar în momentul în care aplicația este publicată.
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>
- dezinstalarea unui modul se face prin intermediul comenzii
npm uninstall <module_name>
; se poate verifica faptul că operația a fost realizată prin inspectarea directoruluinode_modules
sau prin listarea tuturor modulelor disponibile; - căutarea unui modul în cadrul unui depozit la distanță este realizată prin intermediul comenzii
npm search <module_name>
; - publicarea unui modul se face prin intermediul comenzii
npm publish
după ce în prealabil au fost rulate:npm init
- realizează fișierulpackage.json
pe baza unor proprietăți pe care utilizatorul trebuie să le specifice în linie de comandă;npm adduser
- înregistrează utilizatorul la depozitul npm;
- depublicarea unui modul este realizată prin intermediul comenzii
npm unpublish <module_name>
.
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.
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().
addListener()
, iar pe funcția de callback se apelează automat metoda removeListener()
.
Alte metode ale clasei events.EventEmitter
sunt:
- setMaxListeners() - stabilește numărul maxim de ascultători pentru un anumit eveniment (implicit, valoarea acestora este 10); dacă se transmite ca parametru valoarea 0, poate fi precizat un număr nelimitat de ascultători;
- getMaxListeners() - se obține numărul maxim de ascultători pentru un anumit eveniment;
- listeners() - furnizează un tablou de ascultători pentru un anumit eveniment;
- listenerCount() - returnează numărul de ascultători pentru un anumit eveniment; aceasta este o metodă a clasei (statică) și primește ca parametri obiectul de tip
events.EventEmitter
și denumirea evenimentului.
Î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:
- 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:
- în momentul în care s-a terminat de parcurs lista, se generează un eveniment prin care se anunță acest lucru;
- se realizează procesarea, iar la terminarea acesteia, se invocă metoda pentru elementul următor (prin generarea evenimentului corespunzător);
- se adaugă un ascultător pentru evenimentul de procesare a unui element al listei;
- se adaugă un ascultător pentru evenimentul de terminare a procesării tuturor elementelor listei;
- 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:
- stream.Readable - abstractizare a unei surse de date din care se citește;
- stream.Writable - abstractizare a unei destinații de date în care se scrie;
- stream.Duplex - obiect care implementează ambele interfețe (
Readable
, respectivWritable
), putând fi folosite atât pentru citire cât și pentru scriere; - 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:
- new Buffer(array) - creează un obiect de tip
Buffer
pornind de la un tablou (de numere întregi); - new Buffer(buffer) - creează un obiect de tip
Buffer
pornind de la un alt obiect de tipBuffer
, care este duplicat; - new Buffer(size) - creează un obiect de tip
Buffer
având o anumită dimensiune (exprimată în octeți); - new Buffer(string[, encoding]) - creează un obiect de tip
Buffer
pornind de la un șir de caractere și indicându-se (opțional) și un tip de codificare:utf8
(implicit)ascii
utf16le
ucs2
base64
hex
Cele mai frecvent folosite metode pentru obiectele de tip Buffer
sunt:
- buf.toString([encoding][, start][, end]) - citește un obiect de tip
Buffer
pe care îl decodifică folosind un tip de codificare (implicit,utf8
); opțional se pot specifica pozițiile între care se realizează decodificarea (implicit 0, respectivbuffer.length
); - buf.toJSON() - citește un obiect de tip
Buffer
furnizând reprezentarea sa în format JSON; - buf.write(string[, offset][, length][, encoding]) - scrie un șir de caractere în obiectul de tip
Buffer
, pornind de la o anumită poziție (implicit 0), procesând o anumită dimensiune, exprimată ca număr de octeți (implicitbuffer.length
) și folosind un anumit tip de codificare (implicit,utf8
); în situația în care nu există suficient spațiu pentru a se scrie întregul obiect, acesta va fi stocat doar parțial; - buf.copy(targetBuffer[, targetStart][, sourceStart][, sourceEnd]) - copiază date dintr-o regiune a obiectului de tip
Buffer
curent (începând cusourceStart
- implicit 0 și terminând cusourceEnd
, implicitbuffer.length
) - folosit ca sursă, într-o regiune a obiectului de tipBuffer
destinație (targetBuffer
), începând cutargetStart
(implicit 0), chiar dacă regiunile se suprapun; - buf.fill(value[, offset][, end]) - umple obiectul de tip
Buffer
cu valoarea precizată de parametrulvalue
, începând cu pozițiaoffset
(implicit 0) și terminând cu pozițiaend
(implicitbuffer.length
); dacă este necesar, se repetă valoarea transmisă ca argument; - buf.slice([start[, end)]] - creează un obiect de tip
Buffer
, care referă aceeași zonă de memorie, începând cu valoareastart
(implicit 0) și terminând cu valoareaend
(implicitbuffer.length
); întrucât se partajează aceeași zonă de memorie, orice operații realizate pe obiectul furnizat vor fi reflectate și în obiectul original; - buf.indexOf(value[, byteOffset]) - verifică daca o valoare (
Buffer
sau șir de caractere dau număr) se regăsește în obiectul de tipBuffer
, furnizându-se poziția la care a fost identificat sau -1 în caz contrar; căutarea poate fi realizată începând cu o anumită poziție indicată de argumentulbyteOffset
care implicit are valoarea 0; - Buffer.concat(list[, totalLength]) - concatenează o listă de obiecte de tip
Buffer
; se poate specifica dimensiunea valorii obținute (opțional), această abordare fiind recomandată întrucât timpul de execuție al metodei este mai rapid (nu se mai realizează o iterație suplimentară pentru a se determina dimensiunea fiecărui obiect în parte care ulterior este însumată); - Buffer.compare(buf1, buf2), buf.compare(otherBuffer) - compară lexicografic conținutul a două obiecte de tip
Buffer
, furnizând un număr prin care se stabilește relația de precedență între acestea; - buf.equals(otherBuffer) - verifică dacă două obiecte de tip
Buffer
stochează același conținut; - buf.length - furnizează dimensiunea unui obiect de tip
Buffer
exprimată în octeți (cantitatea de memorie alocată - aceasta nu se modifică în momentul în care conținutul este schumbat).
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.
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:
path
- calea către fișierul care se dorește a fi deschis;flags
- valoare prin intermediul cărora se indică comportamentul fișierului care se dorește a fi deschis;
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ă |
mode
- modul (permisiunile și biții murdari) - implicit0666
(citire și scriere) - pentru fișierul care se dorește a fi deschis;callback
- funcția de tip callback care este invocată în momentul în care fișierul este deschis, primind ca argumente o eroare (în cazul în care se produce) și descriptorul pentru fișier.
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:
fd
- descriptorul de fișier;callback
- funcția de tip callback care este invocată în momentul în care fișierul este închis, primind ca argumente o eroare (în cazul în care se produce).
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:
path
- calea către resursa din sistemul local de fișiere pentru care se dorește să se obțină informații;callback
- funcția de tip callback care este invocată în momentul în care s-au obținut informații cu privire la resursa din sistemul local de fișiere, primind ca argumente o eroare (în cazul în care se produce) și descriptorul pentru fișier și un obiect de tipul fs.Stats prin intermediul căruia pot fi obținute diferite informații cu privire la resursa respectivă.
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:
fd
- descriptorul de fișier;buffer
- un obiect de tipBuffer
în care vor fi plasate datele citite;offset
- poziția (în obiectul de tipBuffer
) la care vor fi plasate datele citite;length
- dimensiunea (exprimată ca număr de octeți) care se dorește a fi citită;position
- poziția de la care se dorește să se realizeze citirea (dacă nu este indicată, se folosește poziția curentă);callback
- funcția de tip callback care este invocată în momentul în care s-a realizat o operație de citire din fișier, primind ca argumente o eroare (în cazul în care se produce), numărul de octeți care au fost citiți și un obiect de tipBuffer
în care poate fi accesat conținutul respectiv.
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:
file
- calea către fișierul care se dorește a fi scris sau descriptorul de fișier;data
- un obiect de tip șir de caractere sauBuffer
care conține datele ce se doresc a fi scrise;options
- un obiect care conține următoarele informații:encoding
- mecanismul de codificare utilizat, implicitutf8
;mode
- modul (permisiunile și biții murdari), implicit0666
;flag
- valoare prin intermediul cărora se indică comportamentul fișierului care se dorește a fi scris, implicitw
;
callback
- funcția de tip callback care este invocată în momentul în care s-a realizat o operație de scriere în fișier, primind ca argumente o eroare (în cazul în care se produce).
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:
fd
- descriptorul de fișier;len
- dimensiunea (exprimată în număr de octeți) după care conținutul respectiv va fi ignorat;callback
- funcția de tip callback care este invocată în momentul în care s-a realizat o operația de modificare a fișierului, primind ca argumente o eroare (în cazul în care se produce).
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:
path
- calea către fișierul care se dorește a fi șters;callback
- funcția de tip callback care este invocată în momentul în care s-a realizat o operație de scriere în fișier, primind ca argumente o eroare (în cazul în care se produce).
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:
path
- calea către directorul care se dorește a fi creat;mode
- modul (permisiunile și biții murdari), implicit0777
;callback
- funcția de tip callback care este invocată în momentul în care s-a realizat operația de creare a directorului, primind ca argument o eroare (în cazul în care se produce).
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:
path
- calea către directorul care se dorește a fi șters;callback
- funcția de tip callback care este invocată în momentul în care s-a realizat operația de ștergere a directorului, primind ca argument o eroare (în cazul în care se produce).
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:
path
- calea către directorul pentru care se dorește să se obțină informații;callback
- funcția de tip callback care este invocată în momentul în care s-au obținut informații cu privire la director, primind ca argument o eroare (în cazul în care se produce) și o listă de fișiere conținute în cadrul directorului (exclusiv.
și..
).
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:
- Process - pentru gestiunea operațiilor desfășurate în mod curent; este un obiect derivat din
EventEmitter
astfel încât pune la dispoziție numeroase evenimente legate de acestea (beforeExit
,exit
,message
,rejectionHandled
,uncaughtException
,unhandledRejection
);
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:
- Modulul OS - oferă informații furnizate de sistemul de operare;
- Modulul Path - implementează operații pentru gestiunea căilor (absolute și relative);
- Modulul Net - utilizat pentru funcții ce implică componente de rețea (atât server cât și client), tratete sub formă de fluxuri;
- Modulul DNS - folosit pentru regăsirea de informații pe baza unor operații DNS cât și pentru accesarea funcționalităților sistemului de operarare în vederea realizării operațiilor de rezolvare a numelor în adrese Internet;
- Modulul Domain - furnizează mecanisme pentru gestiunea simultană a unor operații de intrare/ieșire.
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:
- este integrat cu diferite middleware-uri către care delegă procesarea de cereri și de răspunsuri HTTP;
- definește un sistem de rute prin intermediul căruia se stabilește ce acțiune este realizată în funcție de resursa care este solicitată precum și de metoda folosită;
- permite generarea de pagini Internet pe baza unor șabloane către care sunt transmise diverse argumente;
- furnizează acces la informațiile stocate în diferite surse de date.
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
–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:
body-parser
- middleware pentru gestiunea datelor codificate în format JSON, binar, text sau URL;cookie-parser
- middleaware pentru asigurarea persistenței în cadrul unei aplicații web prin utilizarea de cookies; obiectulrequest.cookies
va conține o listă de perechi (atribut, valoare) pe baza informațiilor preluate din antetele HTTP;multer
- middleware pentru procesarea de resurse de tipmultipart/form-data
.
O aplicație web folosind platforma Express va realiza următoare operații:
- va încărca modulul
express
și va construi o instanță a acestuia; - 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); - 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')));
Definirea Rutelor
O definiție a unei rute are structura app.METHOD(PATH, HANDLER)
unde:
app
este o instanță a claseiexpress
;
app.all()
este folosită pentru a ignora tipul de cerere HTTP folosit, deservind resursa respectivă indiferent de aceasta.
PATH
indică o cale pe server; se pot utiliza șabloane (eng. wildcards) pentru a asocia mai multe resurse unei singure acțiuni;?
este utilizat pentru a indica 0 sau 1 caracter;+
este folosit pentru a indica 1 sau mai multe caractere;*
este inclus pentru a indica 0 sau mai multe caractere;()
grupează o anumită expresie, de regulă, pentru a se preciza un multiplicator.
HANDLER
specifică o funcție de tip callback care este invocată în momentul în care este solicitată resursa asociată.
,
), î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.
next()
prin intermediul căruia se realizează transferul către următoarea acțiune.
Obiectul de tip request
are următoarea structură:
- proprietăți
request.app
- referință către instanța aplicației express pe care o folosește middleware-ul;request.baseUrl
- URL-ul pe baza căruia a fost utilizată instanța rutei;request.body
- conține o serie de perechi (atribut, valoare) pe baza parametrilor transmiși prin intermediul unui formular; implicit, acest atribut are valoareaundefined
și este activat doar în momentul în care se folosește un middleware pentru parsarea corpului cererii, cum ar fibody-parser
var bodyParser = require('body-parser'); app.use(bodyParser.urlencoded({ extended: true }));
request.cookies
- include obiectele de tip cookie din cadrul cererii; implicit, acest atribut are valoareaundefined
și este activat doar în momentul în care se folosește un middleware pentru parsarea obiectelor de tip cookie, cum ar ficookie-parser
var cookieParser = require('cookie-parser'); app.use(cookieParser);
request.fresh
- precizează dacă cererea este nouă;request.hostname
- conține valoarea câmpuluiHost
din antetele HTTP;request.ip
- stochează adresa Internet a cererii;request.ips
- reține o listă de adrese Internet, așa cum apar în câmpulX-Forwarded-For
din antetele HTTP în situația în care configurația referitoare la nivelul de încredere în privința proxy-urilor are valoareatrue
;request.originalUrl
- reține URL-ul cererii, permițând ca valoarearequest.url
să fie rescrisă pentru a deservi diferite scopuri legate de mecanismul de rutate;request.params
- furnizează parametrii atașați rutei, accesul la aceștia fiind asigurat prin intermediul operatorului de dereferențiere (valoarea implicită este{}
);request.path
- conține contextul din cadrul URL-ului;request.protocol
- conține protocolul din cadrul URL-ului (http
sauhttps
);request.query
- conține secțiunea de interogare din cadrul URL-ului;request.route
- referință către ruta deservită în mod curent (un șir de caractere);request.secure
- reprezintă o valoare care indică dacă s-a utilizat o conexiune TLS;request.signedCookies
- furnizează o listă de obiecte de tip cookie semnate și transmise prin cerere, care pot fi utilizate;request.stale
- precizează dacă cererea este veche;request.subdomains
- reține o listă cu subdomenii din cadrul domeniului atașat cererii;request.xhr
- include o valoare a cărei valoare estetrue
în situația în care valoarea atributuluiX-Requested-With
din antetele HTTP esteXMLHttpRequest
, indicând faptul că cererea a fost realizată prin intermediul unei biblioteci de tip client.
- metode
request.accepts(types)
- verifică dacă tipurile de conținut specificate pot fi acceptate pe baza câmpuluiAccept
din antetele HTTP;request.get(field)
- furnizează valoarea asociată pentru un anumit câmp din antetele HTTP;request.is(type)
- indică dacă tipul de conținut MIME, transmis ca parametru, este compatibil cu cel specificat în câmpulContent-Type
din antetele HTTP;request.param(name[, defaultValue])
- întoarce valoarea unui parametru identificat prin denumire, în situația în care acesta există.
Obiectul de tip response
are următoarea structură:
- proprietăți
res.app
- referință către instanța aplicației express pe care o folosește middleware-ul;res.headersSent
- indică dacă aplicația a transmis antetele HTTP corespunzătoare răspunsului;res.locals
- conține variabilele locale aferente răspunsului.
- metode
res.append(field[, value])
- atașează o anumită valoare la un câmp din antetele HTTP corespunzătoare răspunsului;res.attachment([filename])
- transmite un fișier ca atașament la un răspuns;res.cookie(name, value[, options])
- adaugă un obiect de tip cookie (pereche de tip atribut, valoare - aceasta putând fi un șir de caractere sau un obiect în format JSON); opțiunile care pot fi precizate includ:domain
,path
,secure
;res.clearCookie(name[, options])
- șterge un obiect de tip cookie;res.download(path[,filename][, fn])
- transferă fișierul localizat într-o anumită cale sub formă de atașament, astfel încât navigatorul va solicita permisiunea utilizatorului pentru a descărca resursa respectivă;res.end([data][, encoding])
- indică faptul că procesul de transmitere a răspunsului a fost încheiat;res.format(object)
- realizează negocierea conținutului pe baza proprietățiiAccept
din cadrul antetelor HTTP, atunci când aceasta este definită;res.get(field)
- furnizează valoarea asociată unui câmp din cadrul antetelor HTTP;res.json([body])
- folosită pentru transmiterea unui răspuns în format JSON;res.jsonp([body])
- utilizată pentru transmiterea unui răspuns în format JSON, cu suport JSONP;res.links(links)
- concatenează legăturile furnizate ca argumente și le transmite câmpuluiLinks
din antetele HTTP;res.location(path)
- precizează valoarea câmpuluiLocation
din antetele HTTP pe baza căii transmise sub formă de parametru;res.redirect([status, ]path)
- redirecționează contextul către URL-ul obținut din calea indicată, folosind un anumit cod de stare HTTP;res.render(view[, locals][, callback])
- redă o anumită pagină (șablon), transmițând codul HTML generat pentru a fi vizualizat prin intermediul navigatorului;res.send([body])
- transmite un răspuns având un anumit corp (conținut);res.sendFile(path[, options][, fn])
- transferă un fișier la o anumită locație, stabilind valoarea câmpuluiContent-Type
din antetele HTTP pe baza extensiei fișierului;res.sendStatus(statusCode)
- trimite un cod de stare precum și reprezentarea sa sub formă de șir de caractere (conținând descrierea sa);res.set(field[, value])
- stabilește valoarea unui câmp din antetele HTTP;res.status(code)
- indică codul de stare pentru un răspuns;res.type(type)
- completează câmpulContent-Type
din antetele HTTP cu tipul MIME corespunzător unei anumite extensii.
Utilizarea Motoarelor pentru Șabloane
Pentru ca platforma Express să poată reda fișiere de tip șablon, este necesar să se specifice anumite configurații:
views
reprezintă directorul în care pot fi regăsite fișierele de tip șablon;view engine
precizează motorul care va fi utilizat pentru redarea acestor fișiere.
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).
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:
cache
- funcțiile compilate vor fi stocate într-o zonă de memorie tampon, simulată printr-un fișier;filename
- utilizat pentru a stoca informațiile care trebuie reținute în zone de memorie tampon;context
- desemnează contextul de execuție al funcțiilor;compileDebug
- determină dacă instrumentele pentru depanare sunt compilate sau nu;client
- furnizează funcția compilată autonomă (independentă);delimiter
- permite suprascrierea delimitatorului standard pentru includerea codului JavaScript, în loc de%
(atât pentru început cât și pentru sfârșit);debug
- permite vizualizarea corpului funcției generate;_with
- stabilește utilizarea structurilorwith() {}
precum și stocarea variabilelor locale în obiectullocals
;rmWhitespace
- elimină toate spațiile care pot fi șterse fără a afecta sintaxa documentului, inclusiv pe cele de la început și pe cele de la sfârșit, precum și o versiune mai sigură a funcționalității oferite de delimitatorul-%>
astfel încât caracterele\n
aparținând unor etichete în interiorul unor linii nu vor fi eliminate.
Î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') -%>
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:
host
- denumirea gazdei pe care rulează sistemul de gestiune pentru baze de date MySQL la care se realizează conexiunea (implicit,localhost
);port
- portul pe care sistemul de gestiune pentru baze de date MySQL poate fi invocat (implicit, 3306);localAddress
- adresa Internet care va fi utilizată pentru o conexiune TCP (opțional);socketPath
- calea către un socket de domeniu unix la care se realizează conexiunea; este ignorat atunci când se folosesc parametrihost
șiport
;user
- numele de utilizator cu care se realizează autentificarea;password
- parola cu care se realizează autentificarea;database
- denumirea bazei de date care va fi utilizată în cadrul conexiunii (opțional);charset
- setul de caractere care va fi utilizat în cadrul conexiunii (corespondentul conceptului de collation); dacă este specificat un set de caractere la nivel SQL, se va utiliza valoarea implicită asociată acestuia la nivelul sistemului de gestiune pentru baze de date (implicit,UTF8_GENERAL_CI
);timezone
- zona de timp utilizată pentru stocarea datelor calendaristice (implicit,local
);connectTimeout
- durata de timp (exprimată în milisecunde) care trebuie să se scurgă înainte de a se genera un eveniment de tip timeout atunci când se realizează o conexiune către sistemul de gestiune pentru baze de date MySQL (implicit,10000
);stringifyObjects
- transmiterea valorilor ca șiruri de caractere (implicit,false
);insecureAuth
- determină politica de securitate cu privire la conectarea la instanțe ale sistemului de gestiune pentru baze de date MySQL care solicită o metodă de autentificare nesigură (implicit,false
);typeCast
- determină dacă valorile din baza de date trebuie să fie convertite la tipuri de date JavaScript native (implicit,true
);queryFormat
- permite definirea unei funcții pentru utilizarea unui format de interogări definit de utilizator;supportBigNumbers
- atribut ce ar trebui utilizat în momentul în care în baza de date există atribute care rețin numere având valori foarte mari (implicit,false
);bigNumberStrings
- pentru numere având valori foarte mari (numai dacă proprietateasupportBigNumbers
are valoareatrue
) determină dacă rezultatul preluat din baza de date este convertit sau nu automat la tipul de date șir de caractere:true
: se realizează întotdeauna conversia la tipul de date șir de caractere:false
: se realizează conversia la tipul de date șir de caractere numai în situația în care valoarea respectivă nu poate fi reprezentată adecvat, ca număr, în JavaScript (nu se află în intervalul [-253, +253]);
dateStrings
- determină dacă tipurile de date care rețin informații de tip dată calendaristică vor fi convertite la șiruri de caractere sau la obiecte de tipDate
(implicit,false
);debug
- determină afișarea unor detalii legate de protocol la consola de ieșire (implicit,false
);trace
- permite reținerea stivei de execuție în cadrul obiectelorError
care să includă apelul metodei din cadrul bibliotecii, cu deprecieri ale performanțelor pentru majoritatea apelurilor (implicit,true
);multipleStatements
- stabilește comportamentul în privința utilizării mai multor expresii MySQL în cadrul aceleiași interogări, ceea ce ar putea reprezenta o breșă de securitate în privința atacurilor prin injecția de SQL malițios (implicit,false
);flags
- stabilește o listă cu valori care vor controla comportamentul conexiunii; aceștia sunt transmiși sub forma unui șir de caractere în care delimitatorul este caracterul,
; un flag care este adăugat în plus față de cele implicite trebuie prefixat de caracterul+
iar în situația în care se dorește ca un flag implicit să fie șters, acesta trebuie prefixat de caracterul-
; lista de flag-uri implicite este:CONNECT_WITH_DB
- permite specificarea denumirii bazei de date în momentul în care este definită o conexiune;FOUND_ROWS
- transmite atributulfound_rows
al argumentuluiresult
din cadrul metodei de tip callback transmisă prin metodaquery()
, în loc deaffected_rows
;IGNORE_SPACE
- interpretorul SQL va ignora spațiile care preced caracterul(
în cadrul interogărilor;LOCAL_FILES
- permite încărcarea înregistrărilor într-o bază de date folosindLOAD DATA LOCAL
;LONG_FLAG
;LONG_PASSWORD
- folosește o versiune îmbunătățită a mecanismului de autentificare;MULTI_RESULTS
- semnalează posibilitatea de gestiune concurentă a mai multe seturi de rezultate;PROTOCOL_41
- folosește versiunea 4.1 a protocolului de conectare la baza de date;PS_MULTI_RESULTS
- semnalează posibilitatea de gestiune concurentă a mai multe seturi de rezultate;SECURE_CONNECTION
- indică faptul că este suportată mecanismul de autentificare nativ din versiunea 4.1 a protocolului de conectare la baza de date;TRANSACTIONS
- solicită utilizarea flag-urilor care indică starea tranzacției;MULTI_STATEMENTS
- transmis în mod automat în situația în care parametrulmultipleStatements
are valoareatrue
;
ssl
- transmite un obiect care conține parametri SSL sau un șir de caracyere care conține denumirea unui profil SSL.
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:
- metoda
end()
se asigură de faptul că toate interogările care au fost invocate dar pentru care nu s-a obținut încă un rezultat sunt terminate înainte ca legătura la sistemul de gestiune pentru baze de date să fie pierdută; în situația în care execuția uneia dintre interogări produce o eroare, aceasta este transmisă metodei de tip callback:connection.end(function(error) { if (error) { console.error('An error has occurred: ' + error.stack); return; } console.log('The connection end() method was successful!'); });
- metoda
destroy()
determină o terminare imediată a legăturii cu sistemul de gestiune pentru baze de date, garantând faptul că nu se vor mai produce alte evenimente și nici nu vor fi invocate metode de tip callback legate de conexiunea în cauză:connection.destroy();
Î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:
query(statement, callback)
- este utilizată de regulă pentru acele interogări care nu folosesc parametri sau pentru care parametrii sunt incluși în interogare; metoda primește ca argument o funcție de tip callback, având ca parametri obiectul eroare (în situația în care aceasta se produce), obiectul care reține rezultatul, acesta având forma unui tablou (chiar dacă rezultatul conține o singură înregistrare) și un obiect în care se regăsesc informații cu privire la câmpurile pentru care s-a realizat interogarea respectivă:
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:
- valorile numerice nu sunt modificate sub nici o formă;
- obiectele ce rețin valori de adevăr sunt convertite la
true
/false
; - valorile de tip dată calendaristică sunt transformate în șiruri de caractere având formatul
YYYY-mm-dd HH:ii:ss
; - obiectele care rețin informații binare (
Buffer
) sunt convertite la șiruri de caractere, reprezentate sub forma codului hexazecimal; - pentru șirurile de caractere, sunt convertite caracterele speciale;
- tablourile sunt convertite în șiruri de caractere având forma unei liste, în care delimitatorul este caracterul
,
; în situația în care se folosesc tablouri imbricate, pe baza acestora se vor genera grupuri de liste, în care un grup este încadrat între caracterele(
și)
; - obiectele sunt transformate în perechi de tip
atribut
=valoare
pentru fiecare atribut (enumerabil) al acestuia (funcțiile fiind ignorate), valorile fiind determinate prin apelarea metodeitoString()
; valorile de tipundefined
saunull
sunt convertite automat laNULL
; această funcționalitate poate fi utilizată pentru adăugarea unei valori într-o tabelă direct dintr-un obiect JavaScript, fără a se mai preciza coloanele care sunt folosite; - valorile de tip
Nan
/Infinity
sunt lăsate ca atare, de vreme ce MySQL nu suportă astfel de valori, astfel că în acest caz va fi generată o eroare.
connection.statement('SELECT * FROM book b WHERE b.id = ' + connection.escape(bookId), function(error, result, fields) { // ... });
query(statement, parameters, callback)
- este utilizată de regulă pentru acele interogări care folosesc parametri; faptul că se utilizează un parametru este marcat prin caracterul?
; argumentul care indică parametrii are forma unei liste și trebuie să respecte exact ordinea în care aceștia sunt menționați în cadrul interogării
connection.query('SELECT * FROM book b WHERE b.id = ?', [bookId], function(error, result, fields) { // ... });
query(options, callback)
- este utilizat atunci când se dorește să se folosească anumite funcționalități pentru controlul modului în care vor fi furnizate rezultatele:nestTables
permite stocarea valorilor corespunzătoare coloanelor având aceeași denumire în momentul în care sunt utilizate operații de asociere; se poate folosi valoareatrue
(situație în care denumirea coloanelor este prefixată de denumirea tabelei din care provin) sau_
(caz în care valorile distincte aparținând unor coloane cu aceeași denumire sunt concatenate folosind acest delimitator);typeCast
stabilește dacă operația de conversie de la tipurile de date MySQL la tipurile de date JavaScript, realizată automat în mod implicit; este recomandat ca o astfel de facilitate să nu fie dezactivată;timeout
indică perioada după care se va genera o eroare dacă nu se furnizează un rezultat în intervalul respectiv;sql
reprezintă interogarea care va fi executată;values
conține valorile parametrilor, sub forma unei liste, în situația în care interogarea este parametrizată.
connection.query({ sql: 'SELECT * FROM books WHERE b.id = ?', values: [bookId], nestTables: true, typeCast: false, // !!! NOT recommended timeout: 10000 }, function (error, result, fields) { // ... });
query(options, parameters, callback)
- este utilizat pentru interogări parametrizate, atunci când valorile acestora sunt furnizate distinct, nu în cadrul atributuluivalues
:
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()
:
insertId
- stochează identificatorul înregistrării care a fost adăugată în baza de date în urma unei interogări de tipINSERT
transmisă ca parametru al metodeiquery()
:var book = { "title": title, "subtitle": subtitle, "description": description, "edition": edition, "printing_year": printing_year, "collection_id": collection_id }; var query = connection.query('INSERT INTO book SET ?', book, function(error, result) { if (error) { console.log('An error has occurred: %s', error); return; } console.log('The record has been successfully inserted with id %d', result.insertId); }); console.log(query.sql);
changedRows
- determină numărul de înregistrări care au fost modificate în urma unei interogări de tipUPDATE
transmisă ca parametru al metodeiquery()
:var query = connection.query('UPDATE book SET edition = edition + 1 WHERE printing_year > ?', [printingYear], function(error, result) { if (error) { console.log('An error has occurred: %s', error); return; } console.log('%s records have been successfully updated', result.changedRows); });
affectedRows
- determină numărul de înregistrări care au fost șterse în urma unei interogări de tipDELETE
transmisă ca parametru al metodeiquery()
:var query = connection.query('DELETE FROM book WHERE collection_id IN ?', [collectionId], function(error, result) { if (error) { console.log('An error has occurred: %s', error); return; } console.log('%s records have been successfully deleted', result.affectedRows); });
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:
- un utilizator tip administrator, care va manipula – prin intermediul aplicaţiei – informaţiile din baza de date; acesta are la dispoziție operații de adăugare, editare și ștergere pentru oricare dintre tabele;
- un utilizator tip client, care va formula o comandă după ce şi-a specificat un anumit coş de cumpărături ca urmare a consultării catalogului de produse; pe baza comenzii va fi emisă o factură (
invoice_header
), caracterizată printr-un număr de identificare (identification_number
), data la care a fost emisă (issue_date
), stare (state
) și identificatorul utilizatorului către care va fi transmisă (user_id
); aceasta conține la rândul ei mai multe înregistrări, corespunzătoare cărților care au fost cumpărate (invoice_line
), definite de factura corespunzătoare (invoice_header_id
), identificatorul formatului de prezentare al cărții (book_presentation_id
) și cantitatea (quantity
).
Structura proiectului BookStore include următoarele componente:
- în rădăcină:
- fișierul
main.js
- reprezintă codul sursă care va fi executat (folosind executabilulnode
), aici fiind realizate diferite configurări, indicându-se rutele prin intermediul cărora vor fi procesate anumite pagini și lansându-se în execuție serverul http (pe un anumit port), pe baza căruia vor fi gestionate cererile și răspunsurile:- se indică faptul că aplicația va folosi framework-ul Express, precum și directorul în care se găsesc resursele care vor putea fi accesate (imagini, foi de stil, șabloane):
var express = require('express'); var app = express(); app.use(express.static(path.join(__dirname, 'public')));
- se precizează care este directorul în care se găsesc paginile care vor fi vizualizate precum și motorul care va realiza această operație (în cazul de față, ejs (Embedded JavaScript Templates)):
var path = require('path'); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs');
- se specifică faptul că aplicația va folosi modulele:
body-parser
pentru prelucrarea parametrilor dintr-o cerere / răspuns:var bodyParser = require('body-parser'); app.use(bodyParser.urlencoded({ extended: true }));
serve-favicon
pentru folosirea unei pictograme pentru aplicația web:var favicon = require('serve-favicon'); app.use(favicon(__dirname + '/public/images/favicon.ico'));
express-session
pentru gestiunea sesiunilor - parametriisaveUninitialized
șiresave
sunt folosiți pentru a indica transmiterea în cadrul sesiunii a variabilelor neinițializate și pentru persistența lor de-a lungul mai multor cicluri cerere / răspuns:var session = require('express-session'); app.use(session({ secret: 'StudentAipi2015', saveUninitialized: true, resave: true }));
events
pentru folosirea de evenimente - se construiește un obiect de tip EventsEmitter prin intermediul căruia vor fi gestionate evenimentele (acesta va fi transmis ca parametru paginilor, pentru a se evita obținerea de noi instanțe în momentul în care pagina este încărcată din nou)var events = require('events'); var EventEmitter = new events.EventEmitter();
- se indică rutele (codul sursă) care va fi executat pentru fiecare pagină în parte, precum și parametrii care sunt transmiși acestora:
require('./routes/login')(app, EventEmitter); require('./routes/administrator')(app, EventEmitter); require('./routes/client')(app, EventEmitter);
- se lansează în execuție serverul HTTP pe un anumit port:
app.listen(8080);
- fișierul
package.json
- conține informații cu privire la aplicația web (denumire, versiune, cuvinte-cheie, autor precum și modulele folosite, descrise sub formă de dependențe - pentru fiecare atribut se indică denumirea și versiunea care se dorește a fi utilizată)- package.json
{ "name": "06-BookStore-NodeJS", "version": "1.0.0", "keywords": ["util", "functional", "server", "client", "browser"], "author": "Andrei", "contributors": [], "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" } }
- directorul
node_modules
- conține codul sursă pentru toate modulele folosite în cadrul aplicației web; descărcarea acestora se face automat, pe baza dependențelor din fișierulpackage.json
în momentul în care este executată comandanpm install
; - directorul
public
- conține resursele care vor fi accesate de aplicația webcss
- foi de stil;images
- imagini;
- directorul
routes
- conține codul sursă care va fi executat pentru fiecare pagină în parte; se indică care sunt contextele pe care le deservesc (parametrul metodeiapp.route()
) precum și metodele prin care pagina poate fi accesată (GET
,POST
,PUT
,TRACE
,OPTIONS
,DELETE
); metoda trebuie să fie făcută publică prinmodule.exports
astfel încât să fie vizibilă la nivelul întregii aplicații web:module.exports = function(app, EventEmitter) { app.route('/') .get(function(request, response) { processLogin(request, response, EventEmitter); }) .post(function(request, response) { processLogin(request, response, EventEmitter); }); };
- directorul
views
- conține șabloanele pentru paginile web care vor fi vizualizate, în care urmează să fie plasate informațiile provenite în urma procesării datelor în modulele de logică a aplicației.
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:
- 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);
- 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ă;
- 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ă);
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
.
bookstore
este deja instalată, nu mai este necesar să se ruleze acest script.
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.
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.
- administrator: mary.smith / -
- client: linda.williams / -
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:
- se afișează șirul de caractere conținând limba;
- se afișează un element de tip <input>, având atributul
type
cu valoareaimage
și denumireadelete_language_{language_name}
. Resursa grafică se regăsește în directorulimages
, în subdirectoruluser_interface
, având denumireadelete.png
(se recomandă să se forțeze dimensiunea acesteia la 16×16, prin specificarea atributelorwidth
șiheight
).
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.
- pentru parametrii având denumirea de tipul
insert_language
, se obține denumirea limbii (ca valoare atașată parametruluicurrentLanguage
, inclus în corpul cereriirequest.body['currentLanguage]
), se verifică dacă aceasta nu este conținută în lista de limbi în funcție de care se face filtrarea și, în caz afirmativ, este adăugată la listă, marcându-se faptul că lista de filtre a fost modificată (parametrulfilterChange
de tipboolean
); - pentru parametrii având denumirea de tipul
delete_language_{language_name}
, se obține denumirea limbii (prin parsarea denumirii parametrului), se verifică dacă aceasta este conținută în lista de limbi în funcție de care se face filtrarea și, în caz afirmativ, este ștearsă din listă, marcându-se faptul că lista de filtre a fost modificată (parametrulfilterChange
de tipboolean
).
Î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.
</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:
- se determină identificatorul formatului de prezentare a cărții și cantitatea corespunzătoare:
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')); }
<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]; }
bookPresentationId
, respectiv quantity
prin care se indică faptul că trebuie realizată o procesare la nivelul coșului de cumpărături.
request.body[parameter]
.
- se iterează asupra conținutului coșului de cumpărături:
- dacă se identifică un element având același identificator al formatului de prezentare a cărții:
- în situația în care cantitatea este nenulă, aceasta este modificată;
- în situația în care cantitatea este nulă, aceasta este ștearsă;
- dacă nu se identifică un element având același identificator al formatului de prezentare a cărții, acesta este adăugată.
Obiectul shoppingCart
, transmis prin intermediul sesiunii, conține următoarele proprietăți:
bookPresentationId
: identificatorul formatului de prezentare a cărții;quantity
: cantitatea;price
: prețul;title
: titlul;format
: formatul de prezentare;language
: limba.
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:
- interogarea propriu-zisă;
- parametrii interogării (opțional);
- o metodă de callback care primește ca rezultat o eroare (în situația în care aceasta se produce) respectiv rezultatul.
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 };
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
:
- în cazul în care lista nu este vidă, se afișează conținutul coșului de cumpărături;
- în cazul în care lista este vidă, se afișează mesajul The shopping cart is empty!.
<% 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:
remove_from_shopping_cart.png
;shopping_cart_accept.png
;
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" /> <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:
- se adaugă o înregistrare în tabela
invoice_header
; se va folosi metodaconnection.query
care va primi ca parametri interogarea propriu-zisă și metoda de callback prin care aceasta va fi realizată asincron; valorile ce trebuie stocate în tabelă sunt:- numărul de identificare (câmpul
identification_number
) va fi generat aleator folosind funcțiagenerateIdentificationNumber()
, care primește ca parametri numărul de caractere de tip literă (3), respectiv cifră (6); - data de emitere (câmpul
issue_date
) - ziua curentă - va fi obținută folosind funcția MySQL CURRENT_DATE(); - starea (câmpul
state
) are valoareaissued
; - identificatorul utilizatorului (câmpul
user_id
) va fi preluat prin intermediul sesiuniirequest.session.user_id
;
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 + '\')';
- se iterează asupra conținutului coșului de cumpărături (obiectul
shoppingCart
):- se obține identificatorul formatului de prezentare a cărții (atributul), cantitatea (valoarea) și stocul corespunzător, așa cum este reținut în tabela
book_presentation
(prin apelul metodeiconnection.query
ce utilizează interogareaSELECT bp.stockpile AS stockpile FROM book_presentation bp WHERE bp.id = ?
); - se compară numărul de exemplare din coşul de cumpărături cu stocul existent şi în situația în care comanda poate fi satisfăcută, se actualizează stocul (se apelează metoda
connection.query
ce utilizează interogareaUPDATE book_presentation bp SET bp.stockpile = ' + (stockpile - currentQuantity) + ' WHERE bp.id = ?
); se adaugă o înregistrare în tabelainvoice_line
; se va folosi metodaconnection.query
care va primi ca parametri interogarea propriu-zisă și metoda de callback prin care aceasta va fi realizată asincron; valorile ce trebuie stocate în tabelă sunt:- identificatorul facturii (câmpul
invoice_header_id
), determinat pe baza rezultatului interogăriiINSERT
pentru tabelainvoice_header
(var invoiceHeaderId = result_insert_invoice_header.insertId;
); - identificatorul formatului de prezentare a cărții (câmpul
book_presentation_id
); - cantitatea (câmpul
quantity
);
- altfel, se adaugă un mesaj de eroare care va fi afișat în cadrul paginii (transmis prin intermediul parametrului
errorMessage
);
- se goleşte coşul de cumpărături.
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 + '\')';
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 distruge sesiunea, folosind metoda
destroy
ce primește ca parametru o metodă de callback prin intermediul căreia se transmite dacă s-a produs o eroare:request.session.destroy(function (error) { if (error) { console.log('An error has occurred: %s', error); } });
- se transferă contextul către pagina
login
, având parametrulerror
cu valoarea șirul vid:response.render('login', { error: '' }); return;
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)