Vue.js permette di sviluppare interfacce web reattive che sfruttano il dual-binding tra modello dati e vista: rende possibile implementare un'applicazione ragionando in termini di dati, variabili e oggetti, astraendosi rispetto all'implementazione e all'aggiornamento del DOM della pagina. È stato definito framework progressivo: è specializzato nella realizzazione delle viste HTML, ma permette di integrarsi facilmente con componenti di altre librerie e progetti.
Dal punto di vista architetturale, Vue implementa il modello MVVM (Model-View-ViewModel), una declinazione del più famoso Model-View-Controller (MVC): i tre componenti sono il Modello (il dominio dati gestito dall'applicazione, come la "M" in MVC), la Vista (il componente grafico renderizzato all'utente, composta da HTML e CSS) e il ViewModel (il collante tra i precedenti, che fornisce alla view i dati in un formato consono alla presentazione e il comportamento dei componenti dinamici).
La principale differenza rispetto a MVC sta tra Controller e ViewModel. Il controller è di fatto una porzione di codice che esegue particolari logiche di business (grazie ai Model) e che ritorna una View da mostrare all'utente. Il ViewModel invece rappresenta un modello parallelo al Model, che viene direttamente bindato alla View e che ne descrive il comportamento con funzioni associate, per esempio, al click su un elemento. In una frase: il Controller esegue logiche di business prima del rendering della View, il ViewModel definisce il comportamento dell'applicazione a runtime.
Vue utilizza un sistema di template basato su HTML e su particolari attributi — chiamati direttive — che permettono di definire il comportamento del layer di presentazione. Il framework è in grado di interpretare il template e di compilarlo in un Virtual DOM, che permette a Vue di offrire reattività dei contenuti ed effettuare cambiamenti alla pagina solo se davvero necessari, evitando sprechi di risorse e di tempo.
Il data-binding è la possibilità di associare un elemento presentazionale a una particolare variabile o oggetto JavaScript ed essere certi che eventuali cambiamenti vengano propagati alla view esclusivamente agendo sulla variabile. Il caso più semplice è l'interpolazione di testi, il binding effettuato attraverso la Mustache Syntax, le doppie parentesi graffe {{...}}.
Il primo esempio delle slide è il classico Hello World: nel modello HTML c'è un elemento con l'interpolazione, e nello script si crea l'applicazione con Vue.createApp, si definisce l'opzione data (che restituisce il modello dati reattivo) e si monta l'app su un container con .mount('#app').
L'esempio successivo mostra il dual binding in azione: la stessa variabile message è sia interpolata sia collegata a un campo di input con v-model. Scrivendo nel campo, l'interpolazione si aggiorna da sola: è la reattività di Vue, ottenuta senza una sola riga di manipolazione del DOM.
Il doppio binding significa che non si scrive mai "aggiorna il DOM": si aggiorna la variabile, e la vista segue. È esattamente il rovescio del modo jQuery di lavorare visto nel capitolo precedente — lo stato non vive più nel DOM.
L'esercitazione vue01 riprende lo stesso concetto con un contatore: prima nella versione "classica" HTML e JavaScript, dove a ogni click si incrementa una variabile e si riscrive il testo del bottone a mano; poi nella versione Vue, dove basta dichiarare lo stato reattivo counter e usare l'evento @click="counter++" nel template. Le slide riassumono le caratteristiche chiave dei componenti Vue: rendering dichiarativo (i template descrivono in modo dichiarativo l'output HTML in base allo stato JavaScript) e reattività (il DOM viene aggiornato quando si verificano modifiche allo stato).
<!-- versione classica: lo stato va aggiornato a mano -->
<button onclick="incrementCounter()" id="button">
You clicked me: 0 times.
</button>
<script>
let counter = 0;
function incrementCounter() {
counter++;
const newText = `You clicked me: ${counter} times.`;
document.getElementById('button').innerText = newText;
}
</script>
<!-- versione Vue: si dichiara lo stato, il DOM segue -->
<div id="app">
<button @click="counter++">
You clicked me: {{ counter }} times.
</button>
</div>
<script>
Vue.createApp({
data() { return { counter: 0 } }
}).mount('#app')
</script>
Le direttive sono speciali attributi HTML che iniziano con il prefisso v- e che permettono di aggiungere comportamenti a livello di presentazione. Le direttive possono avere attributi e comportamenti, nella forma v-{nomedirettiva}:{nomeargomento}. Due esempi fondamentali:
v-bind realizza il dual-binding per gli attributi HTML, con il nome dell'attributo come argomento: <button v-bind:disabled="button1Disabled"> — il bottone risulta disabilitato quando button1Disabled è true;v-on associa una funzione (un comportamento) a un particolare evento generato dall'interfaccia utente: v-on:click.I modificatori sono particolari attributi che agiscono sulla parte funzionale della direttiva. Uno dei più usati è prevent, che usato insieme a v-on blocca il comportamento di default del browser quando un evento viene scatenato (ovvero event.preventDefault()): <a href="#" v-on:click.prevent="goToHomepage">.
Per velocizzare la scrittura esistono due alias: v-bind:attributo può essere sostituito da :attributo, e v-on:click da @click. Le slide mostrano le equivalenze:
<div v-bind:id="customId"></div>
<!-- equivale a -->
<div :id="customId"></div>
<div v-on:click="changePage"></div>
<!-- equivale a -->
<div @click="changePage"></div>
<a v-bind:href="url"></a> <!-- = <a :href="url"></a> -->
<button v-bind:disabled="someDynamicCondition">Button</button>
Queste particolari property assumono valori dinamici in base ad altre property dell'oggetto Vue e reagiscono esattamente come se fossero proprietà statiche. Nell'esempio delle slide, nel template ci sono un input e l'interpolazione di firstname e lastname; nell'istanza Vue, computed definisce fullname come funzione che somma le due variabili:
<div id="app">
<input type="text" v-model="fullname">
{{ firstname }} {{ lastname }}
</div>
Vue.createApp({
data() {
return { firstname: 'Gigi', lastname: 'Rossi' }
},
computed: {
fullname: function() {
return this.firstname + ' ' + this.lastname;
}
}
}).mount('#app')
Si può accedere a fullname come a una normalissima proprietà, al pari di firstname e lastname, ma rimane legata a loro: Vue implementa questo meccanismo come un vero albero di dipendenze e di eventi — al cambio di una property, il framework sa esattamente quali computed properties rigenerare. Da qui deriva il secondo vantaggio, il caching: usare le computed piuttosto che semplici property evita di generare i valori di ritorno ad ogni accesso, rigenerandoli solo se e quando le dipendenze cambiano. Di default le computed property permettono di definire solo il comportamento in lettura, ma è possibile definirne anche quello in scrittura.
Grazie alle direttive v-if, v-else-if e v-else è possibile definire quali elementi del DOM della pagina mostrare in base a condizioni particolari. Per le iterazioni c'è v-for, che cicla una lista di elementi e mostra il contenuto HTML per ciascuno di essi, come un classico for in JavaScript; è in grado di ciclare sia array numerici sia associativi, permettendo di accedere sia al valore sia all'indice. Se sullo stesso nodo HTML sono presenti sia v-for sia v-if, la prima ha la priorità, in modo da poter aggiungere un controllo per ogni elemento della lista.
L'assegnazione di classi CSS può essere fatta con la sintassi ad oggetti: un oggetto composto da una chiave che corrisponde al nome della classe da aggiungere e da un valore che determina se la classe è presente o no. La modalità accetta anche oggetti con diverse proprietà e la possibilità di passare direttamente un oggetto come proprietà:
<div :class="{ 'my-class-name' : hasClass }"><div>
<div :class="{ 'my-class-name-1' : hasClass1, 'my-class-name-2' : hasClass2 }"><div>
<div :class="myCustomClasses"><div>
data() {
return {
myCustomClasses: { 'my-class-name': this.hasClass }
}
}
La sintassi ad array permette invece di passare una lista di classi da attivare su un nodo HTML, inline o come proprietà di tipo array: <div :class="['my-class-name']"><div> oppure <div :class="myCustomClasses"></div> con myCustomClasses: ['my-class-name-1', 'my-class-name-2'].
Per gli stili vale lo stesso doppio approccio. Con la sintassi ad oggetti si comunica a un nodo gli stili da applicare usando la chiave dell'oggetto come nome della property CSS e il valore come valore della regola: <div :style="myCustomStyles"><div> con myCustomStyles: { color: 'red', height: '20px', fontSize: '12em' }. Nel caso di differenti stili si può passare un array di oggetti alla direttiva style: <div :style="[myCustomStyles, { width: '100px' }]"><div>.
Per associare un comportamento a un evento nativo del browser si usa la direttiva v-on o l'alias @: <button v-on:click="onClickButton">Click</button>, con il metodo definito in methods. È possibile scrivere codice JavaScript inline nell'attributo — @click="alert('Button clicked!')" — ma è da preferire l'uso dei metodi.
I modificatori aggiungono comportamenti standard in maniera dichiarativa nel momento stesso in cui avviene l'associazione tra evento e callback, anche concatenandoli:
| Modificatore | Effetto |
|---|---|
.stop | blocca la propagazione dell'evento nella catena di bubbling |
.prevent | previene il comportamento standard dell'elemento HTML (utile per ancore o form) |
.capture | invoca l'evento prima sull'elemento padre e solo successivamente sui figli |
.self | invoca l'evento solo se il nodo corrente è il target (evita invocazioni quando l'evento scatta su elementi figli) |
.once | invoca la callback una sola volta, rimuovendo il listener dopo la prima volta |
Esistono inoltre modificatori dedicati ai tasti della tastiera, da usare con eventi keyboard-based come keyup o keydown — ad esempio <input @keyup.enter="submit"> — e sono: .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right, .ctrl, .alt, .shift, .meta. Ci sono anche modificatori speculari basati sugli eventi del mouse: .left, .right e .middle.
La principale direttiva per la gestione degli input lato utente è v-model, che associa in maniera bidirezionale un modello dati a un campo di una form: <input type="text" v-model="myText"> {{ myText }} mantiene il contenuto di myText associato al valore presente nel campo di testo. Tramite v-bind:true-value, v-bind:false-value e v-bind:value è possibile modificare il valore di checkbox e radiobutton nei form.
Anche v-model ha i suoi modificatori. .lazy cambia la modalità di aggiornamento del valore JavaScript: di default viene usato l'evento input, mentre con .lazy viene usato l'evento nativo change — utile quando servono task lenti o onerosi e si preferisce limitare l'interattività. .number converte automaticamente il valore a numero; .trim rimuove gli spazi all'inizio e alla fine del valore inserito. .trim e .number permettono di evitare bug che potrebbero sorgere a fronte di una validazione dimenticata.
I componenti sono senza dubbio una delle funzionalità più interessanti di Vue: permettono di progettare applicazioni di medie/grandi dimensioni organizzando il codice in moduli riutilizzabili e ben strutturati. Ciascun componente rappresenta una porzione di HTML arricchita da una logica di business incapsulata ed eventualmente da una serie di regole CSS dedicate. Per utilizzare un componente si può usare un custom tag — <mio-componente></mio-componente> — oppure l'attributo is: <div is="mio-componente"></div>. Per poterli usare, i componenti vanno registrati: globalmente, se devono essere disponibili in tutta l'applicazione (app.component("hello-world", HelloWorld)), oppure localmente, se vanno usati solo all'interno di un altro componente — un componente registrato localmente non può essere usato nei discendenti.
Usando i componenti cambia la sintassi dell'opzione data: non può più essere un oggetto, ma deve essere una funzione. Il motivo è che, con diverse istanze dello stesso componente, non possiamo permetterci che i dati vengano sovrascritti. Ogni componente deve avere un template, definibile in tre modi:
app.component('my-component', {
template: '<strong>Io sono il corpo del component</strong>'
});
Il template è definito come stringa all'interno del componente; nel file HTML si usa il custom tag <mio-componente></mio-componente>.
app.component('mio-componente', {
template: '#mio-componente-template'
});
<mio-componente></mio-componente>
<template id="mio-componente-template">
<strong>Io sono il corpo del componente</strong>
</template>
Il template è contenuto in un tag <template> o <script> con type text/x-template: permette componenti con template di complessità notevolmente superiore.
<mio-componente inline-template>
<strong>Io sono il corpo del componente</strong>
</mio-componente>
Il template è inline nel nodo stesso del componente: permette di avere diverse istanze dello stesso componente ma con template differenti, usando l'attributo inline-template.
Le props rappresentano le proprietà esterne che permettono di configurare un componente. Dato che ogni componente ha uno scope di esecuzione isolato, qualsiasi dato esterno che deve essere accessibile dentro il componente va passato tramite una prop: le proprietà devono essere definite a priori con l'opzione props, e se necessario se ne può definire il tipo (String, Boolean, Number…), l'obbligatorietà (required) o un valore di default. Le props possono essere passate in maniera statica o dinamica: in quest'ultimo caso, una modifica della variabile all'esterno del componente produce una modifica anche al valore della proprietà.
Il motore di Vue implementa il cosiddetto one way data flow: le prop passate a un componente possono essere modificate dall'esterno, ma non dal componente stesso, perché il flusso di dati è monodirezionale, dall'alto verso il basso. Se per qualche ragione si cercasse di modificare il valore di una prop all'interno di un componente, Vue lancia un warning in console. La soluzione per comunicare qualcosa da un componente verso l'esterno sono gli eventi custom: ogni componente presenta due metodi, $on (mettersi in ascolto — spesso sostituito dalla direttiva v-on) e $emit (generare un evento). Dato che l'esigenza di propagare modifiche di dati dal componente verso il padre è frequente, dalla versione 2.3.0 esiste il modificatore sync: è rapido e non richiede la scrittura di metodi ad hoc.
Gli slot risolvono il caso in cui il template non sia sufficiente a rappresentare le esigenze, perché alcuni aspetti possono essere dinamici in base all'istanza del componente: sono porzioni di template configurabili dall'esterno del componente e mixate al template originale. Le best practice degli sviluppatori Vue per definire componenti riutilizzabili si riassumono in tre regole: le props per passare un set di dati al componente dal contesto esterno; gli eventi per influenzare il contesto esterno dall'interno del componente; gli slot per far influenzare il contenuto del componente dal contesto esterno. Infine, quando serve intervenire direttamente sul DOM tramite plugin esterni o API native del browser, è possibile creare direttive personalizzate da richiamare nei template esattamente come le direttive native (per esempio v-if o v-model).
Le cinque esercitazioni sul laboratorio Vue seguono un percorso preciso, dall'istanza minima ai plugin. Ecco la mappa, con i concetti che ciascuna introduce.
| Esercitazione | Argomenti |
|---|---|
| vue01 | Introduzione: cos'è Vue.js, i quattro passaggi (import del framework, definizione del template, definizione dello stato reattivo, installazione), il contatore in HTML/JS contro la versione Vue, rendering dichiarativo e reattività. |
| vue02 | L'istanza minimale: template nel DOM o come proprietà dell'istanza (il valore della proprietà ha precedenza), più istanze su container diversi senza condivisione di proprietà, componenti e loro registrazione globale/locale con le convenzioni di naming. |
| vue03 | La nomenclatura delle relazioni (parent, child, sibling) e la comunicazione unidirezionale: parent-child con props (e il loro binding, con l'esempio simple-props.html) e slots; child-parent con gli events. |
| vue04 | API ulteriori: v-model nei componenti custom (props + eventi update:myProp sulla medesima proprietà), watchers (reagire ai cambiamenti dei dati in modo controllato), provide/inject (condividere dati tra componenti lontani senza props nidificate, e quando usarlo), introduzione alla Composition API. |
| vue05 | I plugins: codice autonomo che aggiunge funzionalità globali (rotte, stato globale, logger, i18n), oggetto o funzione con install(app), installazione con app.use(MyPlugin), proprietà globali come $hello e $api, esempio di plugin per chiamate API con axios; Vue Router come libreria ufficiale per il routing lato client, con le sue modalità e funzionalità; e i plugin dell'ecosistema: Pinia, i18n, Vuetify. |
L'esempio di props dell'esercitazione vue03 merita di essere visto da vicino: il parent usa il componente strong-text due volte con messaggi diversi, e il child dichiara la prop message con il tipo String — è il riutilizzo del componente reso parametrizzabile:
<div id="app">
Hello
<strong-text message="my friend"></strong-text>
and welcome to
<strong-text message="this example"></strong-text>
</div>
<script>
const StrongText = Vue.defineComponent({
template: '<strong>{{ message }}</strong>',
props: { message: String }
});
Vue.createApp({
components: { StrongText }
}).mount('#app');
</script>
E l'esempio di plugin dell'esercitazione vue05 mostra il contratto completo: un plugin espone install(app, options), lo si installa in main.js con app.use(MyPlugin, {...}), e in qualsiasi componente si può usare la proprietà globale che ha registrato — in questo caso this.$hello("Manuel") stampa Ciao, Manuel! in console:
// Plugin
export default {
install(app, options) {
app.config.globalProperties.$hello = (name) => {
console.log(`Ciao, ${name}!`)
}
}
}
// installazione in main.js
import MyPlugin from './plugins/MyPlugin.js'
app.use(MyPlugin, {...})
Vue.js è un framework JavaScript per interfacce web reattive con dual-binding tra modello dati e vista: si ragiona in termini di dati, variabili e oggetti, astraendosi dal DOM. È "progressivo" perché è specializzato nella realizzazione delle viste HTML ma si integra facilmente con componenti di altre librerie, e perché si può espandere a seconda della complessità dell'applicazione.
MVVM è una declinazione di MVC con tre componenti: Model (dominio dati), View (componente grafico, HTML e CSS) e ViewModel (il collante che fornisce alla view i dati in formato consono alla presentazione e il comportamento dei componenti dinamici). La differenza: il Controller esegue logiche di business prima del rendering della View; il ViewModel definisce il comportamento dell'applicazione a runtime, bindato alla View con funzioni associate per esempio al click. Vue si focalizza sul ViewModel layer, connettendo View e Model con un two-way data binding.
Sono speciali attributi HTML che iniziano con v- e aggiungono comportamenti a livello di presentazione, nella forma v-{nomedirettiva}:{nomeargomento}. Esempi: v-bind:href (dual binding su attributi HTML, alias :href), v-on:click (associa una funzione a un evento, alias @click), v-if/v-else-if/v-else (rendering condizionale), v-for (iterazione, prioritaria su v-if sullo stesso nodo), v-model (binding bidirezionale sui form).
Sono property che assumono valori dinamici in base ad altre property dell'oggetto Vue e reagiscono come proprietà statiche: si accede a fullname come a una normalissima proprietà, ma rimane legata a firstname e lastname. Vue le gestisce come un albero di dipendenze: al cambio di una property sa esattamente quali computed rigenerare. Il secondo vantaggio è il caching: i valori non vengono rigenerati ad ogni accesso, ma solo quando le dipendenze cambiano. Di default definiscono solo la lettura, ma è possibile definirne anche la scrittura.
Modificatori di evento: .stop (blocca il bubbling), .prevent (previene il comportamento standard), .capture (evento prima sul padre), .self (solo se il nodo corrente è il target), .once (una sola volta); più i modificatori da tastiera (.enter, .tab, .esc, .ctrl, …) e quelli del mouse (.left, .right, .middle). Modificatori di v-model: .lazy (aggiorna sull'evento change invece che su input), .number (converte a numero), .trim (rimuove spazi iniziali/finali).
Globalmente con app.component("hello-world", HelloWorld) prima del mount, se servono in tutta l'applicazione; localmente con l'opzione components di un altro componente, se servono solo lì (un componente registrato localmente non è usabile nei discendenti). L'opzione data nei componenti deve essere una funzione che restituisce l'oggetto dati: con diverse istanze dello stesso componente non possiamo permetterci che i dati vengano sovrascritti.
Le props passate a un componente possono essere modificate dall'esterno ma non dal componente stesso: il flusso è monodirezionale dall'alto verso il basso, e un tentativo di modifica genera un warning in console. Per comunicare verso l'esterno si usano gli eventi custom: $emit per generare un evento, $on/v-on per mettersi in ascolto; dalla versione 2.3.0 c'è anche il modificatore sync. Per componenti non correlati si crea un'istanza extra di Vue che fa da bus di eventi.
Template come stringa (dentro il componente), template come nodo esterno (in un tag <template> o <script type="text/x-template">, per template di complessità superiore) e template inline (nel nodo stesso del componente con l'attributo inline-template, per avere istanze diverse dello stesso componente con template differenti).
Tre modalità di interfacciamento: le props per passare un set di dati al componente dal contesto esterno; gli eventi per influenzare il contesto esterno dall'interno del componente; gli slot perché il contesto esterno possa influenzare il contenuto del componente. A queste si aggiunge la possibilità di creare direttive personalizzate per intervenire direttamente sul DOM.
vue01: introduzione, i quattro passaggi (import, template, stato reattivo, mount), contatore HTML/JS vs Vue, rendering dichiarativo e reattività. vue02: istanza minimale, template nel DOM o come proprietà, più istanze, componenti e registrazione. vue03: nomenclatura parent/child/sibling, comunicazione parent-child (props, slots) e child-parent (events). vue04: v-model nei componenti custom, watchers, provide/inject, Composition API. vue05: plugin (install/app.use/proprietà globali), Vue Router per il routing lato client, ecosistema (Pinia, i18n, Vuetify).