Tehnici Web

Laboratorul 12

(Deadline: -)

Daca nu sunteti logati exercitiile nu se mai afiseaza.

Node

Nota inainte: informatiile sunt prezentate intr-o combinatie de teorie+tutorial. Ideal ar fi sa urmariti teoria si sa aplicati ce invatati in proiect, pe masura ce parcurgeti lectia. Daca e ceva ce nu vi se pare suficient explicat, va rog sa imi spuneti si se rezolva.

Introducere

//TO DO ce este node, cand a aparut cine l-a facut, istoric etc

Puteti testa usor anumite in functii in consola REPL (Read-Eval-Print-Loop) a node-ului. O porniti scriind direct comanda: node Din consola REPL se poate iesi cu comanda .exit. PS C:\Users\irina> node
> 1+2
3
> a=5; b=6; a+b
11
> new Date()
2020-01-02T14:17:58.791Z
> d=new Date(); d.toString()
'Thu Jan 02 2020 16:18:19 GMT+0200 (Eastern European Standard Time)'
> o={a:10,b:20}
{ a: 10, b: 20 }
> o.a=30
30
> o
{ a: 30, b: 20 }
> .exit

Dupa cum vedeti, consola afiseaza rezultatul evaluarii expresiei primite ca parametru, fara a necesita o functie de afisare.

De asemenea consola tine mine variabilele definite la un rand anterior, asa cum se poate observa si in outputul prezentat.

Atentie, e posibil in Power Shell cand vreti sa afisati rezultatul lui new Date() sa nu vedeti nimic, deoarece culoarea de text este setata sa fie acelasialbastru ca si culoarea de background a consolei (in cmd se vede, de exemplu, fiind alt background). Puteti seta culoarea de background a consolei de Power Shell cu click-dreapta pe bara de sus, apoi mers la Preferences->Colors si "Screen Background" pus pe negru.

Pentru a afisa ceva, prin program, in consola in care e rulata aplicatai folosim console.log().

Obiectele request si response

Cand un client cere o anumita resursa de la server (de exemplu, o pagina web, o imagine, un xml etc) spunem ca face un request. Obiectul de tip request cuprinde toate datele cererii primite de la client. Din el putem prelua, de exemplu, datele introduse de utilizator intr-un formular. Request-ul este primit de catre web server, este procesat, si serverul raspunde cu ajutorul unui obiect de tip response. Obiectul de tip response este practic reprezentarea mesajului de tip HTTP oferit de serverul nostru. Putem seta anumite campuri ale acestuia, cum ar fi headerele raspunsului, sau codul intors de server (statusCode), de exemplu 200 (pentru succes), 404 (pentru resursa negasita) etc.

Gasiti exemple in sectiunea despre express.

Variabile globale predefinite

Le vom mentiona pe cele mai utile:

  • __dirname - contine calea catre folderul modulului curent. Daca il folositi in fisierul javascript principal (cu aplicatia) va fi, evident, chiar folderul acelui fisier. Puteti verifica prin: console.log(__dirname);
  • __filename - contine calea catre fisierul curent. Puteti verifica prin: console.log(__filename);
  • Le veti folosi mai tarziu, de exemplu in rutare sau in specificarea cailor statice.

    Atentie, deoarece depind de modul, nu puteti sa le folositi direct in consola REPL, se presupune ca liniile cu console.log() de mai sus au fost scrise intr-un program.

Module si npm

Aproape orice aplicatie de tip node foloseste module, deoarece node-ul efectiv cu modulele predefinite a fost gandit doar ca o baza pe care sa fie rulate alte module. Nu este eficient sa se scrie aplicatii "doar in node" si modulele lui predefinite, deoarece codul ar deveni foarte complex si greu de administrat si modificat.

Pentru a crea un proiect(aplicatie) in node, trebuie stabilit intai un folder in care se vor gasi toate fisierele aplicatiei. Vom numi acest folder, folderul radacina a la site-ului. Pentru o buna organizare si gasire usoara a resurselor site-ului, toate fisierele folosite de site ar fi bine sa se gaseasca in folderul-radacina al site-ului.

Pentru a initializa modulul nostru si pentru a instala modulele care nu sunt predefinite, vom folosi utilitarul npm. Acesta este instalat odata cu framework-ul node.

In cadrul folderului aplicatiei vom rula: npm init Aceasta instructiune are rolul de a crea fisierul package.json care contine diverse date despre aplicatie.

Pentru comenzi putem folosi de exemplu cmd sau PowerShell. Apoi ca sa ne mutam in folderul aplicatiei, folosim comanda cd. Totusi, ca sa nu ne complicam cu scrierea caii proiectului, putem sa deschidem folderul proiectului in windows explorer si sa dam Shift+(click-dreapta). Aceasta combinatie va deschide meniul extins. Atentie, sa nu aveti vreun fisier selectat, fiindca va va da meniul pentru fisier. Din meniul extins alegeti comanda "Open cmd window here" sau "Open PowerShell window here" (sau ceva similar, in functie de versiunea de windows instalata si limba setata).

Aveti mai jos un exemplu de rulare. Valorile din paranteza sunt valorile predefinite, care pot fi suprascrise daca vom completa randul respectiv cu informatia dorita. La apasarea tastei enter, se trece la campul urmator. PS D:\site\tehnici_web_2018\exemplu_node\aplicatie_noua> npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (aplicatie_noua)
version: (1.0.0)
description: Exemplu de aplicatie node
entry point: (index.js)
test command:
git repository:
keywords: node, exemplu
author: Irina Ciocan
license: (ISC)
About to write to D:\site\tehnici_web_2018\exemplu_node\aplicatie_noua\package.json:

{
  "name": "aplicatie_noua",
  "version": "1.0.0",
  "description": "Exemplu de aplicatie node",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "node",
    "exemplu"
  ],
  "author": "Irina Ciocan",
  "license": "ISC"
}


Is this OK? (yes)

In cazul de fata am lasat valorile predefinite pentru numele pachetului, versiune, etc. si am setat propriile mele valori pentru descriere, cuvinte cheie, autor.

