Skip to content

19. Miglioramenti al client di Vue.js

19.1. Introduzione

Testeremo il progetto [vuejs-21] con il server di sviluppo. Pertanto, avremo nuovamente bisogno che il server invii le intestazioni CORS. Il file [config.json] per la versione 14 del server di calcolo delle imposte deve quindi consentire queste intestazioni:

Image

Il progetto [vuejs-21] viene inizialmente creato duplicando il progetto [vuejs-20]. Viene poi modificato [3].

Compaiono nuovi file:

  • [session.js]: esporta un oggetto [session] che incapsulerà le informazioni relative alla sessione corrente;
  • [pluginSession]: rende disponibile il precedente oggetto [session] nella proprietà [$session] delle viste;
  • [NotFound.vue]: una nuova vista visualizzata quando l'utente richiede manualmente un URL che non esiste;

Verranno modificati i seguenti file:

  • [main.js]: inizializzerà la sessione corrente e la ripristinerà quando l'utente inserisce manualmente un URL;
  • [router.js]: vengono aggiunti controlli per gestire gli URL inseriti dall'utente;
  • [store.js]: viene aggiunta una nuova mutazione;
  • [config.js]: viene aggiunta una nuova configurazione;
  • varie viste, principalmente per salvare la sessione corrente in punti chiave del ciclo di vita dell'applicazione. La sessione viene quindi ripristinata ogni volta che l'utente inserisce manualmente degli URL;

19.2. Lo store [Vuex]

Lo script [./store] si evolve come segue:


// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
 
// store Vuex
const store = new Vuex.Store({
  state: {
    // le tableau des simulations
    simulations: [],
    // le n° de la dernière simulation
    idSimulation: 0
  },
  mutations: {
    // suppression ligne n° index
    deleteSimulation(state, index) {
      ...
    },
    // ajout d'une simulation
    addSimulation(state, simulation) {
      ...
    },
    // nettoyage state
    clear(state) {
      // plus de simulations
      state.simulations = [];
      // la numérotation des simulations repart de 0
      state.idSimulation = 0;
    }
  }
});
// export de l'objet [store]
export default store;
  • righe 24-29: l'assegnazione [clear] cancella l'elenco delle simulazioni salvate e reimposta il numero dell'ultima simulazione a 0.

19.3. La sessione

La necessità di una sessione nasce dal fatto che quando l'utente digita un URL nella barra degli indirizzi del browser, lo script [main.js] viene eseguito nuovamente. Tuttavia, questo script contiene la seguente istruzione:


// store Vuex
import store from './store'

Questa istruzione importa il seguente file [./store]:


// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
 
// store Vuex
const store = new Vuex.Store({
  state: {
    // le tableau des simulations
    simulations: [],
    // le n° de la dernière simulation
    idSimulation: 0
  },
  mutations: {
    ...
  }
});
// export de l'objet [store]
export default store;

Come possiamo vedere nelle righe 7–13, importiamo un array vuoto di simulazioni. Quindi, se avevamo delle simulazioni prima che l'utente digitasse un URL nella barra degli indirizzi del browser, dopo non ne abbiamo più. L'idea è:

  • utilizzare una sessione che memorizzi le informazioni che si desidera conservare se l'utente inserisce manualmente gli URL;
  • salvarla in punti chiave dell'applicazione;
  • ripristinarla in [main.js], che viene sempre eseguito quando un URL viene inserito manualmente;

Lo script [./session] è il seguente:


// on importe le store Vuex
import store from './store'
// on importe la configuration
import config from './config';
 
