Skip to content

18. Cliente Vue.js do servidor de cálculo de impostos

18.1. Arquitetura

Iremos implementar uma aplicação cliente/servidor com a seguinte arquitetura:

Image

O servidor de cálculo de impostos será a versão 14, desenvolvida no documento |https://tahe.developpez.com/tutoriels-cours/php7|

18.2. Visualizações da aplicação

As vistas da aplicação [vuejs-10] são as da versão 13 do documento |https://tahe.developpez.com/tutoriels-cours/php7| do servidor de cálculo de impostos quando utilizado no modo HTML. No entanto, nesta aplicação, estas vistas serão geradas pelo cliente JavaScript em vez de pelo servidor PHP.

A primeira vista é a vista de autenticação:

Image

A segunda vista é a vista de cálculo de impostos:

Image

A terceira vista apresenta a lista de simulações realizadas pelo utilizador:

Image

O ecrã acima mostra que é possível eliminar a simulação n.º 1. Isto resulta na seguinte vista:

Image

Se eliminar agora a última simulação, obtém a seguinte nova vista:

Image

18.3. Elementos do projeto [vuejs-20]

A árvore do projeto [vuejs-20] é a seguinte:

Image

Os elementos do projeto são os seguintes:

  • [assets/logo.jpg]: o logótipo do projeto;
  • [layers]: as camadas [business] e [DAO] da aplicação;
  • [plugins]: os plugins da aplicação;
  • [views]: as vistas da aplicação;
  • [config.js]: configura a aplicação;
  • [router.js]: define o roteamento da aplicação;
  • [store.js]: o armazenamento [Vuex];
  • [main.js]: o script principal da aplicação;

18.3.1. As camadas [business] e [DAO]

18.3.1.1. A camada [DAO]

A camada [DAO] é implementada pela classe [Dao] na secção |vuejs-10|

18.3.1.2. A camada [business]

A camada [business] é implementada pela classe [Business] no documento |https://tahe.developpez.com/tutoriels-cours/php7|. O seguinte método [setTaxAdminData] foi adicionado a ela:


// constructeur
  constructor(taxAdmindata) {
    // this.taxAdminData : données de l'administration fiscale
    this.taxAdminData = taxAdmindata;
  }
 
  // setter
  setTaxAdminData(taxAdmindata) {
    // this.taxAdminData : données de l'administration fiscale
    this.taxAdminData = taxAdmindata;
}

O método [setTaxAdminData] faz o mesmo que o construtor. A sua presença permite a seguinte sequência:

  1. instanciar a classe [Métier] com a instrução [métier=new Métier()] quando quiser instanciar a classe, mas ainda não tiver os dados [taxAdminData];
  2. depois preencher a sua propriedade [taxAdminData] mais tarde utilizando uma operação [métier.setTaxAdminData(taxAdmindata)];

18.3.2. O ficheiro de configuração [config]

O ficheiro [config.js] é o seguinte:


// utilisation de la bibliothèque [axios]
const axios = require('axios');
// timeout des requêtes HTTP
axios.defaults.timeout = 2000;
// la base des URL du serveur de calcul de l'impôt
// le schéma [https] pose des problèmes à Firefox parce que le serveur de calcul
// de l'impôt envoie un certificat autosigné. ok avec Chrome et Edge. Safari pas testé.
axios.defaults.baseURL = 'https://localhost/php7/scripts-web/impots/version-14';
// on va utiliser des cookies
axios.defaults.withCredentials = true;
 
// export de la configuration
export default {
  axios: axios
}

Esta configuração destina-se à biblioteca [axios] que a camada [dao] utiliza para efetuar os seus pedidos HTTP. Observe na linha 8 que o servidor opera numa porta segura [https].

18.3.3. Os plugins

Os plugins [pluginDao, pluginMétier, pluginConfig] foram concebidos para criar três novas propriedades para a função/classe [Vue]:

  • [$dao]: terá o valor de uma instância da classe [Dao];
  • [$métier]: terá o valor de uma instância da classe [Métier];
  • [$config]: será definido como o objeto exportado pelo ficheiro de configuração [config];

[pluginDao]


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

[pluginConfig]


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

18.3.4. O armazenamento [Vuex]

A loja [Vuex] é implementada pelo seguinte ficheiro [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: {
    // suppression ligne n° index
    deleteSimulation(state, index) {
      // eslint-disable-next-line no-console
      console.log("mutation deleteSimulation");
      // on supprime la ligne n° [index]
      state.simulations.splice(index, 1);
      // eslint-disable-next-line no-console
      console.log("store simulations", state.simulations);
    },
    // ajout d'une simulation
    addSimulation(state, simulation) {
      // eslint-disable-next-line no-console
      console.log("mutation addSimulation");
      // n° de la simulation
      state.idSimulation++;
      simulation.id = state.idSimulation;
      // on ajoute la simulation au tableau des simulations
      state.simulations.push(simulation);
    },
    // nettoyage state
    clear(state) {
      state.simulations = [];
      state.idSimulation = 1;
    }
  }
});
// export de l'objet [store]
export default store;

