Ingegneria Del Software e Fondamenti Web
Ingegneria Del Software e Fondamenti Web
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
6
JavaScript
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione a
JavaScript
È un linguaggio di scripting client-side introdotto nel 1995 nel browser Netscape Navigator
per rendere le pagine Web “attive” e dinamiche
ECMAScript 3 è stata la versione più utilizzata, fra il 2000 e il 2010, nel periodo di massima ascesa di JavaScript
La maggior parte dei miglioramenti attesi sono arrivati nel 2015 con ECMAScript 6
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione a
JavaScript
JavaScript è famoso per essere stato realizzato in soli 10 giorni
Molte scelte sono state effettuate per ragioni di marketing e non per ragioni
tecniche
Si pensi al nome: JavaScript non ha nulla a che fare con Java, ma si voleva
cavalcare l’onda del successo che Java stava avendo in quel periodo
"I was under marketing orders to make it look like Java but not
make it too big for its britches ... [it] needed to be a silly little brother
language." (source)
– Brendan Eich
Esiste una parte del mondo che dice cose terribili riguardo JavaScript: è un linguaggio che accetta praticamente
qualunque tipo di input, ma può interpretarlo in maniera completamente scorretta (video)
JavaScript è, infatti, estremamente liberale e permissivo: facile per i beginner ma difficile scovare dei bug (si è diffuso
l’uso di "use strict"; per utilizzare un sottoinsieme di JavaScript che evita alcuni problemi)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione a
JavaScript
Dal 2005 (dall’introduzione del termine AJAX) e insieme al suo sviluppo “fuori dal browser”, JavaScript ha conosciuto un
grande processo di standardizzazione (e un rilascio di nuove feature ogni anno)
Col tempo, infatti, l’uso di JavaScript si è esteso anche alla programmazione server-side (come vedremo con Node.js ed il
web framework Express) e alla gestione di database come MongoDB e CouchDB che lo usano come linguaggio di scripting e
query
Per capire l’importanza di JavaScript, si pensi allo stack MERN (o alle alternative MEAN e MEVN), che permette di realizzare
applicazioni web utilizzando un solo linguaggio
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Caratteristiche di
JavaScript
• È un linguaggio interpretato (es. lato client è eseguito dall’interprete del browser)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipi
di dati
Vuoti Il valore undefined rappresenta una variabile dichiarata ma non assegnata
Il valore null denota un non-valore, ovvero l’assenza di valore e di informazione, può
essere immaginato come un placeholder
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipi
di dati
Stringhe Utilizzate per rappresentare del testo delimitandolo con virgolette
`Down on the sea`
"Lie on the ocean"
'Float on the ocean’
In genere, ogni carattere è rappresentato con 16 bit di memoria (a parte caratteri speciali come le emoji)
All’interno delle stringhe, il simbolo \ introduce un carattere speciale (es. \n per una nuova linea e \t per uno
spazio di tabulazione)
Ad esempio "A newline character is written like \"\\n\"."
produce A newline character is written like "\n".
Utilizzando la rappresentazione con backtick (`), le stringhe vengono definite template literals e permettono di
fare l’embedding di altri valori
Ad esempio `La metà di 100 è ${100 / 2}` produce La metà di 100 è 50
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipi
di dati
Booleani JavaScript consente la rappresentazione dei valori booleani true e false
In generale, valori booleani possono essere ottenuti mediante operatori di confronto fra dati (es. fra due numeri)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Type
coercition
Come detto, nella sua flessibilità JavaScript cerca di accettare “qualunque” codice gli venga proposto
Ad esempio, in situazioni in cui gli operatori sono applicati a tipi di dati “scorretti”, JavaScript effettua
delle conversioni automatiche silenziose, dette type coercition, essendo un linguaggio weakly typed
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Espressioni
e statement
Un’espressione è un qualunque frammento di codice che produce un valore
1;
Normalmente uno statement ha l’obiettivo di mostrare qualcosa a schermo o cambiare lo stato interno della macchina o del
programma (side effect)
Gli statement di JavaScript terminano, in genere, con il simbolo ; che in molti casi può comunque essere omesso
Le linee di codice che iniziano con // o che sono racchiuse fra /* e */ sono considerate commenti e vengono ignorate
dall’interprete
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Variabili
e binding
Per conservare valori e cambiare lo stato di un programma, JavaScript utilizza variabili, anche dette binding
let a = 5 * 5;
Per avere variabili che non possono essere riassegnate è possibile utilizzare const anziché let
I nomi della variabili contengono caratteri alfanumerici e sono ammessi i simboli $ e _, ma non possono iniziare con un numero
(in JavaScript si adotta spesso la convenzione camelCase)
Attenzione. Per i nomi dei binding non possiamo utilizzare nessuna keyword riservata!
Nota bene. I binding non “contengono” valori ma puntano a valori nella memoria
Nota bene. JavaScript mette a disposizione la notazione x += y per semplificare x = x + y (valida anche con gli altri operatori
aritmetici) e la notazione x++ per semplificare x = x + 1 (valida anche per la sottrazione)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Controllo
del flusso
Normalmente, gli statement vengono eseguiti nell’ordine in cui sono scritti
Secondo le necessità del programma, è possibile variare il flusso delle istruzioni
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Funzioni Una funzione è detta
pura se
È possibile richiamare la procedura di una funzione mediante il suo binding (o la sua definizione per funzioni anonime) e una coppia
di parentesi tonde al cui interno passare specifici argomenti separati da virgola (se necessari)
prompt("Inserire la password”); console.log(Math.max(2, 4) + 100);
Lo statement return stabilisce se e quali valori vengono restituiti dalla funzione (altrimenti restituisce undefined)
Dichiarazione come funzione Definizione come valore del binding Arrow function
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Funzioni
e scope
I binding dichiarati all’interno di una funzione (o di un blocco, ad esempio quello condizionale) con let e const sono locali
e visibili solo nello scope della funzione (e delle funzioni più interne)
Binding dichiarati all’esterno di una funzione sono globali e accessibili dovunque
Nelle versioni di JavaScript precedenti al 2015 solo le funzioni “generavano” degli scope
Per questo motivo, se utilizziamo la vecchia keyword var per la dichiarazione di binding all’interno di una funzione,
otterremo il comportamento atteso, ma se utilizzata all’interno di un qualsiasi altro blocco, il binding sarà accessibile
globalmente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Funzioni
e scope
Closure
Come visto precedentemente, una funzione può accedere al suo scope locale e allo scope globale, ma anche allo scope delle
funzioni più “esterne” in cui essa è definita
var saluto = "Buongiorno";
function saluta(persona) {
var nomeCognome = persona.nome + " " + persona.cognome;
function visualizzaSaluto() {
console.log(saluto + " " + nomeCognome);
}
visualizzaSaluto();
}
La funzione saluta invoca, internamente, la funzione visualizzaSaluto che si occupa di stampare il messaggio di saluto
utilizzando le variabili definite nello scope della funzione più esterna
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Funzioni
e scope
Closure
Consideriamo ora il seguente caso particolare, in cui una funzione crea (e restituisce) un’altra funzione
var saluto = “Buongiorno";
Anche le funzioni ch
function saluta(persona) { e restituiscono fun
zioni,
così come quelle ch
var nomeCognome = persona.nome + " " + persona.cognome; e accettano funzioni
come argomenti, so
n o dette funzioni di
return () => console.log(saluto + " " + nomeCognome); ordine superiore
}
La funzione saluta non si occupa direttamente di stampare il messaggio di saluto in console, ma restituisce una nuova
funzione che si occupa di stampare il messaggio
Effettivamente, quando visualizzaSaluto viene invocata, la funzione saluta è già terminata ed il suo contesto di
esecuzione non esiste più: come fa, allora, la funzione visualizzaSaluto, ad accedere alla variabile nomeCognome?
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Funzioni
e scope
Closure
Questo comportamento è possibile grazie al meccanismo della closure, che permette ad un oggetto funzione di “ricordare” le
variabili accessibili nel suo scope quando era stata creata, mantenendone un riferimento
Questo succede perché ogni contesto di esecuzione (quello globale o quello di una funzione) contiene un ambiente
lessicale che associa binding a variabili (scope) e mantiene un riferimento all'ambiente lessicale genitore (chiusura)
La tecnica di programmazione funzionale per cui si costruiscono funzioni “parzialmente istanziate” con argomenti è detta
currying o applicazione parziale (per approfondire)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Hoisting
di funzioni e variabili
Possibilmente,
In JavaScript l’hoisting è il processo per cui le dichiarazioni di variabili e classi e evitare l’hoisting
le definizioni di funzioni vengono spostate all’inizio del loro scope
hello(); console.log(num);
// ReferenceError: Can't find variable: num
function hello() {
console.log('Hello!'); console.log(num); console.log(num); Come si vede, solo la
} // undefined // ReferenceError: dichiarazione viene
var num; Cannot access hoistata, non
Funziona ed è equivalente a num = 6; uninitialized variable. l’assegnazione
console.log(num); let num; Le dichiarazioni let
function hello() { // 6 num = 6; e const sono
console.log('Hello!'); console.log(num); hoistate ma non
} console.log(num); // 6 sono accessibili
// undefined
hello(); var num = 6;
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti
(anteprima)
Gli array sono specia
Gli oggetti in JavaScript sono collezioni arbitrarie di proprietà, più precisamente array li tipi di oggetti che
realizzano liste spars
associativi mutabili le cui chiavi rappresentano i nomi delle proprietà e e polimorfiche,
indicizzabili da 0
Es. let arr = [1
let descriptions = { ,2,3,,,’FooBar'
];
console.log(arr
work: "Went to work", [5]);
// ‘FooBar’
"touched tree": "Touched a tree"
};
I valori delle proprietà sono accessibili mediante la notazione obj.prop oppure obj['prop']
È possibile creare o rivalorizzare a posteriori una proprietà con l’operatore di assegnazione
Per eliminare una proprietà è sufficiente usare delete obj.prop
Gli oggetti possono possedere dei metodi, ovvero delle proprietà contenenti funzioni
A differenza di molti linguaggi orientati agli oggetti, in JavaScript non c’è distinzione fra funzione e metodo, a parte il fatto
che la funzione di un oggetto può usare la keyword this per riferirsi all’oggetto stesso
descriptions.change = function(s) { this.work = s };
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
funzionale
Alcuni aspetti di JavaScript si avvicinano a quelli della programmazione funzionale
La programmazione funzionale viene dall’idea che un programma possa essere considerato come una funzione matematica
La programmazione funzionale pura è estremamente forte nella sua formulazione e prevede che:
• Qualunque cosa all’interno del codice sia una funzione oppure un’espressione
• Non vi siano statement
• Non vi siano stati (variabili, oggetti, …)
JavaScript non implementa programmazione funzionale pura ma ne mutua alcune idee: funzioni come oggetti di prima
classe, chiusure, funzioni anonime, …
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
funzionale
Le funzioni in JavaScript sono oggetti di prima classe: le loro definizioni (in un binding o anonime) rappresentano “valori”
che possono essere passati come argomenti ad altre funzioni (callback), possono essere restituiti come valori da altre
funzioni e possono essere inseriti in qualunque struttura dati
Nota bene. Funzioni che accettano come argomenti altre funzioni sono dette funzioni di ordine superiore
Le funzioni di callback sono estremamente utilizzate nell’ambito della programmazione asincrona (funzioni “lente” che
chiamano altre funzioni quando terminano la loro esecuzione) e in generale nella programmazione event-driven
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
funzionale
Callback
array.forEach(callback [, thisArgument])
Ad esempio il metodo forEach di un array (chiariremo più avanti) prende come input una callback che esegue, elemento
per elemento, un’azione particolare
La callback da passare a forEach deve prevedere (al massimo) tre argomenti, ovvero l’elemento corrente, il suo indice e
l’intero array:
function callback(e) {
console.log(e);
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 6.1
Si utilizzi la funzione sort che, se utilizzato con una callback con due argomenti a e b, ordina gli elementi comparandoli
coppia per coppia a e b (array.sort(compareFn) con compareFn(a, b))
In particolare, la callback deve restituire un valore positivo per posizionare b prima di a, un valore negativo per posizionare a
prima di b, 0 per mantenere l’ordine originale
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 6.2
Si supponga di voler riscrivere la funzione removeSong (espressa in maniera imperativa) utilizzando i principi della
programmazione funzionale
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Gli oggetti in JavaScript sono collezioni arbitrarie di proprietà, più precisamente array associativi mutabili le cui chiavi
rappresentano i nomi delle proprietà
Esiste la proprietà w
let descriptions = { ork in descriptio
ns?
Basta chiedere "wor
work: "Went to work", k" in descripti
ons
Quali sono tutte le p
"touched tree": "Touched a tree" roprietà di
descriptions? È
}; possibile ottenerle
mediante Object.k
eys(description
s)
I valori delle proprietà sono accessibili mediante la notazione obj.prop oppure obj['prop']
È possibile creare o rivalorizzare a posteriori una proprietà con l’operatore di assegnazione
Per eliminare una proprietà è sufficiente usare delete obj.prop
Gli oggetti possono possedere dei metodi, ovvero delle proprietà contenenti funzioni
A differenza di molti linguaggi orientati agli oggetti, in JavaScript non c’è distinzione fra funzione e metodo
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
A proposito di this
Per funzioni (o metodi) dichiarate in maniera "classica", la parola chiave this viene associata all’oggetto su cui la funzione
viene invocata
let o = {
oldProp: 'this is an old property’
aMethod: function() {
this.newProp = "this is a new property";
console.log(‘Done’);
}
};
o.aMethod();
Non usare arrow function per definire un metodo, in quanto il this sarà associato all'oggetto globale
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Array
Si tratta di oggetti ridimensionabili e che possono contenere mix di differenti tipi di dati Dai un’occhiata alla
Gli elementi di un array sono indicizzabili a partire da 0 reference
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Array
Il metodo map() è simile a forEach() ma anziché effettuare operazioni per ogni elemento dell’array, ci permette di ottenere
un nuovo array i cui elementi sono il risultato della mappa applicata su ogni elemento dell’array originale
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Array
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 6.3
Si scrivano le funzioni sum e range che permettano, rispettivamente, di calcolare la somma di una lista, e di costruire una
lista di numeri interi dati i due estremi (inclusi)
Esercizio 6.4
Esercizio 6.5
Si scriva una funzione che permetta di effettuare la deep comparison fra due oggetti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Oggetti vs dati primitivi
Gli oggetti appena introdotti si contrappongono ai tipi di dati primitivi, ovvero quelli introdotti finora (null, undefined,
stringhe, numeri, booleani) più i bigint e i Symbol
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Mutabilità degli oggetti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Wrapper di valori primitivi
Eccetto undefined e null, tutti i valori primitivi in JavaScript hanno un oggetto equivalente che svolge il ruolo di wrapper
per quel valore primitivo
Vediamo un esempio per le stringhe, associate all’oggetto String
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Altri oggetti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Prototipi
Se provassimo a creare un oggetto vuoto con let obj = {};, scopriremmo che esso possiede metodi come ad esempio
toString()
Ciò accade perché ogni oggetto in JavaScript deriva da un proprio prototipo da cui “eredita” tutte le proprietà e i metodi
In particolare, gli oggetti “semplici” sono realizzati mediante il prototipo di Object
Mettendo un po’ d’ordine, scopriamo che tutti gli oggetti che abbiamo considerato finora derivano da un loro prototipo
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Prototipi
È possibile creare nuovi oggetti usando un altro oggetto come prototipo mediante il metodo Object.create()
let protoSpeaker = {
type: null,
speak(line) { Ricorda che è possib
ile anche definire un
console.log(`The ${this.type} speaker says '${line}'`); metodo con la segue
nte notazione
} let protoSpeake
r = {
}; speak: function
(line) {
…
}
let humanSpeaker = Object.create(protoSpeaker); }
humanSpeaker.type = 'human';
humanSpeaker.speak(‘Hello, World!');
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Prototipi
Questo sistema di prototipazione degli oggetti può essere informalmente associato al rapporto fra classi e istanze
A questo proposito potremmo pensare di realizzare una funzione che agisca come un costruttore, ovvero che ci restituisca un
oggetto a partire da un certo prototipo
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Costruttori Attenzione!
Va distinto bene il m
odo in cui possiamo
Nella pratica, JavaScript fornisce una maniera semplice per la realizzazione di ottenere il prototipo
di un oggetto x con
prototipi e oggetti basati su di essi, che fa uso della funzione costruttore Object.getProto
typeOf(x), ed il
modo in cui un costr
function Speaker(type) { uttore è associato co
il prototipo degli ogg n
this.type = type; etti che può costruir
e,
ovvero mediante la p
this.speak = function (line) { roprietà prototype
Esempio: cosa resti
tuiscono
console.log(`The ${this.type} speaker says '${line}'`); Object.getProto
typeOf(Speaker)
}; e Speaker.protot
ype?
}
Utilizzando la keyword new davanti al nome di una funzione, quest’ultima sarà trattata come costruttore
Tutte le funzioni, infatti, ricevono una proprietà chiamata prototype che si può sovrascrivere o a cui si possono
aggiungere proprietà: questa proprietà contiene il prototipo degli oggetti che saranno costruiti mediante tale funzione se
usata come costruttore (tale prototipo viene anche inserito nella proprietà .__proto__ dei nuovi oggetti creati)
Nota bene. Per convenzione, i nomi dei costruttori iniziano con una lettera maiuscola
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Catena di prototipi
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Catena di prototipi ed ereditarietà prototipale
Supponiamo di avere un oggetto rabbit e poi di cambiarne il suo prototipo fissandolo a un altro oggetto animal
const animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
const rabbit = {
jumps: true
Le proprietà e i metodi di animal saranno automaticamente disponibili in rabbit in
};
un meccanismo di ereditarietà prototipale
rabbit.__proto__ = animal; Se una proprietà non esiste in un oggetto, verrà cercata nel suo prototipo o nel
rabbit.walk(); prototipo del suo prototipo, risalendo la catena
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Catena di prototipi ed ereditarietà prototipale
Aggiungendo una proprietà ad un oggetto, che sia essa presente o meno nel prototipo, la proprietà viene
aggiunta al solo oggetto e “scollegata” (eventualmente) da quella del prototipo
const animal = {
eats: true,
walk() {
alert("I am walking!")
}
};
const rabbit = {
__proto__: animal
};
Questa tipologia di overriding può essere applicata per dare un
rabbit.walk = function() { comportamento “eccezionale” ad alcuni oggetti, benché
alert("Rabbit! Bounce-bounce!"); derivanti da un prototipo con comportamenti di default
};
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Oggetti,
prototipi e classi
Classi in JS
Dal 2015, JavaScript è stato esteso con le classi ES2015, che si basano sempre sul concetto di prototipo, lo semplificano nella
notazione e lo estendono nelle potenzialità (metodi statici, metodi di istanza, ereditarietà, …)
class Speaker {
constructor(type) {
Sulla definizione dell
this.type = type; e classi
non viene effettuato
} l’hoisting
speak(line) {
console.log(`The ${this.type} speaker says '${line}'`);
}
}
Questa notazione ci permette di specificare un insieme di metodi, compreso il costruttore chiamato sempre constructor
Come le funzioni, anche le classi possono essere create all’interno di una espressione (let Speaker = class { … }), oppure
in maniera anonima (let humanSpeaker = new class { … }(…))
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Analizziamo il comportamento di this all’interno dei metodi di un oggetto, ovvero in una situazione in cui agisce
esattamente per come ci si aspetta
const point = {
x: 10,
y: 20
where() {
return `position (${this.x}, ${this.y})`
}
}
console.log(point.where());
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Consideriamo adesso il seguente esempio
myMethods = {
where() {
return `position (${this.x}, ${this.y})`
},
origin() {
this.x = 0;
this.y = 0;
}
}
const point = {
x: 10,
y: 20,
where: myMethods.where
}
bear.greeting();
Quando chiamiamo una funzione facendola precedere dal nome di un oggetto (es. bear.greeting()), stiamo facendo
un'operazione di binding implicito, ovvero stiamo implicitamente associando quel particolare oggetto al this del contesto
di esecuzione
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this Perché usiamo var
in questi esempi?
Perché le variabili dic
hiarate con var
nello scope globale
diventano
proprietà di window
e diventano
Controlliamo di aver capito bene... quindi accessibili tra
mite this.
quando avviene il bin
ding di default
function doSomething() { var a = 6;
console.log(this.a); const obj = {
} a : 12,
doSomething: function() { console.log(this.a) }
var a = 6; };
Nel comportamento di default, ovvero quando siamo nel contesto di esecuzione globale, avviene il binding di default, cioè
this corrisponde all’oggetto globale (window nel browser e globalThis in Node)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Analizziamo quest'altro esempio
var input = 1;
function square1() {
setTimeout(function() { console.log(this.input * this.input) }, 1000);
};
function square2() {
console.log(this.input * this.input);
};
const obj = {
input : 3,
I due risultati saranno completamente diversi
square1: square1,
square2: square2 Ma perché? Probabilmente la funzione passata come callback viene
}; eseguita in un contesto di esecuzione diverso da quello che ci
aspettiamo
obj.square1();
obj.square2();
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Guardiamo ancora quest'esempio
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Cosa succede, quindi, quando this viene utilizzato dall’esterno, ad esempio mediante una callback?
const persona = {
nome: "Mario",
cognome: "Rossi",
saluta: function() {
alert("Buongiorno " + this.nome + " " + this.cognome);
}
};
document.getElementById("pulsante").addEventListener("click", persona.saluta);
Chiaramente this verrà associato all’elemento con id pulsante della pagina web e non
all’oggetto persona, pertanto this.nome e this.cognome saranno undefined Metodi alternativi sono call() e
apply() che permettono di
Una soluzione a questo "problema" può essere il binding esplicito mediante il metodo bind chiamare direttamente un metodo
delle funzioni, con cui leghiamo il contesto di una funzione (this) a un certo oggetto fissato su un certo oggetto
persona.saluta = persona.saluta.bind(persona)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Ecco ancora un esempio
class Present {
constructor(containerElement) {
this.containerElement = containerElement;
_openPresent(event) {
this.image.src = 'path/to/gift/image';
this.image.removeEventListener('click', this._openPresent);
}
}
Quando cliccheremo sul pacco regalo, il valore di this sarà associato all’immagine e non all’oggetto di classe Present
Si veda l’Esempio 6.1
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
La soluzione per i problemi appena visti è quella di effettuare il binding fra l’istanza di una classe ed un metodo
class Present {
constructor(containerElement) {
Il metodo bind() consente di creare
this.containerElement = containerElement;
una nuova funzione in cui l’oggetto this è
this._openPresent = this._openPresent.bind(this); preimpostato
// Create image and append to container.
this.image = document.createElement('img');
this.image.src = 'path/to/giftbox/image'; Consiglio per il futuro!
this.image.addEventListener('click', this._openPresent); Non dimenticare di usa
this.containerElement.append(this.image); re il
metodo bind() all’inte
} rno
dei costruttori per asso
ciare
_openPresent(event) { gli event listener
this.image.src = 'path/to/gift/image';
this.image.removeEventListener('click', this._openPresent);
}
}
const persona = {
nome: "Mario",
cognome: "Rossi",
nomeCognome: function () {
return this.nome + " " + this.cognome;
}
};
function saluta(nomeCognome) {
console.log("Buongiorno " + nomeCognome());
}
saluta(persona.nomeCognome);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 6.7
Si consideri di avere un vettore di oggetti della classe Present e che essi debbano comunicare all’app che li ha creati di
essere stati scartati
Mentre l’app possiede, chiaramente, un riferimento alle istanze dei pacchi regalo, ciascun oggetto non ha un riferimento
all’app che li ha creati
La soluzione, quindi, è quella di creare una callback che l’app passa alle istanze della classe Present
Si vuole, infatti, che quando tutti i pacchi regalo sono stati scartati, compaia un messaggio di avviso (ricorda che solo l’app
può sapere se tutti i pacchi sono stati scartati!)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Guardiamo questi due casi e cerchiamo di capire se esistono delle differenze
myObject.myMethod() myObject.myMethod()
Le arrow function cambiano completamente le regole del gioco, in quanto effettuano una closure del this all'ambiente
lessicale genitore in cui viene definita
Nel caso a destra, lo scope in cui è definita la funzione myMethod di myObject è quello globale, pertanto this.a
corrisponderà window.a, che non esiste!
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Dal momento che le arrow function non hanno binding, è possibile trarre qualche conclusione generale
showSkills() { showSkills() {
this.skills.forEach(function (skill) { this.skills.forEach(skill => {
console.log(`${this.name} is skilled in ${skill}`); console.log(`${this.name} is skilled in ${skill}`);
}); });
}, },
}; };
person.showSkills(); person.showSkills();
& La callback sarà eseguita in un contesto in cui this non è person ✅ Usa le arrow function come callback o handler "inline"
& Evitare le funzioni classiche come callback ✅ Non c'è necessità di fare binding
& Altrimenti, ricordare di effettuare il binding
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
A proposito
di this
Dal momento che le arrow function non hanno binding, è possibile trarre qualche conclusione generale
person.showSkills(); person.showSkills();
& Non usare le arrow function come metodi di un oggetto in quanto il this ✅ Usa le funzioni classiche come metodi, specie se questi devono essere
sarà associato all'ambiente genitore chiamati mediante binding implicito
E se usassi una arrow function come metodo all'interno di una funzione costruttore?
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
In un modello di programmazione sincrona, le cose accadono una alla volta
Quando chiamiamo una funzione la cui esecuzione richiede del tempo, il programma si ferma per tutto il tempo di esecuzione
della funzione
Un modello di programmazione
asincrona permette alle cose di
avvenire contemporaneamente
Quando un’azione viene iniziata, il
programma continua ad essere
eseguito e sarà poi informato della Nel grafico, le linee più spesse
rappresentano l’esecuzione del
fine dell’esecuzione dell’azione
programma del tempo, mentre le
A destra è schematizzato un linee sottili rappresentano il
programma che richiede due risorse tempo speso nell’attesa delle
dalla rete (azione lenta) e ne deve risorse dalla rete
combinare il risultato
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
In generale, la maggior parte dei programmi JavaScript saranno scritti in maniera tale che alcune porzioni di codice saranno
eseguite ora ed altre saranno eseguite più tardi
console.log(data);
// Oops! `data` non avrà i risultati Ajax, questa è un’azione che vogliamo effettuare più tardi
} );
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
Proviamo a capire qualcosa in più sulla programmazione asincrona, partendo dal funzionamento base di JavaScript
JavaScript è un linguaggio di programmazione single-threaded, con un unico call stack
Quindi JavaScript può fare solo una cosa alla volta function multiply(a, b) {
multiply(n, n) return a * b;
}
square(n) square(n)
function square(n) {
printSquare(4) printSquare(4) printSquare(4) return multiply(n, n);
}
main() main() main() main()
function printSquare(n) {
const squared = square(n);
console.log(squared);
}
square(n) console.log(squared)
printSquare(4);
printSquare(4) printSquare(4) printSquare(4) printSquare(4)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
Cosa succede utilizzando un unico call stack in maniera sincrona?
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
La soluzione è quella della programmazione asincrona che utilizza funzioni non bloccanti
Una possibile strategia è quella di chiamare una funzione “lenta” e passarle una callback che venga eseguita al termine
In generale, la maggior parte dei programmi JavaScript saranno scritti in maniera tale che alcune porzioni di codice saranno
eseguite ora ed altre saranno eseguite più tardi
console.log('Hi!');
setTimeout(function() { console.log(‘Hi!’);
console.log('There');
}, 5000); main() main() main()
console.log('Poliba');
setTimeout(…) console.log(‘Poliba’);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
Ma se il runtime di JavaScript esegue una sola azione alla volta, com’è possibile che possa in maniera concorrente scaricare
delle risorse dal web oppure contare i secondi che passano?
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona console.log(‘Hi!’);
setTimeout(function() {
console.log(‘There’);
}, 5000);
WebAPIs
console.log(‘Poliba’);
timer( ) callback
A sinistra è mostrato il funzionamento
chiama
inserisce dell’event loop con cui opera il runtime di
subito JavaScript nel browser
al termine
Se lo stack è vuoto, il
Cosa succederebb
il primo elemento viene e con un
primo elemento timeout di 0 secon
spostato nello stack di?
della coda dei task
se questo è vuoto
viene eseguito
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
Allo stesso modo funziona una richiesta
AJAX asincrona
WebAPIs
(L’esempio seguente utilizza jQuery per semplicità
XHR( ) callback
di scrittura)
console.log(‘Hi!’);
chiama inserisce
subito al termine $.get(‘url’, function cb(data) {
console.log(data)
});
$.get(…)
console.log(‘Istruzione successiva’);
callback
Prova a speriment
are
il primo elemento viene l’event loop con qu
esto
spostato nello stack tool di visualizzaz
ione!
se questo è vuoto
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione La coda dei microtask contiene le callback delle
asincrona
operazioni considerate più urgenti o importanti, e
saranno eseguite prima
La coda dei macrotask contiene le callback delle
operazioni meno urgenti
WebAPIs
XHR( ) callback
chiama inserisce
subito al termine
$.get(…)
callback
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione
asincrona
Non conosciamo la libreria jQuery,
Il supporto alla programmazione asincrona offerto da JavaScript permette di eseguire
ma ricorda che qui non è
attività “in background” che non interferiscono con il flusso di elaborazione principale
necessario focalizzarsi sulla
Il modello di programmazione asincrona visto finora è basato sull’utilizzo di callback funzione specifica che stiamo
utilizzando per l’esempio, quanto
Partiamo dall’esempio seguente, che effettua una richiesta AJAX utilizzando jQuery invece sui concetti della
programmazione asincrona
$.get(“/users”, { id: "12345" }, function(user) {
$("#resultMessage").html("Nome utente: " + user.Name);
});
Il terzo argomento di $.get() è la funzione di callback che sarà invocata quando l’oggetto JSON relativo all’utente richiesto
sarà stato ricevuto
Supponiamo, una volta ricevuto l’oggetto utente (che contiene l’ID del suo blog) di voler mostrare i post del suo blog
Abbiamo dovuto costruire la callback passata alla prima richiesta come una funzione che fa due richieste, a loro volta ciascuna
con una propria callback
Viene legittimo chiedersi se displayPostList e displayPhotoList saranno effettivamente chiamate una dopo l’altra
Effettivamente non è così, in quanto l’ordine della loro esecuzione sarà determinato dal termine delle relative funzioni chiamanti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Programmazione Il fenomeno di codice
asincrona
estremamente annidato che si
viene a verificare è detto
function doStep1(init, callback) { callback hell oppure pyramid
const result = init + 1;
Supponiamo di trasformare un normale callback(result);
of doom, a causa
codice sincrono utilizzando le callback } dell’indentazione
function doStep2(init, callback) {
function doStep1(init) { const result = init + 2;
return init + 1; callback(result); Questo fenomeno è
} } estremamente frequente con
function doStep2(init) { function doStep3(init, callback) { l’utilizzo di callback nella
return init + 2; const result = init + 3;
} callback(result); programmazione asincrona
}
function doStep3(init) {
return init + 3; function doOperation() { Il debug del codice diventa
} doStep1(0, result1 => {
doStep2(result1, result2 => { estremamente difficile, così
function doOperation() { doStep3(result2, result3 => {
console.log(`result: ${result3}`);
come la gestione degli errori
let result = 0;
result = doStep1(result); });
result = doStep2(result); });
result = doStep3(result); }); Per questo motivo nella
console.log(`result: ${result}`);
} } programmazione asincrona
moderna in JS si adotta il
doOperation(); doOperation();
promise pattern
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Il promise pattern è la fondazione della programmazione asincrona moderna in JavaScript
Questo pattern parte dall’assunto di avere a disposizione degli oggetti che possano rappresentare il valore pendente di una
operazione asincrona
Tale oggetto può essere utilizzato, all’interno del codice, per stabilire le attività da eseguire al termine dell’operazione
asincrona
Nel promise pattern, una funzione asincrona restituisce una promise, ovvero un oggetto che rappresenta, appunto, l’esito
della sua esecuzione
In un certo senso, anziché incorporare la continuazione di un programma asincrono all’interno di una callback, il promise
pattern permette di ottenere un oggetto che rappresenta la capacità di sapere quando un task asincrono è finito e
decidere quindi cosa fare dopo
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Un’analogia reale è quella del fast-food: vado in cassa, ordino un cheeseburger e do al cassiere 1,99€
Ho, di fatto, creato una richiesta di un “valore di ritorno” (il mio cheeseburger)
Ovviamente, il cheeseburger non è immediatamente pronto, quindi il cassiere mi da qualcosa al posto del cheeseburger: una ricevuta
con un numero d’ordine
La ricevuta è una promessa del tipo “ti devo qualcosa”, in questo caso un cheeseburger
La ricevuta fra le mani rappresenta il mio cheeseburger futuro e non mi devo più preoccupare, perché qualcuno lo sta preparando per
noi
Nel frattempo posso fare altro, come inviare un messaggio a un amico e dirgli “Ehi! Vieni a pranzo con me? Sto per mangiare un
cheeseburger”
Non ho ancora il cheeseburger fra le mani, ma posso già ragionare su di esso perché ho un numero d’ordine che lo “sostituisce”
slegandolo dal concetto di tempo e mi assicura di avere un valore futuro
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Stati di una promise
• Resolved: una promise è risolta quando il valore che rappresenta diviene disponibile, Pending
cioè quando l'attività asincrona restituisce un valore
• Rejected: Una promise è rigettata quando l'attività asincrona associata non
restituisce un valore o perché si è verificata un'eccezione oppure perché il valore Resolved Rejected
restituito non è considerato valido
• Pending. Una promise è pendente quando non è né risolta né rigettata, cioè la
richiesta di esecuzione di un'attività asincrona è partita ma non abbiamo ancora
ricevuto un risultato
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Un toy example non reale
let promise =
Un esempio funzionante...
new Promise(function(resolve, reject) { function httpGet(url) {
if (condizione) { return new Promise(function(resolve, reject) {
resolve(valore); var httpReq = new XMLHttpRequest();
httpReq.onreadystatechange = function() {
} else { var data;
reject(motivo); if (httpReq.readyState == 4) {
} if (httpReq.status == 200) {
data = JSON.parse(httpReq.responseText);
}); resolve(data);
} else {
Il costruttore dell’oggetto Promise prevede un
reject(new Error(httpReq.statusText));
parametro che rappresenta il promise handler }
Il promise handler è una funzione che viene invocata }
};
immediatamente e che riceve, a sua volta, due httpReq.open("GET", url, true);
funzioni da invocare rispettivamente per risolvere la httpReq.send();
});
promise a un valore o per rigettarla, in base a }
determinate condizioni
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Uso di promise
Il vantaggio dell’uso di promise è quello di semplificare l’uso di funzioni asincrone evitando di passare attraverso le callback e
potendo immaginare, invece, di disporre immediatamente del risultato (trattasi, in realtà, del valore futuro)
Per lavorare sul risultato di una promise è possibile utilizzare il metodo then
In particolare, then prevede due argomenti: il primo è un resolve handler che sarà eseguito nel caso di promise risolta, il
secondo (reject handler, opzionale) è una funzione che sarà eseguita nel caso di promise rigettata
httpGet(‘url/to/fetch‘)
.then(value => console.log(‘Tutto ok: ‘ + value),
error => console.log(‘Si è verificato un errore’));
In particolare, then restituisce un’altra promise, che risolve al valore che la handler function restituisce oppure, se
quest’ultima restituisce una promise, aspetta quella promise e poi risolve al suo valore
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Torniamo all’esempio del blog dell’utente: dovevamo necessariamente fare due richieste HTTP una di seguito all’altra
Con la nostra nuova funzione httpGet che ci restituisce una promise l’idea potrebbe essere la seguente
httpGet("/utente/12345") In questo tipo di scrittura, la funzione che gestisce la
.then(function(utente) {
risoluzione della prima promise è una nuova chiamata
httpGet("/blog/" + utente.blogId)
.then(function(blog) { ad httpGet
displayPostList(blog.posts);
});
});
Ma questo non è di nuovo il callback hell dovuto all’annidamento delle callback? Sì, lo è
Ma ricordiamoci che il metodo then su una promise ci restituisce una nuova promise… e allora?
httpGet("/utente/12345")
.then(utente => httpGet("/blog/" + utente.blogId))
.then(blog => displayPostList(blog.posts));
Questo approccio è chiamato promise chaining, in quanto realizza, appunto, una catena di promise
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Gestione degli errori
Come detto, il metodo then accetta un resolve handler, mentre il metodo catch accetta un reject handler
httpGet("/utente/12345”)
.then(utente => httpGet("/blog/" + utente.blogId)).catch(e => console.log(e));
Mediante il metodo then è comunque possibile passare sia il resolve handler che il reject handler
httpGet("/utente/12345")
.then(utente => httpGet("/blog/" + utente.blogId), e => console.log(e))
La rejection di una promise viene propagata alla promise generata dal then, che viene quindi a sua volta rigettata
Pertanto, se una promise all’interno di una catena fallisce, l’output dell’intera catena sarà marcato come rigettato e nessun
handler di successo sarà invocato da quel punto in poi
È possibile, quindi, pensare di posizionare un solo metodo catch in fondo alla catena
httpGet("/utente/12345")
.then(utente => httpGet("/blog/" + utente.blogId))
.then(blog => displayPostList(blog.posts)).catch(e => console.log(e));
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 6.8
Realizzare, mediante il promise pattern, una funzione alarm che realizzi una funzione asincrona di sveglia, che dopo un
tempo preimpostato esegua un’azione come un messaggio
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il promise
pattern
Combinazione di promise
Il metodo then utilizzato finora permette di stabilire un ordine fra le funzioni asincrone
A volte è necessario ottenere i valori di diverse promise senza che queste dipendano necessariamente l’una dall’altra
A questo proposito è possibile utilizzare il metodo Promise.all(), la cui promise è risolta solo quando tutte le promise
all’interno dell’array passatogli come argomento sono risolte (rigettata anche quando una sola è rigettata)
L'introduzione delle promise in JavaScript ha consentito di semplificare notevolmente la struttura del codice asincrono
rispetto all'approccio basato sull'utilizzo di callback
La coppia di parole async/await permettono di lavorare con le promise, semplificando la sintassi del codice asincrono e
realizzando una struttura tipica del codice sincrono
La parola chiave async permette di dichiarare una funzione come asincrona
La parola chiave await (utilizzabile solo all'interno di funzioni asincrone) permette di sospendere un’esecuzione in attesa
che la promise associata ad un’attività asincrona venga risolta o rigettata
function getUtente(userId) { async function getUtente(userId) {
httpGet(“/utente/" + userId) try {
.then(response => { let response = await httpGet(“/utente/" + userId);
console.log(response); console.log(response);
}).catch(error => } catch (e) {
console.log(“Errore!" console.log("Si è verificato un errore!");
)); }
} }
All’interno di funzioni async è possibile lavorare come se await permettesse di ottenere valori già risolti
Le funzioni async continuano a restituire all’esterno delle promise
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 6.8 (continua)
Relizzare nuovamente quanto richiesto nell'Esercizio 6.8 utilizzando, questa volta, la sintassi async/await
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Modularità
in JavaScript
Per ragioni pratiche di riuso e leggibilità, programmi complessi sono spesso organizzati in file chiamati moduli
Ciascun modulo specifica quali moduli utilizza e quali interfacce espone
Le relazioni fra moduli sono dette di dependency
Uno o più moduli possono comporre un package, ovvero parti di codice che si possono distribuire e installare (per JavaScript, si
veda l’infrastruttura fornita da NPM)
Un package, a sua volta, può dipendere da altri package
Quando un package viene aggiornato (bug fixing o nuove funzionalità), tutti i programmi che lo usano possono aggiornarlo alla
nuova versione (senza dover correggere codice manualmente)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Modularità
in JavaScript
CommonJS
Progetto proposto nel 2009 per le necessità di modularità nell’uso di JavaScript fuori dal browser
Attualmente implementato in molti scenari, specialmente server-side, come Node.js
Aggiungendo una proprietà o un metodo all’oggetto exports (o module.exports), è possibile creare un’interfaccia
exports.printLine = function(line) {
console.log(line);
}
Chiamando la funzione require, è possibile importare una dependency per utilizzare le sue interfacce
const utils = require(‘./utils’); // oppure require(‘package-name’);
utils.printLine(‘esempio’);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Modularità
in JavaScript È possibile specificare un
alias per un
oggetto importato (ad es.
import
ordinal as od from
Moduli ECMAScript “ordinal”)
È possibile utilizzare impo
rt * as
Ordinal from “ordin
Nel 2015 ECMA ha introdotto il proprio sistema di moduli al” per
importare tutte le interfac
La parola chiave import permette di importare oggetti da moduli ce e accedervi
mediante dot notation
import ordinal from "ordinal";
import {days, months} from "date-names";
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Fondamenti del Web
Ingegneria del Software e Fondamenti Web
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
7 Web
API
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle Web API
Come abbiamo detto, JavaScript può essere eseguito in diversi hosting environment
Uno di questi è il browser, mediante il quale pagine HTML possono incorporare codice JS ed eseguirlo
Non appena un tag <script> viene individuato all’interno di una pagina HTML, verrà eseguito il codice contenuto all’interno
oppure quello specificato mediante attributo src
<h1>Testing alert</h1>
<script src="code/hello.js"></script> alert(“hello!");
index.html code/hello.js
Ci riferiremo al codice JavaScript eseguito nel browser di un utente dell’applicazione web come a codice client-side
Il client utilizzato dall’utente mette a disposizione un (vasto) insieme di interfacce che permetteranno di manipolare diversi
aspetti del browser o del sistema operativo e di gestire dati e servizi ottenuti anche da altri siti web
Queste interfacce prendono il nome di Web API
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle Web API
Possiamo immaginare le API come dei costrutti che permettono agli sviluppatori di
realizzare operazioni complesse in maniera più semplice ed astratta
Un’analogia con il mondo reale è la fornitura di energia nelle nostre case: possiamo collegare i
dispositivi ad una presa di corrente, senza doverci invece preoccupare di fare un collegamento
direttamente ad un erogatore di energia
Allo stesso modo, programmare grafica 3D è molto più semplice attraverso (ad esempio)
funzioni Python che attraverso codice C che controlla direttamente la GPU del computer
Con le Web API, gli sviluppatori hanno a disposizione una serie di funzionalità non native di JavaScript ma costruite al di
sopra di JavaScript e che permettono di estendere notevolmente le potenzialità del linguaggio
Funzionalità offerte dal browser che espongono dati dal Funzionalità offerte da altri servizi nel Web (es. le Twitter
browser e dall’ambiente circostante (es. la Web Audio API API ci permettono di usare particolari costrutti per interrogare
ci permette di manipolare tracce audio nel browser utilizzando Twitter e fare cose come mostrare i nostri ultimi tweet nel nostro
codice di basso livello che non vedremo mai) sito web)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle Web API
Browser API Third-party API
Funzionalità offerte dal browser che espongono dati dal Funzionalità offerte da altri servizi nel Web
browser e dall’ambiente circostante
Twitter Flickr YouTube
HTML DOM CSSOM
Facebook Google Maps …
Fetch XMLHttpRequest
Canvas WebGL
Generalmente, il codice interagisce con le API mediante
File Web Audio oggetti JavaScript
Media Capture and Streams MediaRecorder Ognuna delle API qui specificate è infatti, di solito, un
insieme di oggetti che espongono metodi e proprietà
Web Workers Service Workers
WebStorage IndexedDB …
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM
API
Ricordiamo che il browser utilizza una il DOM per rappresentare le pagine HTML
Il DOM contiene nodi di diverso tipo, in generale un nodo Document (che rappresenta la principale interfaccia del
nostro documento) e nodi discendenti di tipo Element
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Web API valide per tutti i nodi
API Node.firstChild
Node.lastChild
Node.nextSibling
Node.nodeName
Navigare e modificare la gerarchia di nodi nel DOM Node.nodeType
Node.nodeValue
<!doctype html>
<html> childNodes firstChild Node.parentNode
<head> body Node.parentElement
<title>My home page</title> 0 h1 Node.previousSibling
</head> My home page Node.textContent
<body> Node.appendChild()
previousSibling
<h1 class=“bigTitle">My home page</h1> Node.insertBefore()
1 p
<p>Hello, this is my home page.</p> Node.removeChild()
<p id=“libro">I also wrote a book! Hello, I am Marijn... parentNode
Node.replaceChild()
Read it <a href="url">here</a>.</p> nextSibling
</body> 2 p
</html> Web API per i nodi di tipo Element
I also wrote a book! ...
Element.innerHTML
lastChild Element.outerHTML
Element.getAttribute()
Element.setAttribute()
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Web API valide per tutti i nodi
API Node.firstChild
Node.lastChild
Node.nextSibling
Node.nodeName
Navigare e modificare la gerarchia di nodi nel DOM Node.nodeType
Node.nodeValue
<p>Sample <b>bold</b> display</p> Node.parentNode
Node.parentElement
Node.previousSibling
Node.textContent
Node.appendChild()
Node.insertBefore()
Node.removeChild()
Node.replaceChild()
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM
API
Cercare nodi all’interno del DOM
API Node.baseURI
Node.childNodes
Node.firstChild
Node.lastChild
Agire su un nodo del DOM Node.nextSibling
Node.nodeName
Una volta individuato l'elemento o gli elementi presenti su una pagina, possiamo modificarne Node.nodeType
il contenuto o altre caratteristiche sfruttando proprietà e metodi specifiche dei nodi e dei nodi Node.nodeValue
di tipo elemento Node.parentNode
Node.parentElement
<p>Sample <b>bold</b> display</p>
Node.previousSibling
Node.textContent
document.querySelector('p').textContent Node.appendChild()
// Sample bold display Node.insertBefore()
document.querySelector('p').innerHTML
// Sample <b>bold</b> display Web API
Element.innerHTML
document.querySelector('p').outerHTML Element.outerHTML
// <p>Sample <b>bold</b> display</p> Element.getAttribute()
Element.setAttribute()
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM Web API
API Node.baseURI
Node.childNodes
Node.firstChild
Attributi di un elemento HTML Node.lastChild
Node.nextSibling
<img src="picture.png" /> Node.nodeName
Node.nodeType
document.querySelector('img').getAttribute('src') Node.nodeValue
In generale, però, una nuova proprietà non diventa un attributo HTML e non ha effetti sulla Web API
pagina (non è quindi gestibile tramite getAttribute e setAttribute)
Element.innerHTML
Element.outerHTML
Attenzione! I valori di attributi HTML e le proprietà corrispondenti (quando esistono) non sono
Element.getAttribute()
esattamente la stessa cosa (di solito l’attributo contiene il valore presente nell’HTML, mentre
Element.setAttribute()
il valore della proprietà è in qualche modo interpretato)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM Web API
API Node.baseURI
Node.childNodes
Node.firstChild
Node.lastChild
Creare nuovi nodi nel DOM Node.nextSibling
Node.nodeName
const para = document.createElement('p'); Node.nodeType
para.textContent = 'We hope you enjoyed the ride.'; Node.nodeValue
Node.parentNode
var srcAttr = document.createAttribute("id");
srcAttr.value = "example"; Node.parentElement
para.setAttributeNode(srcAttr); Node.previousSibling
Node.textContent
parent.appendChild(para); Node.appendChild()
// oppure parent.insertBefore(element, sibling);
Node.insertBefore()
const para2 = document.createElement('p');
const newContent = document.createTextNode("Hi there and greetings!”);
para2.appendChild(newContent); Web API
Element.innerHTML
parent.removeChild(para);
Element.outerHTML
para.remove(); // non supportato in browser più vecchi
para.parentNode.removeChild(para); Element.getAttribute()
parent.replaceChild(para, para2); Element.setAttribute()
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM
API
Altre API
L’oggetto window mette a disposizione un insieme di metodi propri, in alcuni casi richiamabili anche senza specificare
l’oggetto window
window.console.log("Esempio!");
window.alert("Wow!");
window.confirm("Ok?");
window.location.href = "newpage.html";
window.setTimeout(cb, 1000);
window.setInterval(cb, 1000);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM
API
Elementi di un form
Una volta ottenuto l’oggetto relativo ad un elemento form nella pagina HTML, è possibile accedere ai suoi elementi annidati
mediante il loro attributo name
<form id="myForm">
<p><label>Nome: <input type="text" name="txtNome"/></label></p>
<p><label>Cognome: <input type="text" name="txtCognome"/></label></p>
</form>
<script>
const myForm = document.querySelector(‘#myForm’);
const txtNome = myForm.txtNome;
const txtCognome = myForm.txtCognome;
</script>
Ricordiamoci che l’attributo name nei form viene utilizzato dai browser per costruire il contenuto da inviare al server
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
HTML DOM
API
Elementi di un form
La proprietà value di un oggetto corrispondente a un qualunque controllo di un form contiene il suo valore corrente
<form id="myForm">
<p><input type="text" name=“txtNome"/></p>
<p><label><input type="radio" name="gend" value="M"/>Uomo</label>
<label><input type="radio" name=“gend" value=“F"/>Donna</label></p>
</form>
<script>
const myForm = document.querySelector(‘#myForm’);
const txtNomeVal = myForm.txtNome.value; // → Andrea
const gendVal = myForm.gend.value; // → M
</script>
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
CSSOM
API
Mediante le CSSOM API, JavaScript può manipolare gli stili CSS in diverse maniere
Un modo è accedere alla proprietà style di nodi del tipo HTMLElement (una sottoclasse di Element che contiene qualunque
elemento HTML), che a sua volta contiene le proprietà CSS scritte in camelCase
para.style.color = 'white';
para.style.backgroundColor = 'black';
para.style.padding = '10px';
para.style.width = '250px';
para.style.textAlign = ‘center';
In alternativa, è possibile associare una classe specifica ad un elemento mediante il suo attributo class
para.setAttribute('class', 'highlight');
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 7.1
Si vuole realizzare una semplice shopping list che permette di aggiungere elementi dinamicamente alla lista usando un input
e un pulsante
Cliccando sul pulsante, l’elemento deve apparire nella lista insieme a un pulsante per eliminarlo
Inoltre, la casella di input deve essere immediatamente svuotata e resa in focus
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Eventi
(digressione e recap)
Immaginiamo che l’unico modo per verificare che un tasto sulla tastiera venga premuto sia quello di controllarne
costantemente lo stato
Non potremmo permetterci di fare altre operazioni che impieghino tempo, altrimenti potremmo perderci l’istante in cui un
utente ha premuto il tasto della tastiera
Un’alternativa migliore a questo approccio è quella di lasciare che il sistema operativo invii delle notifiche quando il tasto
viene premuto e le inserisca in una coda
Il nostro programma dovrebbe periodicamente controllare la coda per verificare l’avvenimento di nuovi eventi (polling)
Un meccanismo ancora più avanzato è quello di fare in modo che il sistema notifichi attivamente il nostro codice quando
si verifica un evento
I browser fanno questo permettendoci di registrare degli handler per specifici eventi
Gli eventi, pertanto, sono occorrenze che avvengono nel sistema (non sono parte del core di JavaScript ma sono definiti
nelle Web API)
Quando si verifica l’evento, il sistema produce un segnale e fornisce un meccanismo mediante il quale possa essere
eseguita un’azione di risposta (ricordate l’event loop?)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Eventi
nel browser JavaScript può reagire agli
in altri ambienti
eventi anche
Ad esempio, nell’event mo
Gestiti mediante un event listener/handler, che viene agganciato del di Node.js
ci sono listener che sono in
ascolto di
ad uno specifico oggetto eventi ed emitter che, app
u nto,
emettono degli eventi
<button>Click me</button> La sintassi per registrare g
li
event
<p>No handler here.</p> handler sarà diversa, ma il
modo di
<script> gestirli con l’event loop è
let button = document.querySelector("button"); sostanzialmente lo stesso
button.addEventListener("click", () => {
console.log("Button clicked.");
});
Gli event handler possono accettare un parametro
</script>
(spesso indicato con e o event) che rappresenta
l’event object e che conserva informazioni
La maggior parte degli oggetti nel browser possono registrare un
sull’evento
event handler anche mediante la proprietà (o mediante
Due proprietà sempre disponibili dell’object event
l’attributo – non farlo) onnomeevento, ad esempio onclick
sono type (che contiene il nome dell’evento) e
Per rimuovere l’handler basterà chiamare target (che è un riferimento all’elemento su cui
button.removeEventListener con gli stessi argomenti l’evento è avvenuto)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 7.2
Data una textbox e un contenitore, si inserisca all’interno del contenitore, volta per volta, il nome del tasto premuto dall’utente
mentre digita all’interno della textbox
Per risolvere l’esercizio, è necessario trovare l’evento adeguato ed utilizzare una particolare proprietà dell’event object
Esercizio 7.3
Esplorare, autonomamente, gli eventi legati alla tastiera e al mouse (compresi gli eventi di drag and drop), e gli eventi di tipo
scroll e focus
Verificare, infine, il funzionamento dell’evento load
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Eventi
nel browser
Prevenire comportamenti di default
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Eventi
nel browser
Bubbling
Nell’utilizzo moderno di AJAX con il pattern MVC, con la richiesta HTTP si scarica un oggetto (es. JSON) che contiene tutte
le informazioni aggiornate su un certo modello
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
XMLHttpRequest
L’oggetto XMLHttpRequest rappresenta la prima API per l’implementazione del meccanismo AJAX
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
Fetch API
L’interfaccia moderna mediante cui il browser può effettuare richieste HTTP è fetch, che fa uso di promise
fetch("example/data.txt").then(response => {
console.log(response.status); // → 200
console.log(response.ok); // → true
console.log(response.headers.get("Content-Type")); // → text/plain
response.text();
}).then(text => console.log(text));
La chiamata del metodo fetch restituisce una promise che risolve a un oggetto Response che contiene
informazioni sulla risposta del server, come codice di stato e header
Gli header sono in un oggetto Map-like chiave-valore (uso del metodo get – che è case-insensitive – per ottenere
un valore dalla chiave)
La promise restituita da fetch risolve anche se il server risponde con un errore, mentre viene rigettata se c’è un
errore di rete o se il server non può essere trovato
Il contenuto di una risposta può essere trovato mediante il metodo text() o json() – e altri –, che restituiscono
una promise che risolve al valore testuale o strutturato della risposta
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
Fetch API
Effettuare una richiesta POST ad un server prevede l’invio di un oggetto Request mediante fetch oppure delle
sue impostazioni mediante il secondo argomento di fetch
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
Usare la Fetch API per l’invio dei dati di un form
Di default, la sottomissione di un form prevede l’invio dei dati alla risorsa indicata nell’attributo action, spostando quindi
l’utente dalla pagina in cui si trova
Questa operazione può essere intercettata mediante l’evento submit sul form e può essere innescata mediante un input di
tipo submit oppure via API mediante il metodo submit() del form – ricordandosi di prevenire il comportamento di default!
Alternativamente, possiamo intercettare l’evento submit e utilizzare la L’interfaccia FormData fornisce un modo per inv
iare
Fetch API per inviare i dati di un form in maniera asincrona senza con AJAX coppie chiave/valore con campi e valori
di
necessità di cambiare la pagina in cui l’utente si trova un form come se fosse usato l’encoding type
multipart/form-data
let form = document.querySelector("form"); Alternativamente, l’interfaccia URLSearchParam
s (a
form.addEventListener("submit", event => { cui poter anche passare un oggetto FormData)
fetch(url, { permette di ottenere i dati come se fossero inseri
ti
method: "post", nella query string
body: new FormData(form)
}).then(…); Si veda l’Esempio 7.1 e ll a Fe tc h API
ri c a s i d’us o d
event.preventDefault(); Approfondisci alt
});
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
Cross-Origin Resource Sharing (CORS)
Quando utilizziamo tecniche ed API di comunicazione dobbiamo considerare il meccanismo CORS, basato su header HTTP
Per sicurezza, i browser impediscono di default le richieste HTTP cross-origin iniziate dagli script (politica same-origin)
Il meccanismo CORS, tuttavia, consente a un browser di fare una richiesta HTTP a un'origine diversa da quella del sito web
attuale specificando l'header Origin, contenente la propria origine
Il server controlla l'header di origine e, se tale origine fa parte dell'elenco di origini consentite dal server, risponde con i dati
richiesti e un'intestazione Access-Control-Allow-Origin
HTTP/1.1 200 OK
GET image.png HTTP/1.1 Access-Control-Allow-Origin: *
Host: domain-b.com Content-Type: application/xml
Origin: domain-a.com
[…Data…]
Per metodi diversi da GET, POST e HEAD, i server prevedono che la prima richiesta del client sia
un preflight con il metodo OPTIONS, per determinare se la richiesta che si intende fare è sicura
Tale richiesta deve contenere l’header Access-Control-Request-Method per specificare il
metodo che si intende usare
In risposta, l’header Access-Control-Allow-Methods del server specificherà i metodi
accettati dal server per le richieste cross-origin
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
EventSource API
Tradizionalmente, una pagina web deve inviare una richiesta a un server per ricevere nuovi dati
Con i Server-Sent Events, mediante l’oggetto EventSource, è possibile per un server inviare nuovi dati ad una pagina web in
qualunque momento, effettuando una push ad una pagina web grazie a una connessione HTTP persistente
const evtSource = new EventSource(
“//api.example.com/ssedemo.php”,
È possibile anche aggiungere event listener per
{ withCredentials: true }
un evento “generico”, ad esempio ping, che sarà
);
eseguito quando il client riceve un messaggio in
cui il campo event è impostato su ping
evtSource.onmessage = function(event) {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.textContent = "message: " + event.data;
eventList.appendChild(newElement);
}
I dati generati dal server devono essere dei semplici messaggi di testo codificati come UTF-8, inviati al client usando il tipo
MIME text/event-stream
: linea di commento
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
event: ping
data: Ricordati di fare la spesa
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 7.4
Gli eventi vengono inviati da un server eseguito su runtime Node.js, fornito in Esercizio 7.4 (Server), quando è interrogato
all’interfaccia /tweets/
In particolare, il server simula l’arrivo di nuovi tweet leggendoli da un file di testo ed inviandoli al client ogni 3 secondi
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
WebSocket API
Questa interfaccia rappresenta una tecnologia avanzata che permette di aprire una connessione full-duplex interattiva fra un
browser e un server
Con questa API è possibile inviare messaggi a un server e ricevere risposte event-driven senza effettuare il polling
Rappresenta un’alternativa migliore ai SSE quando si vuole una comunicazione bidirezionale ed efficiente
L’uso di HTTP, infatti, non è indicato per applicazioni a bassa latenza (es. giochi online con componenti realtime)
I WebSocket, quindi, rappresentano una nuova specifica che lavora su TCP senza passare per HTTP
const connection = new WebSocket(‘ws://url.to.websocket.org/echo', ['soap', 'xmpp']);
Per stabilire un WebSocket viene utilizzato il meccanismo di HTTP Upgrade
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per la
comunicazione
WebRTC
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per lo
storage
I browser moderni forniscono ai server diversi modi per far sì che questi memorizzino dati nel computer dell’utente
Esempi di questi dati possono essere documenti per l’uso offline – es., canzoni, impostazioni dell’utente, ed altro
La strategia classica per personalizzare l’esperienza dell’utente è stata quella dei cookie (già visti!)
Oggi i cookie vengono utilizzati principalmente per memorizzare ID di sessione, access token e altre informazioni relative allo
stato
I browser moderni forniscono API pensate appositamente per la memorizzazione locale di dati e informazioni
WebStorage API
Questa API fornisce un meccanismo per la memorizzazione di piccoli dati in forma di chiave-valore (es.
nome utente, stato di login, colore di sfondo, etc.) in maniera separata per ciascun dominio
In particolare essa fornisce due oggetti della classe Storage utilizzabili alla stessa maniera
Si tratta di localStorage (utilizzato per memorizzare i dati in maniera persistente) e sessionStorage
Qui un esempio di
(i cui dati sono eliminati alla chiusura della finestra) funzionamento e qui
il relativo codice
localStorage.setItem('name','Chris');
localStorage.getItem(‘name');
localStorage.removeItem('name');
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per lo
storage
IndexedDB API
L’API IndexedDB è un sistema di database completo disponibile nel browser, in cui è possibile memorizzare dati complessi di
qualunque tipo
Questa funzione opera in maniera asincrona e genera un evento fra tre possibili eventi (success, error, upgradeneeded),
per i quali bisogna mettersi in ascolto
L’evento upgradeneeded si verifica quando il database non è ancora stato creato o se il database viene aperto con un numero di
versione più grande di quello correntemente memorizzato
Si tratta dell’unico modo per modificare la struttura di un database, quindi per farlo bisogna generare questo evento aprendo il
database con un numero di versione maggiore
openRequest.addEventListener('upgradeneeded', e => {
db = openRequest.result;
// Creiamo un objectStore per memorizzare le nostre note con una chiave auto-increment
const objectStore = db.createObjectStore('notes_os', { keyPath: ‘id', autoIncrement: true });
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
API per lo
storage
IndexedDB API
È possibile utilizzare i metodi add, get e delete con la chiave primaria su un Object Store recuperato mediante transazione
per aggiungere, ottenere o eliminare elementi
function addData(t, b) {
const newItem = { title: t, body: b };
// Aprire una transazione db per la lettura/scrittura (ogni operazione su un object store deve essere eseguita con una
transazione)
const transaction = db.transaction(['notes_os'], ‘readwrite');
È possibile utilizzare i metodi add, get e delete con la chiave primaria su un Object Store recuperato mediante transazione
per aggiungere, ottenere o eliminare elementi
function getData(t, b) {
const newItem = { title: t, body: b };
// Aprire una transazione db per la lettura (ogni operazione su un object store deve essere eseguita con una
transazione)
const transaction = db.transaction(['notes_os'], ‘readonly');
// Utilizziamo il metodo get per richiedere all’objectStore un item in base alla sua chiave
const getRequest = objectStore.get(itemKey);
addRequest.addEventListener('success', e => { console.log(e.target.result) });
L’utilizzo degli indici creati mediante createIndex è fondamentale se vogliamo recuperare un elemento utilizzando altri campi
diversi dalla chiave primaria
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 7.5
Spotify mette a disposizione API REST a sviluppatori esterni per l’ottenimento di metadati su artisti, album, tracce dal proprio
catalogo
Le query agli endpoint delle API di Spotify restituiscono un oggetto JSON
Le API sono documentate al seguente indirizzo: https://developer.spotify.com/documentation/web-api/
Utilizzare l’endpoint Search di Spotify per realizzare un’applicazione che, data una casella di testo, permetta di ritrovare tutti gli
album dell’artista digitato e mostrarne tutte le copertine
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Fondamenti del Web
Ingegneria del Software e Fondamenti Web
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
8 Web Application
Architecture
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle architetture
Il termine web application architecture descrive la struttura ad alto livello di un’applicazione web
L’architettura rappresenta la base per la costruzione di un’applicazione e dev’essere attentamente pensata per evitare grossi
cambiamenti di infrastruttura e di codice in un secondo momento
La qualità di un’architettura web determina l’affidabilità, le performance e la sicurezza di un prodotto
L’architettura di un’applicazione web contiene un insieme di componenti ed una descrizione della loro logica di interazione e
determina la realizzazione del prodotto, dell’infrastruttura, della user experience, dei moduli software, etc.
La scelta di una particolare architettura impatta su tanti aspetti dell’applicazione web:
• Livello di sicurezza e stabilità
• Velocità di elaborazione delle richieste
• Riusabilità dei componenti
• Chiarezza del codice
• Capacità di testare/modificare/aggiungere componenti indipendentemente
• Scalabilità del prodotto e dei componenti
Lavorare sull’architettura è il primo passo per la creazione di un’applicazione web e la realizzazione di tutti i suoi componenti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle architetture
Un tier è una separazione logica e fisica dei componenti all’interno di una applicazione o di un servizio
Si tratta di una separazione a livello dei componenti dell’applicazione, e non a livello di codice
Messaging server
Backend
User interface Caching
application Database
(User client) server
server
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle architetture
Applicazione single-tier
In un’applicazione single-tier, l’interfaccia utente, la backend business logic e il database risiedono nella stessa macchina
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle architetture
Applicazione two-tier
Backend Request
User Utile per applicazioni come una to-do list, in cui la logica è gestita
business Database
interface localmente e l’eventuale accesso al codice non mette a repentaglio il
Response
logic business
Le chiamate al database sono fatte solo quando si vuole rendere i dati
Client* Server persistenti
Un altro esempio sono i giochi nel browser o in-app, i cui file sono
*Fat/thick client
troppo pesanti per essere scaricati ogni volta e le chiamate al server
sono fatte solo per salvare lo stato del gioco
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle architetture
Applicazione three-tier e N-tier
Nella maggior parte dei casi, il system designer sceglie di spostare la business logic su un server dedicato, aggiungendo quindi
un nuovo tier e realizzando un’applicazione three-tier
Backend Database
Client* *Thin client
server server
L’introduzione di altri componenti (es. cache, message queue, load balancer, search server, …) estende l’applicazione rendendola
– in generale – N-tier (anche conosciuta come sistema distribuito)
Tutte le applicazioni social come Instagram, Facebook, TikTok, servizi a larga scala come Airbnb, Uber e giochi multiplayer di massa sono realizzate in
maniera N-tier
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Introduzione
alle architetture
Perché realizzare applicazioni multi-tier?
Esse permettono di rispettare i principi di progettazione di single responsibility e di separation of concerns
Single responsibility
Questo principio specifica che ogni componente di un sistema (oppure ogni parte di un programma) incapsula una sola
responsabilità sulle funzionalità del sistema (o del programma), cioè una sola ragione di cambiare
Con questo approccio, i componenti sono debolmente accoppiati in termini di responsabilità, garantendo grande flessibilità
e facilità di gestione del sistema
In particolare, cambiamenti su un componente del sistema non impattano le funzionalità di altri componenti
Secondo l’approccio di single responsibility, dovremmo evitare stored procedures all’interno di un database, per non inserire
logica business laddove invece dovremmo inserire solo dati
Separation of concerns
Secondo questo principio di astrazione – a livello di tier o a livello di codice –, ogni componente (oppure ogni parte del
programma) deve incapsulare una particolare “preoccupazione” dell’intera applicazione in maniera modulare,
esponendo verso l’esterno delle apposite interfacce che nascondano invece i dettagli implementativi interni
Quando i concern sono ben separati viene favorito l’aggiornamento di moduli, il riuso e lo sviluppo indipendente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
La maggior parte delle applicazioni web seguono – a prescindere dal numero di tier – un’architettura client/server
Il principale modello di comunicazione fra client e server è il classico richiesta/risposta basato su HTTP, dove il client invia una
richiesta cui fa seguito una risposta del server (generalmente accompagnata dai dati richiesti) – in questo modello, se non c’è
richiesta non c’è nemmeno risposta
I modelli di comunicazione fra client e server nel contesto di una web app sono molteplici e devono favorire lo scambio di dati
e risorse in entrambi i sensi
Con questo metodo “primitivo” e tradizionale di computazione distribuita è possibile chiamare una funzione su una macchina
remota
Gli argomenti della funzione vengono impacchettati dal browser e inviati al server, mentre i risultati della funzione
rappresentano la risposta alla richiesta
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
REST API
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
REST API
Il termine REST sta per REpresentational State Transfer e indica uno stile architetturale per l’implementazione di sistemi
distribuiti
Il paradigma REST è basato su (è un’astrazione di) HTTP e utilizza solo HTTP per la trasmissione dei dati
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
REST API
La rappresentazione di una risorsa può essere di qualunque tipo, ad esempio un file JSON o XML
Generalmente, una REST API ben progettata utilizza la content negotiation di HTTP per fornire la migliore rappresentazione
L’identificatore di una risorsa deve fornire una maniera unica, non variabile e inequivocabile per accedervi (il suo URI)
GET /api/v1/books/last // questo non è un buon identificatore
GET /api/v1/books/j-k-rowling/harry-potter-and-the-deathly-hollows
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
REST API
REST utilizza HTTP e prevede di poter compiere, mediante quest’ultimo, una serie di azioni
L’insieme base di azioni che un sistema resource-oriented dovrebbe mettere a disposizione sono almeno le azioni CRUD (create,
retrieve, update, delete), che possono essere mappate direttamente ai metodi HTTP
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
REST API
Di seguito, un esempio di REST API per gestire via web i riavvii di macchine virtuali
{ {
"When": "now" "last-executed": "2018-02-23 15:02:02+0300",
} "params": {
"when": {
"Required": true,
GET /virtual-machines/<your-vm-id>/reboots "Docs": “Describes the system reboot needs to take place,
relative to the moment the action is processed”
{ }
"reboots": [ }
{ "received": "2001-10-23 13:30:00+0100”, }
"params": { "when": "now" } },
{ "received": "2001-12-03 03:30:00+0100”,
"params": { "when": "5 min" } },
{ "received": "2002-10-23 13:30:00+0100”,
“params": { "when": "now" } }
]}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra client e server
Push server-to-client e comunicazioni full-duplex
Abbiamo parlato finora di comunicazione iniziata dal client che fa pull di determinate risorse quando necessario
In molte situazioni, il client dovrebbe richiedere continui aggiornamenti per una certa risorsa (es. un sito di live news)
Questo scenario (polling) è chiaramente causa di un inutile consumo di banda
L’idea è quella di realizzare una push, ovvero fare in modo che il server invii aggiornamenti all’occorrenza
Web socket I web socket permettono di realizzare una comunicazione persistente full-duplex a bassa latenza fra client e server (es.
messaggistica, giochi multiplayer web-based, videoscrittura collaborativa, etc…)
Questo meccanismo è eseguito direttamente su TCP senza l’utilizzo di HTTP, e deve essere supportato sia dal client che dal
server
AJAX long polling Questa tecnica permette al server di rimanere in attesa dopo una richiesta AJAX finché non è disponibile un update
La connessione rimane aperta più tempo rispetto al polling e deve essere ristabilita in caso di rottura
HTML5 EventSource I Server Sent Events offerti da questa interfaccia permettono ai server stessi di inviare unidirezionalmente dati ai client sulla base
di eventi, dopo un’iniziale richiesta dei client, grazie a una connessione HTTP persistente
Stream API Questa tecnica è utilizzata principalmente per lo streaming di contenuti multimediali, come grandi immagini e video
Funziona su HTTP e permette, ad esempio, di guardare un video mentre viene scaricato in chunk
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Scalabilità
di un sistema Normalmente le applicazioni non sono soggette
all’utilizzo di un solo utente, ma di molti utenti in
concorrenza
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Scalabilità
di un sistema
Per scalare un’applicazione è possibile agire orizzontalmente o verticalmente
Scaling verticale
Scalare verticalmente un’applicazione (scaling up) significa aggiungere potenza al nostro App App
server per rispondere a un maggior numero di richieste concorrenti
RAM RAM
16 GB 32 GB
Si tratta di un miglioramento hardware semplice che non richiede refactoring di codice o
complessi sforzi amministrativi, di monitoraggio o di gestione Prima Dopo
Tuttavia, c’è un limite nell’aumentare la potenza computazionale di una risorsa
Pertanto lo scaling verticale non può essere fatto “all’infinito” per gestire una mole sempre
crescente di traffico
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Scalabilità
di un sistema
Per scalare un’applicazione è possibile agire orizzontalmente o verticalmente
App App
Scaling orizzontale
RAM RAM RAM
Scalare orizzontalmente un’applicazione (scaling out) significa aggiungere nuove risorse 16 GB 16 GB 16 GB
hardware al pool di risorse attualmente disponibile
Prima Dopo
Potenzialmente, non esiste un limite a quante risorse possiamo utilizzare per scalare
orizzontalmente un sistema
Attenzione! Lo scaling
out richiede che non
vi sia uno stato “perm
anente” all’interno
Uno dei vantaggi dello scaling orizzontale è quello di potersi adeguare dinamicamente (on del codice (ad es. vari
abili di istanza o
the fly) in base alle necessità (l’elasticità del cloud computing ne è un esempio) variabili statiche), ch
e possono essere
Sistemi come Kubernetes permettono di automatizzare il processo di deployment di compromesse se il se
rver che le gestisce
va giù
applicazioni containerizzate aggiungendo o rimuovendo istanze on the fly in base alle
Usare, invece, memo
rie distribuite come
necessità Redis e Memcache, e
programmazione
funzionale che non m
emorizza stati
Lo scaling orizzontale, inoltre, aumenta significativamente la disponibilità del sistema
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Scalabilità
di un sistema
Vi sono una serie di fattori che possono limitare la scalabilità di un sistema rappresentando dei colli di bottiglia
Database
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Scalabilità
di un sistema
Vi sono una serie di fattori che possono limitare la scalabilità di un sistema rappresentando dei colli di bottiglia
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Disponibilità
di un sistema
La disponibilità di un sistema rappresenta la sua capacità di rimanere in linea nonostante fallimenti che si possono
verificare a livello infrastrutturale
Per far sì che un sistema sia altamente disponibile, è necessario renderlo fault-tolerant e ridondante
In particolare, la fault-tolerance è la capacità di un sistema di rimanere attivo (anche a livello ridotto) laddove dovessero
verificarsi errori interni (es. uno o più nodi vanno giù)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Disponibilità
di un sistema
La ridondanza dei nodi permette di garantire
disponibilità tenendone alcuni in stand-by come
backup per nodi dell’infrastruttura che vanno giù
(il meccanismo in figura a sinistra è quello
dell’Active-Passive High Availability)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Load
balancing
Il load balancing permette a un’applicazione di scalare bene e rimanere disponibile anche in caso di grandi quantità di
traffico
I load balancer rappresentano un componente chiave nell’architettura di un’applicazione web in quanto permettono di
distribuire il traffico fra i diversi server di un cluster mediante differenti algoritmi al fine di ottimizzare l’utilizzo delle risorse,
massimizzare il throughput, ridurre la latenza e rimanere fault-tolerant
Il load balancer rappresenta un unico punto di contatto per tutte le richieste dei client
Essi possono essere posizionati davanti a qualunque cluster di macchine che realizzi un componente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Load
balancing Un servizio a larga scala come Amazon è eseguito su numerosi data
center in diverse regioni geografiche
DNS Load Balancing Uno dei modi più semplici per suddividere il carico fra i diversi data
center è quello del DNS load balancing, implementato a livello DNS
sull’authoritative server (gestito dal proprietario del nome di dominio),
in dorato nella figura a sinistra
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Load
balancing
Load balancing hardware Load balancing software
Load balancer di tipo hardware sono macchine I software di load balancing (es. HAProxy) possono essere
performanti posizionate davanti ai server, che eseguiti su commodity server o macchine virtuali, così come
distribuiscono il carico in base a: sono disponibili Load Balancers as a Service (LBaaS)
• Numero di connessioni attualmente aperte verso un Questi software permettono di effettuare valutazioni
server avanzate su un numero elevato di parametri per distribuire il
• Utilizzo delle risorse computazionali carico, come ad esempio dati presenti sui diversi server,
• Altri parametri cookie, header HTTP, uso di CPU e memoria, carico sulla rete,
Queste macchine, comunque, richiedono grande e etc.
complessa gestione e manutenzione e aggiornamenti Essi permettono inoltre di effettuare health check sui server
regolari, così come è richiesto che vengano configurati per mantenere una lista aggiornata dei server attivi
per gestire grandi picchi di traffico Algoritmi utilizzati per suddividere il carico sono:
• Round robin e round robin pesato
• Numero minimo di connessioni
• Random
• Hashing degli IP dei client
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Architetture
monolitiche
In un’architettura monolitica, tutti i servizi di un’applicazione sono strettamente
collegati in un’unica codebase
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
In un’architettura a microservizi, feature diverse di un grande
a microservizi
come servizi più piccoli debolmente accoppiati chiamati
microservizi
I microservizi lavorano insieme per formare un più grande servizio
online distribuito
Notiamo che ogni microservizio può essere a sua volta un cluster di server
La gestione può diventare abbastanza complessa e richiedere l’uso di manager
come Apache Zookeeper
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Comunicazione
fra componenti
Vi sono principalmente due tipi di comunicazione fra i componenti di un sistema:
• Richieste HTTP, utili per richieste di dati o effettuare modifiche in maniera sincrona (ricordate le REST API)
• Code di messaggi, utili per azioni asincrone dove per l’utente non conta “immediatamente” se l’azione è andata a buon fine
o meno
Richieste di azioni asincrone dal frontend sono normalmente effettuate tramite richieste AJAX HTTP, così come il check del
loro stato è effettuato ancora tramite query REST
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
code di messaggi
Una coda di messaggi porta i messaggi da una sorgente a una destinazione seguendo una politica FIFO o prioritaria
La coda di messaggi facilita la comunicazione asincrona fra diverse entità, fornendo uno storage temporaneo in
un’architettura eterogenea
Le implementazioni usano generalmente i protocolli AMQP e STOMP, con tecnologie come RabbitMQ e Apache Kafka
Ad esempio, quando un utente si registra all’interno di un portale, viene immediatamente portato all’homepage
dell’applicazione; tuttavia, il processo di registrazione non è completato in quanto il sistema deve mandare una mail di
conferma all’utente (processo di background asincrono inserito in una coda)
Un altro esempio è l’update consistente del numero di like su un post con un grande numero di osservatori, che possono
essere “raccolti” in ordine all’interno di una coda
In questo modello, implementato in diverse maniere, uno o più In questo modello, i messaggi inseriti da un
produttori possono inviare messaggi in una coda dalla quale producer all’interno di una coda sono
leggono tutti i consumatori di destinazione (broadcasting) destinati a un solo consumer
Questo pattern è molto utilizzato, ad esempio, nell’invio di news,
aggiornamenti e notifiche in real-time agli utenti finali
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
code di messaggi
Si pensi a come può essere strutturato il sistema di notifiche di un social network, in cui gli utenti sono connessi formando
un grafo
Se l’applicazione usa un database relazione, possiamo pensare che quando un utente crea un post, quest’ultimo viene
inserito in una tabella Post
Come notificare l’avvenuta pubblicazione di un nuovo post? Vediamo due diverse strategie
• Ciascun utente del sito, a intervalli regolari, dovrebbe interrogare tutte le proprie connessioni nel grafo per verificare la
presenza di nuovi post (polling)
• Nel momento della pubblicazione, oltre all’inserimento del post nel database, la transazione prevede anche di inserire il
post in una coda di messaggi publish-subscribe destinata a tutte le connessioni dell’utente: se il client dei subscriber è
connesso in una connessione persistente con il server (ad esempio, mediante le Stream API di JavaScript), la coda di
messaggi può fare direttamente push verso gli utenti interessati, altrimenti è possibile attendere che questo si connetta
Vari usi delle code di messaggi sono esecuzione di processi asincroni in background e lavori in batch, ordinamento delle
transazioni in caso di connessioni concorrenti, attesa di utenti che richiedono contemporaneamente la stessa risorsa affinché
essa venga prima cachata e poi servita dalla cache (es. live streaming di Facebook), etc.
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipologie
di web app
Server-side rendering
In questa architettura di base, quando un client
effettua una richiesta a un web server,
quest’ultimo si occupa di realizzare una pagina
HTML ed inviarla al client
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipologie
di web app
Static-site generation
Rendering classico Static-site generation Questo meccanismo coinvolge l’uso di un generatore che
automatizza la codifica di pagine HTML, creandole a partire da
template
La pagina HTML è precedentemente generata e non deve essere
rigenerata ad ogni richiesta
Questo approccio è utile per siti web, ma non per applicazioni con
contenuto estremamente dinamico
Ad ogni nuovo contenuto, infatti, il sito web deve essere nuovamente
rigenerato
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipologie Le SPA lavorano all’interno del browser e non
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
I micro frontend rappresentano componenti
Tipologie
debolmente accoppiati del frontend di
un’applicazione, sviluppati applicando al frontend il
Integrazione client-side
Una semplice soluzione prevede che gli utenti navighino da un frontend all’altro, ad esempio mediante link
I diversi frontend potrebbero essere ospitati presso server differenti, rendendo il passaggio non trasparente all’utente
Alternativamente, i diversi micro frontend potrebbero essere visualizzati mediante iframe (soluzione non idonea)
Una soluzione raccomandata è quella dei Web Components, un insieme di API che permettono di creare nuovi tag HTML
personalizzati, riusabili e incapsulati
Un’altra soluzione è quella di single-spa, un framework JavaScript che permette di effettuare il routing verso diverse
“micro”-applicazioni, anche scritte in framework diversi
Integrazione server-side
Al costo di ulteriore logica di integrazione, il server potrebbe occuparsi dell’integrazione prima di inviare la pagina al client,
sollevando quest’ultimo dal carico computazionale
Framework che facilitano questa operazione sono Project Mosaic, Open Components e Podium
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Tipologie Qui un approfondime
service worker
nto sull’API
di web app
Le Progressive Web App sono applicazioni che hanno il feel di
Progressive Web App app native e che possono essere eseguite in browser desktop
e mobile, potendo essere installate sul dispositivo mobile
direttamente dal browser
In questi casi, si possono utilizzare i webhook, ovvero delle API guidate da eventi anziché da richieste
L’entità che richiede una risorsa ad un servizio, registra un webhook, ovvero un endpoint HTTP, chiedendo al servizio di
essere notificato all’occorrenza dell’evento che rende disponibile la risorsa (si tratta, in sostanza, di una reverse API)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Possibili
scenari
Immaginiamo di dover realizzare un’applicazione web
Quali architetture, tecnologie, pattern e strategie dobbiamo utilizzare?
Non esiste una regola generale, ma studiare lo scenario, prevederne le necessità e fare delle scelte
Interazioni real-time
Supponiamo di essere in uno scenario come Netflix o Spotify, in cui è necessaria un’interazione continua con il backend server
per ottenere uno stream di contenuti: avremo sicuramente bisogno di stabilire connessioni persistenti fra client e server ed
utilizzare tecnologie non bloccanti
Ad esempio, Uber ha utilizzato Node.js per gestire un gran numero di connessioni concorrenti per comunicare con l’engine di
gestione dei viaggi
Applicazioni CRUD-based
Per scenari CRUD-based, ovvero i più comuni nelle web app (portali di prenotazione, social network, gestione di impiegati,
…), utilizzeremo architetture che seguano il pattern MVC
Tecnologie che aiutano lo sviluppo di web app con questo pattern sono Spring MVC, Python Django, Ruby on Rails, PHP
Laravel, and ASP .NET MVC
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Possibili
scenari
Piccole web app
Web app di piccole dimensioni (blog, semplici form, …) possono essere realizzate in maniera monolitica mediante linguaggi
come PHP o framework come Springboot e Ruby on Rails
Applicazioni CPU-intensive
Task computazionali pesanti non possono essere gestiti mediante i classici web framework, ma devono essere scritti in
linguaggi come C++ o Java
La maggior parte dei sistemi aziendali di larga scala sono scritti in Java
Erlang, inoltre, è un linguaggio funzionale con supporto a concorrenza, fault-tolerance e sistemi distribuiti e facilita lo
sviluppo di sistemi scalabili (questo linguaggio era utilizzato da Facebook e da Whatsapp per i backend dei loro servizi di
chat)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Casi
di studio
Caso di studio 1: Google Maps
Un’applicazione come Google Maps è read-heavy piuttosto che write-heavy: ciò significa che i dati potranno essere
largamente cachati
L’applicazione dovrà essere scalabile in maniera elastica (prevediamo picchi di traffico in orari d’ufficio o festività)
Non vi sono molte relazioni fra i dati, quindi può essere preferito un database
NoSQL che è intrinsecamente scalabile
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Casi
di studio
Caso di studio 1: Google Maps
La feature principale è la ricerca, ma vi sono moltissimi altri servizi a supporto delle diverse funzionalità
L’architettura è, quindi, quella dei microservizi che offrono apposite API (Direction, Distance
Matrix, Geocoding, Places, Roads, Elevation, Time zone, e Custom search)
Inoltre, Google Maps lavora mediante composizione di diversi tile che permettono di non dover ricaricare la pagina quando si
effettua zoom in o out ed aggiornare la pagina in sezioni
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Casi
di studio
Caso di studio 2: Portale di prenotazione
Partendo, ad esempio, dal database degli acquisti, è necessario che esso sia transazionale, che garantisca consistenza forte
e che sia ACID-compliant, ad esempio MySQL
Un altro aspetto critico è la gestione della concorrenza di utenti, specie nell’acquisto di biglietti nel giorno e ora di uscita
Una soluzione è quella delle code di messaggi
Generalmente, gli utenti navigano questo genere di siti e vogliono sapere quanti sono i biglietti ancora a disposizione e
informarsi sui prezzi
È possibile pensare di fornire informazioni cachate, inconsistenti e non accurate sul numero di biglietti ancora a disposizione in
modo da non sovraccaricare il database
La base di dati sarà, invece, interrogata nel momento vero e proprio in cui l’utente vorrà effettuare la transazione
Eventuali notifiche agli utenti potranno essere inviate mediante code di messaggi come RabbitMQ o Apache Kafka
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Fondamenti del Web
Ingegneria del Software e Fondamenti Web
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
9 Node.js
e Express
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Cos’è
Node.js
Node.js è un “runtime asincrono di JavaScript guidato da eventi"
• runtime JavaScript: Node.js è un programma scritto in C++ che legge ed esegue del codice JavaScript grazie all’engine V8
(lo stesso incluso in Chrome) senza necessità di un browser ed effettua compilazione Just in Time
Possiamo quindi spostarci su un server e costruire web app: JS diventa un linguaggio lato server!
• guidato da eventi: una volta lanciata, l'applicazione rimane in ascolto di specifici eventi e reagisce mediante callback –
Node.js lavora secondo il pattern listener/emitter
• asincrono: Node.js completa i task in maniera asincrona all'interno di un event loop eseguito su un unico thread, grazie a
primitive di I/O che evitano operazioni bloccanti passandole al kernel di sistema e riprendendo l'esecuzione al loro termine
Questo permette a Node.js di gestire migliaia di connessioni concorrenti con un singolo thread senza doverne gestire la
concorrenza
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Cos’è
Node.js
Node.js è un “runtime asincrono di JavaScript guidato da eventi"
Il modello di Node.js è in contrasto con la maggior parte dei modelli di concorrenza odierni, in cui per la gestione delle
connessioni vengono impiegati i thread del sistema operativo
Un'applicazione Node.js, invece, viene eseguita in un singolo processo, senza la creazione di un nuovo thread per ogni
richiesta di rete
La strategia di Node.js evita deadlock, in quanto quasi nessuna funzione di Node.js effettua operazioni I/O bloccanti
Fornisce, invece, un insieme di primitive di I/O asincrone (così come le librerie di Node.js sono generalmente scritte
utilizzando paradigmi non bloccanti)
In questo modo, quando è necessario effettuare un'operazione I/O come leggere dalla rete, accedere a un DB o al file
system, anziché bloccare il thread e sprecare CPU, il task viene eseguito in maniera asincrona e Node.js riprende le
operazioni quando ottiene una risposta
Queste caratteristiche permettono a Node.js di gestire migliaia di connessioni concorrenti su un singolo server senza dover
gestire la concorrenza di thread
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Perché
Node.js
Quali sono i vantaggi di Node.js?
• Permette di realizzare un’architettura event-driven che permette di ottimizzare la scalabilità (specie di applicazioni di rete)
grazie al fatto che le operazioni I/O (la maggior parte eseguite dai servizi web) possono essere autonomamente eseguite
all’interno di thread in background e le richieste concorrenti possono essere agevolmente gestite
• Permette di evitare il “context shift” fra linguaggi per lo sviluppo di frontend e backend
• È inserito in un ecosistema ricco con oltre 1,3 milioni di librerie open source, documentate e supportate
• È portabile e multi-piattaforma
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Cos’è
Node.js
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
L’event loop
di Node.js
Una volta eseguito lo script di input, Node.js entra – senza alcuna chiamata bloccante – all'interno di un event loop
L’event loop è ciò che permette a Node.js di effettuare operazioni I/O non bloccanti nonostante JavaScript sia single-thread,
chiedendo quando possibile al kernel di sistema di effettuare queste operazioni
La maggior parte dei kernel moderni è infatti multi-thread, e possono quindi gestire operazioni multiple in background
Quando una di queste operazioni viene completata, il kernel notifica Node.js affinché la callback appropriata venga aggiunta
alla poll queue per essere poi eseguita
Durante la fase di poll, se la coda è non vuota l’event loop
eseguirà le callback in maniera sincrona, fino a svuotarla
Se la coda è vuota, rimane in attesa che venga aggiunta una
nuova callback alla coda
Tuttavia, se sono state schedulate istruzioni del tipo
setImmediate(), esse vengono immediatamente
eseguite mediante la fase di check
Subito dopo, vengono eseguite le callback specificate nei
timer con setTimeout()
(Ricordiamo che i timer specificano la soglia minima dopo
cui una callback deve essere eseguita)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Installazione e
configurazione
Node.js è platform-independent
È possibile gestire più installazioni di Node.js mediante Node Version Manager (NVM)
nvm list
nvm ls-remote
nvm install 9.3.0
node -v
L'installazione di Node.js rende disponibile anche npm per la gestione di librerie esterne (simile a pip in Python)
Alternativamente, è possibile interpretare il codice JS all'interno del file mycode.js con il comando node mycode.js o con
node mycode
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Modularità e
Un classico comando
che è possibile
definire in package.js
on è start che
creazione package
sarà richiamabile con
npm run
start o con npm st
art
Per configurarlo è po
ssibile inserire
nell’elenco degli scrip
t di
package.json ad esem
Per la gestione dei moduli Node.js utilizza la strategia di CommonJS pio la voce
“start”: “node i
Diversi moduli possono essere organizzati in pacchetti gestiti mediante Node Package Manager ndex.js”
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il modulo
process
Il modulo process (facente parte del core di Node.js) fornisce una serie di metodi e proprietà utili per la gestione del
processo in esecuzione
Esso non necessita di essere importato, in quanto è automaticamente disponibile
process.exit()
È un modo programmatico per terminare l’esecuzione del processo Come impostare una
variabile d’ambiente?
USER_ID=239482 U
SER_KEY=foobar
process.env.VAR_NAME node app.js quan
do lanciamo
l’applicazione, oppure
Permette di ottenere la variabile d’ambiente VAR_NAME mediante
require(‘dotenv'
).config(); se le
variabili sono imposta
te in un file .env nella
process.argv root del progetto
Permette di ottenere in un array gli argomenti passati allo script
I primi due elementi dell’array sono il path di Node.js e il path dello script
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il primo
web server
const port = 3000;
const http = require("http");
const httpStatus = require("http-status-codes");
const app = http.createServer((req, res) => {
La porta 3000 è
console.log("Received a request!");
solitamente utilizzata
res.writeHead(httpStatus.OK, { per i
server di sviluppo
"Content-Type": "text/html"
});
res.write("<h1>Hello, World!</h1>");
res.end();
console.log(“Risposta inviata!”);
})
app.listen(port, () => {
console.log(`The server has started listening on port ${port}`)
});
http è una libreria standard che introduce un supporto first-class per il networking
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il primo
web server
Trasformiamo il codice precedente in maniera tale da realizzare un event handler per l’evento request sull’oggetto app
Notiamo che la sintassi per realizzare un event handler in Node.js è EventEmitter.on(“evento”, callback)
const app = http.createServer();
app.on(“request”, (req, res) => {
res.writeHead(httpStatus.OK, {
Un oggetto di tipo
"Content-Type": "text/html"
EventEmitter disp
}); one
del metodo emit per
res.end("<h1>Hello, World!</h1>"); generare un evento e
console.log(“Risposta inviata!”); realizzare quindi il pa
ttern
}) listener/emitter
app.listen(port, () => {
console.log(`The server has started listening on port ${port}`)
});
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il primo
web server La classe Buffer perm
di manipolare dati bin
ette
ari
come un array di byte
L’oggetto della richiesta è una istanza di http.IncomingMessage È utilizzata, ad esemp
io,
Proprietà interessanti di questo oggetto permettono di analizzare il messaggio di richiesta come risposta di molt
i
metodi di lettura dei
file
req.method req.url req.headers
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Routing
in Node.js
Con il termine routing ci riferiamo alla determinazione di come
un’applicazione risponde ad una particolare richiesta ad un certo
endpoint (URL) e per un certo metodo
Con il seguente esempio, è possibile richiamare file .html in views/
mediante il loro nome dopo l’hostname
const fs = require(‘fs’);
const getViewUrl = (url) => `views${url}.html`;
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 9.1
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Routing
in Node.js
Immaginiamo di realizzare un modulo router.js (a sinistra) che ci permetta di registrare una serie di route e relative azioni
all’interno di un dizionario e che si occupi poi di gestirle
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Framework
per Node.js
Come abbiamo visto, le API di Node.js risultano essere a basso livello e ciò rende difficoltoso eseguire operazioni più
complesse di routing o di elaborazione di richieste e risposte
A tale proposito, con il tempo sono stati sviluppati framework di più alto livello per Node.js che permettono di velocizzare
e semplificare lo sviluppo di applicazioni web
I web framework permettono, infatti, di superare alcune problematiche comuni di sviluppo come metodi e moduli per
semplificare la gestione di richieste con diversi metodi HTTP, il serving di contenuti statici e dinamici mediante template, la
connessione di database, etc.
Express è abbastanza minimalista, ma gli sviluppatori hanno creato middleware compatibili per la gestione di qualunque
problema di sviluppo (cookie, sessioni, login, parametri di URL, dati da POST, header di sicurezza, …)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
La prima web app
con Express
const express = require('express');
app.get() req.params res.send()
const app = express();
const port = 3000;
app.post() req.body res.write()
app.get('/', (req, res) => {
res.send('Hello World!');
app.put() req.url res.end()
});
Notiamo che il metodo send() sull’oggetto di risposta si comporta in maniera simile al write() del modulo http, i cui
metodi sono ancora supportati da Express
Ricordiamo che write() deve essere però accompagnato da end()
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Routing
in Express
Catena di route
app.route('/book')
.get((req, res) => {
res.send('Get a random book’);
})
.post((req, res) => {
res.send('Add a book’);
})
.put((req, res) => {
res.send('Update the book’);
});
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Routing
in Express
All’interno dei metodi di routing è possibile specificare percorsi espliciti, o basati su pattern o basati su espressioni regolari, o
basati su parametri
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
middleware
Dopo la ricezione di una richiesta, Express può eseguire una serie di chiamate a funzioni
middleware intermedie, ad esempio prima che essa venga passata alla logica applicativa
dell’applicazione
Queste funzioni hanno accesso all’oggetto richiesta, all’oggetto risposta e alla funzione
middleware successiva del ciclo di richiesta/risposta (a cui ci si riferisce normalmente con next)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
middleware
app.use()
Ad esempio
app.use(callback)
esegue callback per richieste su qualunque percorso
app.use('/users', callback)
permette di eseguire callback sul percorso /users e su tutti i
sottopercorsi come /users/tizio e
/users/tizio/photos
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
middleware app.use(‘/users’, myLogger);
app.get('/', callback1);
app.get(‘/users/:userId’, callback2)
app.use()
app.listen(3000);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
middleware
app.get('/user/:id', (req, res, next) => {
// se l’ID dell’utente è 0 vai al prossimo route
if (req.params.id === '0') next('route')
app.get('/user/:id', (req, res, next) => {
// altrimenti continua con questo stack
console.log('ID:', req.params.id);
else next()
next();
}, (req, res, next) => {
}, (req, res, next) => {
// send a regular response
res.send('User Info’);
res.send('regular')
});
})
// il middleware seguente non verrà mai chiamato
// middleware per una risposta speciale
app.get('/user/:id', (req, res, next) => {
app.get('/user/:id', (req, res, next) => {
res.send(req.params.id);
res.send('special')
});
})
eren ze fra ap p. us e( ) e ap p. all()?
D iff
e che
In questo esempio vediamo un middleware substack che Il primo funziona per tutte le rout
ta,
estendono la base route specifica
esegue due callback, ed un middleware che non sarà mai te monta
mentre il secondo semplicemen
utilizzato in quanto il middleware precedente termina il ciclo un middleware per tutti i metod
i su uno
di richiesta/risposta specifico percorso
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
middleware
express.Router
express.Router permette di creare un router in maniera modulare che fornisca un sistema di routing e di middleware
completo (mini-app)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso di
middleware
In molti casi può essere utile utilizzare middleware built-in come express.urlencoded,
express.static e express.json oppure middleware esterni come cookie-parser
app.use(express.static('public', options))
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso del
pattern MVC
La strategia principale per utilizzare il pattern MVC all’interno delle nostre web app Express è quella di spostare le funzioni
di callback all’interno di moduli separati che riflettono gli scopi di tali funzioni
Nell’esempio a sinistra, le definizioni delle callback invocate dalle route per la gestione degli account utente svolgono il
ruolo di controller e possono finire in un modulo userController.js all’interno della cartella controllers/
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Uso del
pattern MVC
Per quanto riguarda la realizzazione delle view, Express permette di utilizzare template engine come Pug, Mustache e EJS
Con i template engine è possibile realizzare template di view con variabili popolate dinamicamente e che vengono trasformati
in HTML da inviare ai client
EJS
Il template engine EJS può essere installato con npm install ejs ed il suo utilizzo può essere dichiarato con
app.set("view engine", "ejs")
All’interno dei simboli <% %> è possibile Il controller si occupa di fare il rendering della view mediante il
scrivere codice JavaScript metodo render sulla risposta, a cui viene passato il nome della view
Mediante <%= %> è possibile stampare il (anche senza estensione) e un oggetto contenente eventuali parametri/
valore di una variabile variabili
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 9.2
Realizzare un’applicazione web per un’università utilizzando il pattern MVC e realizzando le viste mediante EJS
In particolare, realizzare una pagina che mostri i diversi corsi offerti dall’ateneo inseriti all’interno di un array, insieme ai
loro metadati
var courses = [
{
name: "Ingegneria Informatica e dell’Automazione",
type: “Corso di Laurea”, Nota bene!
imagePath: … Le viste possono accedere alle
},
{ proprietà dell’oggetto res.locals
name: "Ingegneria Informatica", come se fossero variabili locali
type: “Corso di Laurea Magistrale”, Ad esempio, settando
imagePath: … res.locals.name = “Prova”
},
{ potremo accedere direttamente alla
name: "Ingegneria Civile e Ambientale", variabile name all’interno della vista
type: “Corso di Laurea”, senza dover passare alcun oggetto
imagePath: …
}
];
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il database
MongoDB
MongoDB è un database open-source non relazionale che organizza i record come documenti BSON (JSON-like binari)
All’interno di un database, è possibile inserire una o più collezioni, corrispondenti ai modelli dei nostri dati
All’interno di ciascuna collezione è possibile inserire (e ritrovare) tutti i documenti relativi ad uno specifico modello
Ogni documento è un oggetto che consente di memorizzare proprietà mediante il pattern chiave-valore
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il database
MongoDB
Il database MongoDB può essere installato in locale, oppure è possibile ottenere gratuitamente un’istanza multi-cloud
con MongoDB Atlas
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Il database
MongoDB
MongoDB fornisce il supporto alle operazioni CRUD sui documenti
db.courses.insertOne({ db.courses.find()
name: "Ingegneria dell’Automazione”, db.courses.find({type: “Corso di Laurea”})
type: “Corso di Laurea Magistrale”, db.courses.find({type: {$in: [“Master”, “Corso di
students: 300 Laurea Magistrale”]}}
}) db.courses.find({status: “active", $or:
db.courses.insertMany([…]) [{students: {$lt: 30}}, {other: /^p/}]})
db.courses.updateMany(
db.courses.deleteMany({…}) {students: {$lt: 10}},
db.courses.deleteOne({…}) {$set: {status: “deactivated"}}
)
Per garantire l’unicità dei documenti, MongoDB utilizza una classe ObjectId che permette di rappresentare univocamente
ogni documento nel database
Inoltre MongoDB supporta l’uso di indici, ovvero particolari strutture dati (B-tree) che permettono di memorizzare il valore di
uno o più specifici campi, per poter reperire documenti in maniera più efficiente mediante l’attraversamento dell’albero
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
La libreria Mongoose è un object-document mapper che permette di mappare oggetti JavaScript con i documenti di
MongoDB, consentendo di costruire modelli da utilizzare nel pattern MVC in termini di oggetti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
All’interno di uno schema di Mongoose è possibile configurare ciascuna proprietà mediante uno SchemaType, che specifica
il tipo che una proprietà deve avere e quali valori sono validi
const breakfastSchema = mongoose.Schema({
• String
eggs: {
type: Number, • Number Ciascun tipo di dato
min: [6, 'Too few eggs'], • Date possiede dei validatori
max: 12 • Buffer propri: approfondisci qui
}, • Boolean
bacon: { • Schema.Types.Mixed
type: Number,
required: [true, 'Why no bacon?']
• Schema.Types.ObjectId
}, • Schema.Types.Decimal128
drink: { • Map
type: String, • [<type>] (array)
enum: {
values: ['Coffee', 'Tea'],
message: '{VALUE} is not supported' I validatori sono regole da poter applicare alle proprietà dei
} modelli per impedire di salvare documenti non validi
required: function() {
return this.bacon > 3; La realizzazione di indici in uno schema Mongoose è
} possibile mediante index: true, mentre un indice
}
}); univoco è realizzabile come unique: true
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
È possibile aggiungere dei middleware sugli schemi (anche detti pre e post hook) per eseguire delle operazioni prima/dopo
che certe operazioni vengano eseguite
Il middleware andrà inserito subito dopo la definizione dello schema e prima della registrazione del modello
I middleware sono utili per la realizzazione di plugin, ovvero funzionalità preconfezionate per aggiungere funzionalità agli
schemi
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
Una volta realizzato un modello, esso può essere utilizzato all’interno dei controller per effettuare operazioni CRUD asincrone
Ad esempio, un controller come il seguente permette di agire all’interno della collezione courses del database in cui stiamo
lavorando (nome lowercase plurale)
exports.insertCourse = (req, res, next) => { exports.insertCourse = (req, res, next) => {
const newCourse = new Course({ Course.create({
name: 'Ingegneria delle Telecomunicazioni', name: 'Ingegneria delle Telecomunicazioni',
type: 'Corso di Laurea Magistrale', type: 'Corso di Laurea Magistrale',
}); })
newCourse.save() .then(savedDocument => res.send('Corso inserito'))
.then(savedDocument => res.send('Corso inserito')) .catch(error => res.send(error));
.catch(error => res.send(error)); };
};
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
Mongoose fornisce un insieme di metodi per effettuare query sui documenti
.find(…)
.findOne(…)
.findById(…)
.findByIdAndUpdate(…) Esplora tutti i metodi per le
.findByIdAndRemove(…) query e le API offerte
.distinct(…)
.delete(…)
.deleteOne(…)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Associazione one-to-one embedded
con Mongoose })
…
con Mongoose
text: String,
imageUrl: String
})
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Preferiamo una strategia di reference quando vogliamo
con Mongoose certa parola chiave (se i post fossero embedded negli
utenti dovrei prima leggere tutti gli utenti)
Avere bisogno di accedere a un oggetto in sé è una
Come detto precedentemente, MongoDB è un database basato su buona ragione per non renderlo embedded
documenti, pertanto non esistono tabelle e join
Le associazioni fra modelli possono essere fatte in
t i pmaniera
ol o g ie di Associazione one-to-many mediante reference (padre ref. figli)
ed ue
Quale dell
embedded (sottodocumenti) oppure mediantee p r e fe rriferimento
ire? const userSchema = mongoose.Schema({
referen c f igli
l'a r ra y d e i …,
ia m o c h e
Se pens o (t a n t o d a posts: [{
r e s ce r e t ropp
possa c re fe r i a mo type: mongoose.Schema.Types.ObjectId,
One-to-one One-to-many dere")Many-to-many, allor a p
"espl o d re ref: "Post"
n o il p a
h e re f e re nzia }]
figli c
});
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB Associazione many-to-many mediante reference
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
Associare un documento a un altro documento è possibile direttamente lavorando come oggetti JavaScript
Ad esempio, è possibile aggiungere un post all’elenco dei post di un utente mediante push sull’array
const testUser;
User.findOne({name: “Mario”})
.then(result => testUser = result)
.then(() => Post.create({…}))
.then(createdPost => {
testUser.posts.push(createdPost);
testUser.save();
});
Inoltre, il metodo populate di un modello permette di popolare (ovvero espandere) un documento rispetto a un
particolare modello associato
const testUser;
User.findOne({name: “Mario”}).then(result => testUser = result);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 9.3
Realizzare un’applicazione web per un’università che permetta di gestire gli studenti e i corsi di laurea
Realizzare le seguenti funzionalità:
• Visualizzazione di tutti gli studenti
• Inserimento di un nuovo studente mediante form
• Visualizzazione di tutti i corsi di laurea
• Inserimento di un nuovo corso di laurea mediante form
• Iscrizione di uno studente ad un corso di laurea
Nota che è possibile realizzare un controller che faccia il redirect verso una view (ad esempio, dopo aver inserito un
nuovo studente) nella maniera seguente, avendo settato res.locals.redirect nel controller precedente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Gestione del DB
con Mongoose
Finora abbiamo realizzato operazioni CR, ovvero di Create e Read
Come detto, nel linguaggio dei metodi HTTP per le operazioni CRUD (e delle REST API) le operazioni di Update e Delete
corrispondono rispettivamente ai metodi HTTP PUT e DELETE
Ricordiamoci, tuttavia, che HTML supporta solo i metodi GET e POST all’interno dei form
Benché tecnicamente queste operazioni potrebbero essere realizzate con richieste POST, è buona pratica aderire a questo
standard
Una possibile soluzione è quella di inviare richieste POST ad Express ed utilizzare il package method-override per
“sovrascrivere” un metodo in base alla query string dell’URL di richiesta
In questa maniera una POST all’URL /students/:studentID/update?_method=PUT sarà interpretata come una PUT
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 9.3 (segue)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Realizzazione
di API
Nella prima parte di questa introduzione ad Express abbiamo realizzato delle risposte che fornissero agli utenti una vista, ad
es. mediante un approccio a template (server-side rendering)
Andiamo adesso a generalizzare, realizzando delle REST API che siano capaci non solo di fornire viste HTML in risposta alle
richieste, ma che possano essere capaci di far interagire i client (non solo i browser!) con le nostre risorse e rispondere con
qualunque altro formato di dati
Questa strategia è utile, ad esempio, per realizzare Single Page Application (client-side rendering) che richiedono
l’informazione, ricevono l’informazione in file JSON e modificano di conseguenza la pagina dell’applicazione oppure è utile per
fornire a terze parti l'accesso alla nostra informazione
Inoltre, faremo in modo che ciascun utilizzatore acceda solo alle risorse per le quali è autorizzato ad accedere
Per prima cosa, per mettere ordine, ricordiamoci di organizzare la nostra app mediante namespace di route, spostando le
route di ciascun namespace in un router a sé stante
res.json(res.locals.courses);
Possiamo anche immaginare un controller che supporti diverse tipologie di risposta in base alla richiesta effettuata, ad
esempio in base alla query string potrebbe decidere di fare rendering o meno
A questo punto il client potrà fare una richiesta AJAX appendendo ?format=json al path per ottenere il file JSON da
applicare client-side alla vista
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Realizzazione
di API
Un’altra idea è quella di realizzare un namespace apposito per l’endpoint di API ed utilizzare il relativo middleware
all’interno del router, in maniera tale da raggiungere la stessa API all’indirizzo /api/courses
Il router per le API potrebbe essere implementato nella maniera seguente: Ricordiamo che i controller
vo n o ve ri fic a re c h e l’u te n te sia
d e
const router = require(‘express’).Router(), loggato, ma…
coursesController = require(‘../controllers/courses’);
Chiaramente, dobbiamo supporre di aver creato due middleware appositi per l’invio di risposte JSON
Autenticazione
Autorizzazione
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Esempio base
// Protected route
router.get('/', verifyToken, (req, res) => {
res.status(200).json({ message: 'Protected route accessed' });
});
Chiaramente si tratta di una soluzione artigianale che diventa “pericolosa” se il token viene diffuso in modo incontrollato
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Gestione di registrazione e login
Un approccio per gestire la registrazione ed il login è quello di prevedere un campo per la password nello schema degli utenti
Al fine di salvare le password in maniera hash&salt all’interno del database, potremmo utilizzare bcrypt
Ad esempio un pre hook potrebbe gestire l’hashing della password prima del salvataggio nel database e un metodo di
istanza passwordComparison sullo schema degli utenti potrebbe effettuare la comparazione della password inserita
durante il login con la password hashata memorizzata nel database
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Autenticazione stateful
Il server crea una sessione per l'utente dopo il login e memorizza l'informazione di sessione lato server
Il server invierà un cookie contenente l'ID di sessione al browser dell'utente, che sarà poi memorizzato e rinviato dall'utente ad
ogni richiesta successiva
Nel cookie non ci saranno dati, ma solo un ID di sessione: i dati saranno memorizzati lato server, grazie ad un apposito store
In Express, possiamo usare la libreria express-session che, letto il cookie, imposta i dati di sessione in req.session
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Autenticazione stateful
app.post('/login', (req, res) => { function isAuthenticated (req, res, next) {
// verifica username e password, poi... if (req.session.username) next()
if (authenticated) { else next('route')
req.session.regenerate(() => { }
// memorizza le informazioni necessarie
// nella sessione, ad esempio lo username app.use(session({
req.session.username = req.body.username secret: 'segreto_per_firmare_il_cookie',
req.session.save(() => { cookie: { maxAge: 60000 }
res.redirect('/') }))
})
}) app.get('/', isAuthenticated, function (req, res) {
}) res.send('hello, ' + req.session.username + '!')
})
app.get('/logout', (req, res, next) => {
req.session.username = null app.get('/', function (req, res) {
req.session.save(() => { res.send(loginPage)
req.session.regenerate(() => { })
res.redirect('/')
})
})
})
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Autenticazione stateless
Il server non conserva alcuna informazione sulla sessione, pertanto ogni richiesta conterrà tutti i dati necessari per autenticare
l'utente
Se necessario, queste informazioni vengono memorizzate dal client
Si possono utilizzare schemi come la Basic Authentication di HTTP, oppure Access Token come JWT e Bearer token
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Autenticazione stateless con Basic Autenthication
app.use('/',
basicAuth({
users: { username: "password" }, // Elenco (o funzione) di utenti e password ammessi
challenge: true,
unauthorizedResponse:
"Unauthorized access. Please provide valid credentials.",
})
);
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Autenticazione stateless con JWT
Il JSON Web Token (RFC 7519) è uno standard aperto e usato a livello industriale per trasmettere informazione in maniera
sicura e self-contained fra due parti
Un JWT è rappresentato da tre parti, poi codificate in Base64Url e separate fra loro mediante punti
{ { HMACSHA256( eyJhbGciOiJIUzI1NiIsInR5c
"alg": "HS256", "sub": "1234567890", base64UrlEncode(header) CI6IkpXVCJ9.eyJzdWIiOiIxM
"typ": "JWT" "name": "John Doe", + "." + jM0NTY3ODkwIiwibmFtZSI6Ik
} "admin": true base64UrlEncode(payload),
} "mio_segreto" pvaG4gRG9lIiwiYWRtaW4iOnR
) ydWV9.VFnB5SMAgpV0dVLNxkm
ueMxPguP9J7D8KFTBi7ls4mM
Supponiamo, quindi, di voler realizzare un meccanismo di login alle nostre API che produca un token JWT
Per far ciò, in Node.js, possiamo utilizzare la libreria jsonwebtoken
Alle richieste successive, il client invierà nuovamente il token, che sarà utilizzato all’interno di un middleware per il controllo dei
dati di autenticazione
verifyJWT: (req, res, next) => {
let token = ...; // DOBBIAMO RECUPERARE IL TOKEN DALLA RICHIESTA DELL'UTENTE
if (token) {
const decoded = jsonWebToken.verify(token, "secret_encoding_passphrase")
if (decoded) {
User.findById(decoded.data).then(user => {
if (user) {
req.userId = user._id
next(); // Protected route
} else { router.get('/', verifyJWT, (req, res) => {
res.status(httpStatus.FORBIDDEN).json({ res.status(200).json({
error: true, message: 'Protected route accessed' });
message: "No User account found." });
});
}
})
} else { ... }
} else { ... }
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API Puoi approfondire qu
i, qui e qui
Come detto, il JWT conterrà le informazioni sull’utente e il client dovrà sempre rispedirlo al server che l'ha generato
Ma come può fare il client per memorizzare questo token e rispedirlo al server?
Memorizzazione del token nel localStorage del client e invio come Bearer token nell'header Authorization
Soggetto ad attacchi di tipo XSS (Cross-Site Scripting)
Invio da parte del server Memorizzazione sul client Invio di richieste successive da parte
res.json({ jwtToken: token }); localStorage.setItem('jwtToken', <token>) del client con header
Authorization: Bearer <token>
Memorizzazione del token come cookie
Se non adeguatamente impostato, soggetto ad attacchi di tipo CSRF (Cross-Site Request Forgery)
Invio da parte del server Invio di richieste successive da parte del client con cookie in automatico (su HTTP)
res.cookie("jwtToken", signedToken, { Per Fetch API tramite JS può essere necessario impostare
expires: ..., fetch('http://localhost:9999/example',{
path: '/', ...,
httpOnly: true, credentials: 'include'
secure: true, });
sameSite: 'strict'
})
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
OAuth2.0 per l'autorizzazione
Una scelta avanzata per l'autorizzazione delle nostre API è quella di utilizzare il protocollo
OAuth2.0 (Open Authorization), che rappresenta uno standard per l'autorizzazione
delegata delle nostre API
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
OAuth2.0 per l'autorizzazione
Il cuore del protocollo OAuth è l’Access Token, ovvero una stringa “segreta” che l'applicazione client OAuth utilizza per fare
richieste al server essendo autorizzata ad agire al posto dell'utente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
OAuth2.0 per l'autorizzazione
Il cuore del protocollo OAuth è l’Access Token, ovvero una stringa “segreta” che l'applicazione client OAuth utilizza per fare
richieste al server essendo autorizzata ad agire al posto dell'utente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
OpenID e OpenID Connect per l'autenticazione
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
Passport è un middleware di autenticazione per Node.js che incapsula la sola funzionalità di autenticazione delle richieste,
delegando altri dettagli all'applicazione (separation of concerns)
Passport permette ad un'applicazione di fare uso di un "qualunque" meccanismo di autenticazione, ovvero una qualunque
strategia, ad esempio mediante login locale, sia mediante servizi di terze parti (es. Facebook, Google, …)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
La gestione dell'autenticazione con Passport avviene semplicemente attraverso un middleware che, in caso di successo,
imposterà l'oggetto req.user all'utente autenticato, stabilirà una sessione e chiamerà il middleware successivo
app.post('/login/password',
passport.authenticate('local', { failureRedirect: '/login', failureMessage: true }),
function(req, res) {
res.redirect('/~' + req.user.username);
});
Il meccanismo utilizzato per autenticare la richiesta è implementato da una particolare strategia (nell'esempio in alto, quella
locale che permette di verificare username e password)
Dal momento che Passport implementa numerosissime strategie, il meccanismo di autenticazione definirà come codificare e
come verificare delle credenziali, ad es. un password oppure un'informazione proveniente da un identity provider
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
Come detto, è necessario specificare come deve essere trattata la richiesta di autenticazione dell'utente
Ad esempio, la strategia locale legge le credenziali contenute nella richiesta (di default, le proprietà username e password
del corpo della POST) e chiama una funzione (specificata come parametro del costruttore di LocalStrategy) che è
responsabile di determinare l'identità dell'utente
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false); }
if (!user.verifyPassword(password)) { return done(null, false); }
return done(null, user);
});
}
));
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
Passport può fare uso delle sessioni per mantenere lo stato di autenticazione dell'utente
app.use(expressSession({secret: "secret_passcode"}));
La serializzazione permetterà di
app.use(passport.initialize());
app.use(passport.session()); memorizzare l'oggetto utente all'interno
della sessione
passport.use(new LocalStrategy(...); Alle richieste successive, una volta
passport.serializeUser(function(user, done) {
done(null, { id: user.id, username: user.username }); autenticato l'utente, l'oggetto viene
}); recuperato dalla sessione e
passport.deserializeUser(function(user, done) { deserializzato, per essere quindi
done(null, user)
impostato come oggetto req.user
});
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
req.isAuthenticated();
// true o false
req.user;
req.logout();
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
L’uso del plugin di Mongoose passport-local-mongoose è conveniente per gestire in maniera semplice la connessione fra
passport-local e Mongoose
userSchema.plugin(passportLocalMongoose, {
usernameField: “email”
});
A questo punto siamo pronti per gestire la registrazione e il login degli utenti all’interno dei controller della nostra
applicazione
authenticate: passport.authenticate("local", {
failureRedirect: “/users/login”,
failureFlash: “Login non riuscito”,
successRedirect: “/“,
successFlash: “Login riuscito”
})
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sicurezza
delle API
Passport
app.use(expressSession({secret: "secret_passcode"}));
app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(User.authenticate());
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Fondamenti del Web
Ingegneria del Software e Fondamenti Web
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
10 Frontend
e React
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Evoluzione
dei frontend
Nei primi anni del Web, i siti web erano serviti staticamente come pagine HTML
Le interfacce di frontend utilizzate dagli utenti erano sostanzialmente statiche e l’interazione poteva avvenire generalmente solo
tramite form
Quando è nata la prima generazione di applicazioni web, gli utenti potevano iniziare a vedere contenuti generati
dinamicamente lato server
Solitamente venivano utilizzati approcci a template che permettevano di realizzare delle “viste” mescolando codice HTML con la
logica applicativa
Questo modello è migliorato quando lo sviluppo di applicazioni web ha iniziato a seguire il pattern MVC, che ha permesso di
disaccoppiare la logica applicativa dalla presentazione del contenuto (es. quello che abbiamo realizzato con Node.js e il
rendering delle viste)
Successivamente, le web app sono diventate molto più simili ad applicazioni desktop capaci di funzionare all’interno del
browser mediante JavaScript e che connettono i modelli scaricati da un server con le viste realizzate (e gestite) client-side
Questa nuova generazione di frontend è realizzata mediante framework come React, Vue.js e Angular(v2) e si basa su viste
assemblate mediante componenti riusabili piuttosto che semplici pagine
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Cosa è e
perché React
React è una libreria JavaScript per lo sviluppo dichiarativo di complesse interfacce utente basate
su componenti
React si occupa principalmente della gestione dello stato dei componenti e del rendering di tali
componenti all’interno del DOM
React permette di conservare lo stato dell’applicazione e di aggiornare in maniera efficiente, ad ogni cambio di stato, solo le
parti della UI che dipendono da tali dati
Ciò avviene grazie al concetto di Virtual DOM, una rappresentazione interna della UI composta da elementi React che viene
volta per volta sincronizzata con il DOM reale mediante un processo detto rinconciliazione
L’uso del Virtual DOM permette a React di interpretare in maniera efficiente le differenze fra stati successivi dell’interfaccia e
aggiornare, dopo ogni re-rendering, solo le parti del DOM che necessitano di un cambiamento
Questo processo rende l’aggiornamento del DOM molto più veloce di vanilla JavaScript
React è open-source e mantenuto da Meta: ciò significa che migliora ed evolve costantemente
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
La prima app
in React
<html>
<head>
<title>React App</title>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script>
const greeting = React.createElement("h1", { id: “recipe-0" }, "Hello World!");
ReactDOM.render(greeting, document.getElementById("root"));
</script>
</head> Al posto di "Hello World!"
<body> Possiamo anche inserire diversi
<div id=“root”></div> elementi React
</body>
Il secondo argomento prende il
</html>
nome di props, e le sue
proprietà possono essere
Il Virtual DOM di React è fatto di elementi React
utilizzate nel contenuto
Elementi del DOM HTML ed elementi React non sono la stessa cosa: gli elementi React sono
dell’elemento
una descrizione di come gli elementi del DOM HTML dovrebbero apparire
Il ReactDOM contiene gli strumenti necessari per renderizzare gli elementi React nel browser
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
La prima app
in React
React permette di semplificare notevolmente la classica creazione di elementi React mediante lo pseudolinguaggio JSX
(che viene convertito in chiamate a funzioni JavaScript), molto simile a quella di HTML
Mediante JSX è inoltre possibile incorporare espressioni JavaScript all’interno di {}, come nell’esempio seguente
JSX non può specificare due elementi adiacenti se questi non sono racchiusi in un unico elemento
ReactDOM.render(<div><h1>Hello World!</h1><p>Benvenuti!</p></div>,
document.getElementById(“root”)); // corretto
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
La prima app
in React
<html>
<head>
<title>React App</title>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src=“https://unpkg.com/babel-standalone/babel.min.js”></script>
<script type="text/babel">
ReactDOM.render(<h1>Hello World!</h1>, document.getElementById("root"));
</script>
</head>
<body>
<div id=“root”></div>
</body>
</html>
Il codice JSX viene “compilato” mediante il preprocessore Babel e trasformato in elementi React
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
La prima app
in React
Il secondo argomento di ReactDOM.render() specifica il nodo del DOM all’interno del quale vogliamo renderizzare un
elemento React
Il React DOM si occupa di tenere aggiornato il DOM del browser affinché sia consistente con gli elementi React
function tick() {
const element = (
<div> Gli elementi React sono
<h1>Hello, world!</h1> oggetti semplici (non
<h2>It is {new Date().toLocaleTimeString()}.</h2> derivati da altri prototipi,
</div> ma direttamente da
); Object) e quindi veloci da
ReactDOM.render(element, document.getElementById('root')); creare
}
setInterval(tick, 1000);
L’elemento da renderizzare ogni secondo rappresenta l’intero albero della UI, ma il React DOM lo confronta volta per volta con
il precedente e aggiorna solo i nodi necessari nel DOM reale (in questo caso, il nodo testo in h2)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
in React
Il primo argomento di ReactDOM.render() ci ha permesso di inserire codice JSX che rappresenta il contenuto che vogliamo
renderizzare
Chiaramente, aumentando la complessità della vista potremmo ottenere centinaia di elementi all’interno di questo
argomento, rendendo impossibile la manutenzione del codice
Uno dei motivi per cui React è popolare è la possibilità di creare componenti funzionali realizzabili, appunto, mediante
funzioni che ritornano un elemento React
ReactDOM.render( function MyApp() {
<ul> return ( Successivamente,
<li>Elemento 1</li> <ul> nell’organizzazione di
<li>Elemento 2</li> <li>Elemento 1</li>
un’app in React, andremo
</ul>, <li>Elemento 2</li>
document.getElementById(“root”) </ul> ad inserire ogni
); ); componente funzionale in
} un file separato
I nomi dei componenti
seguono la notazione
ReactDOM.render(<MyApp />,
document.getElementById(“root”));
PascalCase
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.1
Creare e renderizzare un componente funzionale MyInfo che rappresenti l’interfaccia di una card biografica con i
seguenti elementi:
• Un elemento h3 con il proprio nome
• Un paragrafo con una breve bio
• Una lista non ordinata (es. skill principali)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Transpiling
e toolchain
React è eseguibile direttamente dentro il browser grazie all’importazione di Babel, che effettua il transpiling di JSX
Nonostante ciò sia possibile, è preferibile che il transpiling di un’applicazione React in produzione venga effettuato offline per
diverse ragioni:
• Evitare che gli utenti scarichino ogni volta lo script di Babel, che è nell’ordine di qualche megabyte
• Il processo di compilazione richiede del tempo
• Il risultato della compilazione non viene cachato dai browser
Il transpiling offline è possibile grazie a Node.js (utilizziamo Node come interprete di JavaScript che ci consente di potenziare il
nostro ambiente di sviluppo e non per realizzare un server!)
npm init -y
npm install babel-cli@6 babel-preset-react-app@3
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Transpiling
e toolchain
Esistono una serie di toolchain che aiutano a scalare su molti file, utilizzare librerie di terze parti, individuare errori, visualizzare
le modifiche in tempo reale, ottimizzare l’output per la produzione
Ad esempio, Create React App supporta lo sviluppo di Single Page Application in React e risulta utile nella fase di
apprendimento di React
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Composizione
di componenti
Negli esempi precedenti, abbiamo creato componenti semplici che restituivano function App() {
return (
un semplice elemento React
<div>
I componenti, però, possono essere fra loro composti, facendo in modo che un <MyHeader />
componente faccia riferimento ad altri componenti nel suo output <MyInfo />
In questo modo, React permette di separare le responsabilità dei diversi <MyFooter />
</div>
componenti
);
<App /> }
ReactDOM.render(<App />,
document.getElementById(“root”));
<MyHeader /> <MyInfo /> <MyFooter />
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.2
Creare la UI della slide precedente utilizzando una toolchain come Create React App (ad esempio con WebStorm) o
quella offerta da Replit e organizzando i componenti in moduli separati
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e proprietà
La realizzazione di componenti è di estrema utilità
nei framework frontend in quanto permettono di
realizzare parti riusabili dell’interfaccia
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e proprietà
In React è possibile rendere “diverso” ciascuna istanza di un componente mediante le sue
proprietà: quando viene inserito nella pagina un componente, infatti, ad esso possono essere
passate delle proprietà mediante l’oggetto props, che può essere ricevuto come argomento dalla
funzione che renderizza il componente
In JSX, tali proprietà vengono passate al nuovo componente come fossero attributi HTML
Tutti i componenti React devono comportarsi come funzioni pure rispetto alle proprie props
function InfoCard(props) { function App() {
return ( return (
<div className="info-card"> <div className="cards">
<img src={props.imgUrl} /> <InfoCard name="Mr. Whiskerson"
<h3>{props.name}</h3> imgUrl="…" phone="…" email="…"
<p>Phone: {props.phone}</p> />
<p>E-mail: {props.email}</p> <InfoCard name="Fluffykins"
</div> imgUrl="…" phone="…" email="…" />
); <InfoCard name="Mr. Whiskerson"
} imgUrl="…" phone="…" email="…" />
</div>
);
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Liste È buona pratica associare ad ogni elem
della lista un attributo key univoco fra
ento/componente
e chiavi
i diversi sibling
Questo aiuta React a fare più velocemen
te i confronti tra
due alberi quando deve aggiornare la
UI
Nell’esempio seguente, nel primo tipo
di confronto
React ci permette di lavorare liberamente con vanilla JavaScript, anche per gestire dovremmo fare una mutazione per ogni
elemento; nel
la logica di rendering di componenti React secondo tipo di confronto React capisc
e che è stato solo
aggiunto un nuovo elemento in cima
Ad esempio, se le informazioni relative alle info card vengono scaricate da un
server mediante API oppure sono presenti all’interno di un file JSON, possiamo <ul>
<li>Duke</li>
mappare l’array di informazioni con componenti di React <li>Villanova</li>
</ul>
Successivamente, React ci permette di renderizzare direttamente l’array di
<ul>
componenti (sempre racchiuso all’interno di un altro elemento) <li>Connecticut</li>
<li>Duke</li>
import infoData from ‘./infoData’; <li>Villanova</li>
</ul>
<div>
<ul>
{cards} <li key="2014">Connecticut</
li>
</div> <li key="2015">Duke</li>
<li key="2016">Villanova</li
>
); </ul>
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.3
Realizzare un’applicazione capace di mostrare le ricette presenti in recipes.json mediante una lista di componenti
Recipe
Ogni componente possederà il contenuto della ricetta, eventualmente suddiviso in diversi componenti (es.
IngredientsList e Instructions)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato
I dati sono ciò che danno vita ai componenti (si pensi all’array di ricette nell’esercizio precedente)
Abbiamo visto come in React i dati possono fluire nella gerarchia di componenti come proprietà
Tuttavia, le proprietà sono solo una faccia della medaglia, in quanto esse sono “immutabili”, ovvero i componenti devono
comportarsi come funzioni pure rispetto alle loro proprietà
L’altra faccia della medaglia, allora, è lo stato di un componente, che ha la possibilità di cambiare
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato function Gallery() {
let index = 0;
let showDetails = false;
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato
Abbiamo quindi bisogno di una variabile di stato (e di un suo setter) che:
• Conservi i dati fra due rendering
• Ogni volta che viene settata a un nuovo valore, viene triggerato il re-rendering del componente (e dei suoi figli)
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.4
Correggere il componente Gallery affinché utilizzi correttamente le due variabili di stato necessarie per il suo
funzionamento
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato
Si consideri il seguente esempio, in cui i due componenti MyButton possiedono stati
indipendenti
export default function MyApp() {
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
function MyButton() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato
Come potremmo, invece, fare in modo che i due pulsanti si aggiornino “insieme”?
In generale, come facciamo a far fluire uno stato unico verso componenti a livello più
basso della gerarchia?
Spostiamo lo stato ad un livello più alto della gerarchia e lo passiamo come proprietà
Ogni volta che lo stato in alto cambierà, i componenti che dipendono da questo
verrano renderizzati nuovamente
Come faranno, quindi, i componenti più interni ad agire sulle funzioni che cambiano lo
stato di un componente più in alto?
Semplicemente, anche la funzione che gestisce l’evento dovrà essere passata come
proprietà ai componenti più interni
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato
export default function MyApp() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<div>
<h1>Counters that update separately</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
return (
<button onClick={onClick}>
Clicked {count} times
</button>
);
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
export default function Form() {
In questo render number è 0 (tutte e tre le volte) e viene Il valore di una variabile di stato non cambia mai durante il
settato a 1 per il render successivo rendering
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Componenti
e stato
Facciamo attenzione a stati che non contengono variabili semplici, ma array oppure oggetti
non dovrebbe essere modificato con position.x = 10, ma sempre utilizzando il setter apposito affinché venga triggerato un re-
rendering
In altre parole, anche gli oggetti devono essere trattati come dati immutabili
Per far ciò, è necessario creare un nuovo oggetto e passarlo alla funzione setter
Se volessimo lasciare invariate tutte le proprietà dell’oggetto di partenza eccetto che alcune potremmo usare lo spread operator
Allo stesso modo, gli array possono essere ritornati mediante lo spread operator, oppure ottenuti mediante filter, map o slice
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Rendering
condizionale
Abbiamo già incontrato la possibilità di effettuare rendering condizionale all’interno dell’Esercizio 10.2
Si tratta, in generale, di una tecnica estremamente utilizzata in React, specialmente quando è coinvolto lo stato di un
componente
function MyApp() {
[isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
{ isLoggedIn ? <AdminPanel /> : <LoginForm /> }
</div>
)
}
function MyApp() {
[isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div>
{isLoggedIn && <AdminPanel />}
</div>
)
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.5
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Form
in React
L’idea con cui viene realizzato un form in React è che il valore dei suoi elementi venga mantenuto all’interno dello stato del
componente che lo realizza – ciò permette di rispettare il cosiddetto principio di Single Source Of Truth (unica fonte
attendibile)
Lo stato del componente che realizza un form, pertanto, verrà aggiornato ad ogni modifica dei valori del form: dobbiamo,
cioè, guardare tutte le modifiche che l’utente effettua
A sua volta, il form verrà renderizzato riflettendo lo stato attuale
function InputBox() {
const [name, setName] = useState('');
L’uso di checkbox presenta il problema dell’uso dell’attributo checked anziché dell’attributo value
Tale differenza con gli altri elementi dovrà essere appositamente gestita nell’handleChange
Per i radio button, invece, possiamo continuare ad utilizzare lo stesso handler, ricordandoci che la proprietà checked
proverrà dal valore dello stato
function handleChange(event) {
event.target.type === "checkbox" ?
setMyForm({...myForm, [event.target.name]: event.target.checked}) :
setMyForm({...myForm, [event.target.name]: event.target.value})
}
return ( <>
<input type="text" name="firstName" value={myForm.firstName} onChange={handleChange} />
<input type="checkbox" name="isFriendly" checked={myForm.isFriendly} onChange={handleChange} />
<input type="radio" name="gender" value="male"
checked={myForm.gender === "male"} onChange={handleChange} />
<input type="radio" name="gender" value="female"
checked={myForm.gender === "female"} onChange={handleChange} /> </> )
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.5 (continua)
Continuare l’Esercizio 10.5 inserendo un gestore per l’evento change sul componente
ToDoItem che, nel caso di un click su un item, aggiorni lo stato del componente ToDoList per
tenere conto della variazione
Suggerimento
Il gestore dell’evento deve cambiare ToDoList, pertanto va definito come metodo di questo
componente
Tuttavia, esso deve essere utilizzato come event handler sull’evento change di ToDoItem,
pertanto dovrà essere passato come proprietà a quest’ultimo
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.6
Si realizzi un’applicazione che contiene una variabile di stato unreadMessages, ovvero un array di messaggi non letti
Si realizzi, quindi, un componente che venga mostrato se sono presenti messaggi non letti
Tale componente deve a sua volta mostrare i nuovi messaggi, ciascuno con un pulsante per leggerlo e rimuoverlo
dall’array di messaggi non letti
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Form
in React
Per inoltrare un form, potremmo inserire un pulsante ed ascoltare l’evento submit sul form e agire, eventualmente con un
handler asincrono
export default function Form() {
const [answer, setAnswer] = useState(''); Come ragionare per realizzare la nostra
const [error, setError] = useState(null); UI
const [status, setStatus] = useState('typing');
interattiva?
return ( <>
<p>In quale città si trova il miglior Politecnico d'Italia?</p>
<form onSubmit={handleSubmit}>
<textarea value={answer} onChange={e => setAnswer(e.target.value)} disabled={status === 'submitting'} />
<button disabled={answer.length === 0 || status === 'submitting'}>Submit</button>
{error !== null && <p className="Error">{error.message}</p>}
</form>
</> );
}
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Esercizi
Esercizio 10.7
Antonio Ferrara | Fondamenti del Web (Ingegneria del Software e Fondamenti Web)
Sincronizzazione
con gli Effect
Alcuni componenti hanno necessità di sincronizzazione con sistemi esterni (ad esempio con un server per ricevere dati o
inviare un log) quando il componente appare sullo schermo
Gli Effect permettono di eseguire codice subito dopo il primo rendering e dopo il re-rendering relativo ad alcune dipendenze
function MyComponent() {
useEffect(() => {
// Codice da eseguire
});
return <div />;
}