// l'objet [session]
const session = {
  // session démarrée
  started: false,
  // authentification
  authenticated: false,
  // heure de sauvegarde
  saveTime: "",
  // couche [métier]
  métier: null,
  // état Vuex
  state: null,
 
  // sauvegarde de la session dans une chaîne jSON
  save() {
    // on ajoute à la session quelques proprités
    this.saveTime = Date.now();
    this.state = store.state;
    // on la transforme en jSON
    const json = JSON.stringify(this);
    // on la stocke sur le navigateur
    localStorage.setItem("session", json);
    // eslint-disable-next-line no-console
    console.log("session save", json);
  },
 
  // restauration de la session
  restore() {
    // on récupère la session jSON à partir du navigateur
    const json = localStorage.getItem("session")
    // si on a récupéré qq chose
    if (json) {
      // on restaure toutes les clés de la session
      const restore = JSON.parse(json);
      for (var key in restore) {
        if (restore.hasOwnProperty(key)) {
          this[key] = restore[key];
        }
      }
      // si on a dépassé une certaine durée d'inactivité depuis le début de la session, on repart de zéro
      let durée = Date.now() - this.saveTime;
      if (durée > config.duréeSession) {
        // on vide la session - elle sera également sauvegardée
        session.clear();
      } else {
        // on régénère le store Vuex
        store.replaceState(JSON.parse(JSON.stringify(this.state)));
      }
    }
    // eslint-disable-next-line no-console
    console.log("session restore", this);
  },
 
    // on nettoie la session
  clear() {
    // eslint-disable-next-line no-console
    console.log("session clear");
    // raz de certains champs de la session
    this.authenticated = false;
    this.saveTime = "";
    this.started = false;
    if (this.métier) {
      // on réinitialise le champ [taxAdminData]
      this.métier.taxAdminData = null;
    }
    // le store Vuex est nettoyé également
    store.commit("clear");
    // on sauvegarde la nouvelle session
    this.save();
  },
}
 
// export de l'objet [session]
export default session;