Comentários

  • linhas 2-4: o plugin [Vuex] é integrado na estrutura [Vue];
  • linhas 8-13: colocamos os seguintes elementos no armazenamento [Vuex]:
    • [simulations]: a lista de simulações realizadas pelo utilizador;
    • [idSimulation]: o ID da última simulação realizada pelo utilizador;

Note que o store será partilhado entre as vistas e que o seu conteúdo é reativo: quando é modificado, as vistas que o utilizam são automaticamente atualizadas. Na nossa aplicação, apenas o elemento [simulations] precisa de ser reativo, não o elemento [simulationId]. Mantivemos este elemento no store por uma questão de conveniência;

  • linhas 14–40: as mutações permitidas no objeto [state] das linhas 8–13. Note que estas recebem sempre o objeto [state] das linhas 8–13 como seu primeiro parâmetro;
    • linha 16: a mutação [deleteSimulation] permite eliminar uma simulação especificando o seu número [index];
    • linha 25: a mutação [addSimulation] permite adicionar uma nova simulação à matriz de simulações;
    • linha 35: a operação [clear] reinicia o objeto [state] das linhas 8–13;

18.3.5. O ficheiro de encaminhamento [router]

O ficheiro de encaminhamento é o seguinte:


// 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'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des routes dans le navigateur
  mode: 'history',
})
 
// export du router
export default router

Comentários

  • linha 16: Quando a aplicação é iniciada, a vista [Authentication] é apresentada porque a sua URL é a raiz [/];
  • linha 20: a vista [TaxCalculation] é exibida quando a URL [/tax-calculation] é solicitada;
  • linha 24: a vista [SimulationList] é exibida quando a URL [/simulation-list] é solicitada;
  • linha 28: a vista [Authentication] é exibida quando a URL [/end-session] é solicitada;
  • linhas 33–38: é criado um objeto [router] com estas rotas (linha 35) e o modo [history] (linha 37) para gestão de URLs;
  • linha 41: este router é exportado;

18.3.6. O script principal [main.js]

O script [main.js] é o seguinte:


// imports
import Vue from 'vue'
 
// vue principale
import Main from './views/Main.vue'
 
// plugin [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'
 
// routeur
import router from './router'
 
// plugin [config]
import config from './config';
import pluginConfig from './plugins/pluginConfig'
Vue.use(pluginConfig, config)
 
// instanciation couche [dao]
import Dao from './couches/Dao';
const dao = new Dao(config.axios);
 
// plugin [dao]
import pluginDao from './plugins/pluginDao'
Vue.use(pluginDao, dao)
 
// 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'
 
// 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),
})

Observe os seguintes pontos:

  • linhas 18–21: o objeto exportado pelo script [./config] estará disponível na propriedade [Vue.$config] e, portanto, disponível para todas as visualizações na aplicação. Isso não era necessário aqui porque o objeto [config] é usado apenas pelo script [main] (linha 25). No entanto, é comum que a configuração seja necessária para várias visualizações. Por isso, quisemos manter o princípio de disponibilizá-la num atributo de vista;
  • linhas 24–25: instanciação da camada [dao]. A classe [Dao] é importada na linha 24 e, em seguida, instanciada na linha 25. O seu construtor recebe o objeto [axios] — uma propriedade de configuração — como único parâmetro;
  • linhas 27–29: a camada [dao] é disponibilizada no atributo [$dao] de todas as vistas;
  • linhas 31–37: repetimos a mesma sequência para a camada [business]. O construtor da classe [Business] recebe [taxAdminData] como parâmetro, que representa dados de administração fiscal. Ainda não dispomos destes dados. O objeto [business] na linha 33 terá, portanto, de ser preenchido posteriormente;
  • linha 40: importamos o armazenamento [Vuex];
  • linhas 43–51: instanciamos a vista principal [Main] (linhas 5 e 50), passando-lhe dois parâmetros:
    • linha 46: o router [router] definido na linha 16;
    • linha 48: o armazenamento [Vuex] [store] definido na linha 40;
    • em ambos os casos, o nome da propriedade está à esquerda e o seu valor à direita. Os nomes das propriedades [router, store] são definidos pelos frameworks [vue-router] e [vuex]. Os valores associados podem ser quaisquer;

18.4. As vistas da aplicação

18.4.1. A vista principal [Main]

O código para a vista principal [Main] é o seguinte:


