Skip to content

18. Cliente Vue.js del servidor de cálculo de impuestos

18.1. Architecture

Vamos a implementar una aplicación cliente/servidor con la siguiente arquitectura:

Image

El servidor de cálculo de impuestos será la versión 14 desarrollada en el documento |https://tahe.developpez.com/tutoriels-cours/php7|

18.2. Las vistas de la aplicación

Las vistas de la aplicación [vuejs-10] son las de la versión 13 del documento |https://tahe.developpez.com/tutoriels-cours/php7| del servidor de cálculo de impuestos cuando se utiliza en modo HTML. Sin embargo, en la aplicación actual, estas vistas serán generadas por el cliente JavaScript y no por el servidor PHP.

La primera vista es la vista de autenticación:

Image

La segunda vista es la del cálculo del impuesto:

Image

La tercera vista es la que muestra la lista de simulaciones realizadas por el usuario:

Image

La pantalla anterior muestra que se puede eliminar la simulación n.º 1. A continuación, se obtiene la siguiente vista:

Image

Si ahora eliminamos la última simulación, obtenemos la siguiente vista:

Image

18.3. Elementos del proyecto [vuejs-20]

La estructura del proyecto [vuejs-20] es la siguiente:

Image

Los elementos del proyecto son los siguientes:

  • [assets/logo.jpg]: el logotipo del proyecto;
  • [couches]: las capas [métier] y [dao] de la aplicación;
  • [plugins]: los complementos de la aplicación;
  • [views]: las vistas de la aplicación;
  • [config.js]: configura la aplicación;
  • [router.js]: define el enrutamiento de la aplicación;
  • [store.js]: el almacén de [Vuex];
  • [main.js]: el script principal de la aplicación;

18.3.1. Las capas [métier] y [dao]

18.3.1.1. La capa [dao]

La capa [dao] está implementada por la clase [Dao] del apartado |vuejs-10|

18.3.1.2. La capa [métier]

La capa [métier] está implementada por la clase [Métier] del documento |https://tahe.developpez.com/tutoriels-cours/php7|. Se ha añadido el siguiente método [setTaxAdminData]:


// constructor
  constructor(taxAdmindata) {
    // this.taxAdminData: datos de la administración tributaria
    this.taxAdminData = taxAdmindata;
  }

  // setter
  setTaxAdminData(taxAdmindata) {
    // this.taxAdminData: datos de la administración tributaria
    this.taxAdminData = taxAdmindata;
}

El método [setTaxAdminData] hace lo mismo que el constructor. Su presencia permite la siguiente secuencia:

  1. instanciar la clase [Métier] con una instrucción [métier=new Métier()] cuando se desea instanciar la clase pero aún no se dispone del dato [taxAdminData];
  2. y, posteriormente, rellenar su propiedad [taxAdminData] mediante una operación [métier.setTaxAdminData(taxAdmindata)];

18.3.2. El archivo de configuración [config]

El archivo [config.js] es el siguiente:


// uso de la biblioteca [axios]
const axios = require('axios');
// tiempo de espera de las solicitudes HTTP
axios.defaults.timeout = 2000;
// la base de datos del servidor de cálculo de impuestos
// el esquema [https] causa problemas en Firefox porque el servidor de cálculo
// del impuesto envía un certificado autofirmado. Funciona bien con Chrome y Edge. No se ha probado con Safari.
axios.defaults.baseURL = 'https://localhost/php7/scripts-web/impots/version-14';
// vamos a utilizar cookies
axios.defaults.withCredentials = true;

// exportación de la configuración
export default {
  axios: axios
}

Esta configuración es la de la biblioteca [axios] que la capa [dao] utiliza para realizar sus consultas HTTP. Cabe destacar, en la línea 8, que el servidor opera en un puerto seguro [https].

18.3.3. Los complementos

Los complementos [pluginDao, pluginMétier, pluginConfig] tienen como objetivo crear tres nuevas propiedades en la función/clase [Vue]:

  • [$dao]: tendrá como valor una instancia de la clase [Dao];
  • [$métier]: tendrá como valor una instancia de la clase [Métier];
  • [$config]: tendrá como valor el objeto exportado por el archivo de configuración [config];

[pluginDao]


export default {
  install(Vue, dao) {
    // añade una propiedad [$dao] a la clase Vue
    Object.defineProperty(Vue.prototype, '$dao', {
      // cuando se hace referencia a Vue.$dao, se devuelve el segundo parámetro [dao]
      get: () => dao,
    })
  }
}

[pluginMétier]

export default {
  install(Vue, métier) {
    // añade una propiedad [$métier] a la clase Vue
    Object.defineProperty(Vue.prototype, '$métier', {
      // cuando se hace referencia a Vue.$métier, se devuelve el segundo parámetro [métier]
      get: () => métier,
    })
  }
}

[pluginConfig]


export default {
  install(Vue, config) {
    // añade una propiedad [$config] a la clase Vue
    Object.defineProperty(Vue.prototype, '$config', {
      // cuando se hace referencia a Vue.$config, se devuelve el segundo parámetro [config]
      get: () => config,
    })
  }
}

18.3.4. La tienda [Vuex]

El control [Vuex] se implementa mediante el siguiente archivo [store]:


// plugin Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// almacén Vuex
const store = new Vuex.Store({
  state: {
    // la tabla de simulaciones
    simulations: [],
    // el n.º de la última simulación
    idSimulation: 0
  },
  mutations: {
    // eliminación de la línea n.º índice
    deleteSimulation(state, index) {
      // eslint-disable-next-line no-console
      console.log("mutation deleteSimulation");
      // se elimina la línea n.º [index]
      state.simulations.splice(index, 1);
      // eslint-disable-next-line no-console
      console.log("store simulations", state.simulations);
    },
    // se añade una simulación
    addSimulation(state, simulation) {
      // eslint-disable-next-line no-console
      console.log("mutation addSimulation");
      // n.º de la simulación
      state.idSimulation++;
      simulation.id = state.idSimulation;
      // se añade la simulación a la tabla de simulaciones
      state.simulations.push(simulation);
    },
    // limpieza del estado
    clear(state) {
      state.simulations = [];
      state.idSimulation = 1;
    }
  }
});
// exportación del objeto [store]
export default store;