Commenti

  • riga 2: la sessione incapsulerà anche lo store [Vuex] (elenco delle simulazioni, ID dell'ultima simulazione eseguita);
  • righe 7-17: informazioni memorizzate dalla sessione:
    • [avviata]: indica se la sessione JSON con il server è stata avviata o meno;
    • [authenticated]: se l'utente si è autenticato o meno;
    • [saveTime]: la data in millisecondi dell'ultimo salvataggio;
    • [business]: un riferimento al livello [business]. Contiene i dati [taxAdminData] utilizzati per calcolare l'imposta;
    • [state]: lo stato dello store [Vuex] (elenco delle simulazioni, numero dell'ultima simulazione eseguita);
  • righe 20–30: il metodo [save] salva la sessione localmente sul browser che esegue l'applicazione;
    • riga 22: viene registrato il momento del salvataggio;
    • riga 23: viene recuperato lo [state] dello store [Vuex];
    • riga 25: viene creata la stringa JSON della sessione;
    • riga 27: viene memorizzata localmente sul browser associata alla chiave [session];
  • righe 33–57: il metodo [restore] ripristina una sessione dal suo salvataggio locale nel browser;
    • riga 35: recupera il backup JSON locale;
    • riga 37: se è stato recuperato qualcosa;
    • righe 39–44: l'oggetto [session] viene ricostruito;
    • riga 46: viene calcolato il tempo trascorso dall'ultimo backup;
    • righe 47–50: se questa durata supera un valore [config.sessionDuration] impostato nella configurazione, la sessione viene resettata (riga 49) e salvata in quel momento;
    • riga 52: altrimenti, l'attributo [state] dello store [Vuex] viene rigenerato;
  • righe 60–75: il metodo [clear] resetta la sessione;
    • righe 64–70: le proprietà della sessione vengono reimpostate ai loro valori iniziali;
    • riga 72: così come lo store [Vuex];
    • riga 74: la nuova sessione viene salvata;

19.4. Il file di configurazione [config]

Il file [./config] si evolve come segue:


// utilisation de la bibliothèque [axios]
const axios = require('axios');
// timeout des requêtes HTTP
axios.defaults.timeout = 2000;
...
 
// export de la configuration
export default {
  // objet [axios]
  axios: axios,
  // délai maximal d'inactivité de la session : 5 mn = 300 s = 300000 ms
  duréeSession: 300000
}
  • riga 12: gestiremo la sessione dell'applicazione in modo molto simile a come gestiamo una sessione web. Qui, impostiamo una durata massima di inattività di 5 minuti;

19.5. Il plugin [pluginSession]

Come già fatto molte volte in precedenza, il plugin [pluginSession] consentirà alle viste di accedere alla sessione tramite la proprietà [this.$session]:


export default {
  install(Vue, session) {
    // ajoute une propriété [$session] à la classe vue
    Object.defineProperty(Vue.prototype, '$session', {
      // lorsque Vue.$session est référencé, on rend le 2ième paramètre [session]
      get: () => session,
    })
  }
}

19.6. Lo script principale [main]

Lo script principale [./main.js] si evolve come segue:


// log de démarrage
// eslint-disable-next-line no-console
console.log("main started");
 
// imports
import Vue from 'vue'
 
...

// instanciation couche [métier]
import Métier from './couches/Métier';
const métier = new Métier();
 
// plugin [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)
 
// store Vuex
import store from './store'
 
// session
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)
 
// on restore la session avant de redémarrer
session.restore();
 
// on restaure la couche [métier]
if (session.métier && session.métier.taxAdminData) {
  métier.setTaxAdminData(session.métier.taxAdminData);
}
 
// démarrage de l'UI
new Vue({
  el: '#app',
  // le routeur
  router: router,
  // le store Vuex
  store: store,
  // la vue principale
  render: h => h(Main),
})
 
// log de fin
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
  • riga 19: importazione della sessione;
  • riga 20: importare il plugin;
  • riga 21: il plugin [pluginSession] viene integrato in [Vue]. Dopo questa istruzione, tutte le viste hanno la sessione disponibile nell'attributo [$session];
  • riga 27: la sessione viene ripristinata. La sessione importata alla riga 11 viene quindi inizializzata con il contenuto del suo ultimo salvataggio;
  • dopo la riga 16, le viste dispongono di una proprietà [$métier] inizializzata alla riga 12. Questa proprietà non contiene le informazioni [taxAdminData] utilizzate per calcolare l'imposta;
  • righe 30–32: se il ripristino appena eseguito ha ripristinato la proprietà [session.métier.taxAdminData], allora la proprietà [$métier] delle viste viene inizializzata con questo valore;

19.7. Il file di routing [router]

Il file di routing [./router] si evolve come segue:


// imports
import Vue from 'vue'
import VueRouter from 'vue-router'
// les vues
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// la session
import session from './session'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification', component: Authentification },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession'
  },
  // page inconnue
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des URL
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// vérification des routes
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // route réservée aux utilisateurs authentifiés ?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // on passe à l'authentification
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // cas particulier de la fin de session
  if (to.name === "finSession") {
    // on nettoie la session
    session.clear();
    // on va sur la vue [authentification]
    next({
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // autres cas - vue suivante normale du routage
  next();
})
 
// export du router
export default router

Commenti

  • righe 16–38: alcuni percorsi sono stati arricchiti con informazioni aggiuntive;
  • riga 19: è stato creato un nuovo percorso per accedere alla vista [Authentication];
  • righe 21–24: il percorso che porta alla vista [TaxCalculation] ora ha una proprietà [meta] (questo nome è obbligatorio). Il contenuto di questo oggetto può essere qualsiasi cosa ed è impostato dallo sviluppatore;
  • riga 23: aggiungiamo la proprietà [authenticated] a [meta] (il nome può essere qualsiasi cosa). Ciò significa che per accedere alla vista [TaxCalculation], l'utente deve essere autenticato;
  • righe 26–29: facciamo lo stesso per il percorso che porta alla vista [ListeSimulations]. Anche in questo caso, l'utente deve essere autenticato;
  • La proprietà [meta.authenticated] ci permetterà di verificare che un utente che digita manualmente gli URL delle viste [CalculImpot] e [ListeSimulations] non possa accedervi se non è autenticato;
  • righe 51–76: il metodo [beforeEach] viene eseguito prima che una vista venga instradata. Questo è il momento giusto per eseguire i controlli;
    • [to]: il percorso successivo se non viene fatto nulla;
    • [from]: l'ultimo percorso visualizzato;
    • [next]: funzione per modificare il percorso successivo visualizzato;
  • riga 55: verifichiamo se il percorso successivo richiede l'autenticazione dell'utente;
  • righe 56–59: in tal caso, e se l'utente non è autenticato, modifichiamo il percorso successivo alla vista [Authentication];
  • righe 64–73: gestiamo il caso speciale del percorso [endSession] delle righe 30–32. Questo percorso non ha una vista associata;
    • riga 66: la sessione viene reimpostata al suo valore iniziale;
    • righe 68–70: impostiamo la vista [Authentication] come vista successiva;
  • riga 75: se nessuno dei due casi precedenti si applica, si procede semplicemente al percorso specificato dal file di routing;
  • righe 35–37: viene fornita una vista [NotFound] se il percorso inserito dall'utente non corrisponde a nessun percorso conosciuto. Questa vista viene importata alla riga 8. I percorsi vengono controllati nell'ordine del file di routing. Pertanto, se si raggiunge la riga 36, significa che il percorso richiesto non è nessuno dei percorsi delle righe 18–33;

19.8. La vista [NotFound]

La vista [NotFound] viene visualizzata se il percorso inserito dall'utente non corrisponde a nessun percorso conosciuto:

Image

Il codice della vista è il seguente:


<!-- définition HTML de la vue -->
<template>
  <!-- mise en page -->
  <Layout :left="true" :right="true">
    <!-- alerte dans la colonne de droite -->
    <template slot="right">
      <!-- message sur fond jaune -->
      <b-alert show variant="danger" align="center">
        <h4>Cette page n'existe pas</h4>
      </b-alert>
    </template>
    <!-- menu de navigation dans la colonne de gauche -->
    <Menu slot="left" :options="options" />
  </Layout>
</template>
 
<script>
// imports
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne du composant
  data() {
    return {
      // options du menu de navigation
      options: [
        {
          text: "Authentification",
          path: "/"
        }
      ]
    };
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("NotFound created");
    // on regarde quelles options de menu offrir
    if (this.$session.authenticated && this.$métier.taxAdminData) {
      // l'utilisateur peut faire des simulations
      Array.prototype.push.apply(this.options, [
        {
          text: "Calcul de l'impôt",
          path: "/calcul-impot"
        },
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        }
      ]);
    }
  }
};
</script>