<!-- définition HTML de la vue -->
<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>
      <!-- erreur requête HTTP -->
      <b-alert
        show
        variant="danger"
        v-if="showError"
      >L'erreur suivante s'est produite : {{error.message}}</b-alert>
      <!-- vue courante -->
      <router-view v-if="showView" @loading="mShowLoading" @error="mShowError" />
      <!-- loading -->
      <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 {
  // name
  name: "app",
  // inner state
  data() {
    return {
      // controls waiting alert
      showLoading: false,
      // controls error alert
      showError: false,
      // controls the display of the current routing view
      showView: true,
      // an error message
      error: ""
    };
  },
  // event managers
  methods: {
    // asynchronous request error
    mShowError(error) {
      // eslint-disable-next-line
      console.log("Main evt error");
      // error msg is displayed
      this.error = error;
      this.showError = true;
      // hide the routed view
      this.showView = false;
      // we hide the waiting message
      this.showLoading = false;
    },
    // whether or not to display a waiting icon
    mShowLoading(value) {
      // eslint-disable-next-line
      console.log("Main evt showLoading");
      // whether or not to display the waiting alert
      this.showLoading = value;
    }
  }
};
</script>

Comentários

  • A vista [Main] controla o layout da vista encaminhada e exibida. Linha 23:

Image

  • As linhas 5–15 mostram a zona 1;
  • a linha 23 apresenta a vista de encaminhamento [2];
  • Linhas 16–19: um alerta exibido apenas em caso de erro de comunicação com o servidor de cálculo de impostos;
  • Linhas 25–28: uma mensagem de carregamento exibida para cada pedido HTTP feito ao servidor;
  • todas as vistas serão apresentadas com este layout, uma vez que cada vista encaminhada é apresentada pelas linhas 20–24. A vista [Principal] é utilizada para separar o que pode ser partilhado pelas diferentes vistas;
  • Linha 23: Cada vista encaminhada pode desencadear três eventos:
    • [loading]: foi enviada uma solicitação HTTP. A mensagem de carregamento deve ser exibida;
    • [error]: a solicitação HTTP terminou em erro. A mensagem de erro deve ser exibida e a vista roteada ocultada;
  • linhas 38–49: o estado da vista:
    • linha 41: [showLoading] controla a exibição da mensagem indicando que uma solicitação HTTP está em andamento (linha 25);
    • linha 43: [showError] controla a exibição da mensagem de erro para uma solicitação HTTP (linhas 17–21);
    • linha 45: [showView] controla a exibição da vista encaminhada (linha 23);
  • linhas 53–63: o método [mShowError] trata do evento [error] emitido pela vista encaminhada (linha 23);
  • Linhas 65–70: O método [mShowLoading] trata do evento [loading] emitido pela vista encaminhada (linha 23);
  • linha 23: Vamos concentrar-nos nos eventos [error] e [loading]. Estes só são interceptados se a vista encaminhada for exibida [showView=true]. É por isso que a vista encaminhada é inicialmente exibida (linha 45). Só é ocultada em caso de erro (linha 60). Para evitar este problema, poderíamos ter usado a diretiva [v-show] em vez de [v-if]. A diferença entre estas duas diretivas é a seguinte:
    • [v-if=’false’] oculta o bloco controlado removendo-o do HTML global. Os eventos da vista encaminhada deixam então de poder ser interceptados;
    • [v-show=’false’] oculta o bloco controlado manipulando o seu CSS, mas o código do bloco permanece presente no HTML global e pode, assim, interceptar eventos da vista encaminhada;

18.4.2. A vista [Layout]

O código da vista [Layout] é o seguinte:


<!-- definition HTML of the routed view layout -->
<template>
  <!-- line -->
  <div>
    <b-row>
      <!-- three-column zone on the left -->
      <b-col cols="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- nine-column zone on the right -->
      <b-col cols="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
  export default {
    // paramètres de la vue
    props: {
      // contrôle la colonne de gauche
      left: {
        type: Boolean
      },
      // contrôle la colonne de droite
      right: {
        type: Boolean
      }
    }
  };
</script>

Comentários

  • A vista [Layout] permite dividir a vista encaminhada em duas áreas:
    • uma área Bootstrap de 3 colunas à esquerda (linhas 7–9). Esta área conterá o menu de navegação, caso exista;
    • uma área de 9 colunas à direita (linhas 11–13). Esta área exibirá as informações fornecidas pela vista encaminhada;

18.4.3. A vista [Authentication]

A vista de autenticação é a seguinte:

Image

Esta vista é derivada da [Layout] através da remoção da coluna esquerda para exibir apenas a coluna direita.

O seu código é o seguinte:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- titre -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1ère ligne -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- zone de saisie user -->
          <b-col cols="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2ième ligne -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- zone de saisie password -->
          <b-col cols="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- 3ième ligne -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- bouton de type [submit] sur une 3ième ligne -->
        <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>
 