Comentarios

  • líneas 2-4: el plugin [Vuex] se integra en el framework [Vue];
  • líneas 8-13: colocamos en el almacén de [Vuex] los siguientes elementos:
    • [simulations]: la lista de simulaciones realizadas por el usuario;
    • [idSimulation]: el número de la última simulación realizada por el usuario;

Recordamos que el almacén se compartirá entre las vistas y que su contenido es reactivo: cuando se modifica, las vistas que lo utilizan se actualizan automáticamente. En nuestra aplicación, solo el elemento [simulations] necesita ser reactivo, no el elemento [idSimulation]. Hemos dejado este elemento en el almacén por comodidad;

  • líneas 14-40: las modificaciones permitidas en el objeto [state] de las líneas 8-13. Recordamos que estas siempre reciben el objeto [state] de las líneas 8-13 como primer parámetro;
    • línea 16: la mutación [deleteSimulation] permite eliminar una simulación cuyo número es [index];
    • línea 25: la mutación [addSimulation] permite añadir una nueva simulación a la tabla de simulaciones;
    • línea 35: la mutación [clear] permite reiniciar el objeto [state] de las líneas 8-13;

18.3.5. El archivo de enrutamiento [router]

El archivo de enrutamiento es el siguiente:


// importaciones
import Vue from 'vue'
import VueRouter from 'vue-router'
// las vistas
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'

// plugin de enrutamiento
Vue.use(VueRouter)

// las rutas de la aplicación
const routes = [
  // autenticación
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // cálculo de impuestos
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // lista de simulaciones
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de sesión
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]

// el enrutador
const router = new VueRouter({
  // las rutas
  routes,
  // modo de visualización de las rutas en el navegador
  mode: 'history',
})

// exportación del enrutador
export default router

Comentarios

  • línea 16: al iniciar la aplicación, se muestra la vista [Authentification], ya que su URL es la raíz [/];
  • línea 20: se muestra la vista [CalculImpot] cuando se solicita la vista URL [/calcul-impot];
  • línea 24: la vista [ListeSimulations] se muestra cuando se solicita la vista URL [/liste-des-simualtions];
  • línea 28: se muestra la vista [Authentification] cuando se solicita la vista URL [/fin-session];
  • líneas 33-38: se crea un objeto [router] con estas rutas (línea 35) y el modo [history] (línea 37) de gestión de URL;
  • línea 41: se exporta este enrutador;

18.3.6. El script principal [main.js]

El script [main.js] es el siguiente:


// importaciones
import Vue from 'vue'

// vista principal
import Main from './views/Main.vue'

// complemento [bootstrap-vue]
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);

// CSS bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// enrutador
import router from './router'

// complemento [config]
import config from './config';
import pluginConfig from './plugins/pluginConfig'
Vue.use(pluginConfig, config)

// instanciación de capa [dao]
import Dao from './couches/Dao';
const dao = new Dao(config.axios);

// complemento [dao]
import pluginDao from './plugins/pluginDao'
Vue.use(pluginDao, dao)

// instanciación de capa [métier]
import Métier from './couches/Métier';
const métier = new Métier();

// complemento [métier]
import pluginMétier from './plugins/pluginMétier'
Vue.use(pluginMétier, métier)

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

// inicio de la aplicación UI
new Vue({
  el: '#aplicación,
  // el router
  router: router,
  // la persiana Vuex
  store: store,
  // la vista principal
  render: h => h(Main),
})

Cabe destacar los siguientes puntos:

  • líneas 18-21, el objeto exportado por el script [./config] estará disponible en el atributo [Vue.$config], por lo que estará disponible en todas las vistas de la aplicación. En este caso no era necesario, ya que el objeto [config] solo lo utiliza el script [main] (línea 25). Sin embargo, es frecuente que la configuración sea necesaria en varias vistas. Por lo tanto, en este caso se ha querido mantener el principio de hacerlo disponible en un atributo de la vista;
  • líneas 24-25: instanciación de la capa [dao]. La clase [Dao] se importa en la línea 24 y se instancia en la línea 25. Su constructor admite como único parámetro el objeto [axios], propiedad de configuración;
  • líneas 27-29: la capa [dao] se pone a disposición en el atributo [$dao] de todas las vistas;
  • líneas 31-37: se repite la misma secuencia para la capa [métier]. El constructor de la clase [Métier] tiene como parámetro [taxAdminData], que representa los datos de la administración tributaria. Aún no disponemos de estos datos. Por lo tanto, el objeto [métier] de la línea 33 deberá completarse más adelante;
  • línea 40: se importa el almacén [Vuex];
  • líneas 43-51: se instancia la vista principal [Main] (líneas 5 y 50), pasándole dos parámetros:
    • línea 46: el enrutador [router] definido en la línea 16;
    • línea 48: el store [Vuex] [store] definido en la línea 40;
    • en ambos casos, el nombre de la propiedad está a la izquierda y su valor a la derecha. Los nombres de las propiedades [router, store] vienen determinados por los marcos [vue-router] y [vuex]. Los valores asociados, por su parte, pueden ser cualesquiera;

18.4. Las vistas de la aplicación

18.4.1. La vista principal [Main]

El código de la vista principal [Main] es el siguiente:


<!-- definición de la vista -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col cols="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col cols="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      <!-- error de solicitud HTTP -->
      <b-alert
        show
        variant="danger"
        v-if="showError"
      >L'erreur suivante s'est produite : {{error.message}}</b-alert>
      <!-- vista actual -->
      <router-view v-if="showView" @loading="mShowLoading" @error="mShowError" />
      <!-- cargando -->
      <b-alert show v-if="showLoading" variant="light">
        <strong>Requête au serveur de calcul d'impôt en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
    </b-card>
  </div>
</template>

<script>
export default {
  // nombre
  name: "app",
  // estado interno
  data() {
    return {
      // controla la alerta de espera
      showLoading: false,
      // controla la alerta de error
      showError: false,
      // controla la visualización de la vista de enrutamiento actual
      showView: true,
      // un mensaje de error
      error: ""
    };
  },
  // gestores de eventos
  methods: {
    // error de solicitud asíncrona
    mShowError(error) {
      // eslint-disable-next-line
      console.log("Main evt error");
      // se muestra el mensaje de error
      this.error = error;
      this.showError = true;
      // se almacena en caché la vista enrutada
      this.showView = false;
      // se oculta el mensaje de espera
      this.showLoading = false;
    },
    // Mostrar o no un icono de espera
    mShowLoading(value) {
      // eslint-disable-next-line
      console.log("Main evt showLoading");
      // se muestra o no la alerta de espera
      this.showLoading = value;
    }
  }
};
</script>

Comentarios

  • La vista [Main] se encarga del diseño de la vista enrutada y mostrada en la línea 23:

Image

  • las líneas 5-15 muestran el área 1;
  • la línea 23 muestra la vista enrutada [2];
  • líneas 16-19: se muestra una alerta solo en caso de error de comunicación con el servidor de cálculo de impuestos;
  • líneas 25-28: un mensaje de espera que se muestra con cada solicitud HTTP enviada al servidor;
  • todas las vistas se mostrarán con este diseño, ya que cada vista enrutada se muestra en las líneas 20-24. La vista [Main] sirve para factorizar lo que pueden compartir las diferentes vistas;
  • línea 23: cada vista enrutada puede emitir tres eventos:
    • [loading]: se ha lanzado una consulta HTTP. Hay que mostrar el mensaje de espera de la respuesta;
    • [error]: la solicitud HTTP ha finalizado con un error. Hay que mostrar el mensaje de error y ocultar la vista enrutada;
  • líneas 38-49: el estado de la vista:
    • línea 41: [showLoading] controla la visualización del mensaje de espera del final de una solicitud HTTP (línea 25);
    • línea 43: [showError] controla la visualización del mensaje de error de una consulta HTTP (líneas 17-21);
    • línea 45: [showView] controla la visualización de la vista enrutada (línea 23);
  • líneas 53-63: el método [mShowError] gestiona el evento [error] emitido por la vista enrutada (línea 23);
  • líneas 65-70: el método [mShowLoading] gestiona el evento [loading] emitido por la vista enrutada (línea 23);
  • línea 23: prestaremos atención a los eventos [error] y [loading]. Solo se interceptan si se muestra la vista enrutada [showView=true]. Por eso, la vista enrutada se muestra inicialmente (línea 45). Solo se oculta en caso de error (línea 60). Para evitar este problema, se podría haber utilizado la directiva [v-show] en lugar de [v-if]. La diferencia entre estas dos directivas es la siguiente:
    • [v-if=’false’] oculta el bloque controlado eliminándolo del código global HTML. Los eventos de la vista enrutada ya no pueden ser interceptados;
    • [v-show=’false’] oculta el bloque controlado modificando su CSS, pero el código del bloque sigue presente en el HTML global y, por lo tanto, puede interceptar los eventos de la vista enrutada;

18.4.2. La vista de diseño [Layout]

El código de la vista [Layout] es el siguiente:


<!-- definición HTML del diseño de la vista enrutada -->
<template>
  <!-- línea -->
  <div>
    <b-row>
      <!-- área de tres columnas a la izquierda -->
      <b-col cols="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- zona de nueve columnas a la derecha -->
      <b-col cols="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

<script>
  export default {
    // parámetros de la vista
    props: {
      // controla la columna de la izquierda
      left: {
        type: Boolean
      },
      // controla la columna de la derecha
      right: {
        type: Boolean
      }
    }
  };
</script>

Comentarios

  • La vista [Layout] permite dividir la vista enrutada en dos zonas:
    • una zona de 3 columnas Bootstrap a la izquierda (líneas 7-9). Esta zona albergará el menú de navegación cuando haya uno;
    • una zona de 9 columnas a la derecha (líneas 11-13). Esta zona albergará la información proporcionada por la vista enrutada;

18.4.3. La vista [Authentification]

La vista de autenticación es la siguiente:

Image

Esta vista se obtiene a partir de [Layout] eliminando la columna de la izquierda para mostrar únicamente la columna de la derecha.