Dupa initializare, putem gasi in folderul proiectului un fisier de tip json numit package.json care are drept continut chiar jsonul din afisajul de mai sus. Puteti oricand sa modificati manual campurile din el.

Valoarea din dreptul campului main este numele fisierului js principal (cel care contine aplicatia, se ocupa de crearea serverului si de procesarea cererilor si trimiterea raspunsurilor. In cazul acesta se numeste index.js.

Pentru modulele pe care dorim sa le instalam mai departe, scriem: npm install --save nume_modul Toate modulele astfel instalate vor aparea in lista de dependente din fisierul package.json.

Creem un fisier javascript cu numele dat in entry point. Acesta va fi codul principal al serverului. Dupa ce il vom completa cu cod, il vom putea rula cu: node index.js Evident, ce urmeaza dupa comanda node este calea relativa (dar poate fi si absoluta) a fisierului-server. Fiind chiar in folderul aplicatiei, calea relativa este chiar numele fisierului.

Vom da un exemplu pur didactic de script pentru index.js. In mod obisnuit nu se lucreaza direct cu node ci cu diverse module care au deja multe functionalitati gata implementate: const http = require('http');

http.createServer((request, response) => {
    request.on('data', (date) => {
        console.log(date);
    }).on('error', (eroare) => {
        console.error(eroare);
    }).on('end', () => {
        console.log("Am primit o cerere");
        response.statusCode = 200;
        response.setHeader('Content-Type', 'text/html');
        
        response.write("<html><body>O pagina cu node</body></html>");
        response.end();
        
        console.log("Am trimis un raspuns");
    });
}).listen(8080);

console.log('Serverul a pornit pe portul 8080');

Putem deja sa vedem o prima cerere catre serverul nostru, scriind in bara de adrese: http://localhost:8080 Vom vedea in browser raspunsul HTML trimis in urma cererii, iar in consola cele doua afisari din console.log().

Numarul 8080 este portul pe care asculta aplicatia.

Nu voi intra in detalii prea multe cu privire la cod deoarece vom lucra in express.

Pentru a opri serverul dam ctrl+c in consola.

Modulul nodemon

Daca facem modificari in resursele aplicatiei (fisiere html, imagini etc) aceste modificari vor fi vazute fara a restarta serverul. Daca insa, modificam fisierul js al aplicatiei, obligatoriu trebuie sa repornim serverul.

Modulul nodemon ne scuteste de aceste reporniri la fiecare modificare, repornind el singur aplicatia.

Pagina oficiala a modulului este https://nodemon.io/

Pentru instalare folosim npm si instalam modulul global, cu optiunea -g npm install -g nodemon Din acest moment in loc sa pornim aplicatia cu node o pornim cu nodemon: nodemon index.js

Express

In marea majoritate a aplicatiilor de node se foloseste modulul express pentru crearea serverelor. De ce? Este un framework care usureaza foarte mult implementarea serverului (cod mai concis si mai clar).

Instalare:

npm install --save express

Procesarea cererilor. Crearea de rute

Rutele pe care le cream in express reprezinta de fapt modul de procesare al cereriiin functie de tipul ei si de resursa ceruta. Tipurile de cereri sunt date de asa numitele verbe HTTP (din care le ilustram pe cele mai importante):

  • GET - care e folosit de obicei pentru a obtine date/resurse de pe server (corespondent in express: metoda get())
  • POST - care e folosit de obicei pentru a transmite date catre server (corespondent in express: metoda post())
  • PUT - care e folosit de obicei pentru a transmite date intr-un anumit context/locatie. Comportamentul dorit se aseamana unui update, de exemplu daca dorim sa actualizam niste date existente pe server (corespondent in express: metoda put())
  • DELETE - care e folosit de obicei pentru a sterge date/resurse de pe server (corespondent in express: metoda delete())

Toate cele patru metode de rutare au aceeasi sintaxa. Consderand app ca fiind obiectul de tip server express, sintaxa va fi: app.metoda(cale_ruta, callback) Se pot specifica mai multe metode callback: app.metoda(cale_ruta, callback_1, callback_2, ..., callback_n )

Primul parametru este o expresie regulata. Astfel, putem specifica ruta ca sir exact sau sub forma unui format care trebuie indeplinit.

Al doilea parametru este o functie middleware care are trei parametri (ale caror valori sunt setate de frameworkul node):

  1. obiectul de tip request
  2. obiectul de tip response
  3. functia middleware urmatoare

In general se folosesc doar primii doi parametri fiindca rar dorim sa trecem la alt middleware odata ce s-a raspuns la cerere.

De exemplu: app.get('/pagina', function(request, response) {
    //procesare request
    response.sendFile('html/pagina');
});
Mai sus practic transmitem fisierul aflat la aceeasi cale indicata de ruta.

Un alt exemplu in care folosim o expresie regulata, asteptand cereri de forma "pagina_"+numar. La primirea unei astfel de cereri, in loc sa transmitem un fisier, trimitem doar un text de forma "Ai cerut pagina cu numarul", urmata de numarul din cerere: const express = require('express');
var app = express();

app.get(/^\/pagina_\d*$/,function (req, res) {
  res.send("Ai cerut pagina cu numarul "+ req.path.substring(req.path.indexOf("_")+1));  
 
});


app.use(function(req,res,next){
    console.log("procesarea ignorata de toti care sta singura in camera si plange.");
});

app.listen(8080)
console.log('Serverul a pornit pe portul 8080');
Daca scriem in bara de adrese, de exemplu: http://localhost:8080/pagina_123 Se va afisa in viewport: Ai cerut pagina cu numarul 123

Daca dorim sa facem o ruta chiar catre radacina, folosim doar "/" pe post de ruta: app.get('/', function(request, response) {
    //procesare request
    response.sendFile('html/index.html');
});

Redirectari

Uneori dorim sa redirectionam cererea unui utilizator catre alta pagina decat cea ceruta. Motive posibile:

  • nu e autorizat sa acceseze pagina
  • site-ul este in lucru si orice cerere este redirectionata spre o pagina cu un mesaj de instiintare
  • cererea e catre o versiune mai veche a paginii si clientul e redirectat catre o versiune mai noua
  • etc.

Redirectionarea se poate face si client-side insa daca necesitatea ei este critica, nu e recomandat sa ne bazam pe o redirectionare facuta din browser, deoarece poate fi dezactivata de catre client.

Pentru a realiza o redirectionare folosim metoda redirect: app.get('/pagina', function(req, res) {     res.redirect('/nope_alta_pagina'); });

Metoda use()

Pentru a intelege functionalitatea ei, trebuie sa intelegem intai conceptul de middleware. Middleware se foloseste pentru a adauga procesare si functionalitati noi pe obiectele request si response. In node, ceea ce numim middleware sunt niste functii care primesc, ca si functiile callback din metodele de rutare, trei argumente:

  1. obiectul de tip request
  2. obiectul de tip response
  3. functia middleware urmatoare

Valorile celor trei parametri vor fi transmise in mod automat de frameworkul node

Ca sa setam un astfel de middleware, folosim: app.use(referinta_functie)

Atentie, ordinea de setare a functiilor conteaza, deoarece procesarile se fac in ordinea in care au fost definite: const express = require('express');
var app = express();

app.use(function(req,res,next){
    console.log("procesarea 1 spune hey! catre "+req.path);
    next();
});

app.use(function(req,res,next){
    console.log("procesarea 2 spune hey-yey! catre "+req.path);
    next();
});

app.get("/*",function (req, res, next) {
  res.sendFile(__dirname+"/"+req.path);  
  console.log("trimitere raspuns catre "+req.path);
  next();
});

app.use(function(req,res,next){
    console.log("procesarea 3 spune salut! catre "+req.path);
});

app.use(function(req,res,next){
    console.log("procesarea 4, ignorata de toti, care sta singura si plange.");
});


app.listen(8080)
console.log('Serverul a pornit pe portul 8080');

Pentru codul de mai sus, in urma accesarii in browser a adresei: http://localhost:8080/ceva.html" avem outputul: PS D:\site\tehnici_web_2018\exemplu_node\mini_proiect_testare> node exemplu_app_use.js
Serverul a pornit pe portul 8080
procesarea 1 spune hey! catre /ceva.html
procesarea 2 spune hey-yey! catre /ceva.html
trimitere raspuns catre /ceva.html
procesarea 3 spune salut! catre /ceva.html
Observam ca la procesarea 4 nu se mai ajunge niciodata, deoarece la procesarea 3 nu s-a mai apelat functia next() (care ar fi trecut la urmatorul middleware).

Observatie, daca am sterge apelul functiei next() de la procesarea 2 nu s-ar mai ajunge nici macar la get, ceea ce ar face ca pagina sa nu se mai afiseze deloc.

Atentie, procesarile se fac pentru absolut orice cerere, inclusiv pentru fisiere-resurse cerute de catre pagina html. De exemplu, daca pagina noastra are un fiser css extern (stil.css) si un fisier js extern (script.js) vom avea afisarea: PS D:\site\tehnici_web_2018\exemplu_node\mini_proiect_testare> node exemplu_app_use.js
Serverul a pornit pe portul 8080
procesarea 1 spune hey! catre /ceva.html
procesarea 2 spune hey-yey! catre /ceva.html
trimitere raspuns catre /ceva.html
procesarea 3 spune salut! catre /ceva.html
procesarea 1 spune hey! catre /script.js
procesarea 2 spune hey-yey! catre /script.js
trimitere raspuns catre /script.js
procesarea 3 spune salut! catre /script.js
procesarea 1 spune hey! catre /stil.css
procesarea 2 spune hey-yey! catre /stil.css
trimitere raspuns catre /stil.css
procesarea 3 spune salut! catre /stil.css

Fisiere si directoare statice

Vom folosi middleware-ul express.static. Prin intermediul express.static nu putem servi fisiere statice de sine statatoare, acesta primeste ca parametru doar un folder (caz in care toate fisierele din acel folder vor fi considerate statice).

Sa presupunem ca avem in directorul-radacina al aplicatiei folderul imagini. Am vrea sa nu trecem fisierele respective prin vreo procesare, de exemplu folosind app.get(), ci pur si simplu sa fie livrate asa cum se gasesc pe server.

Putem defini o ruta statica catre folderul imagini astfel: app.use("/imagini",express.static(path.join(__dirname, 'imagini'))); In felul acesta, orice cerere de tip "/imagini/fisier" va fi tratata static.

Putem scrie si fara a da primul parametru: app.use(express.static(path.join(__dirname, 'imagini'))); caz in care, desi fisierul se afla in folderul imagini, putem face o cerere de forma "/fisier". Chiar daca acesta se gaseste in folderul imagini va fi gasit cu ajutorul rutei statice. Totusi aceasta forma de scriere nu este foarte indicata deoarece putem intra in conflict de nume cu un fisier care se afla chiar in radacina, sau alte fisiere cu acelasi nume, aflate in alte foldere statice.

De exemplu, sa zicem ca avem cate un fisier numit imagine.png atat in folderul images cat si in folderul uploads, dar cele doua fisiere desi au acelasi nume, sunt diferite. Daca avem in cod: app.use(express.static(path.join(__dirname, 'imagini')));
app.use(express.static(path.join(__dirname, 'uploads')));
si facem cererea: http://localhost:8080/imagine.png vom primi imaginea din primul folder, adica imagini

Daca in schimb inversam liniile de cod: app.use(express.static(path.join(__dirname, 'uploads')));
app.use(express.static(path.join(__dirname, 'imagini')));
Vom primi imagine.png din directorul uploads.

Deci cel mai indicat este sa setam si primul parametru cu formatul cererii. app.use("/imagini",express.static(path.join(__dirname, 'imagini'))); Putem astfel chiar sa si "mascam" numele directorului, punand un alias in parametrul-request si folosind acel alias in cererile de fisiere: app.use("/alias_imagini",express.static(path.join(__dirname, 'imagini'))); astfel cererile toate le vom formula: http://localhost:8080/alias_imagini/imagine.png

Observatie importanta! Un bug frecvent este sa nu plasati rutele statice la locul bun din program. De exemplu, ce credeti ca se va intampla la cererea http://localhost:8080/alias_imagini/imagine.png daca avem codul de mai jos, iar imaginea se afla in folderul imagini app.get("/*",function (req, res, next) {
  res.sendFile(__dirname+"/"+req.path);  
  console.log("trimitere raspuns catre "+req.path);
  next();
});