<!-- 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: "",
      // session started
      sessionStarted: false
    };
  },
 
  // components used
  components: {
    Layout
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.sessionStarted;
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit("loading", true);
        // blocking server authentication
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // end of loading
        this.$emit("loading", false);
        // response analysis
        if (response.état != 200) {
          // error is displayed
          this.message = response.réponse;
          this.showError = true;
          return;
        }
        // no error
        this.showError = false;
        // --------- we now request data from the tax authorities
        // 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;
        }
        // no error
        this.showError = false;
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse);
        // we move on to the tax calculation view
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // the error is traced back to the main component
        this.$emit("error", error);
      }
    }
  },
  // life cycle: the component has just been created
  created() {
    // eslint-disable-next-line
    console.log("authentification", "created");
    // start a jSON session with the server
    // 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;
        }
        // the session has started
        this.sessionStarted = true;
      })
      // in case of error
      .catch(error => {
        // the error is traced back to the [Main] view
        this.$emit("error", error);
      });
  }
};
</script>

Comentários

  • linha 3: a vista [Authentication] utiliza apenas a coluna direita do [Layout] (linhas 3 e 4);
  • linhas 6–38: o formulário Bootstrap que gera a área 1 na captura de ecrã acima;
  • linha 6: o evento [@submit] ocorre quando o utilizador clica no botão [submit] na linha 35. O modificador [prevent] garante que a página não seja recarregada após o [submit]. Também poderíamos ter escrito:
    • uma tag <b-form> sem tratar o evento [submit];
    • uma tag <b-button> com o evento [@click='login'] e sem o atributo [type='submit'];

Isso também funciona. A vantagem da solução escolhida é que o formulário é enviado não só clicando no botão [Submit], mas também premindo a tecla [Enter] nos campos de entrada. A solução [<b-form @submit.prevent="login">] foi, portanto, escolhida aqui para conveniência do utilizador;

  • linhas 33–37: um alerta que aparece quando o servidor rejeita as credenciais introduzidas pelo utilizador:

Image

  • linha 35: o botão [Submit] nem sempre está ativo. O seu estado depende do atributo calculado [valid] nas linhas 71–73. O atributo [valid] é verdadeiro se:
    • houver algo nos campos [user, password] do formulário;
    • a sessão JSON tiver começado. Inicialmente, esta sessão não começou (linha 59) e, por isso, o botão [Validate] está inativo.
  • linhas 49–60: o estado da vista;
    • [user] representa a entrada do utilizador no campo [user] (linhas 12–17) do formulário. A diretiva [v-model] na linha 15 estabelece uma ligação bidirecional entre a entrada do utilizador e o atributo [user] da vista;
    • [password] representa a entrada do utilizador no campo [password] (linhas 19–24) do formulário. A diretiva [v-model] na linha 22 estabelece uma ligação bidirecional entre a entrada do utilizador e o atributo [password] da vista;
    • [showError] controla (linha 29) a exibição do alerta nas linhas 26–31;
    • [message] é a mensagem de erro (linha 31) a ser exibida no alerta nas linhas 26–31;
    • [sessionStarted] indica se a sessão JSON com o servidor foi iniciada ou não. Inicialmente, este atributo tem o valor [false] (linha 59). A sessão JSON com o servidor é inicializada no evento [created] do ciclo de vida da vista, linhas 126–156. Se o servidor responder positivamente, então o atributo [sessionStarted] é definido como [true] (linha 149);
  • linhas 126–156: a função [created] é executada quando a vista [Authentication] é criada (embora ainda não necessariamente exibida). Em segundo plano, é então inicializada uma sessão JSON com o servidor. Sabemos que esta é a primeira ação a realizar com o servidor de cálculo de impostos. Para tal, utilizamos a camada [dao] da aplicação (linha 134). Todos os métodos nesta camada são assíncronos. Aqui, usamos a Promise devolvida pelo método [$dao.initSession], que inicializa a sessão JSON com o servidor.
  • Linhas 138–150: o código executado quando o servidor devolve a sua resposta sem erros;
  • linha 142: verificamos a propriedade [status] da resposta. Deve ter o valor [700] para uma operação bem-sucedida. Caso contrário, ocorreu um erro, cuja causa é indicada na propriedade [response.response] (linha 144). Em seguida, exibimos a mensagem de erro na vista (linha 145);
  • linha 149: verificamos que a sessão JSON foi iniciada;
  • linhas 152–155: o código executado em caso de erro. Este erro é propagado para a vista pai [Main], que
    • exibirá o erro;
    • oculta a mensagem de espera;
    • oculta a vista encaminhada, a vista [Authentication];
  • linhas 79–124: o método [login] trata do clique no botão [Validate];
  • linha 79: o método foi prefixado com a palavra-chave [async] para permitir o uso da palavra-chave [await], linhas 84 e 103;
  • linhas 84–87: chamada de bloqueio ao método [$dao.authenticateUser(user, password)]. Poderíamos ter usado um [Promise], tal como foi feito na função [created]. Queríamos variar os estilos. Não há risco de bloquear o utilizador, pois definimos um [timeout] de 2 segundos em todas as solicitações HTTP. Eles não terão de esperar muito tempo. Além disso, não podem fazer nada até que o servidor tenha devolvido a sua resposta, uma vez que o botão [Validate] permanece inativo até então;
  • linha 91: o servidor de cálculo de impostos envia respostas JSON, todas com a estrutura [{‘action’:action, ‘status’:val, ‘response’:response}]. A autenticação foi bem-sucedida se [status==200]. Caso contrário, é exibida uma mensagem de erro, linhas 93-94;
  • linha 98: quaisquer mensagens de erro de uma operação anterior são ocultadas;
  • linhas 99–116: solicitamos agora ao servidor os dados da autoridade fiscal necessários para calcular o imposto. Em [this.$métier], temos uma instância da classe [Métier] que não pode fazer nada neste momento, pois não possui esses dados;
  • linha 103: os dados da autoridade fiscal são solicitados ao servidor através de uma operação de bloqueio;
  • linhas 107–112: a resposta do servidor é analisada. Deve ter um valor de estado de 1000; caso contrário, ocorreu um erro. Neste último caso, é apresentada uma mensagem de erro (linhas 109–110);
  • linhas 113–118: se a operação for bem-sucedida, nós:
    • ocultamos a mensagem de erro (linha 114);
    • passamos os dados da autoridade fiscal para a camada [business] (linha 116);
    • exibimos a vista [CalculImpot], linha 118. Recorde-se que [this.$router] se refere ao router da aplicação. O método [push] é utilizado para definir a próxima vista encaminhada. Aqui, referimo-nos a ela pelo seu atributo [name]. Também poderíamos ter-nos referido a ela pelo seu atributo [path]. Esta informação encontra-se no ficheiro de encaminhamento:

// calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  • linhas 119–122: O bloco [catch] é acionado quando uma das duas solicitações HTTP falha (servidor não encontrado, tempo limite excedido, etc.). O erro é então reportado à vista pai [Main], que o exibirá, ocultará a mensagem de carregamento e ocultará a vista [Authentication];

18.4.4. A vista [CalculImpot]

A vista [CalculImpot] é a seguinte:

Image

  • [1]: Um menu de navegação ocupa a coluna esquerda da vista encaminhada;
  • [2]: O formulário de cálculo de impostos ocupa a coluna direita da vista encaminhada;

O código da vista [TaxCalculation] é o seguinte:


<!-- 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 cols="3" />
      <!-- nine-column zone -->
      <b-col cols="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</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
      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;
      // affichage du résultat
      this.résultatObtenu = true;
      // ---- maj du store [Vuex]
      // une simulation de +
      this.$store.commit("addSimulation", résultat);
    }
  }
};
</script>

Comentários

  • linha 4: as duas colunas do [Layout] estão presentes aqui;
  • linha 6: o formulário de cálculo de impostos ocupa a coluna da direita. Este aciona o evento [resultatObtenu] quando o resultado do cálculo de impostos é obtido. Note que os nomes dos eventos e dos métodos que os tratam não podem conter caracteres acentuados;
  • linha 8: o menu de navegação ocupa a coluna da esquerda;
  • linhas 11–20: o resultado do cálculo do imposto é apresentado abaixo do formulário:

Image

  • linha 11: o resultado é exibido apenas se o atributo [resultObtained] (linha 47) for [true];
  • linhas 34–48: o estado da vista:
    • [options]: a lista de opções do menu de navegação. Esta matriz é passada como parâmetro para o componente [Menu] na linha 8;
    • [result]: o resultado do cálculo do imposto. Este resultado é uma cadeia de caracteres HTML. É por isso que a diretiva [v-html] foi utilizada na linha 17 para o apresentar;
    • [resultObtained]: o valor booleano que controla a exibição do resultado, linha 11;
  • linhas 59–81: o método [handleResultatObtenu] exibe o resultado do cálculo do imposto que lhe foi enviado pela vista secundária [FormCalculImpot], linha 6. Este resultado é um objeto com as propriedades [imposto, desconto, redução, sobretaxa, taxa, casado, filhos, salário];
  • linhas 61–75: o objeto [tax, discount, reduction, surcharge, rate] é inserido no texto HTML que é renderizado pela linha 17 do modelo;
  • linha 77: este resultado é apresentado;
  • Linha 80: Chama o método [addSimulation] do armazenamento Vuex, que adiciona [result] às simulações já presentes no armazenamento;

18.4.5. O menu de navegação [Menu]

O menu de navegação é exibido na coluna esquerda das vistas encaminhadas:

Image

O código para a vista [Menu] é o seguinte:


<!-- definition HTML of the view -->
<template>
  <!-- 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>
</template>
 
<script>
export default {
  // paramètres de la vue
  props: {
    options: {
      type: Array
    }
  }
};
</script>