Su código es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulario HTML: se envían sus valores con la acción [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- título -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- Primera línea -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- campo de entrada de usuario -->
          <b-col cols="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- segunda línea -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- campo de introducción de contraseña -->
          <b-col cols="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- tercera línea -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- botón de tipo [submit] en una tercera línea -->
        <b-row>
          <b-col cols="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

<!-- dinámica de la vista -->
<script>
import Layout from "./Layout";
export default {
  // estado del componente
  data() {
    return {
      // usuario
      user: "",
      // su contraseña
      password: "",
      // controla la visualización de un mensaje de error
      showError: false,
      // el mensaje de error
      message: "",
      // sesión iniciada
      sessionStarted: false
    };
  },

  // componentes utilizados
  components: {
    Layout
  },

  // propiedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.sessionStarted;
    }
  },

  // gestores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      try {
        // Inicio de espera
        this.$emit("loading", true);
        // autenticación bloqueante en el servidor
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // fin de la carga
        this.$emit("loading", false);
        // análisis de la respuesta
        if (response.état != 200) {
          // se muestra el error
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // sin error
        this.showError = false;
        // --------- ahora se solicitan los datos de la administración tributaria
        // Inicio de la espera
        this.$emit("loading", true);
        // solicitud bloqueada en el servidor
        const response2 = await this.$dao.getAdminData();
        // fin de la carga
        this.$emit("loading", false);
        // análisis de la respuesta
        if (response2.état != 1000) {
          // se muestra el error
          this.message = response2.réponse;
          this.showError = true;
          return;
        }
        // sin error
        this.showError = false;
        // se almacenan los datos recibidos en la capa [métier]
        this.$métier.setTaxAdminData(response2.réponse);
        // se pasa a la vista del cálculo del impuesto
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // se remite el error al componente principal
        this.$emit("error", error);
      }
    }
  },
  // ciclo de vida: el componente acaba de crearse
  created() {
    // eslint-disable-next-line
    console.log("authentification", "created");
    // se inicia una sesión jSON con el servidor
    // Inicio de la espera
    this.$emit("loading", true);
    // se inicializa la sesión con el servidor - solicitud asíncrona
    // se utiliza la promesa devuelta por los métodos de la capa [dao]
    this.$dao
      // se inicializa una sesión jSON
      .initSession()
      // se ha obtenido la respuesta
      .then(response => {
        // fin de la espera
        this.$emit("loading", false);
        // análisis de la respuesta
        if (response.état != 700) {
          // se muestra el error
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // la sesión ha comenzado
        this.sessionStarted = true;
      })
      // en caso de error
      .catch(error => {
        // se remite el error a la vista [Main]
        this.$emit("error", error);
      });
  }
};
</script>

Comentarios

  • línea 3: la vista [Authentification] utiliza únicamente la columna de la derecha del [Layout] (líneas 3 y 4);
  • líneas 6-38: el formulario Bootstrap que genera el área 1 de la captura de pantalla anterior;
  • línea 6: el evento [@submit] se produce cuando el usuario hace clic en el botón de tipo [submit] de la línea 35. El modificador [prevent] indica que la página no se recargue durante el [submit]. También se podría haber escrito:
    • una etiqueta <b-form> sin gestión del evento [submit];
    • una etiqueta <b-button> con el evento [@click=’login’] y sin el atributo [type=’submit’];

También funciona. La ventaja de la solución elegida es que el envío se realiza no solo con un clic en el botón [Valider], sino también al pulsar Intro (tecla [Entrée]) en los campos de entrada. Por lo tanto, se ha elegido aquí la solución [<b-form @submit.prevent="login">] por comodidad para el usuario;

  • líneas 33-37: una alerta que aparece cuando el servidor ha rechazado las credenciales introducidas por el usuario:

Image

  • línea 35: el botón [Valider] no siempre está activo. Su estado depende del atributo calculado [valid] de las líneas 71-73. El atributo [valid] es verdadero si:
    • hay algo en los campos [user, password] del formulario;
    • se ha iniciado la sesión jSON. Al principio, esta sesión no se ha iniciado (línea 59) y, por lo tanto, el botón [Valider] está inactivo.
  • líneas 49-60: el estado de la vista;
    • [user] representa la entrada del usuario en el campo [user] (líneas 12-17) del formulario. La directiva [v-model] de la línea 15 establece un enlace bidireccional entre la entrada del usuario y el atributo [user] de la vista;
    • [password] representa la entrada del usuario en el campo [password] (líneas 19-24) del formulario. La directiva [v-model] de la línea 22 establece un enlace bidireccional entre la entrada del usuario y el atributo [password] de la vista;
    • [showError] controla (línea 29) la visualización de la alerta de las líneas 26-31;
    • [message] es el mensaje de error (línea 31) que se mostrará en la alerta de las líneas 26-31;
    • [sessionStarted] indica si la sesión jSON con el servidor se ha iniciado o no. Inicialmente, este atributo tiene el valor [false] (línea 59). La sesión jSON con el servidor se inicializa en el evento [created] del ciclo de vida de la vista, líneas 126-156. Si el servidor responde afirmativamente, el atributo [sessionStarted] pasa a ser [true] (línea 149);
  • líneas 126-156: la función [created] se ejecuta cuando se ha creado la vista [Authentification] (aunque aún no se haya mostrado necesariamente). En segundo plano, se inicializa entonces una sesión jSON con el servidor. Sabemos que esta es la primera acción que hay que realizar con el servidor de cálculo de impuestos. Para ello, se utiliza la capa [dao] de la aplicación (línea 134). Todos los métodos de esta capa son asíncronos. Aquí se utiliza la promesa (Promise) devuelta por el método [$dao.initSession], que inicializa la sesión jSON con el servidor.
  • líneas 138-150: el código que se ejecuta cuando el servidor ha devuelto su respuesta sin errores;
  • línea 142: se comprueba la propiedad [état] de la respuesta. Debe tener el valor [700] para que la operación se haya realizado correctamente. De lo contrario, se ha producido un error cuya causa se indica en la propiedad [response.réponse] (línea 144). A continuación, se muestra el mensaje de error de la vista (línea 145);
  • línea 149: se observa que la sesión jSON se ha iniciado;
  • líneas 152-155: el código ejecutado en caso de error. Este se remite a la vista principal [Main], que
    • mostrará el error;
    • ocultará el mensaje de espera;
    • ocultará la vista enrutada, la vista [Autentification];
  • líneas 79-124: el método [login] gestiona el clic en el botón [Valider];
  • línea 79: el método se ha prefijado con la palabra clave [async] para permitir el uso de la palabra clave [await], líneas 84 y 103;
  • líneas 84-87: llamada bloqueante al método [$dao.authentifierUtilisateur(user, password)]. Se podría haber utilizado una promesa [Promise], tal y como se ha hecho en la función [created]. Hemos querido variar los estilos. No hay riesgo de bloquear al usuario, ya que hemos puesto un [timeout] de 2 segundos en todas las solicitudes HTTP. No tendrá que esperar mucho. Además, no puede hacer nada mientras el servidor no haya devuelto su respuesta, ya que entonces el botón [Valider] permanece inactivo;
  • línea 91: el servidor de cálculo de impuestos envía respuestas jSON que tienen todas la estructura [{‘action’:action, ‘état’:val, ‘réponse’:réponse}]. La autenticación se ha realizado correctamente si [état==200]. Si no es así, se muestra un mensaje de error, líneas 93-94;
  • línea 98: se oculta cualquier posible mensaje de error de una operación anterior;
  • líneas 99-116: ahora se solicitan al servidor los datos de la administración tributaria que permiten el cálculo del impuesto. En [this.$métier] tenemos una instancia de la clase [Métier] que, por el momento, no puede hacer nada porque no dispone de esos datos;
  • línea 103: los datos de la administración tributaria se solicitan al servidor mediante una operación bloqueante;
  • líneas 107-112: se analiza la respuesta del servidor. Debe tener un valor de estado igual a 1000; de lo contrario, se ha producido un error. En este último caso, se muestra el mensaje de error (líneas 109-110);
  • líneas 113-118: si la operación se realiza con éxito, se:
    • se oculta el mensaje de error, línea 114;
    • se transmiten los datos de la administración tributaria a la capa [métier] (línea 116);
    • se muestra la vista [CalculImpot], línea 118. Recordemos que [this.$router] designa el enrutador de la aplicación. El método [push] permite fijar la siguiente vista enrutada. Aquí la designamos por su atributo [name]. También podríamos haberla designado por su atributo [path]. Esta información se encuentra en el archivo de enrutamiento:

// cálculo del impuesto
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  • líneas 119-122: el [catch] se activa cuando una de las dos solicitudes HTTP ha fallado (servidor no disponible, tiempo de espera agotado, etc.). A continuación, se notifica el error a la vista principal [Main], que lo mostrará, ocultará el mensaje de espera y la vista [Authentification];

18.4.4. La vista [CalculImpot]

La vista [CalculImpot] es la siguiente:

Image

  • [1]: un menú de navegación ocupa la columna izquierda de la vista enrutada;
  • [2]: el formulario de cálculo del impuesto ocupa la columna derecha de la vista enrutada;

El código de la vista [CalculImpot] es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulario de cálculo de impuestos a la derecha -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- menú de navegación a la izquierda -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- Área de visualización de los resultados del cálculo del impuesto debajo del formulario -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- área de tres columnas vacía -->
      <b-col cols="3" />
      <!-- área de nueve columnas -->
      <b-col cols="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

<script>
// importaciones
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";

export default {
  // informe interno
  data() {
    return {
      // opciones del menú
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // resultado del cálculo del impuesto
      résultat: "",
      résultatObtenu: false
    };
  },
  // componentes utilizados
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // métodos de gestión de eventos
  methods: {
    // resultado del cálculo del impuesto
    handleResultatObtenu(résultat) {
      // se construye el resultado en cadena HTML
      const impôt = "Montant de l'impôt : " + résultat.impôt + " euro(s)";
      const décôte = "Décôte : " + résultat.décôte + " euro(s)";
      const réduction = "Réduction : " + résultat.réduction + " euro(s)";
      const surcôte = "Surcôte : " + résultat.surcôte + " euro(s)";
      const taux = "Taux d'imposition : " + résultat.taux;
      this.résultat =
        impôt +
        "<br/>" +
        décôte +
        "<br/>" +
        réduction +
        "<br/>" +
        surcôte +
        "<br/>" +
        taux;
      // visualización del resultado
      this.résultatObtenu = true;
      // ---- actualización del almacén [Vuex]
      // una simulación de +
      this.$store.commit("addSimulation", résultat);
    }
  }
};
</script>

Comentarios

  • línea 4: aquí están presentes las dos columnas de [Layout];
  • línea 6: el formulario de cálculo de impuestos ocupa la columna de la derecha. Emite el evento [resultatObtenu] cuando se ha obtenido el resultado del cálculo de impuestos. Cabe señalar que los nombres de los eventos y los nombres de los métodos que los gestionan no pueden contener caracteres acentuados;
  • línea 8: el menú de navegación ocupa la columna de la izquierda;
  • líneas 11-20: el resultado del cálculo del impuesto se muestra debajo del formulario:

Image

  • línea 11: el resultado solo se muestra si el atributo [résultatObtenu] (línea 47) tiene el valor [true];
  • líneas 34-48: el estado de la vista:
    • [options]: la lista de opciones del menú de navegación. Esta tabla se pasa como parámetro al componente [Menu], línea 8;
    • [résultat]: el resultado del cálculo del impuesto. Este resultado es una cadena HTML. Por eso se ha utilizado la directiva [v-html] en la línea 17 para mostrarlo;
    • [résultatObtenu]: el valor booleano que controla la visualización del resultado, línea 11;
  • líneas 59-81: el método [handleResultatObtenu] muestra el resultado del cálculo del impuesto que le ha enviado la vista hija [FormCalculImpot], línea 6. Este resultado es un objeto con las propiedades [impot, décôte, réduction, surcôte, taux, marié, enfants, salaire];
  • líneas 61-75: se inscribe el objeto [impot, décôte, réduction, surcôte, taux] en un texto HTML que se visualiza en la línea 17 de la plantilla;
  • línea 77: se muestra este resultado;
  • línea 80: se llama a la mutación [addSimulation] del almacén Vuex, que añadirá [résultat] a las simulaciones ya presentes en el almacén;