Commenti

  • riga 4: utilizza entrambe le colonne delle viste instradate;
  • righe 6–11: un messaggio di errore;
  • riga 13: il menu di navigazione occupa la colonna di sinistra;
  • righe 31–36: le opzioni predefinite del menu;
  • righe 40–57: codice eseguito quando viene creata la vista;
  • riga 44: verifica se l'utente può eseguire simulazioni;
  • righe 45–55: in tal caso, al menu di navigazione vengono aggiunte due opzioni: quelle che richiedono l'autenticazione e un livello operativo [aziendale] (righe 46–55);

19.9. La vista [Autenticazione]

La vista [Autenticazione] si evolve come segue:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    ...
  </Layout>
</template>
 
<!-- dynamique de la vue -->
<script>
import Layout from "./Layout";
export default {
  // component status
  data() {
    return {
      // user
      user: "",
      // password
      password: "",
      // controls the display of an error msg
      showError: false,
      // the error message
      message: ""
    };
  },
 
  // components used
  components: {
    Layout
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$session.started;
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit("loading", true);
        // you are not yet authenticated
        this.$session.authenticated = false;
        // blocking server authentication
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // end of loading
        this.$emit("loading", false);
        // server response analysis
        if (response.état != 200) {
          // error is displayed
          this.message = response.réponse;
          this.showError = true;
          // return to event loop
          return;
        }
        // no error
        this.showError = false;
        // you are authenticated
        this.$session.authenticated = true;
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null);
        // start waiting
        this.$emit("loading", true);
        // blocking request to the server
        const response2 = await this.$dao.getAdminData();
        // end of loading
        this.$emit("loading", false);
        // response analysis
        if (response2.état != 1000) {
          // error is displayed
          this.message = response2.réponse;
          this.showError = true;
          // return to event loop
          return;
        }
        // no error
        this.showError = false;
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse);
        // we can move on to tax calculation
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // the error is traced back to the main component
        this.$emit("error", error);
      } finally {
        // maj session
        this.$session.métier = this.$métier;
        // save the session
        this.$session.save();
      }
    }
  },
  // life cycle: the component has just been created
  created() {
    // eslint-disable-next-line
    console.log("Authentification created");
    // can the user run simulations?
    if (
      this.$session.started &&
      this.$session.authenticated &&
      this.$métier.taxAdminData
    ) {
      // then the user can run simulations
      this.$router.push({ name: "calculImpot" });
      // return to event loop
      return;
    }
    // if the jSON session has already been started, it will not be restarted
    if (!this.$session.started) {
      // start waiting
      this.$emit("loading", true);
      // initialize the session with the server - asynchronous request
      // we use the promise rendered by the [dao] layer methods
      this.$dao
        // initialize a jSON session
        .initSession()
        // we got the answer
        .then(response => {
          // end waiting
          this.$emit("loading", false);
          // response analysis
          if (response.état != 700) {
            // error is displayed
            this.message = response.réponse;
            this.showError = true;
            // return to event loop
            return;
          }
          // the session has started
          this.$session.started = true;
        })
        // in case of error
        .catch(error => {
          // the error is traced back to the [Main] view
          this.$emit("error", error);
        })
        // in all cases
        .finally(() => {
          // save the session
          this.$session.save();
        });
    }
  }
};
</script>