Comentários

  • as opções do menu são fornecidas pelo parâmetro [options] (linhas 7, 20–22);
  • cada elemento da matriz [options] tem uma propriedade [text] (linha 12), que é o texto do link, e uma propriedade [path] (linha 9), que será o caminho para a vista de destino do link;

18.4.6. A vista [FormCalculImpot]

Esta vista fornece o formulário de cálculo de impostos:

Image

O seu código é o seguinte:


  <!-- définition HTML de la vue -->
  <template>
  <!-- formulaire HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- message sur 12 colonnes sur fond bleu -->
    <b-alert show variant="primary">
      <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
    </b-alert>
    <!-- éléments du formulaire -->
    <!-- première ligne -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?" label-cols="4">
      <!-- boutons radio sur 5 colonnes-->
      <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>
    <!-- deuxième ligne -->
    <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"
      />
      <!-- message d'erreur éventuel -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- troisème ligne -->
    <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"
      />
      <!-- message d'erreur éventuel -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- quatrième ligne, bouton [submit] sur 5 colonnes -->
    <b-col cols="5">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>
 
<!-- script -->
<script>
export default {
  // inner state
  data() {
    return {
      // married or not
      marié: "non",
      // number of children
      enfants: "",
      // annual salary
      salaire: ""
    };
  },
  // calculated internal state
  computed: {
    // form validation
    formInvalide() {
      return (
        // disabled salary
        !this.salaireValide ||
        // or disabled children
        !this.enfantsValide ||
        // or tax data not obtained
        !this.$métier.taxAdminData
      );
    },
    // salary validation
    salaireValide() {
      // must be numeric >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/));
    },
    // child validation
    enfantsValide() {
      // must be numeric >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/));
    }
  },
  // event manager
  methods: {
    calculerImpot() {
      // tax is calculated using the [business] layer
      const résultat = this.$métier.calculerImpot(
        this.marié,
        this.enfants,
        this.salaire
      );
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // complete the result
      résultat.marié = this.marié;
      résultat.enfants = this.enfants;
      résultat.salaire = this.salaire;
      // the [resultatObtenu] event is issued
      this.$emit("resultatObtenu", résultat);
    }
  }
};
</script>

Comentários

  • linhas 4–51: o formulário Bootstrap;
  • linhas 11–17: um grupo de botões de opção com os respetivos rótulos;
  • linhas 14–15: a tag <b-form-radio> exibe um botão de opção:
    • linha 14: a diretiva [v-model] garante que, quando o botão for clicado, o atributo [married] na linha 61 será definido como [yes] (atributo [value="yes"]);
    • linha 15: a diretiva [v-model] garante que, quando o botão for clicado, o atributo [married] na linha 61 será definido como [no] (atributo [value="no"]);
  • linhas 19–29: a secção para introduzir o número de filhos:
    • linha 24: a entrada para o número de filhos está ligada ao atributo [children] na linha 63;
    • linha 25: a validade da entrada é verificada pelo atributo calculado [validChildren] nas linhas 87–89;
    • linha 28: garante que seja exibida uma mensagem de erro se a entrada for inválida;
  • linhas 31–45: a secção de introdução do salário anual:
    • linha 35: exibe uma mensagem de ajuda logo abaixo do campo de entrada;
    • linha 41: a entrada do salário está ligada ao atributo [salary] na linha 65;
    • linha 42: a validade da entrada é verificada pelo atributo calculado [validSalary] nas linhas 82–85;
    • linha 45: exibe uma mensagem de erro se a entrada for inválida;
  • linhas 48–50: um botão [submit]. Quando este botão é clicado ou quando uma entrada é validada com a tecla [Enter], o método [calculateTax] é executado (linha 94);
    • Linha 49: O estado ativo/inativo do botão é controlado pelo atributo calculado [formInvalid] nas linhas 71–80;
  • Linhas 71–80: O formulário é válido se:
    • o número de filhos for válido;
    • o salário for válido;
    • a aplicação tiver obtido os dados da administração fiscal do servidor para calcular o imposto. Note-se que estes dados estão armazenados na propriedade [$métier.taxAdminData]. A vista [FormCalculImpot] pode ser apresentada antes de estes dados terem sido obtidos, uma vez que são solicitados de forma assíncrona ao mesmo tempo que a vista está a ser apresentada. Aqui, garantimos que o utilizador não pode clicar no botão [Validate] até que os dados tenham sido recuperados;
  • linhas 94–109: o método de cálculo do imposto:
    • linhas 96–100: a camada [business] realiza este cálculo. Trata-se de um cálculo síncrono. Assim que o [taxAdminData] for recuperado, o cliente [View] já não precisa de comunicar com o servidor. Tudo é feito localmente. Obtemos um objeto [result] com as propriedades [tax, discount, surcharge, reduction, rate];
    • linhas 104–106: as propriedades [casado, filhos, salário] são adicionadas ao resultado;
    • linha 108: o resultado é passado para a vista pai [CalculImpot] através do evento [resultatObtenu]. Esta vista é responsável por apresentar o resultado;