app.use("/alias_imagini",express.static(path.join(__dirname, 'imagini')));//GRESIT!!!!!
Va trece intai prin app.get() si negasind fisierul de trimis (pentru ca nu se afla in "/alias_imagini/imagine.png" asa cum dam parametrul in sendFile, nu vom primi fisierul. Deci corect este sa avem definite intai rutele statice: app.use("/alias_imagini",express.static(path.join(__dirname, 'imagini')));//GRESIT!!!!!

app.get("/*",function (req, res, next) {
  res.sendFile(__dirname+"/"+req.path);  
  console.log("trimitere raspuns catre "+req.path);
  next();
});

Best practice! Pentru a nu defini manual drept static fiecare director cu resurse, puteti sa puneti toate directoarele cu resurse (directorul css, directorul cu imagini etc) intr-un singur director, si sa il declarati static pe acela.

Pagina custom 404

In primul rand va trebui sa cream o pagina html pe care dorim sa o afisam in cazul in care utilizatorul cere in bara de adrese o pagina inexistenta. Sa presupunem ca avem deja html-ul respectiv, acesta numindu-se 404.html.

Trimiterea paginii 404.html ar trebui definita ultima, dupa toate rutele, fiindca la ea ar trebui sa se ajunga numai daca nu s-a intrat pe vreo ruta anterioara. Vom folosi o functie middleware in care daca se intra, pur si simplu trimite pagina 404: app.use(function(req,res){
    res.status(404).sendFile('html/404');
});
Puteti folosi si varianta cu render, daca ati setata un view engine: res.status(404).render('html/404');

Preluarea datelor din formular. Modulul formidable

Am ales prezentarea modulului formidable deoarece cu el putem accesa usor atat datele de tip text cat si fisierele din inputurile de tip file.

Salvarea datelor din formular (in fisiere JSON)

In general, datele corespunzatoare unei aplicatii sunt stocate intr-o baza de date. Dar, pentru date putine putem folosi si fisiere JSON sau XML. Despre sintaxa JSON puteti citi pe w3schools.

Sa presupunem ca avem un formular, de exemplu pentru introducerea datelor unor studenti (presupunem ca pagina se numeste inregistrare_studenti): <form class="date" method="post" enctype="multipart/form-data">
    <p>
    <label>
        Nume: <input type="text" name="nume" value="Zumzulesteanu">
    </label>
    </p>
    <p>
    <label>
        Prenume: <input type="text" name="prenume" value="George">
    </label>
    </p>
    <p>
    <label>
        Grupa: <input type="text" name="grupa" value="100">
    </label>
    </p>
    <p>
    <label>
        Poza: <input type="file" id="poza" name="poza">
    </label>
    </p>
    <p>            
    <input type="submit" value="Submit">
    </p>
</form>
Deoarece formularul cuprinde si un input de tip file enctype a fost setat la valoarea multipart/form-data (fara inputuri de tip fisier, cel mai eficient ar fi fost sa il setati la valoarea application/x-www-form-urlencoded.

Metoda de transmitere a datelor este de tip "post", astfel ca va fi nevoie sa facem o ruta de tip post catre inregistrare_studenti: app.post('/inregistrare_studenti', function(req, res) {
...
});
In interiorul functiei de procesare a cererii va trebui sa preluam si sa prelucram datele transmise. Pentru a crea in node un obiect de tip formular (care de fapt ajuta la procesare) vom folosi clasa IncomingForm din modulul formidable: var form = new formidable.IncomingForm();

Pentru a parsa datele din cererea post, vom folosi metoda parse a obiectului de tip formular. Aceasta primeste ca parametrii cererea si o functie callback care va prelucra datele, dupa parsare: form.parse(req, function(err, fields, files) {
...
});

Primul parametru, notat cu err primeste o valoare doar in caz de eroare, dei putem testa eventuale probleme cu: if (err) {
//proceseaza eroarea
});

In parametrul al doilea, notat cu fields, avem toate campurile din formular (cu exceptia celor de tip fisier). Campurile se regasesc in proprietatile obiectului fields. Astfel ca, daca avem un input cu name-ul prenume, va exista o proprietate prenume a lui fields avand ca valoare cea introdusa de utilizator in input.

Uploadul fisierelor

Datele pentru inputurile de tip file se regasesc in parametrul al treilea, notat aici cu files. Fiecare name de input de tip file devine proprietate a lui files. In cazul nostru avem un singur astfel de input, deci datele fisierului se gasesc in files.poza.

.

Calea la care uploadam fisierele poate fi setata in doua moduri:

  • fie folosind proprietatea uploadDir a obiectului de tip IncomingForm: form.uploadDir = "/uploads"; dar daca ne folosim strict de aceasta proprietate numele fisierelor uploadate vor ramane aceleasi cu cele date de utilizator.
  • fie folosind proprietatea path a obiectului de tip fisier. Aceasta proprietate se seteaza in evenimentul fileBegin (eveniment declansat la detectarea unui inceput de upload) form.on('fileBegin', function (name, file){
        file.path = __dirname + '/uploads/' + file.name;
    });
    In exemplul de mai sus, calea este tot uploads si se pastreaza numele initial al fisierului (care se gaseste in file.name), dar, in cazul de fata, putem sa setam noi orice nume dorim.

Uneori dorim sa aflam cand s-a terminat uploadul unui fisier, de exemplu ca sa obtinem anumite informatii, cum ar fi dimensiunea fisierului uploadat. Pentru asta vom folosi evenimentul file. form.on('file', function (name, file){
    console.log('Uploadat ' + file.name+' cu dimensiunea '+file.size);
});

Sa presupunem ca vrem sa salvam datele de mai sus intr-un JSON. Ideal ar fi ca pentru fiecare entitate salvatasa avem un identificator unic, ca sa putem referi usor acea entitate (in cazul de fata entitatea fiind reprezentata de datele studentului). Putem alege drept id:

  • un numar care creste la fiecare noua entitate adaugata (de exemplu prima entitate va avea id-ul 1, a doua 2 etc.)
  • un timestamp (cu momentul adaugarii)
  • un numar aleator (dar aici trebuie verificat daca numarul nu exista deja, fiindca exista totusi probabilitatea de a se genera doua numere aleatore identice)

Vom folosi prima varianta; cea cu numarul care creste la fiecare entitate adaugata. Va fi nevoie sa il salvam in JSON. Nu il putem deduce din entitati deoarece ordinea lor poate fi modificata, sau putem sterge uneori entitati (deci nu ne putem ghida, de exemplu, dupa numarul lor). VOm considera drept nume pentru id: nextId. De asemenea, ne trebuie un camp in JSON in care sa memoram vectorul de entitati; sa-l numim studenti.json. Vom crea un JSON initial, fara entitati deja adaugate, dar care sa aiba deja formatul discutat: {
  "nextId": 1,
  "studenti": []
}
Am dori, dupa ce adaugam, de exemplu, trei studenti, JSON-ul sa arate asa: {
  "nextId": 4,
  "studenti": [
    {
      "id": 1,
      "nume": "Tache",
      "prenume": "Ion",
      "grupa": "123",
      "poza": "profil.png"
    },
    {
      "id": 2,
      "nume": "Popescu",
      "prenume": "Gigel",
      "grupa": "321",
      "poza": "gigel.png"
    },
    {
      "id": 3,
      "nume": "Gogulescu",
      "prenume": "Gogu",
      "grupa": "123",
      "poza": "buletin.png"
    }
  ]
}

Pentru a face asta, ar trebui sa preluam continutul fisierului in program, sa il transformam in obiect JavaScript, si apoi pur si simplu sa adaugam un nou element in vectorul de entitati.

Ne facem intai o referinta la obiectul corespunzator modulului fs (file system): const fs = require('fs');

Pentru asta, vom citi continutul fisierului si il vom parsa: let rawdata = fs.readFileSync('studenti.json'); let jsfis = JSON.parse(rawdata); In acest moment, in variabila jsfis avem obiectul corespunzator JSON-ului din fisier.

Obiectul jsfis are proprietatile nextId (numar) si studenti(vector). Vom adauga in vectorul din proprietatea studenti un nou obiect cu datele din formular: var calePoza=(files.poza && files.poza.name!="")?files.poza.name:"";
jsfis.studenti.push({id:jsfis.nextId, nume:fields.nume, prenume:fields.prenume, grupa: fields.grupa, poza: calePoza})
neuitand sa incrementez id-ul pentru urmatoarea adaugare: jsfis.nextId++;

Avand obiectul actualizat, trebuie sa il salvam inapoi in fisier, suprascriind continutul vechi: let data = JSON.stringify(jsfis);//transform in sir
fs.writeFileSync("studenti.json", data);//scriu in fisier

Astfel, codul complet pentru preluarea cererii post ar fi: app.post('/inregistrare_studenti', function (req, res) {
    var form = new formidable.IncomingForm();// obiect de tip form cu care parsez datele venite de la utilizator
    form.parse(req, function(err, fields, files) {
        var calePoza=(files.poza && files.poza.name!="")?files.poza.name:""; //verific daca exista poza (poza este numele campului din form
        let rawdata = fs.readFileSync('studenti.json');//citesc fisierul si pun tot textul in rawdata
        let jsfis = JSON.parse(rawdata);//parsez textul si obtin obiectul asociat JSON-ului
        jsfis.studenti.push({id:jsfis.nextId, nume:fields.nume, prenume:fields.prenume, grupa: fields.grupa, poza: calePoza});//adaug elementul nou
        jsfis.nextId++;//incrementez id-ul ca sa nu am doi studenti cu acelasi id
        let data = JSON.stringify(jsfis);//transform in sir
        fs.writeFileSync("studenti.json", data);//scriu in fisier
        res.render('html/date_introduse');//afisez o pagina cu un mesaj de date introduse (sau pot sa trimit utilizatorul tot catre formular)
    });

    form.on('fileBegin', function (name, file){
        file.path = __dirname + '/uploads/' + file.name;//inainte de upload setez calea la care va fi uploadat
    });

    form.on('file', function (name, file){
        console.log('Uploadat ' + file.name+' cu dimensiunea '+file.size);//la finalul uploadului afisez un mesaj
    });
});

Trimiterea de mailuri

Se va folosi modulul nodemailer. Pentru instalare scrieti: npm install nodemailer --save

Pentru a trimite un mail trebuie sa folosim un server SMTP. Putem sa ne instalam noi unul, de exemplu pe acelasi dispozitiv pe care ruleaza si serverul. Aveti un astfel de exemplu la: https://nodemailer.com/extras/smtp-server/. Sau putem sa folosim un cont de email existent si sa folosim serverul SMTP extern, asociat acelui cont.

Vom prezenta o solutie folosind un cont de gmail. Incepeti prin a va face un cont de gmail special pentru aplicatie. Eu voi folosi contul test.tweb.node@gmail.com.

Ca sa putem trimite mailuri de pe acest cont, printr-o aplicatie, va trebui sa relaxam setarile de securitate ale contului. Mergeti pe profil (click pe cerculetul din dreapta sus si intrati pe "Manage your Google Account":
[printscreen setari] Intrati apoi pe Security si mergeti pana la optiunea "Less secure app access", pe care o setati pe On (implicit e Off), asa cum vedeti si in printscreen-ul de mai jos:
[printscreen setari]
Puteti ajunge direct la acest box si prin linkul: https://myaccount.google.com/lesssecureapps

Vom crea apoi o functie trimiteMail(adresa) care va trimite un mail la adresa specificata in parametru din partea adresei de gmail stabilite mai devreme.

Pasii sunt:

  1. Crearea unui obiect transportor cu datele de autentificare pentru adresa de mail de pe care trimitem mesajul
  2. Apelarea metodei sendMail a transportorului, in care specificam:
    • expeditorul
    • destinatarul
    • subiectul mesajului
    • continutul mesajului in plain text
    • continutul mesajului in format HTML
  3. Apelarea functiei trimiteMail in urma unei actiuni a utilizatorului (de exemplu: s-a inregistrat pe site, a plasat o comanda, a completat un formular de contact si vrem sa ii trimitem o confirmare etc.)

Vom crea intai un obiect de tip nodemailer: const nodemailer = require("nodemailer");

Un mod de implementare a functiei de trimitere a e-mailului gasiti mai jos. Puteti adauga paramteri in plus pentru diverse alte date pe care le doriti incluse in e-mail. async function trimiteMail(email) {
    let transporter = nodemailer.createTransport({
        service: 'gmail',

        secure: false,
        auth: {
            user: "test.tweb.node@gmail.com", //mailul site-ului (de aici se trimite catre user)
            pass: "tehniciweb"
        },
        tls: {
            rejectUnauthorized: false//pentru gmail
        }
    });

    //trimitere mail
    let info = await transporter.sendMail({
        from: '"test.tweb.node" <test.tweb.node@example.com>',
        to: email,
        subject: "Salut",
        text: "Ce mai faci?",
        html: "<p>Ce mai faci?</p>"
    });

    console.log("Mesaj trimis: %s", info.messageId);
}

Pagini cu continut generat. Modulul EJS

Pentru instalarea modulului EJS dam comanda: npm install ejs --save

Pentru a invata mai usor EJS, puteti folosi https://ionicabizau.github.io/ejs-playground/ unde puteti testa diverse instructiuni.

Pentru a folosi modulul ejs in randarea paginilor, trebuie sa il setam ca view engine: app.set('view engine', 'ejs');

Din acest moment, in mod default paginile sunt preluate din folderul views aflat in radacina aplicatiei. De asemenea, tot in mod default, acestea ar trebui sa aiba extensia ejs.

La ce am putea folosi modulul EJS?

  • Includerea anumitor fragmente care se repeta in mai multe pagini (precum headerul si footerul)
  • Afisarea informatiilor dintr-o pagina in functie de o anumita conditie
  • crearea de template-uri si folosirea lor

De exemplu, sa presupunem ca vrem sa includem headerul si footerul pe fiecare pagina a site-ului, fara sa le scriem manual in fisierele html. De ce am vrea asta? De obicei headerul si footerul arata la fel pe toate paginile site-ului. Daca le scriem de mana (cu copy-paste), dar apoi vrem sa corectam/modificam ceva, va fi nevoie sa umblam in toate paginile site-ului, ceea ce ar lua mult timp si ar putea genera buguri (de exemplu, uitam sa modificam unul dintre fisiere).

In directorul views din directorul-radacina al aplicatiei este indicat sa cream doua directoare:

  • html (cu paginile efective ale site-ului)
  • fragmente (cu bucatile de html pe care vrem sa le inseram dinamic)

Sa presupunem ca avem urmatorul format de pagina: <!DOCTYPE html>
<html lang="ro">
<head>
    <title>O pagina</title>
    <meta charset="utf-8">
</head>
<body class="container">

<!-- headerul care se repeta pagina de pagina -->
<header>
    <nav>
    <ul class="menu">
            <li><a href="/">Home</a></li>
            <li><a href="/pagina1.html">Pagina1</a></li>
            <li><a href="/pagina2.html">Pagina1</a></li>
        </ul>
    </nav>
</header>

<p>Niste continut specific paginii pe care ne aflam</p>

<!-- footerul care se repeta pagina de pagina -->
<footer>
    <p>Copyright &copy; 2020</p>
</footer>

</body>
</html>

Avand in vedere ca toate cererile trec prin server, nu mai e nevoie sa punem in linkuri si extensia html. Vom modifica linkurile astfel incat sa apara doar numele paginii, de exemplu: <a href="/pagina1">Pagina1</a>

Vrem ca headerul si footerul sa se repete in acelasi format pagina de pagina. In acest caz, vom crea doua fisiere noi (pe care le adaugam in folderul fragmente): header.ejs: <header>
    <nav>
    <ul class="menu">
            <li><a href="/">Home</a></li>
            <li><a href="/pagina1.html">Pagina1</a></li>
            <li><a href="/pagina2.html">Pagina1</a></li>
        </ul>
    </nav>
</header>
si footer.ejs: <footer>
    <p>Copyright &copy; 2020</p>
</footer>

In fiecare pagina din site vom include cele doua fisiere, cu ajutorul instructiunii include: <!DOCTYPE html>
<html lang="ro">
<head>
    <title>O pagina</title>
    <meta charset="utf-8">
</head>
<body class="container">

<!-- headerul care se repeta pagina de pagina -->
<%- include("../fragmente/header") %>

<p>Niste continut specific paginii pe care ne aflam</p>

<%- include("../fragmente/footer") %>

</body>
</html>

Calea din include este relativa la directorul in care se afla fisierul ejs curent (adica directorul html din views): simbolul "../" ne duce in directorul views, si de acolo se coboara in directorul fragmente.

S-a folosit simbolul EJS "<%-" ca sa fie interpetate tagurile si sa nu fie afisata ca atare cu simbolurile <>.

Pentru paginile respective va trebui sa definim in aplicatie niste rute de tip get, in care sa randam pagina. De exemplu strict pentru pagina1 am avea codul: app.get('/pagina1', function(req, res) {
    res.render('/pagina1');
});

Evident, daca pentru toate paginile nu facem decat randarea, fara procesari specifice, putem folosi o expresie regulata prin care sa le selectam pe toate, ca sa nu scriem cate o cerere get pentru fiecare.

Sa presupunem acum ca avem un vector de obiecte, in javascript si dorim sa le afisam intr-un tabel. Evident, cel mai simplu este sa facem asta in mod dinamic, folosind EJS. Sa presupunem ca avem vectorul (care poate fi incarcat de exemplu dintr-o baza de date, dintr-un fisier JSON sau XML): vec_studenti = [
    {
      "id": 1,
      "nume": "Tache",
      "prenume": "Ion",
      "grupa": "123",
      "poza": "profil.png"
    },
    {
      "id": 2,
      "nume": "Popescu",
      "prenume": "Gigel",
      "grupa": "321",
      "poza": "gigel.png"
    },
    {
      "id": 3,
      "nume": "Gogulescu",
      "prenume": "Gogu",
      "grupa": "123",
      "poza": "buletin.png"
    }
]

Dorim sa il transmitem catre o pagina numita "studenti": app.get('/studenti', function(req, res) {
    res.render('/studenti',{studenti:vec_studenti});
});
Functia render primeste in al doilea parametru un obiect cu date care pot fi folosite in afisare. Toate proprietatile obiectului respectiv ajung in fisierul ejs sa fie proprietatile obiectului locals. Putem sa folosim proprietatile direct cu numele lor, insa daca vrem sa verificam, de exemplu, ca a fost definita o anumita proprietate (daca folosim in EJS o variabila nedefinita vom primi o eroare), trebuie sa folosim locals.proprietate. <% if (locals.studenti) { %>
... afisare studenti ...
<% } else { %>
    <p>Nu avem studenti de afisat</p>
<% } %>
Acoladele sunt obligatorii chiar si atunci cand avem o singura instructiune (cum e in cazul lui else). Acest fapt se aplica pentru orice tip instructiune conditionala sau repetitiva: if...else, do...while, while, for, switch.

Daca stim ca proprietatea lui locals exista, o putem folosi direct cu numele ei. De exemplu daca vrem sa afisam studentii din vectorul studenti transmis catre pagina, am avea un for: <% for (var i = 0; i < studenti.length; i++) { %>
...afiseaza date studenti...
<% } %>
Pentru fiecare student ar trebui sa generam codul html corespunzator unui rand din tabel.

Codul complet pentru afisarea studentilor in tabel este: <% if (locals.studenti) { %>
    <table>
        <tr>    
            <th>id</th>
            <th>Nume</th>
            <th>Prenume</th>
            <th>Grupa</th>
            <th>Imagine</th>
        </tr>
        <% for (var i = 0; i < studenti.length; i++) { %>
            <tr>
                <td><%= studenti[i].id %></td>
                <td><%= studenti[i].nume %></td>
                <td><%= studenti[i].prenume %></td>
                <td><%= studenti[i].grupa %></td>

                <td><img src="/uploads/<%= studenti[i].poza %>" alt="nu are poza"/></td>
            </tr>    
        <% } %>
    </table>
<% } else { %>
    <p>Nu avem studenti de afisat</p>
<% } %>

Sa presupunem acum ca vrem sa afisam studentii intr-un format care sa se repete si pe alte pagini. Am vrea sa nu dam copy-paste formatului respectiv. Putem aplica ce am invatat mai devreme la repetarea headerului si a footerului.

Vom crea un fisier numit template_student.ejs cuprinzand codul pentru afisarea unui singur student: <div class="template_user">
    <section class="student">
        <h2>Student: <%= student.nume %></h2>
        <img src="/uploads/<%= student.poza %>" alt="[<%= student.nume %> nu are poza]" />
        <p id="id_student">Id: <%= student.id %></p>        
        <p id="grupa">Id: <%= student.grupa %></p>
    </section>
</div>
Catre el al trebui cumva sa transmitem informatiile pentru un singur student. Putem face asta cu un for asemanator celui de mai devreme. Presupunem ca suntem in pagina studenti.ejs, dar in loc sa afisam tabelul afisam cate un div pentru fiecare student din vector: <% for (var i = 0; i < studenti.length; i++) { %>
    <%- include('../fragmente/template_student', {student: studenti[i]}); %>
<% } %>
Ne-am folosit de al doilea parametru al lui include, care ca si functia render primeste un obiect cu date ce pot fi folosite de fisierul ejs inclus.

Sesiuni. Login. Logout

Pentru a realiza un site care admite utilizatori trebuie sa implementam urmatoarele functionalitati:

  • inregistrarea utilizatorilor pe site (cu exceptia cazului in care avem un set strict determinat de utilizatori - insa acest lucru se intampla foarte rar)
  • logarea pe site (autentificarea utilizatorilor)
  • logout

Sunt de asemenea utile si alte functionalitati precum:

  • mail la inregistrare, eventual cu un link de confirmare a contului
  • o metoda de recuperare a parolei in cazul in care utilizatorul a uitat-o
  • capacitatea utilizatorului de a schimba parola sau alte informatii din profil
  • utilizatori cu roluri (in functie de rol au sau nu au acces la numite resurse si actiuni in cadrul site-ului)
  • blocarea accesului unui utlizator la site sau anumite resurse ale site-ului
  • luarea unei actiuni in cazul mai multor logari esuate repetate intr-un interval scurt de timp
  • etc.

Presupunem ca avem intr-una din pagini formularul de login (sau pe toate paginile, daca punem, de exemplu, in header). Formularul ar trebui sa cuprinda un input text pentru username si un input de tip password pentru parola: <form id="login" method="post" >
    <label>
        Username: <input type="text" name="username" >
    </label>
    <label>
        Parola: <input type="password" name="parola" >
    </label>
    
    <input type="submit" value="Submit">
</form>

Daca nu am specificat action, inseamna ca datele se transmit chiar catre pagina curenta. Sa prespunem ca aceasta e chiar pagina principala (de obicei notata cu index). Ruta va arata astfel: app.post('/', function(req, res) {
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
        ...procesare date login...
    });

});