Commenti

  • Le istruzioni che utilizzano la sessione introdotta in questa versione del client [Vue.js] sono evidenziate in giallo;
  • righe 97, 148: alla fine dei metodi [login, created], la sessione viene salvata indipendentemente dal risultato delle richieste HTTP che avvengono all'interno di questi metodi (la clausola [finally] in entrambi i casi);
  • il metodo [created] nelle righe 102–150 viene eseguito ogni volta che viene creata la vista [Authentication]. Se l'utente ha digitato l'URL della vista, la sessione ci dirà cosa fare;
  • righe 106–115: se la sessione JSON viene avviata, l'utente viene autenticato e i dati [this.$métier.taxAdminData] vengono inizializzati, quindi l'utente può passare direttamente al modulo di calcolo delle imposte (riga 112);
  • riga 117: il metodo [created] è stato utilizzato nella versione precedente per inizializzare una sessione JSON con il server. Questo passaggio non è necessario se è già stato eseguito;
  • righe 42–66: il metodo di autenticazione;
  • riga 66: se l'autenticazione ha esito positivo, viene annotata nella sessione;
  • righe 67–92: la richiesta al server per i dati di amministrazione fiscale [taxAdminData];
  • riga 95: al termine di questa fase, la proprietà [business] della sessione viene aggiornata indipendentemente dal fatto che l'operazione abbia avuto esito positivo o meno;

19.10. La vista [CalculImpot]

Il codice della vista [CalculImpot] si evolve come segue:


<!-- définition HTML de la vue -->
<template>
  ...
</template>

<script>
// imports
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";
 
export default {
  // état interne
  data() {
    return {
      // options du menu
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // résultat du calcul de l'impôt
      résultat: "",
      résultatObtenu: false
    };
  },
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // méthodes de gestion des évts
  methods: {
    // résultat du calcul de l'impôt
    handleResultatObtenu(résultat) {
      // on construit le résultat en chaîne HTML
      ...
      // une simulation de +
      this.$store.commit("addSimulation", résultat);
      // on sauvegarde la session
      this.$session.save();
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  }
};
</script>

Commenti

  • riga 45: la simulazione calcolata viene aggiunta allo store [Vuex]. Ciò influisce sulla sessione, che comprende la proprietà [state] dello store. Pertanto, salviamo la sessione (riga 47);
  • riga 51: creiamo un metodo [created] per tracciare le creazioni delle viste nei log;

19.11. La vista [SimulationList]

La vista [ListeSimulations] si presenta come segue:


<!-- définition HTML de la vue -->
<template>
  ...
  </div>
</template>
 
<script>
// imports
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne
  data() {
    ...
  },
  // état interne calculé
  computed: {
    // liste des simulations prise dans le store Vuex
    simulations() {
      return this.$store.state.simulations;
    }
  },
  // méthodes
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // suppression de la simulation n° [index]
      this.$store.commit("deleteSimulation", index);
      // on sauvegarde la session
      this.$session.save();
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  }
};
</script>