18.4.7. A vista [SimulationList]

A vista [SimulationList] exibe a lista de simulações realizadas pelo utilizador:

Image

O código da vista é o seguinte:


<!-- définition HTML de la vue -->
<template>
  <div>
    <!-- mise en page -->
    <Layout :left="true" :right="true">
      <!-- simulations dans colonne de droite -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- pas de simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- il y a des simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tableau des simulations -->
          <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>
      <!-- menu de navigation dans colonne de gauche -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>
 
<script>
  // imports
  import Layout from "./Layout";
  import Menu from "./Menu";
  export default {
    // composants
    components: {
      Layout,
      Menu
    },
    // état interne
    data() {
      return {
        // options du menu de navigation
        options: [
          {
            text: "Calcul de l'impôt",
            path: "/calcul-impot"
          },
          {
            text: "Fin de session",
            path: "/fin-session"
          }
        ],
        // paramètres de la table 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" }
        ]
      };
    },
    // é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);
      }
    }
  };
</script>

Comentários

  • linha 5: a vista ocupa ambas as colunas do [Layout] para vistas encaminhadas;
  • linhas 7–26: as simulações vão na coluna da direita;
  • linha 28: o menu de navegação fica na coluna da esquerda;
  • linhas 8, 14, 20, 75: as simulações provêm do armazenamento [Vuex] [$this.store];
  • linhas 8–13: alerta exibido quando a lista de simulações está vazia;
  • linhas 14–25: a tabela HTML é exibida quando a lista de simulações não está vazia;
  • linhas 20–24: a tabela HTML é gerada por uma tag <b-table>;
    • linha 20: a tabela de simulações é fornecida pelo atributo calculado [simulations] das linhas 74–76;
    • linha 20: a tabela HTML é configurada pelo atributo calculado [fields] nas linhas 58–69. Linha 67: a coluna da chave [action] é a última coluna da tabela HTML;
    • Linhas 21–23: modelo para a última coluna da tabela HTML;
    • linha 22: um botão de link é colocado aqui. Quando clicado, o método [deleteSimulation(data.index)] é chamado, onde [data] representa a linha atual (linha 21). [data.index] representa o número desta linha na lista de linhas exibidas;
  • linha 28: geração do menu de navegação. As suas opções são fornecidas pelo atributo [options] nas linhas 47–56;
  • linhas 80–85: o método que responde a um clique no link [Delete] na página HTML;
    • linha 84: o método [deleteSimulation] do armazenamento [Vuex] é chamado (ver secção |vuejs-15|);

18.5. Executar o projeto

Image