Avand in vedere ca name-ul inputului text este username, vom primi valoarea introdusa de utilizator in fields.username. Parola se va gasi in fields.password.

O varianta total nerecomandata este sa hardcodati userul si parola direct in cod si sa testati: if(fields.username=="user" && fields.parola="parola123") {
    //proceseaza login
}
Este cel mai rudimentar mod de a face o verificare a datelor utilizatorului dar are o gramada de dezavantaje: nu e scalabil (trebuie modificat in cod ca sa se adauge alte teste pentru utilizatori noi), nu e modular (datele sunt amestecate cu codul), nu este sigur (parola e scrisa in clar in interiorul scriptului), parola nu poate fi schimbata de catre utilizator si in plus se schimba doar din cod etc. Deci, pe scurt, never, ever, do that!

Cel mai bun mod e stocarea datelor utilizatorilor intr-o baza de date, avand o coloana cu username-urile si una cu parolele criptate. Totusi, vom aborda deocamdata o avrianta mai putin buna, si anume stocarea datelor intr-un JSON.

Consideram ca am preluat datele din JSON-ul cu utilizatori si avem in el o proprietate useri continand un vector de obiecte cu informatiile utilizator. Mai consideram ca obiectele respective au, printre altele, proprietatile username si parola: {
...
"useri":[
    {
        ...
        "username":"test",
        "parola":"271624bc9f80679e46daf74a1470142b",
        ...
    },
    {
        ...
        "username":"admin",
        "parola":"724d895f87823bf2ee377b3bc5fb894f"
        ...
    },
    ...
]
...
}