18.4.5. El menú de navegación [Menu]

El menú de navegación se muestra en la columna izquierda de las vistas enrutadas:

Image

El código de la vista [Menu] es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <!-- menú Bootstrap vertical -->
  <b-nav vertical>
    <!-- opciones del menú -->
    <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>
</template>

<script>
export default {
  // parámetros de la vista
  props: {
    options: {
      type: Array
    }
  }
};
</script>

Comentarios

  • las opciones del menú se proporcionan mediante el parámetro [options] (líneas 7, 20-22);
  • cada elemento de la tabla [options] tiene una propiedad [text] (línea 12) que es el texto del enlace y una propiedad [path] (línea 9) que será la ruta de la vista de destino del enlace;

18.4.6. La vista [FormCalculImpot]

Esta vista proporciona el formulario de cálculo del impuesto:

Image

Su código es el siguiente:


  <!-- definición HTML de la vista -->
  <template>
  <!-- formulario HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- mensaje en 12 columnas sobre fondo azul -->
    <b-alert show variant="primary">
      <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
    </b-alert>
    <!-- elementos del formulario -->
    <!-- primera línea -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?" label-cols="4">
      <!-- botones de opción en 5 columnas-->
      <b-col cols="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>
    <!-- segunda línea -->
    <b-form-group label="Nombre d'enfants à charge" label-cols="4" label-for="enfants">
      <b-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      />
      <!-- posible mensaje de error -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- tercera línea -->
    <b-form-group
      label="Salaire annuel"
      label-cols="4"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      />
      <!-- posible mensaje de error -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- cuarta línea, botón [submit] en 5 columnas -->
    <b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

<!-- script -->
<script>
export default {
  // estado interno
  data() {
    return {
      // casado o soltero
      marié: "non",
      // número de hijos
      enfants: "",
      // salario anual
      salaire: ""
    };
  },
  // estado interno calculado
  computed: {
    // validación del formulario
    formInvalide() {
      return (
        // salario no válido
        !this.salaireValide ||
        // o hijos no válidos
        !this.enfantsValide ||
        // o datos fiscales no obtenidos
        !this.$métier.taxAdminData
      );
    },
    // validación del salario
    salaireValide() {
      // debe ser un número >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/));
    },
    // validación de hijos
    enfantsValide() {
      // debe ser un número >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/));
    }
  },
  // gestor de eventos
  methods: {
    calculerImpot() {
      // se calcula el impuesto utilizando la capa [métier]
      const résultat = this.$métier.calculerImpot(
        this.marié,
        this.enfants,
        this.salaire
      );
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // se completa el resultado
      résultat.marié = this.marié;
      résultat.enfants = this.enfants;
      résultat.salaire = this.salaire;
      // se emite el evento [resultatObtenu]
      this.$emit("resultatObtenu", résultat);
    }
  }
};
</script>

Comentarios

  • líneas 4-51: el formulario Bootstrap;
  • líneas 11-17: un grupo de botones de radio con su etiqueta;
  • líneas 14-15: la etiqueta <b-form-radio> garantiza la visualización de un botón de radio:
    • línea 14: la directiva [v-model] garantiza que, al hacer clic en el botón, el atributo [marié] de la línea 61 reciba el valor [oui] (atributo [value="oui"]);
    • línea 15: la directiva [v-model] garantiza que, al hacer clic en el botón, el atributo [marié] de la línea 61 reciba el valor [non] (atributo [value="non"]);
  • líneas 19-29: la parte de introducción del número de hijos:
    • línea 24: la introducción del número de hijos está vinculada al atributo [enfants] de la línea 63;
    • línea 25: la validez de la entrada se comprueba mediante el atributo calculado [enfantsValide] de las líneas 87-89;
    • línea 28: garantiza la visualización de un mensaje de error si la entrada no es válida;
  • líneas 31-45: la parte de introducción del salario anual:
    • línea 35: muestra un mensaje de ayuda justo debajo del campo de entrada;
    • línea 41: la entrada del salario está vinculada al atributo [salaire] de la línea 65;
    • línea 42: la validez de la entrada se comprueba mediante el atributo calculado [salaireValide] de las líneas 82-85;
    • línea 45: garantiza la visualización de un mensaje de error si la entrada no es válida;
  • líneas 48-50: un botón de tipo [submit]. Al hacer clic en este botón o al validar una entrada con la tecla [Entrée], se ejecuta el método [calculerImpot] (línea 94);
    • línea 49: el estado del botón (activo/inactivo) se controla mediante el atributo calculado [formInvalide] de las líneas 71-80;
  • líneas 71-80: el formulario es válido si:
    • el número de hijos es válido;
    • el salario es válido;
    • la aplicación ha obtenido del servidor los datos de la administración tributaria que permiten el cálculo del impuesto. Se recuerda que este dato se registra en la propiedad [$métier.taxAdminData]. La vista [FormCalculImpot] puede mostrarse antes de que se haya obtenido este dato, ya que se solicita de forma asíncrona al mismo tiempo que se produce la visualización de la vista. Aquí nos aseguramos de que el usuario no pueda hacer clic en el botón [Valider] mientras no se hayan obtenido los datos;
  • líneas 94-109: el método de cálculo del impuesto:
    • líneas 96-100: es la capa [métier] la que realiza este cálculo. Se trata de un cálculo sincrónico. Una vez obtenido el dato [taxAdminData], el cliente [Vue] ya no tiene que comunicarse con el servidor. Todo se realiza localmente. Se obtiene un objeto [résultat] con las propiedades [impôt, décôte, surcôte, réduction, taux];
    • líneas 104-106: se añaden las propiedades [marié, enfants, salaire] al resultado;
    • línea 108: el resultado se pasa a la vista principal [CalculImpot] a través del evento [resultatObtenu]. Esta vista se encarga de mostrar el resultado;