Deve também iniciar o servidor [Laragon] (consulte o documento |https://tahe.developpez.com/tutoriels-cours/php7|) para que o servidor de cálculo de impostos fique online.

18.6. Implantação da aplicação num servidor local

Atualmente, o nosso cliente [Vue] está implementado num servidor de teste na URL [http://localhost:8080]. Iremos implementá-lo no servidor [Laragon] na URL [http://localhost:80]. Existem vários passos a seguir para chegar lá.

Passo 1

Primeiro, vamos garantir que o cliente [Vue] está implementado no servidor de teste na URL [http://localhost:8080/client-vuejs-impot/].

Criamos um ficheiro [vue.config.js] na raiz do nosso projeto atual do [VSCode]:

Image

O ficheiro [vue.config.js] [1] terá o seguinte conteúdo:


// vue.config.js
module.exports = {
  // l'URL de service du client [vuejs] du serveur de calcul de l'impôt
  publicPath: '/client-vuejs-impot/'
}

Também precisamos de modificar o ficheiro de encaminhamento [router.js] [2]:


// 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'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  {
    path: '/', name: 'authentification', component: Authentification
  },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession', component: Authentification
  }
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des routes dans le navigateur
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// export du router
export default router
  • linha 39: indicamos ao router que os caminhos das rotas definidas nas linhas 13–30 são relativos ao caminho definido na linha 39. Por exemplo, o caminho na linha 20 [/calcul-impot] passará a ser [/client-vuejs-impot/calcul-impot];

Podemos então testar novamente o projeto [vuejs-20] para verificar a alteração nos caminhos da aplicação:

Image

Passo 2

Vamos agora compilar a versão de produção do projeto [vuejs-20]:

Image

  • Em [1-2], configuramos a tarefa [build] [2] no ficheiro [package.json] [1];
  • Em [3-5], executamos esta tarefa. Ela irá compilar a versão de produção do projeto [vuejs-20];

A tarefa [build] é executada num terminal [VSCode]:

Image

Image

  • em [3-6], os avisos indicam que o código gerado é demasiado grande e deve ser dividido [8]. Isto está relacionado com a otimização da arquitetura do código, que não abordaremos aqui;
  • Em [7], é-nos indicado que a pasta [dist] contém a versão de produção gerada:

Image

  • Em [3], o ficheiro [index.html] é aquele que será utilizado quando a URL [https://localhost:80/client-vue-js-impot/] for solicitada;

Aqui temos um site estático que pode ser implementado em qualquer servidor. Iremos implementá-lo no servidor Laragon local (ver documento |https://tahe.developpez.com/tutoriels-cours/php7|). A pasta [dist] [2] é copiada para a pasta [<laragon>/www] [4], onde <laragon> é a pasta de instalação do servidor Laragon. Renomeamos esta pasta para [client-vuejs-impot] [5], uma vez que configurámos a versão de produção para ser executada na URL [/client-vuejs-impot/].

Passo 3

Adicionamos o seguinte ficheiro [.htaccess] à pasta [client-vuejs-import] que acabámos de criar:


<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 ficheiro é um ficheiro de configuração do servidor web Apache. Se não o incluirmos e solicitarmos diretamente a URL [https://localhost/client-vuejs-impot/calcul-impot], sem passar primeiro pela URL [https://localhost/client-vuejs-impot/], obtemos um erro 404. Com este ficheiro, conseguimos aceder com sucesso à vista [CalculImpot].

Depois de fazer isso, inicie o servidor Laragon, caso ainda não o tenha feito, e aceda à URL [https://localhost/client-vuejs-impot/]:

Image

Convidamos os leitores a testar a versão de produção da nossa aplicação.

Podemos modificar o servidor de cálculo de impostos num aspeto: os cabeçalhos CORS que este envia sistematicamente aos seus clientes. Isto era necessário para a versão cliente executada a partir do domínio [localhost:8080]. Agora que tanto o cliente como o servidor estão a ser executados no domínio [localhost:80], os cabeçalhos CORS já não são necessários.

Modificamos o ficheiro [config.json] para a versão 14 do servidor:

Image

  • em [4], especificamos que as solicitações CORS são agora rejeitadas;

Vamos guardar esta alteração e solicitar novamente a URL [https://localhost/client-vuejs-impot/]. Deverá continuar a funcionar.

18.7. Tratamento de URLs manuais

Em vez de utilizar os links do menu de navegação, o utilizador pode querer digitar manualmente as URLs da aplicação na barra de endereços do navegador. Por exemplo, vamos solicitar a URL [https://client-vuejs-impot/calcul-impot] sem passar pela página de autenticação. Um hacker certamente tentaria isso. Obtemos a seguinte visualização:

Image

De facto, obtemos a vista de cálculo de impostos. Agora, vamos tentar preencher os campos de entrada e enviá-los:

Image

Descobrimos então que o botão [1] [Submit] permanece desativado, mesmo que as entradas estejam corretas. Vejamos o código da vista [FormCalculImpot]:


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

Na linha 2, vemos que o seu estado ativo/inativo depende da propriedade [formInvalide]. Esta é a seguinte propriedade calculada:


formInvalide() {
      return (
        // salaire invalide
        !this.salaireValide ||
        // ou enfants invalide
        !this.enfantsValide ||
        // ou données fiscales pas obtenues
        !this.$métier.taxAdminData
      );
},

A linha 8 mostra que, para o formulário ser válido, os dados fiscais devem ter sido obtidos. No entanto, estes dados são obtidos durante a validação da vista [Authentication], que o utilizador «ignorou». Por conseguinte, não poderá enviar o formulário. Se tivesse conseguido fazê-lo, teria recebido uma mensagem de erro do servidor a indicar que não estava autenticado. A validação deve ser sempre realizada no lado do servidor. A validação do lado do navegador pode sempre ser contornada. Basta um cliente como o [Postman] que envie pedidos em bruto para o servidor.

Agora, vamos solicitar a URL [https://localhost/client-vuejs-impot/liste-des-simulations]. Obtemos a seguinte visualização:

Image

Agora a URL [https://localhost/client-vuejs-impot/fin-session]. Obtemos a seguinte visualização:

Image

Agora, uma página que não existe [https://localhost/client-vuejs-impot/abcd]:

Image

A nossa aplicação lida muito bem com URLs digitadas manualmente. Quando estas são chamadas, o router da aplicação reconhece-as. É, portanto, possível intervir antes de a vista ser finalmente apresentada. Veremos isto no projeto [vuejs-21].

Outro ponto a considerar é o seguinte. Imaginemos que o utilizador executou algumas simulações de acordo com as regras:

Image

Agora, vamos atualizar a página premindo F5:

Image

Fizemos algo que não é recomendado: digitar o URL manualmente (pressionar F5 é essencialmente a mesma coisa). Como resultado, perdemos as nossas simulações.

O projeto seguinte [vuejs-21] tem como objetivo introduzir duas melhorias:

  • validar as URLs introduzidas pelo utilizador;
  • manter o estado da aplicação mesmo que o utilizador digite um URL. Acima, vemos que perdemos a lista de simulações;