Evident asta presupune sa fi existat un formular de inregistrare a utilizatorilor unde, printre alte date, acestia au trecut parola in clar, serverul a rpimit datele si a criptat parola, apoi a scris datele impreuna cu parola criptata in fisierul JSON.

In acest caz, verificarea corectitudinii parolei se face urmarind pasii:

  1. Se preiau username-ul si parola introduse de utilizator
  2. Se cripteaza parola trimisa de utilizator cu acelasi algoritm si aceeasi cheie de criptare care au fost folosite la inregistrare
  3. Pentru username-ul introdus de la utilizator si parola proaspat criptata, se cauta in date o pereche identica de (username, parola criptata). Daca se gaseste, logam utilizatorul. Daca nu, evident, nu il logam, si, eventual ii trimitem un mesaj in care il anuntam ca a introdus datele gresit
  4. Logarea presupune setarea unei sesiuni.

Pentru sesiuni vom folosi modulul express-session: npm install express-session --save

Ne vom crea obiectul corespunzator modulului: const session = require('express-session');

Pentru a folosi sesiuni obligatoriu trebuie setat middleware-ul pentru express-session: app.use(session({     secret: 'abcdefg',//folosit de express session pentru criptarea id-ului de sesiune
    resave: true,
    saveUninitialized: false
}));
Din acest moment, in obiectele de tip request va fi disponibila o proprietate noua, numita chiar session (care este de fapt un obiect in care putem seta campuri(proprietati) cu valorile pe care dorim sa le salvam in sesiunea curenta). Aceasta proprietate e "globala" pentru toate rutele, in sensul ca daca setam un camp in session, orice request va vedea de la acel moment incolo noul camp din session cu valoarea lui. Daca, de exemplu vrem sa setam in sesiune campul abc cu valoarea 10, vom scrie: request.session.abc=10 unde request e variabila de tip "cerere" dintr-un middleware