Commenti

  • riga 36: dopo aver eliminato una simulazione alla riga 34, salviamo la sessione per riflettere questo cambiamento di stato;
  • righe 40–43: continuiamo a monitorare la creazione delle viste;

19.12. Esecuzione del progetto

Image

Durante il test, verificare i seguenti punti:

  • se l'utente "utilizza" l'applicazione tramite i link del menu di navigazione e i pulsanti/link di azione, funziona;
  • se l'utente inserisce manualmente gli URL, l'applicazione continua a funzionare. In particolare, eseguire il seguente test:
    • eseguire le simulazioni;
    • una volta nella vista [SimulationList], ricaricare (F5) la vista. Nell'applicazione precedente [vuejs-20], le simulazioni andavano perse a quel punto. Qui non è così: le simulazioni già eseguite sono ancora presenti;
  • Controlla i log per capire:
    • quando viene eseguito lo script [main]. Dovresti vedere che viene eseguito ogni volta che l'utente inserisce manualmente un URL;
    • quando vengono create le viste. Dovresti vedere che vengono create ogni volta che stanno per essere visualizzate;
    • come funziona il routing. Prima di ogni routing, viene generato un log che indica:
      • il percorso da cui si proviene;
      • il percorso verso cui stai andando;

19.13. Distribuzione dell'applicazione su un server locale

Come esercizio, segui la sezione |Distribuzione su un server locale| per distribuire il progetto [vuejs-21] sul server Laragon locale. Quindi provalo.

19.14. Sviluppo della versione mobile

In teoria, l'uso di Bootstrap dovrebbe permetterci di avere un'applicazione che funziona su diversi dispositivi: smartphone, tablet, laptop e desktop. Ciò che differenzia questi dispositivi è la dimensione dello schermo.

Se proviamo la versione [vuejs-21] su un dispositivo mobile, vediamo che la visualizzazione è un disastro. La versione [vuejs-22] risolve questo problema. Tutte le modifiche sono state apportate nei modelli di visualizzazione. Riguardavano principalmente l'ottimizzazione della visualizzazione per lo schermo di uno smartphone. Una volta ottimizzata, la visualizzazione su schermi più grandi funziona senza intoppi grazie a Bootstrap.

Image

19.14.1. La vista [Main]

La vista [Main] si evolve come segue:


<!-- definition HTML of the view -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col sm="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col sm="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      ....
    </b-card>
  </div>
</template>

Commenti

  • riga 8: dove prima c'era scritto [cols=’4’], ora scriviamo [sm=’4’]. [sm] sta per [small]. Gli schermi degli smartphone rientrano in questa categoria. Le altre categorie sono [xs=extra small, md=medium, lg=large, xl=extra large];
  • riga 11: come sopra;

19.14.2. La vista [Layout]

La vista [Layout] si evolve come segue:


<!-- definition HTML of the routed view layout -->
<template>
  <!-- line -->
  <div>
    <b-row>
      <!-- three-column zone on the left -->
      <b-col sm="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- nine-column zone on the right -->
      <b-col sm="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

19.14.3. La vista [Autenticazione]

La vista [Autenticazione] si evolve come segue:


<!-- definition HTML of the view -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- form HTML - post its values with the [authenticate-user] action -->
      <b-form @submit.prevent="login">
        <!-- title -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1st line -->
        <b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
          <!-- user input field -->
          <b-col sm="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2nd line -->
        <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
          <!-- password input field -->
          <b-col sm="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- 3rd line -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- submit] button on a 3rd line -->
        <b-row>
          <b-col sm="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

Commenti

  • Righe 11 e 19: Abbiamo rimosso l'attributo [label-cols], che impostava il numero di colonne per l'etichetta dell'input. Senza questo attributo, l'etichetta appare sopra il campo di input. Ciò è più adatto agli schermi degli smartphone;

19.14.4. La vista [CalculImpot]

