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:

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:

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

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

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

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

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

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:
- 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];
- 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:

- 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:

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:

- 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:

- [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:

- 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:

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:

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:

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

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]:

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:

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

- 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]:


- 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:

- 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>

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/]:

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:

- 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:

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

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:

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

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

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:

Agora, vamos atualizar a página premindo F5:

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;