Un schelet de cod pentru logare ar arata astfel: app.post('/', function(req, res) {
    var form = new formidable.IncomingForm();
    form.parse(req, function(err, fields, files) {
        /* .... s-ar completa aici cu un cod prin care
        obtinem in variabila useri vectorul de utilizatori
        din JSON sau baza de date.
        Vom folosi vectorul useri ca si cum ar fi fost deja initializat
        */
        var cifru = crypto.createCipher('aes-128-cbc', 'mypassword');//creez un obiect de tip cifru cu algoritmul aes
        var encrParola= cifru.update(fields.parola, 'utf8', 'hex');//cifrez parola
        encrParola+=cifru.final('hex');//inchid cifrarea (altfel as fi putut adauga text nou cu update ca sa fie cifrat
        let user=useri.find(function(x){//caut un user cu acelasi nume dat in formular si aceeasi cifrare a parolei
            
            return (x.username==fields.username&& x.parola == encrParola );
        });
        if(user){
            req.session.username=user;//setez userul ca proprietate a sesiunii
        }
        res.render('html/index',{user: req.session.username});//trimit username-ul catre pagina - se poate folosi ca o confirmare a logarii
    });

});

In acest moment putem afisa in pagina, de exemplu in header faptul ca utilizatorul e logat; eventual si username-ul ca sa stie ca e pe contul lui si nu pe al altuia (daca de exemplu folosesc mai multi utilizatori acelasi calculator).

De exemplu in header (in ejs) putem adauga un div continand username-ul, numai daca avem campul user-ul setat (in user am pus mai sus valoarea din req.session.username): <% if(locals.user){ %>
    <div class="info_login">User logat: <%= user.username %></div>
<% } %>

Pentru a realiza logout, trebuie sa distrugem sesiunea (aceasta operatie va sterge toate proprietatile setate in session). Presupunem ca avem o pagina de logout (la care utilizatorul ajunge dand click pe un link/buton): app.get('/logout', function(req, res) {
    req.session.destroy();//distrug sesiunea cand se intra pe pagina de logout
    res.render('html/logout');
});

//TO DO https in node; passport.js

Punerea aplicatiei pe internet

//TO DO https://www.heroku.com
============================ //TO DO Variabile globale bibliografie https://stackabuse.com/using-global-variables-in-node-js/ obs: nu-s vazut client side //TO DO socket.io //TO DO conectare la baza de date