La vista [CalculImpot] è stata aggiornata come segue:


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

19.14.5. La vista [FormCalculImpot]

La vista [FormCalculImpot] si evolve come segue:


<!-- definition HTML of the view -->
  <template>
  <!-- form HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- 12-column message on blue background -->
    <b-row>
      <b-col sm="12">
        <b-alert show variant="primary">
          <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </b-alert>
      </b-col>
    </b-row>
    <!-- form elements -->
    <!-- first line -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- 5-column radio buttons-->
      <b-col sm="5">
        <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
        <b-form-radio v-model="marié" value="non">Non</b-form-radio>
      </b-col>
    </b-form-group>
    <!-- second line -->
    <b-form-group label="Nombre d'enfants à charge" label-for="enfants">
      <b-form-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      ></b-form-input>
      <!-- possible error message -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- third line -->
    <b-form-group
      label="Salaire annuel net imposable"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-form-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      ></b-form-input>
      <!-- possible error message -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- fourth line, [submit] button -->
    <b-col sm="3">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

Commenti

  • Righe 15, 23, 35: L'attributo [label-cols] è stato rimosso;

Inoltre, stiamo aggiornando i test di convalida:


...
// état interne calculé
  computed: {
    // validation du formulaire
    formInvalide() {
      return (
        // salaire invalide
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // ou enfants invalide
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // ou données fiscales pas obtenues
        !this.$métier.taxAdminData
      );
    },
    // validation du salaire
    salaireValide() {
      // doit être numérique >=0
      return Boolean(
        this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
      );
    },
    // validation des enfants
    enfantsValide() {
      // doit être numérique >=0
      return Boolean(
        this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
      );
    }
},
...

Commenti

  • riga 19: quando non è stato inserito nulla, l'input viene considerato valido. Ciò garantisce che l'input sia valido quando la vista viene visualizzata inizialmente. Nella versione precedente, l'input appariva inizialmente come errato;
  • riga 26: come sopra;
  • Righe 5–14: il pulsante di invio è attivo solo se entrambi i campi contengono dati e sono validi;

19.14.6. La vista [Menu]

La vista [Menu] si evolve come segue:


<!-- definition HTML of the view -->
<template>
  <b-card class="mb-3">
    <!-- bootstrap vertical menu -->
    <b-nav vertical>
      <!-- menu options -->
      <b-nav-item
        v-for="(option,index) of options"
        :key="index"
        :to="option.path"
        exact
        exact-active-class="active"
      >{{option.text}}</b-nav-item>
    </b-nav>
  </b-card>
</template>

Commenti

  • Riga 3: Aggiungiamo il tag <b-card> per circondare il menu con un bordo sottile. Questo aiuta a rendere il menu più facile da individuare su uno smartphone;

19.14.7. La vista [SimulationList]

La vista [ListeSimulations] rimane invariata:


<!-- definition HTML of the view -->
<template>
  <div>
    <!-- layout -->
    <Layout :left="true" :right="true">
      <!-- simulations in right-hand column -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- no simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- there are simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- simulation table -->
          <b-table striped hover responsive :items="simulations" :fields="fields">
            <template v-slot:cell(action)="data">
              <b-button variant="link" @click="supprimerSimulation(data.index)">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- navigation menu in left-hand column -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

Commenti

  • Riga 20: Si noti l'attributo [responsive], che garantisce che la visualizzazione della tabella si adatti alle dimensioni dello schermo:

Image

  • in [2], sugli schermi di piccole dimensioni, una barra di scorrimento orizzontale consente la visualizzazione della tabella;

19.14.8. La vista [NotFound]

Rimane invariata.

19.14.9. Visualizzazioni per dispositivi mobili

Image

Image

Nota: è certamente possibile creare visualizzazioni ancora più adatte ai dispositivi mobili. Mi riferisco in particolare al menu di navigazione, che potrebbe essere migliorato, ma ci sono anche altre aree. L'obiettivo principale di questo documento non era quello di creare un'app mobile. In tal caso, avremmo potuto ricorrere a un framework come Ionic |https://ionicframework.com/|.