18.4.7. La vista [ListeSimulations]

La vista [ListeSimulations] muestra la lista de simulaciones realizadas por el usuario:

Image

El código de la vista es el siguiente:


<!-- definición HTML de la vista -->
<template>
  <div>
    <!-- diseño -->
    <Layout :left="true" :right="true">
      <!-- simulaciones en la columna de la derecha -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- sin simulaciones -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- hay simulaciones -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tabla de simulaciones -->
          <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>
      <!-- menú de navegación en la columna de la izquierda -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

<script>
  // importaciones
  import Layout from "./Layout";
  import Menu from "./Menu";
  export default {
    // componentes
    components: {
      Layout,
      Menu
    },
    // estado interno
    data() {
      return {
        // opciones del menú de navegación
        options: [
          {
            text: "Calcul de l'impôt",
            path: "/calcul-impot"
          },
          {
            text: "Fin de session",
            path: "/fin-session"
          }
        ],
        // parámetros de la tabla HTML
        fields: [
          { label: "#", key: "id" },
          { label: "Marié", key: "marié" },
          { label: "Nombre d'enfants", key: "enfants" },
          { label: "Salaire", key: "salaire" },
          { label: "Impôt", key: "impôt" },
          { label: "Décôte", key: "décôte" },
          { label: "Réduction", key: "réduction" },
          { label: "Surcôte", key: "surcôte" },
          { label: "", key: "action" }
        ]
      };
    },
    // estado interno calculado
    computed: {
      // lista de simulaciones extraída del almacén Vuex
      simulations() {
        return this.$store.state.simulations;
      }
    },
    // métodos
    methods: {
      supprimerSimulation(index) {
        // eslint-disable-next-line
        console.log("supprimerSimulation", index);
        // eliminación de la simulación n.º [index]
        this.$store.commit("deleteSimulation", index);
      }
    }
  };
</script>

Comentarios

  • línea 5: la vista ocupa las dos columnas del diseño [Layout] de las vistas enrutadas;
  • líneas 7-26: las simulaciones van en la columna de la derecha;
  • línea 28: el menú de navegación va en la columna de la izquierda;
  • líneas 8, 14, 20, 75: las simulaciones proceden del almacén [Vuex] [$this.store];
  • líneas 8-13: se muestra una alerta cuando la lista de simulaciones está vacía;
  • líneas 14-25: la tabla HTML se muestra cuando la lista de simulaciones no está vacía;
  • líneas 20-24: la tabla HTML se genera mediante una etiqueta <b-table>;
    • línea 20: la tabla de simulaciones es proporcionada por el atributo calculado [simulations] de las líneas 74-76;
    • línea 20: la configuración de la tabla HTML se realiza mediante el atributo calculado [fields] de las líneas 58-69. Línea 67: la columna clave [action] es la última columna de la tabla HTML;
    • líneas 21-23: plantilla de la última columna de la tabla HTML;
    • línea 22: se coloca un botón de tipo enlace. Al hacer clic en él, se invoca el método [supprimerSimulation(data.index)], donde [data] representa la línea actual (línea 21). [data.index] representa el número de esta línea en la lista de líneas mostradas;
  • línea 28: generación del menú de navegación. Las opciones de este se proporcionan mediante el atributo [options] de las líneas 47-56;
  • líneas 80-85: el método que responde al clic en un enlace [Supprimer] de la página HTML;
    • línea 84: se invoca la mutación [deleteSimulation] del store [Vuex] (véase el apartado |vuejs-15|);

18.5. Ejecución del proyecto

Image

