In quest’ultima lezione, introdurremo brevemente Vue Router che rappresenta la soluzione ufficiale per implementare un meccanismo di navigazione lato client in applicazioni realizzate con Vue.js.
Prima di parlare di Vue Router, è bene chiarire alcuni concetti chiave.
SPA e Routing lato client
In un sito internet classico, ogni volta che l’utente vuole visitare una nuova pagina, il browser effettua una richiesta al server e scarica nuovamente un nuovo file HTML ed eventuali fogli di stile e file javascript.
L’avvento delle SPA (Single-page applications) e dei vari framework e librerie front-end (Angular, Ember.js, React, Vue, Svelte, Alpine.js ecc…) ha introdotto un modo del tutto nuovo di navigare all’interno di un sito internet.
L’utente effettua una prima richiesta al server per scaricare una pagina iniziale, successivamente il processo di navigazione all’interno dell’applicazione viene gestito direttamente dal browser. Ogni volta che cambia il percorso nella barra degli indirizzi, entra in scena un componente chiave dell’applicazione, ovvero il Router che si occupa di sostituire dinamicamente parte della pagina corrente caricando le nuove informazioni richieste.
Il processo di navigazione avviene quindi direttamente lato client senza scaricare delle nuove pagine, ma recuperando dal server eventuali dati attraverso richieste AJAX.
Così facendo, migliora l’esperienza dell’utente a cui sembrerà di spostarsi fra le sezioni dell’applicazione in modo più fluido e più simile al meccanismo di navigazione di applicazioni native Desktop e mobili.
Spieghiamo Vue Router tramite un esempio
Qual è il modo più semplice per creare un’applicazione con Vue Router? A questo punto della guida la risposta è abbastanza scontata: Vue CLI.
Cercheremo allora di spiegare il funzionamento di Vue Router tramite un esempio. Realizzeremo un sito internet in cui accedendo alla pagina principale si viene sempre reindirizzati verso il percorso ‘/ristoranti’ dove è presente un elenco di catene fittizie di ristoranti. Per ognuna possiamo visualizzare una pagina con una descrizione e dei link che consentono di vedere l’indirizzo dei diversi ristoranti e il loro punteggio.
La versione finale dell’applicazione è disponibile su Bitbucket
Iniziamo a realizzare la nuova applicazione spostandoci in una cartella e lanciando il comando vue create router-example
. Selezioniamo come funzionalità Babel, Vue Router, Vuex e Linter / Formatter (ci spostiamo con i tasti freccia e confermiamo con la barra spaziatrice).
Dopo aver selezionato le diverse opzioni, visualizzeremo la seguente scheda di riepilogo.
I file generati da Vue CLI sono simili a quelli già visti negli esempi illustrati nelle precedenti lezioni. La sola differenza consiste nella creazione di una nuova cartella per la configurazione di Vue Router e della modifica del file main.js
.
tree router-example -aF --dirsfirst -I node_modules
router-example
├── public/
│ ├── favicon.ico
│ └── index.html
├── src/
│ ├── assets/
│ │ └── logo.png
│ ├── components/
│ │ ├── RestaurantAddress.vue
│ │ └── RestaurantDescription.vue
│ ├── router/
│ │ └── index.js
│ ├── services/
│ │ └── RestaurantService.js
│ ├── store/
│ │ └── index.js
│ ├── views/
│ │ ├── 404.vue
│ │ ├── About.vue
│ │ ├── Restaurant.vue
│ │ ├── RestaurantLocation.vue
│ │ └── Restaurants.vue
│ ├── App.vue
│ └── main.js
├── .browserslistrc
├── .env.development
├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── README.md
├── babel.config.js
├── db.json
├── package-lock.json
└── package.json
8 directories, 25 files
Prima di parlare in maggior dettaglio di Vue Router, installiamo un’altra dipendenza necessaria alla realizzazione del nostro esempio. Lanciamo il comando npm install axios
per aggiungere Axios che consente di eseguire richieste di tipo AJAX ad un server locale fittizio. Per quest’ultimo useremo JSON Server che abbiamo in precedenza installato globalmente con il comando npm i -g json-server
(‘i’ è un alias abbreviato di ‘install’).
(È bene evidenziare che Vue Router è assolutamente indipendente da Vuex, Axios o altre dipendenze che abbiamo installato per creare un esempio completo.)
Nel file db.json
aggiungiamo i seguenti dati che verranno serviti da JSON server il quale è stato lanciato con il comando json-server -p 5555 --watch db.json
.
{
"restaurants": [
{
"id": 0,
"name": "Pizza Pazza",
"slug": "pizza-pazza",
"description": "Sed posuere consectetur est at lobortis.",
"locations": [
{
"id": 0,
"address": "via Napoli, 1",
"rating": 4
},
{
"id": 1,
"address": "via Pascoli, 19",
"rating": 3
},
{
"id": 2,
"address": "via Mazzini, 33",
"rating": 4
},
{
"id": 3,
"address": "Corso Giulio Cesare, 79",
"rating": 3
},
{
"id": 4,
"address": "viale Giacomo Leopardi, 98",
"rating": 4
}
]
},
{
"id": 1,
"name": "Mille Pasti",
"slug": "mille-pasti",
"description": "Donec ullamcorper nulla non metus auctor fringilla.",
"locations": [
{
"id": 0,
"address": "Piazza Garibaldi, 64",
"rating": 5
}
]
},
{
"id": 2,
"name": "El Cucurucho del Mar",
"slug": "el-cucurucho-del-mar",
"description": "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.",
"locations": [
{
"id": 0,
"address": "Corso Maggiore, 53",
"rating": 4
},
{
"id": 1,
"address": "Via Gioacchino Rossini, 11",
"rating": 5
},
{
"id": 2,
"address": "Piazza Grande, 44",
"rating": 4
}
]
},
{
"id": 3,
"name": "Lando's",
"slug": "landos",
"description": "Morbi leo risus, porta ac consectetur ac, vestibulum at eros.",
"locations": [
{
"id": 0,
"address": "via del Castello, 85",
"rating": 2
},
{
"id": 1,
"address": "Corso Italia, 66",
"rating": 5
}
]
}
]
}
Configurare Vue Router
Torniamo a concentrare la nostra attenzione su Vue Router e vediamo quali sono i passaggi da seguire per una corretta configurazione.
Il primo passo da compiere consiste nel definire dei percorsi (‘routes’) e creare un’istanza di Vue Router. Per questo motivo, Vue CLI ha già creato una nuova cartella router
in cui è presente il file index.js
. In quest’ultimo file definiamo un array di routes che poi utilizziamo per ottenere una nuova istanza di Vue Router.
// file: src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Restaurants from '../views/Restaurants.vue';
import Restaurant from '../views/Restaurant.vue';
import RestaurantLocation from '../views/RestaurantLocation.vue';
import Error404 from '../views/404.vue';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'home',
redirect: { name: 'restaurants' }
},
{
path: '/ristoranti',
name: 'restaurants',
component: Restaurants
},
{
path: '/ristoranti/:slug',
name: 'restaurant',
component: Restaurant,
children: [
{
path: ':id',
name: 'restaurant-location',
component: RestaurantLocation
}
]
},
{
path: '/chi-siamo',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '*',
name: 'all',
component: Error404
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
linkExactActiveClass: 'active',
linkActiveClass: 'active',
routes
});
export default router;
Per creare una nuova istanza di Vue Router usiamo un oggetto di configurazioni in cui, oltre alle routes, indichiamo quale deve essere l’indirizzo base dell’applicazione (base: process.env.BASE_URL
). Se per esempio base è pari a ‘/’, la pagina iniziale sarà raggiungibile, in fase di sviluppo, all’indirizzo http://localhost:8080/
Con le opzioni linkActiveClass
linkExactActiveClass
scegliamo il nome della classe che deve essere applicata da Vue router ai link attivi usati per la navigazione. Vedremo infatti che al posto dei normali elementi <a>, dovremo usare dei componenti <router-link>. Vue Router si occuperà di applicare automaticamente la classe ‘active’ agli elementi di navigazione corrispondenti al percorso corrente. Supponiamo per esempio di avere un menu di navigazione con un link verso ‘/ristoranti’. Quando nella barra degli indirizzi avremo qualcosa del tipo http://localhost:8080/ristoranti
, al link che puntava verso ‘/ristoranti’ viene aggiunta la classe ‘active’. L’opzione linkExactActiveClass
vale in caso di un match esatto. Per esempio, un link che punta a ‘/ristoranti’ avrà la classe ‘active’ solo se il percorso è esattamente pari a ‘/ristoranti’. L’opzione linkExactActiveClass
non applica la classe se invece il percorso è del tipo ‘/ristoranti/nome-ristorante’. Per casi meno restrittivi possiamo allora usare linkActiveClass
.
Per l’istanza del Router selezioniamo poi la modalità in cui deve operare (mode: 'history'
). La modalità predefinità (hash
) usa il frammento (#frammento) dell’indirizzo per la navigazione. In questo caso, spostandoci da una view all’altra, verrà modificato solo il frammento dell’indirizzo. Per esempio potremo avere un indirizzo del tipo http://localhost:8888/#/ristoranti
. Usando il frammento, non verrà mai fatta una nuova richiesta al server.
La modalità da noi usata è quella più comune e richiede che il browser supporti la History API. È inoltre necessario configurare opportunamente il server (in fase di sviluppo si occupa di tutto Vue CLI) in modo da non visualizzare un errore 404 quando si cambia view. Il server dovrà sempre restituire la pagina iniziale index.html
a meno che non si cerca di accedere ad una qualche risorsa statica come un’immagine, uno script o un foglio di stile. Gli URL avranno un aspetto più piacevole in quanto vengono usati solo i percorsi dell’indirizzo. Facendo riferimento all’indirizzo visto nella modalità precedente, in questo caso sarà rimosso il simbolo ‘#’ (hash) ed avremo semplicemente http://localhost:8888/ristoranti
Passiamo ora ad analizzare l’array dei percorsi.
const routes = [
{
path: '/',
name: 'home',
redirect: { name: 'restaurants' }
},
{
path: '/ristoranti',
name: 'restaurants',
component: Restaurants
},
{
path: '/ristoranti/:slug',
name: 'restaurant',
component: Restaurant,
children: [
{
path: ':id',
name: 'restaurant-location',
component: RestaurantLocation
}
]
},
{
path: '/chi-siamo',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '*',
name: 'all',
component: Error404
}
];
I percorsi dell’applicazione sono definiti in un array. Per ciascun percorso usiamo un oggetto di opzioni. Gli oggetti presentano una proprietà path
che indica per quale percorso è valida la configurazione di ciascun oggetto. In ogni oggetto usiamo inoltre la proprietà name
per identificare ciascun percorso tramite un nome che potremo usare poi per la navigazione nel resto dell’applicazione.
Se iniziamo ad analizzare il primo oggetto dell’array, notiamo che per il percorso ‘/’, identificato dal nome ‘home’, deve essere eseguito il redirect verso il percorso identificato dal nome ‘restaurants’, ovvero verso il percorso ‘/ristoranti’. Quando nella barra degli indirizzi ci spostiamo verso il percorso ‘/ristoranti’, dovrà essere visualizzato il componente Restaurants
.
Il componente <router-view>
Ma dove verrà visualizzato esattamente il componente? In quale punto della nostra applicazione?
Per indicare l’area in cui inserire i componenti al variare del percorso dovremo usare <router-view>
nel template di un certo componente. Possiamo pensare a <router-view>
come una sorta di segnaposto. Quando il percorso dell’indirizzo cambia, Vue Router preleva il componente corretto dalla configurazione e lo inserisce nello spazio dell’applicazione riservata da <router-view>
.
Nel nostro caso inseriremo il componente <router-view>
principale (possono esserci più componenti <router-view>
nell’applicazione e descriveremo un caso in cui ciò accade a breve) nel template del componente base App
.
<template>
<div id="app">
<div id="nav">
<router-link to="/ristoranti">Ristoranti</router-link> |
<router-link to="/chi-siamo">Chi Siamo</router-link>
</div>
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</template>
Abbiamo racchiuso ><router-view>
fra i tag <transition>
in modo da applicare una transizione in dissolvenza quando vengono sostituiti i componenti in risposta alla variazione del percorso dell’indirizzo. Ritorneremo a parlare di <App>
più in tardi, per il momento continuiamo ad analizzare l’array routes
.
Percorsi annidati
Torniamo ad analizzare il resto dell’array routes
presente nel file src/router/index.js
.
{
path: '/ristoranti/:slug',
name: 'restaurant',
component: Restaurant,
children: [
{
path: ':id',
name: 'restaurant-location',
component: RestaurantLocation
}
]
}
Il terzo oggetto dell’array è un po’ più complicato. In questo caso usiamo l’identificativo ‘restaurant’ grazie alla proprietà name
, ma non si tratta più di un percorso statico come nei casi precedenti. Notiamo infatti che alla proprietà path
associamo una stringa particolare &apos/ristoranti/:slug'
che intercetterà tutti i percorsi che iniziano con ‘/ristoranti/’ seguiti da una stringa che può variare e che sarà inclusa nel parametro slug
. Il segmento dinamico è sempre preceduto dal carattere ‘:’ (:slug
nel nostro caso) ed è accessibile nel template del componente Restaurant
attraverso $route.params.slug
(this.$route.params.slug
nella sezione javascript del componente).
Se per esempio l’indirizzo nella barra del browser è http://localhost:8080/ristoranti/pizza-pazza
, allora all’interno di Restaurant
avremo accesso ad un oggetto $route.params
con una proprietà slug: 'pizza-pazza'
.
Un’altra novità rispetto ai percorsi analizzati in precedenza è rappresentata dalla proprietà children
che è a sua volta un array di oggetti per definire dei percorsi annidati (Nested Routes) relativi al percorso /ristoranti/:slug
. Si tratta di un array che può avere un certo numero di oggetti del tutto simili a quelli analizzati finora per le altre routes. Nel caso particolare abbiamo definito un solo percorso annidato che è inoltre di tipo dinamico ed utilizza un parametro id
. Avremmo però potuto definire altri oggetti per percorsi differenti anche statici.
Cerchiamo di fare chiarezza su questo punto prendendo come riferimento l’oggetto dell’esempio. Quando visitiamo l’indirizzo http://localhost:8080/ristoranti/pizza-pazza
viene inserito il componente Restaurant
al posto del segnaposto <router-view>
presente in <App>
.
Nel template di <Restaurant>
è presente a sua volta un altro componente <router-view>
.
<!-- template del componente Restaurant -->
<template>
<div>
<!-- resto del template -->
<!-- ... -->
<router-view />
</div>
</template>
Quando l’indirizzo del browser è nella forma http://localhost:8080/:slug/:id
(per esempio http://localhost:8080/pizza-pazza/0
), allora viene inserito il componente Restaurant
al posto di <router-view>
presente nel componente <App>
e RestaurantLocation
al posto del componente <router-view>
nel template di <Restaurant>
.
Notiamo che tutti i componenti usati all’interno dell’array delle Routes sono nella cartella src/views
a differenza degli altri componenti che si trovano invece in src/components
.
Code splitting
{
path: '/chi-siamo',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue')
}
Sempre nella cartella src/views
è situato il file About.vue
che importiamo dinamicamente. Infatti, il quarto oggetto dell’array routes
non usa un riferimento ad un componente come valore della proprietà component
. Al contrario si affida a Webpack che tramite la funzione import()
consente di implementare la tecnica denominata code-splitting. Ciò significa che al contrario degli altri componenti, <About>
non viene inserito nel bundle finale generato da Webpack e scaricato all’apertura dell’applicazione. Invece, viene creato un file separato, il cui nome è scelto attraverso il commento /* webpackChunkName: "nome-del-file" */
, che viene scaricato solo nel momento in cui si visita il percorso /chi-siamo
come mostrato nel video sottostante. (Ovviamente avremmo potuto fare la stessa scelta anche per gli altri percorsi)
Gestione degli errori tramite carattere *
L’ultimo oggetto dell’array routes
è identificato dal nome 'all'
e utilizza '*'
come valore della proprietà path
.
{
path: '*',
name: 'all',
component: Error404
}
Tramite il carattere asterisco indichiamo che vogliamo intercettare tutti i percorsi. Per questo motivo è importante dire che Vue Router scorre in ordine tutto l’array dei percorsi e si ferma non appena ne incontra uno che ritiene valido. Ecco per quale ragione abbiamo inserito tale oggetto per ultimo nell’array. In caso contrario verrebbe sempre visualizzato il componente Error404
. In questo modo vogliamo invece mostrare Error404
solo se nessun altro oggetto dell’array routes
è valido per il percorso corrente.
Navigare all’interno dell’applicazione
Per spostarci da una pagina all’altra in un ‘normale’ sito internet usiamo degli elementi <a>
con attributo href
.
Con Vue Router abbiamo invece due possibilità.
La prima consiste nell’utilizzare il componente <router-link>
all’interno del template di un componente.
<router-link>
supporta diverse props. Per esempio possiamo stabilire direttamente a livello di componente quali classi applicare se il link è attivo tramite exact-active-class
e active-class
. Maggiori informazioni sulle altre props sono disponibili sul sito ufficiale.
Vogliamo invece presentare in maggior dettaglio la prop to
che è sicuramente quella più utilizzata. Nella sua forma più semplice, alla prop to
viene assegnato un valore di tipo stringa che rappresenta un percorso.
<router-link to="/ristoranti">Ristoranti</router-link>
Al posto del componente riportato sopra, viene inserito nel DOM un elemento <a>
con attributo href
.
<a href="/ristoranti">Ristoranti</a>
Trattandosi però di una prop di un componente, possiamo usare delle espressioni javascript attraverso la direttiva v-bind
. È possibile sostituire una semplice stringa con un oggetto javascript che presenta una proprietà path
. Il frammento di codice sottostante è del tutto equivalente a quello riportato sopra.
<router-link :to="{ path: '/ristoranti'}">Ristoranti</router-link>
Supponendo di avere una proprietà restaurant
nel componente, possiamo passare anche un parametro ed eventualmente una stringa di ricerca come mostrato sotto.
<router-link
:to="{ path: `/ristoranti/${restaurant.slug}`, query: { abc: 'test' } }"
>Dettagli</router-link>
Se restaurant.slug
è pari a ‘pizza-pazza’, alla fine avremo nel DOM il seguente elemento.
<a href="/ristoranti/pizza-pazza?abc=test">Ristoranti</a>
Se però facciamo nuovamente riferimento all’array routes
, ad ogni percorso avevamo assegnato un nome. Alla prop to
possiamo assegnare un oggetto con una proprietà name
per i percorsi con nome (Named Routes). Così facendo, se in futuro decidiamo di cambiare la proprietà path
di una certa route, non dovremo preoccuparci di ritrovare e modificare tutti i <router-link>
.
Possiamo allora usare un componente <router-link>
come quello riportato sotto.
<router-link
:to="{ name: 'restaurant', params: { slug: restaurant.slug } }"
>Dettagli</router-link
>
Se restaurant.slug
è sempre pari a ‘pizza-pazza’, nel DOM verrà inserito il seguente elemento.
<a href="/ristoranti/pizza-pazza" >Dettagli</a>
Navigazione programmatica
Quando clicchiamo su <router-link>
viene usato internamente il metodo router.push()
. All’interno di ciascun componente abbiamo infatti accesso, oltre alla proprietà this.$route
, a this.$router
che presenta una serie di metodi per effettuare la navigazione in maniera programmatica all’interno del codice javascript del componente. In modo simile a quanto visto per <router-link>
, router.push()
può ricevere in ingresso una stringa o un oggetto con le proprietà incontrate negli esempi riportati sopra (path
, name
, params
, query
).
Oltre a router.push()
sono disponibili altri metodi come router.replace()
e router.go(n)
. Quest’ultimo permette di muoversi avanti e indietro nella cronologia di n passi (n può essere un interno positivo o negativo). Per maggiori dettagli è possibile consultare la pagina dedicata della documentazione.
Esempio completo con Vue Router
Ricapitolando quanto visto finora. Abbiamo inizializzato un nuovo progetto con Vue CLI selezionando Vue Router fra le funzionalità da aggiungere insieme a Vuex.
Nel file src/router/index.js
abbiamo definito un array routes
che contiene degli oggetti i quali descrivono il comportamento per una serie di percorsi dell’applicazione. Abbiamo poi usato, insieme ad altre opzioni, l’array routes
per creare una nuova istanza di Vue Router.
Continuando quindi col nostro esempio, spostiamoci nel file src/main.js
in cui importiamo il router e lo inseriamo nell’oggetto di configurazione dell’istanza base di Vue come riportato sotto.
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
new Vue({
router, // Aggiungiamo Vue Router
store,
render: (h) => h(App)
}).$mount('#app');
A questo punto, all’interno del template del componente base <App>
aggiungiamo l’area (componente <router-view>
) in cui dovranno essere opportunamente caricati i componenti corrispondenti ai percorsi di primo livello che abbiamo definito nell’array routes
.
<template>
<div id="app">
<nav id="nav">
<router-link to="/ristoranti">Ristoranti</router-link> |
<router-link to="/chi-siamo">Chi Siamo</router-link>
</nav>
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</template>
<script>
export default {
name: 'App',
created() {
this.$store.dispatch('getAllRestaurants');
}
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
text-decoration: none;
}
#nav a.active {
color: #42b983;
text-decoration: underline;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 500ms ease;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>
Il componente <App>
presenta un lifecycle hook created()
in cui lanciamo un’action per recuperare la lista dei ristoranti da mostrare.
Nella cartella store
abbiamo infatti un file index.js
in cui definiamo lo store Vuex come mostrato sotto.
import Vue from 'vue';
import Vuex from 'vuex';
import RestaurantService from '@/services/RestaurantService';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
restaurants: []
},
mutations: {
SET_RESTAURANTS(state, restaurants) {
state.restaurants = restaurants;
}
},
actions: {
async getAllRestaurants({ commit }) {
try {
const response = await RestaurantService.getAllRestaurants();
commit('SET_RESTAURANTS', response.data);
} catch (error) {
console.log(error);
}
}
},
getters: {
getRestaurantBySlug: (state) => (slug) =>
state.restaurants.find((restaurant) => restaurant.slug === slug),
getRestaurantLocationById: (state) => ({ slug, id }) => {
const restaurant = state.restaurants.find(
(restaurant) => restaurant.slug === slug
);
const locations = restaurant.locations;
return locations.find((location) => location.id === +id);
}
}
});
Nello store sono presenti due getters getRestaurantBySlug
e getRestaurantLocationById
. Il primo consente di filtrare la lista di ristoranti in base alla proprietà slug
. Nel secondo caso recuperiamo gli indirizzi di uno specifico ristorante identificato da un certo valore di slug
che insieme ad id
costituiscono un oggetto passato a getRestaurantLocationById
come argomento (A breve vedremo come invocare i getters).
All’interno dell’action getAllRestaurants()
usiamo async/await per gestire la promise restituita dal servizio che abbiamo definito nel file src/services/RestaurantService.js
.
import axios from 'axios';
const { VUE_APP_REMOTE_ADDRESS: ADDRESS, VUE_APP_PORT: PORT } = process.env;
const axiosInstance = axios.create({
baseURL: `//${ADDRESS}:${PORT}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
export default {
getAllRestaurants() {
return axiosInstance.get('/restaurants');
},
getRestaurant(id) {
return axiosInstance.get(`/restaurants/${id}`);
}
};
In modo del tutto simile a quanto già visto nelle precedenti lezioni su Vuex, usiamo Axios per effettuare una richiesta di tipo GET al server locale (Abbiamo in precedenza eseguito json-server -p 5555 --watch db.json
in una finestra del terminale per lanciare un server locale)
Nel template del componente App
abbiamo una barra di navigazione che consente di visitare i due percorsi /chi-siamo
e /ristoranti
.
Nel primo caso carichiamo il componente definito in src/views/About.vue
al posto di <router-view>
che è nel template di App
.
<template>
<div class="about">
<h1>Chi siamo</h1>
</div>
</template>
Nel secondo caso, visualizziamo invece il componente definito in src/views/Restaurants.vue
.
<template>
<div class="restaurants">
<h1>Restaurants</h1>
<div v-for="restaurant in restaurants" :key="restaurant.id">
<h2>{{ restaurant.name }}</h2>
<router-link
:to="{ name: 'restaurant', params: { slug: restaurant.slug } }"
>Dettagli</router-link
>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'Restaurants',
computed: mapState(['restaurants'])
};
</script>
Nel componente riportato sopra, definiamo una computed property restaurants
che dipende dall’omonima proprietà dello state dello store Vuex. Per far ciò usiamo la funzione mapState()
il cui funzionamento è stato già illustrato nelle precedenti lezioni.
Nel template di Restaurants
è presente un elenco di ristoranti per ognuno dei quali creiamo un link alla pagina che contiene la sua descrizione e i suoi indirizzi. Cliccando su ‘Dettagli’, cambia l’indirizzo dell’applicazione. Per esempio per il ristorante ‘Pizza Pazza’ ci spostiamo all’indirizzo http://localhost:8080/ristoranti/pizza-pazza
e il componente Restaurants
viene sostituito con quello definito nel file src/views/Restaurant.vue
.
<template>
<div v-if="restaurant">
<RestaurantDescription
:name="restaurant.name"
:description="restaurant.description"
/>
<h3>I nostri ristoranti</h3>
<router-link
v-for="restaurantLocation in restaurant.locations"
:key="restaurantLocation.id"
:to="{
name: 'restaurant-location',
params: { id: restaurantLocation.id }
}"
>
Ristorante {{ restaurantLocation.id }}
</router-link>
<router-view />
</div>
</template>
<script>
import RestaurantDescription from '@/components/RestaurantDescription.vue';
export default {
name: 'Restaurant',
components: {
RestaurantDescription
},
computed: {
restaurant() {
return this.$store.getters.getRestaurantBySlug(this.$route.params.slug);
}
}
};
</script>
<style scoped>
a {
padding: 1rem;
text-decoration: none;
}
a.active {
color: #42b983;
text-decoration: underline;
}
</style>
In <Restaurant>
importiamo il componente definito in src/components/RestaurantDescription.vue
che, al contrario di quelli visti finora, non viene usato nell’array dei percorsi di Vue Router. Non contiene nessuna logica, ma solo un template per mostrare le informazioni ricevute tramite le due props name
e description
.
<template>
<div class="restaurant-description">
<h2>{{ name }}</h2>
<p>{{ description }}</p>
</div>
</template>
<script>
export default {
name: 'RestaurantDescription',
props: {
name: {
type: String,
required: true
},
description: {
type: String,
required: true
}
}
};
</script>
Tornando invece a parlare del componente Restaurant
, in esso abbiamo definito una computed property restaurant()
che contiene le informazioni relative al ristorante corrente. Per ottenerle ci siamo avvalsi del getter getRestaurantBySlug
a cui passiamo come argomento il valore del parametro slug
che Vue Router consente di accedere tramite $route.params
. Per esempio, quando visitiamo l’indirizzo http://localhost:8080/ristoranti/pizza-pazza
, il valore di slug
è pari a 'pizza-pazza'
e quindi la computed property restaurant
sarà un oggetto contenente le informazioni relative a quella catena di ristoranti. All’interno del template verranno quindi mostrati il nome e la descrizione di ‘Pizza Pazza’ e una serie di link per visualizzare gli indirizzi di ciascun ristorante. Per costruire i link usiamo il componente <router-link>
a cui passiamo una prop to
. A questa assegniamo un oggetto che contiene il nome della named route 'restaurant-location'
, che abbiamo definito nell’array routes
nel file src/router/index.js
, e un parametro id
relativo all’identificativo di ciascun ristorante.
Facendo riferimento all’array routes
, 'restaurant-location'
è il nome del percorso annidato che utilizza un parametro dinamico :id
.
{
path: '/ristoranti/:slug',
name: 'restaurant',
component: Restaurant,
children: [
{
path: ':id',
name: 'restaurant-location',
component: RestaurantLocation
}
]
}
Per questo motivo, quando clicchiamo su uno dei link del tipo ‘Ristorante 0’, l’indirizzo dell’applicazione diventa http://localhost:8080/ristoranti/pizza-pazza/0
e viene caricato il componente RestaurantLocation
al posto di <router-view>
presente nel template del componente Restaurant
. Quest’ultimo resta quindi visibile sullo schermo insieme a RestaurantLocation
.
<template>
<div v-if="location">
<RestaurantAddress :address="location.address" :rating="location.rating" />
</div>
</template>
<script>
import RestaurantAddress from '@/components/RestaurantAddress.vue';
export default {
name: 'RestaurantLocation',
components: {
RestaurantAddress
},
computed: {
location() {
return this.$store
.getters.getRestaurantLocationById(this.$route.params);
}
}
};
</script>
Il componente RestaurantLocation
presenta una computed property location
corrispondente ad uno degli oggetti presenti nell’array locations
di ciascuna catena di ristoranti. Il getter getRestaurantLocationById
estrae tramite assegnamento di destrutturazione le proprietà slug
e id
dall’argomento ricevuto in ingresso che è l’intero oggetto this.$route.params
.
Le informazioni relative all’indirizzo di quel particolare ristorante vengono passate poi al componente definito nel file src/components/RestaurantAddress.vue
.
<template>
<div class="restaurant-address">
<address>
{{ address }}
</address>
<div>
valutazione <strong>{{ rating }}</strong>
/5
</div>
</div>
</template>
<script>
export default {
name: 'RestaurantAddress',
props: {
address: {
type: String,
required: true
},
rating: {
type: Number
}
}
};
</script>
Usare delle props al posto di $route.params
Sia nel componente Restaurant
che in RestaurantLocation
accediamo ai parametri dinamici slug
e id
attraverso $route.params
.
Esiste però un modo alternativo che consente ad un componente di ricevere i parametri dinamici come props. Così facendo non sarà necessario usare $route.params
e potremo disaccoppiare il componente dal router. Ciò permette di poter ristrutturare in seguito il componente o parte dell’applicazione più facilmente.
Rispetto a quanto visto in precedenza, dovremo apportare solo alcune semplici modifiche.
Nell’array routes
all’interno del file src/router/index.js
dovremo aggiungere una proprietà props: true
agli oggetti dei percorsi che usano dei parametri dinamici.
const routes = [
{
path: '/',
name: 'home',
redirect: { name: 'restaurants' }
},
{
path: '/ristoranti',
name: 'restaurants',
component: Restaurants
},
{
path: '/ristoranti/:slug',
name: 'restaurant',
props: true, // per ricevere i parametri via props
component: Restaurant,
children: [
{
path: ':id',
props: true, // per ricevere i parametri via props
name: 'restaurant-location',
component: RestaurantLocation
}
]
},
{
path: '/chi-siamo',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '*',
name: 'all',
component: Error404
}
];
Dovremo poi indicare quali props aspettano i componenti Restaurant
e RestaurantLocation
. Per esempio, per consentire a <Restaurant>
di ricevere il parametro slug
sotto forma di props, dovremo elencare ‘slug’ fra le props dell’oggetto di opzioni che definiscono il componente.
import RestaurantDescription from '@/components/RestaurantDescription.vue';
export default {
name: 'Restaurant',
components: {
RestaurantDescription
},
props: ['slug'],
computed: {
restaurant() {
return this.$store.getters.getRestaurantBySlug(this.slug);
}
}
};
Fatto ciò, potremo passare ai getters le props slug
ed id
invece di accedere alla proprietà $route.params
.
import RestaurantAddress from '@/components/RestaurantAddress.vue';
export default {
name: 'RestaurantLocation',
components: {
RestaurantAddress
},
props: ['slug', 'id'],
computed: {
location() {
return this.$store.getters.getRestaurantLocationById({
slug: this.slug,
id: this.id
});
}
}
};
Riepilogo
In quest’ultima lezione della guida abbiamo introdotto Vue Router che consente di realizzare applicazioni SPA in modo facile e veloce.
Vue Router è uno strumento completo con numerose funzionalità che non è possibile presentare in un’unica lezione. Per maggiori informazioni e per scoprire le funzioni più avanzate, è possibile consultare la documentazione ufficiale.
Prossimi passi
Con questa guida abbiamo voluto presentare i concetti fondamentali per iniziare a creare delle applicazioni in Vue.js.
Con le conoscenze acquisite non dovrebbe essere problematico cimentarsi con framework come Nuxt.js o generatori di siti statici come Gridsome.
Vue è un framework in continua crescita e forse il migliore da imparare ed usare fra gli innumerevoli framework front-end oggi disponibili. Nella versione 3 sono state introdotte alcune interessanti novità come la Composition API, Suspense e Teleport che prendono ispirazione da simili funzionalità già presenti in React.