También es necesario iniciar el servidor [Laragon] (véase el documento |https://tahe.developpez.com/tutoriels-cours/php7|) para que el servidor de cálculo de impuestos esté en línea.

18.6. Implementación de la aplicación en un servidor local

Actualmente, nuestro cliente [Vue] está implementado en un servidor de pruebas en URL [http://localhost:8080]. Vamos a implementarlo en el servidor [Laragon] en URL [http://localhost:80]. Hay varios pasos que seguir para llegar hasta ahí.

Paso 1

En primer lugar, vamos a asegurarnos de que el cliente [Vue] se implemente en el servidor de pruebas en URL [http://localhost:8080/client-vuejs-impot/].

Creamos un archivo [vue.config.js] en la raíz de nuestro proyecto actual [VSCode]:

Image

El archivo [vue.config.js] [1] tendrá el siguiente contenido:


// vue.config.js
module.exports = {
  // el servicio URL del cliente [vuejs] del servidor de cálculo de impuestos
  publicPath: '/client-vuejs-impot/'
}

También debemos modificar el archivo de enrutamiento [router.js] [2]:


// importaciones
import Vue from 'vue'
import VueRouter from 'vue-router'
// las vistas
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'

// complemento de enrutamiento
Vue.use(VueRouter)

// rutas de la aplicación
const routes = [
  // autenticación
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // cálculo de impuestos
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // lista de simulaciones
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de sesión
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]

// el enrutador
const router = new VueRouter({
  // las rutas
  routes,
  // modo de visualización de las rutas en el navegador
  mode: 'history',
  // la base de la aplicación URL
  base: '/client-vuejs-impot/'
})

// exportación del enrutador
export default router
  • línea 39: se indica al enrutador que las rutas definidas en las líneas 13-30 son relativas a la ruta definida en la línea 39. Por ejemplo, la ruta de la línea 20 [/calcul-impot] pasará a ser [/client-vuejs-impot/calcul-impot];

A continuación, se puede volver a probar el proyecto [vuejs-20] para verificar el cambio en las rutas de la aplicación:

Image

paso 2

Ahora compilamos la versión de producción del proyecto [vuejs-20]:

Image

  • en [1-2], configuramos la tarea [build] [2] en el archivo [package.json] [1];
  • en [3-5], ejecutamos esta tarea. Es ella la que va a construir la versión de producción del proyecto [vuejs-20];

La ejecución de la tarea [build] tiene lugar en un terminal de [VSCode]:

Image

Image

  • en [3-6], unas advertencias nos indican que el código generado es demasiado grande y que habría que dividirlo en [8]. Esto forma parte de la optimización de la arquitectura del código, tema que no abordaremos aquí;
  • en [7], se nos indica que la carpeta [dist] contiene la versión de producción generada:

Image

  • en [3], el archivo [index.html] es el que se utilizará cuando se solicite el URL [https://localhost:80/client-vue-js-impot/];

Aquí tenemos un sitio web estático que se puede implementar en cualquier servidor. Lo vamos a implementar en el servidor Laragon local (véase el documento |https://tahe.developpez.com/tutoriels-cours/php7|). La carpeta [dist] [2] se copia en la carpeta [<laragon>/www] [4], donde <laragon> es la carpeta de instalación del servidor Laragon. Renombramos esta carpeta [client-vuejs-impot] [5], ya que hemos configurado la versión de producción para que funcione en URL [/client-vuejs-impot/].

Paso 3

Añadimos en la carpeta [client-vuejs-impot] que acabamos de crear el siguiente archivo [.htaccess]:


<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /client-vuejs-impot/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /client-vuejs-impot/index.html [L]
</IfModule>

Image

Este archivo es un archivo de configuración del servidor web Apache. Si no lo incluimos y solicitamos directamente el URL [https://localhost/client-vuejs-impot/calcul-impot], sin pasar primero por el URL [https://localhost/client-vuejs-impot/], obtenemos un error 404. Con este archivo, obtenemos correctamente la vista [CalculImpot].

Una vez hecho esto, iniciamos el servidor Laragon, si aún no lo hemos hecho, y solicitamos el URL [https://localhost/client-vuejs-impot/]:

Image

Se invita al lector a probar la versión de producción de nuestra aplicación.

Podemos modificar el servidor de cálculo de impuestos en un aspecto: los encabezados CORS que envía sistemáticamente a sus clientes. Esto había sido necesario para la versión del cliente ejecutada desde el dominio [localhost:8080]. Ahora que tanto el cliente como el servidor se ejecutan en el dominio [localhost:80], los encabezados CORS ya no son necesarios.

Modificamos el archivo [config.json] de la versión 14 del servidor:

Image

  • por [4], indicamos que a partir de ahora se rechazan las solicitudes CORS;

Guardemos este cambio y volvamos a solicitar el URL [https://localhost/client-vuejs-impot/]. Debería seguir funcionando.

18.7. Gestión de las URL manuales

En lugar de utilizar correctamente los enlaces del menú de navegación, es posible que el usuario quiera escribir manualmente las URL de la aplicación en la barra de direcciones del navegador. Solicitemos, por ejemplo, el URL [https://client-vuejs-impot/calcul-impot] sin pasar por el paso de autenticación. Un hacker seguramente lo intentaría. Se obtiene la siguiente vista:

Image

Efectivamente, se muestra la vista del cálculo del impuesto. Ahora intentemos rellenar los campos de entrada y validarlos:

Image

Descubrimos entonces que el botón [1] [Valider] sigue desactivado aunque los datos introducidos sean correctos. Veamos el código de la vista [FormCalculImpot]:


<b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
</b-col>

En la línea 2, vemos que su estado activo/inactivo depende de la propiedad [formInvalide]. Esta es la siguiente propiedad calculada:


formInvalide() {
      return (
        // salario no válido
        !this.salaireValide ||
        // o hijos no válidos
        !this.enfantsValide ||
        // o datos fiscales no obtenidos
        !this.$métier.taxAdminData
      );
},

En la línea 8, vemos que para que el formulario sea válido, es necesario haber obtenido los datos fiscales. Sin embargo, estos se obtienen durante la validación de la vista [Authentification] que el usuario se ha «saltado». Por lo tanto, no podrá validar el formulario. Si hubiera podido hacerlo, habría recibido un mensaje de error del servidor indicándole que no estaba autenticado. Las verificaciones siempre deben realizarse en el lado del servidor. Las verificaciones en el lado del navegador siempre pueden eludirse. Basta con utilizar un cliente de tipo [Postman] que envíe solicitudes sin procesar al servidor.

Ahora solicitemos el URL [https://localhost/client-vuejs-impot/liste-des-simulations]. Obtenemos la siguiente vista:

Image

Ahora el URL [https://localhost/client-vuejs-impot/fin-session]. Obtenemos la siguiente vista:

Image

Ahora una vista que no existe [https://localhost/client-vuejs-impot/abcd]:

Image

Nuestra aplicación resiste bastante bien los URL introducidos manualmente. Cuando se invocan, el enrutador de la aplicación lo sabe. Por lo tanto, es posible intervenir antes de que la vista se muestre finalmente. Analizaremos este punto en el proyecto [vuejs-21].

Otro punto a tener en cuenta es el siguiente. Imaginemos que el usuario ha realizado algunas simulaciones según las reglas:

Image

Ahora actualicemos la página con un F5:

Image

Hemos hecho algo que no se recomienda: escribir URL a mano (hacer F5 equivale a eso). Entonces hemos perdido nuestras simulaciones.

El siguiente proyecto, [vuejs-21], se propone aportar dos mejoras:

  • controlar los URL introducidos por el usuario;
  • mantener un historial de la aplicación incluso si el usuario escribe un URL. Arriba, vemos que hemos perdido la lista de simulaciones;