Skip to content

19. Melhorias no cliente Vue.js

19.1. Introduction

Vamos testar o projeto [vuejs-21] no servidor de desenvolvimento. Portanto, precisaremos novamente que o servidor envie os cabeçalhos CORS. Portanto, é necessário que o arquivo [config.json] da versão 14 do servidor de cálculo de impostos permita esses cabeçalhos:

Image

O projeto [vuejs-21] é criado inicialmente por meio da duplicação do projeto [vuejs-20]. Em seguida, ele é alterado para [3].

Surgem novos arquivos:

  • [session.js]: exporta um objeto [session] que irá encapsular informações sobre a sessão atual;
  • [pluginSession]: disponibiliza o objeto [session] anterior na propriedade [$session] das visualizações;
  • [NotFound.vue]: uma nova visualização exibida quando o usuário solicita manualmente um URL que não existe;

Serão modificados os seguintes arquivos:

  • [main.js]: inicializará a sessão atual e, quando o usuário digitar manualmente um URL, irá restaurá-la;
  • [router.js]: são adicionadas verificações para lidar com o caso de URL digitados pelo usuário;
  • [store.js]: uma nova alteração é adicionada;
  • [config.js]: uma nova configuração é adicionada;
  • diferentes visualizações, essencialmente para salvar a sessão atual em momentos-chave do ciclo de vida do aplicativo. Essa sessão é então restaurada sempre que o usuário digita manualmente os códigos URL;

19.2. O store [Vuex]

O script [./store] sofre as seguintes alterações:


// plug-in Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// loja Vuex
const store = new Vuex.Store({
  state: {
    // a tabela de simulações
    simulations: [],
    // o número da última simulação
    idSimulation: 0
  },
  mutations: {
    // exclusão da linha nº de índice
    deleteSimulation(state, index) {
      ...
    },
    // adição de uma simulação
    addSimulation(state, simulation) {
      ...
    },
    // limpeza do estado
    clear(state) {
      // sem mais simulações
      state.simulations = [];
      // a numeração das simulações recomeça do 0
      state.idSimulation = 0;
    }
  }
});
// exportação do objeto [store]
export default store;
  • linhas 24-29: a alteração [clear] exclui a lista de simulações registradas e zera o número da última simulação.

19.3. A sessão

A necessidade de uma sessão decorre do fato de que, quando o usuário digita um URL no campo de endereço do navegador, o script [main.js] é executado novamente. Ora, este contém a instrução:


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

Essa instrução importa o seguinte arquivo [./store]:


// plug-in Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);

// armazenamento Vuex
const store = new Vuex.Store({
  state: {
    // a tabela de simulações
    simulations: [],
    // o número da última simulação
    idSimulation: 0
  },
  mutations: {
    ...
  }
});
// exportação do objeto [store]
export default store;

Vemos, nas linhas 7 a 13, que é importada uma matriz de simulações vazia. Portanto, se houvesse simulações antes de o usuário digitar um URL no campo de endereço do navegador, depois não haveria mais. A ideia é:

  • utilizar uma sessão que armazene as informações que queremos conservar caso o usuário digite manualmente URL;
  • salvá-la em momentos-chave do aplicativo;
  • restaurá-la em [main.js], que sempre é executado quando um URL é digitado manualmente;

O script [./session] é o seguinte:


// importamos o store Vuex
import store from './store'
// importa-se a configuração
import config from './config';

// o objeto [session]
const session = {
  // sessão iniciada
  started: false,
  // autenticação
  authenticated: false,
  // hora do salvamento
  saveTime: "",
  // camada [métier]
  métier: null,
  // estado Vuex
  state: null,

  // salvamento da sessão em uma string jSON
  save() {
    // adiciona-se algumas propriedades à sessão
    this.saveTime = Date.now();
    this.state = store.state;
    // transforma-se em jSON
    const json = JSON.stringify(this);
    // armazenando-a no navegador
    localStorage.setItem("session", json);
    // eslint-disable-next-line no-console
    console.log("session save", json);
  },

  // restauração da sessão
  restore() {
    // recuperamos a sessão jSON do navegador
    const json = localStorage.getItem("session")
    // se tiver sido recuperado algo
    if (json) {
      // restauram-se todas as chaves da sessão
      const restore = JSON.parse(json);
      for (var key in restore) {
        if (restore.hasOwnProperty(key)) {
          this[key] = restore[key];
        }
      }
      // se tiver passado um determinado tempo de inatividade desde o início da sessão, recomeça do zero
      let durée = Date.now() - this.saveTime;
      if (durée > config.duréeSession) {
        // esvaziamos a sessão — ela também será salva
        session.clear();
      } else {
        // regenera-se o store do Vuex
        store.replaceState(JSON.parse(JSON.stringify(this.state)));
      }
    }
    // eslint-disable-next-line no-console
    console.log("session restore", this);
  },

    // limpa-se a sessão
  clear() {
    // eslint-disable-next-line no-console
    console.log("session clear");
    // zerando alguns campos da sessão
    this.authenticated = false;
    this.saveTime = "";
    this.started = false;
    if (this.métier) {
      // reinicializa-se o campo [taxAdminData]
      this.métier.taxAdminData = null;
    }
    // o store Vuex também é limpo
    store.commit("clear");
    // a nova sessão é salva
    this.save();
  },
}

// exportação do objeto [session]
export default session;

Comentários

  • linha 2: a sessão também encapsulará o store [Vuex] (lista de simulações, número da última simulação realizada);
  • linhas 7-17: as informações mantidas pela sessão:
    • [started]: a sessão jSON com o servidor foi iniciada ou não;
    • [authenticated]: se o usuário se autenticou ou não;
    • [saveTime]: a data, em milissegundos, do último salvamento;
    • [métier]: uma referência à camada [métier]. Esta contém o dado [taxAdminData], que permite o cálculo do imposto;
    • [state]: o estado do armazenamento [Vuex] (lista de simulações, número da última simulação realizada);
  • linhas 20-30: o método [save] salva a sessão localmente no navegador que está executando o aplicativo;
    • linha 22: registra-se a hora do salvamento;
    • linha 23: recupera-se o [state] da loja [Vuex];
    • linha 25: cria-se a string jSON da sessão;
    • linha 27: é armazenada localmente no navegador, associada à chave [session];
  • linhas 33-57: o método [restore] permite restaurar uma sessão a partir de seu backup local no navegador;
    • linha 35: recupera-se o backup local jSON;
    • linha 37: se algo tiver sido recuperado;
    • linhas 39-44: o objeto [session] é reconstituído;
    • linha 46: calcula-se o tempo decorrido desde o último backup;
    • linhas 47-50: se esse tempo for superior a um valor [config.duréeSession] definido na configuração, a sessão é reinicializada (linha 49) e, nessa ocasião, é gravada;
    • linha 52: caso contrário, regenera-se o atributo [state] do armazenamento [Vuex];
  • linhas 60-75: o método [clear] reinicializa a sessão;
    • linhas 64-70: as propriedades da sessão são reinicializadas para seus valores iniciais;
    • linha 72: assim como o armazenamento [Vuex];
    • linha 74: a nova sessão é salva;

19.4. O arquivo de configuração [config]

O arquivo [./config] sofre as seguintes alterações:


// uso da biblioteca [axios]
const axios = require('axios');
// tempo limite das solicitações HTTP
axios.defaults.timeout = 2000;
...

// exportação da configuração
export default {
  // objeto [axios]
  axios: axios,
  // tempo máximo de inatividade da sessão: 5 min = 300 s = 300.000 ms
  duréeSession: 300000
}
  • linha 12: vamos gerenciar a sessão do aplicativo de maneira semelhante à de uma sessão da web. Aqui, definimos um tempo máximo de inatividade de 5 minutos;

19.5. O plugin [pluginSession]

Como já foi feito várias vezes, o plug-in [pluginSession] permitirá que as visualizações tenham acesso à sessão por meio da propriedade [this.$session]:


export default {
  install(Vue, session) {
    // adiciona uma propriedade [$session] à classe da visualização
    Object.defineProperty(Vue.prototype, '$session', {
      // quando Vue.$session é referenciado, o segundo parâmetro é definido como [session]
      get: () => session,
    })
  }
}

19.6. O script principal [main]

O script principal [./main.js] sofre as seguintes alterações:


// log de inicialização
// eslint-disable-next-line no-console
console.log("main started");

// importações
import Vue from 'vue'

...

// instanciação da camada [métier]
import Métier from './couches/Métier';
const métier = new Métier();

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

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

// sessão
import session from './session';
import pluginSession from './plugins/pluginSession'
Vue.use(pluginSession, session)

// a sessão é restaurada antes da reinicialização
session.restore();

// restaurando a camada [métier]
if (session.métier && session.métier.taxAdminData) {
  métier.setTaxAdminData(session.métier.taxAdminData);
}

// inicialização do UI
new Vue({
  el: '#aplicativo,
  // o roteador
  router: router,
  // a persiana Vuex
  store: store,
  // a tela principal
  render: h => h(Main),
})

// log de encerramento
// eslint-disable-next-line no-console
console.log("main terminated, session=", session);
  • linha 19: importa-se a sessão;
  • linha 20: importa-se seu plugin;
  • linha 21: o plug-in [pluginSession] é integrado ao [Vue]. Após essa instrução, todas as visualizações passam a dispor da sessão em seu atributo [$session];
  • linha 27: a sessão é restaurada. A sessão importada na linha 11 é então inicializada com o conteúdo de seu último salvamento;
  • após a linha 16, as visualizações passam a dispor de uma propriedade [$métier] inicializada na linha 12. Essa propriedade não contém a informação [taxAdminData], necessária para calcular o imposto;
  • linhas 30-32: se a restauração que acabou de ser feita restaurou a propriedade [session.métier.taxAdminData], então a propriedade [$métier] das visualizações é inicializada com esse valor;

19.7. O arquivo de roteamento [router]

O arquivo de roteamento [./router] sofre as seguintes alterações:


// importações
import Vue from 'vue'
import VueRouter from 'vue-router'
// as visualizações
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// a sessão
import session from './session'

// plug-in de roteamento
Vue.use(VueRouter)

// as rotas do aplicativo
const routes = [
  // autenticação
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification', component: Authentification },
  // cálculo do imposto
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // lista de simulações
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fim da sessão
  {
    path: '/fin-session', name: 'finSession'
  },
  // página desconhecida
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]

// o roteador
const router = new VueRouter({
  // as rotas
  routes,
  // o modo de exibição do URL
  mode: 'history',
  // o URL básico do aplicativo
  base: '/client-vuejs-impot/'
})

// verificação das rotas
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // rota reservada para usuários autenticados?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // passando para a autenticação
      name: 'authentification',
    })
    // retorno ao ciclo de eventos
    return;
  }
  // caso específico do fim da sessão
  if (to.name === "finSession") {
    // limpa-se a sessão
    session.clear();
    // navegando para a visualização [authentification]
    next({
      name: 'authentification',
    })
    // retorno ao ciclo de eventos
    return;
  }
  // outros casos — próxima visualização normal do roteamento
  next();
})

// exportação do roteador
export default router

Comentários

  • linhas 16-38: algumas rotas foram complementadas com informações adicionais;
  • linha 19: foi criada uma nova rota para acessar a vista [Authentification];
  • linhas 21-24: a rota que leva à visualização [CalculImpot] agora possui uma propriedade [meta] (esse nome é obrigatório). O conteúdo desse objeto pode ser qualquer um e é definido pelo desenvolvedor;
  • linha 23: insere-se em [meta] a propriedade [authenticated] (esse nome pode ser qualquer um). Isso significará, para nós, que, para acessar a visualização [CalculImpot], o usuário deve estar autenticado;
  • linhas 26-29: fazemos o mesmo para a rota que leva à visualização [ListeSimulations]. Nesse caso também, o usuário deve estar autenticado;
  • a propriedade [meta.authenticated] nos permitirá verificar que um usuário que digitar manualmente os URL das visualizações [CalculImpot, ListeSimulations] não poderá obtê-los se não estiver autenticado;
  • linhas 51-76: o método [beforeEach] é executado antes que uma visualização seja encaminhada. Esse é o momento certo para realizar verificações;
    • [to]: a próxima rota caso não seja realizada nenhuma ação;
    • [from]: a última rota exibida;
    • [next]: função que permite alterar a próxima rota exibida;
  • linha 55: verifica-se se a próxima rota exige que o usuário esteja autenticado;
  • linhas 56-59: se sim e o usuário não estiver autenticado, altera-se a próxima rota para a visualização [Authentification];
  • linhas 64-73: trata-se do caso específico da rota [finSession] das linhas 30-32. Essa rota não possui uma visualização associada;
    • linha 66: reinicializa-se a sessão para seu valor inicial;
    • linhas 68-70: programa-se a visualização [Authentification] como próxima visualização;
  • linha 75: se não estivermos nos dois casos anteriores, simplesmente passamos para a rota prevista pelo arquivo de roteamento;
  • linhas 35-37: prevê-se uma vista [NotFound] caso a rota digitada pelo usuário não corresponda a nenhuma rota conhecida. Essa vista é importada na linha 8. As rotas são verificadas na ordem do arquivo de roteamento. Portanto, se chegarmos à linha 36, é porque a rota solicitada não é nenhuma das rotas das linhas 18 a 33;

19.8. A visualização [NotFound]

A visualização [NotFound] é exibida se a rota digitada pelo usuário não corresponder a nenhuma rota conhecida:

Image

O código da visualização é o seguinte:


<!-- definição HTML da visualização -->
<template>
  <!-- formatação -->
  <Layout :left="true" :right="true">
    <!-- alerta na coluna da direita -->
    <template slot="right">
      <!-- mensagem em fundo amarelo -->
      <b-alert show variant="danger" align="center">
        <h4>Cette page n'existe pas</h4>
      </b-alert>
    </template>
    <!-- menu de navegação na coluna da esquerda -->
    <Menu slot="left" :options="options" />
  </Layout>
</template>

<script>
// importações
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // componentes
  components: {
    Layout,
    Menu
  },
  // estado interno do componente
  data() {
    return {
      // opções do menu de navegação
      options: [
        {
          text: "Authentification",
          path: "/"
        }
      ]
    };
  },
  // ciclo de vida
  created() {
    // eslint-desativar-linha-seguinte
    console.log("NotFound created");
    // analisamos quais opções de menu oferecer
    if (this.$session.authenticated && this.$métier.taxAdminData) {
      // o usuário pode realizar simulações
      Array.prototype.push.apply(this.options, [
        {
          text: "Calcul de l'impôt",
          path: "/calcul-impot"
        },
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        }
      ]);
    }
  }
};
</script>

Comentários

  • linha 4: utiliza as duas colunas das visualizações roteadas;
  • linhas 6-11: uma mensagem de erro;
  • linha 13: o menu de navegação ocupa a coluna da esquerda;
  • linhas 31-36: as opções padrão do menu;
  • linhas 40-57: código executado quando a visualização é criada;
  • linha 44: verifica-se se o usuário pode realizar simulações;
  • linhas 45-55: se sim, adicionam-se duas opções ao menu de navegação, aquelas que exigem autenticação e uma camada [métier] operacional (linhas 46-55);

19.9. A visualização [Authentification]

A visualização [Authentification] evolui da seguinte forma:


<!-- definição HTML da visualização -->
<template>
  <Layout :left="false" :right="true">
    ...
  </Layout>
</template>

<!-- dinâmica da visualização -->
<script>
import Layout from "./Layout";
export default {
  // estado do componente
  data() {
    return {
      // usuário
      user: "",
      // sua senha
      password: "",
      // controla a exibição de uma mensagem de erro
      showError: false,
      // a mensagem de erro
      message: ""
    };
  },

  // componentes utilizados
  components: {
    Layout
  },

  // propriedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.$session.started;
    }
  },

  // gerenciadores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      try {
        // início da espera
        this.$emit("loading", true);
        // ainda não estamos autenticados
        this.$session.authenticated = false;
        // autenticação bloqueada pelo servidor
        const response = await this.$dao.authentifierUtilisateur(
          this.user,
          this.password
        );
        // fim do carregamento
        this.$emit("loading", false);
        // análise da resposta do servidor
        if (response.état != 200) {
          // exibindo o erro
          this.message = response.réponse;
          this.showError = true;
          // retorno ao ciclo de eventos
          return;
        }
        // sem erro
        this.showError = false;
        // autenticação realizada
        this.$session.authenticated = true;
        // --------- agora solicitamos os dados da administração fiscal
        // inicialmente, sem dados
        this.$métier.setTaxAdminData(null);
        // início da espera
        this.$emit("loading", true);
        // solicitação em espera no servidor
        const response2 = await this.$dao.getAdminData();
        // fim do carregamento
        this.$emit("loading", false);
        // análise da resposta
        if (response2.état != 1000) {
          // exibição do erro
          this.message = response2.réponse;
          this.showError = true;
          // retorno ao ciclo de eventos
          return;
        }
        // sem erro
        this.showError = false;
        // os dados recebidos são armazenados na camada [métier]
        this.$métier.setTaxAdminData(response2.réponse);
        // é possível prosseguir para o cálculo do imposto
        this.$router.push({ name: "calculImpot" });
      } catch (error) {
        // o erro é repassado ao componente principal
        this.$emit("error", error);
      } finally {
        // atualização da sessão
        this.$session.métier = this.$métier;
        // a sessão é salva
        this.$session.save();
      }
    }
  },
  // ciclo de vida: o componente acaba de ser criado
  created() {
    // eslint-disable-next-line
    console.log("Authentification created");
    // o usuário pode realizar simulações?
    if (
      this.$session.started &&
      this.$session.authenticated &&
      this.$métier.taxAdminData
    ) {
      // então o usuário pode realizar simulações
      this.$router.push({ name: "calculImpot" });
      // retorno ao ciclo de eventos
      return;
    }
    // se a sessão jSON já tiver sido iniciada, ela não será reiniciada
    if (!this.$session.started) {
      // início da espera
      this.$emit("loading", true);
      // inicializa-se a sessão com o servidor — solicitação assíncrona
      // utiliza-se a promessa retornada pelos métodos da camada [dao]
      this.$dao
        // inicializando uma sessão jSON
        .initSession()
        // a resposta foi obtida
        .then(response => {
          // fim da espera
          this.$emit("loading", false);
          // análise da resposta
          if (response.état != 700) {
            // exibindo o erro
            this.message = response.réponse;
            this.showError = true;
            // retorno ao ciclo de eventos
            return;
          }
          // a sessão foi iniciada
          this.$session.started = true;
        })
        // em caso de erro
        .catch(error => {
          // o erro é encaminhado para a visualização [Main]
          this.$emit("error", error);
        })
        // em todos os casos
        .finally(() => {
          // a sessão é salva
          this.$session.save();
        });
    }
  }
};
</script>

Comentários

  • destacamos em amarelo as instruções que utilizam a sessão inserida nesta versão do cliente [Vue.js];
  • linhas 97, 148: ao final dos métodos [login, created], a sessão é salva independentemente do resultado das consultas HTTP que ocorrem nesses métodos (cláusula [finally] em ambos os casos);
  • o método [created] das linhas 102-150 é executado sempre que a visualização [Authentification] é criada. Se foi o usuário quem digitou o URL da visualização, a sessão nos permitirá saber o que fazer;
  • linhas 106-115: se a sessão jSON estiver iniciada, o usuário autenticado e os dados [this.$métier.taxAdminData] inicializados, então o usuário pode acessar diretamente o formulário de cálculo do imposto (linha 112);
  • linha 117: o método [created] era utilizado na versão anterior para inicializar uma sessão jSON com o servidor. Essa etapa é desnecessária se já tiver ocorrido;
  • linhas 42-66: o método de autenticação;
  • linha 66: se a autenticação for bem-sucedida, isso é registrado na sessão;
  • linhas 67-92: a solicitação ao servidor dos dados da administração fiscal [taxAdminData];
  • linha 95: ao final desta fase, atualiza-se a propriedade [métier] da sessão, independentemente de a operação ter sido bem-sucedida ou não;

19.10. A visualização [CalculImpot]

O código da visualização [CalculImpot] é alterado da seguinte forma:


<!-- definição HTML da visualização -->
<template>
  ...
</template>

<script>
// importações
import FormCalculImpot from "./FormCalculImpot";
import Menu from "./Menu";
import Layout from "./Layout";

export default {
  // estado interno
  data() {
    return {
      // opções do menu
      options: [
        {
          text: "Liste des simulations",
          path: "/liste-des-simulations"
        },
        {
          text: "Fin de session",
          path: "/fin-session"
        }
      ],
      // resultado do cálculo do imposto
      résultat: "",
      résultatObtenu: false
    };
  },
  // componentes utilizados
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // métodos de gerenciamento de eventos
  methods: {
    // resultado do cálculo do imposto
    handleResultatObtenu(résultat) {
      // o resultado é construído em cadeia HTML
      ...
      // uma simulação de +
      this.$store.commit("addSimulation", résultat);
      // a sessão é salva
      this.$session.save();
    }
  },
  // ciclo de vida
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  }
};
</script>

Comentários

  • linha 45: a simulação calculada é adicionada ao store [Vuex]. Isso afeta a sessão, que inclui a propriedade [state] do store. Por isso, salvamos a sessão (linha 47);
  • linha 51: cria-se um método [created] para acompanhar, nos logs, a criação das visualizações;

19.11. A visualização [ListeSimulations]

A visão [ListeSimulations] sofre as seguintes alterações:


<!-- definição HTML da visualização -->
<template>
  ...
  </div>
</template>

<script>
// importações
import Layout from "./Layout";
import Menu from "./Menu";
export default {
  // componentes
  components: {
    Layout,
    Menu
  },
  // estado interno
  data() {
    ...
  },
  // estado interno calculado
  computed: {
    // lista de simulações selecionadas no armazenamento Vuex
    simulations() {
      return this.$store.state.simulations;
    }
  },
  // métodos
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // exclusão da simulação nº [index]
      this.$store.commit("deleteSimulation", index);
      // salvamos a sessão
      this.$session.save();
    }
  },
  // ciclo de vida
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  }
};
</script>

Comentários

  • linha 36: após a exclusão de uma simulação na linha 34, salva-se a sessão para refletir essa mudança de estado;
  • linhas 40-43: continua-se acompanhando a criação das visualizações;

19.12. Execução do projeto

Image

Durante os testes, verifique os seguintes pontos:

  • se o usuário “utilizar” o aplicativo por meio dos links do menu de navegação e dos botões/links de ação, o aplicativo funciona;
  • se o usuário digitar manualmente URL, o aplicativo continua funcionando. Faça, em especial, o seguinte teste:
    • faça simulações;
    • uma vez na tela [ListeSimulations], atualize (F5) a tela. No aplicativo anterior, [vuejs-20], as simulações eram perdidas nesse momento. Aqui, isso não acontece: as simulações já realizadas são recuperadas;
  • verifique os logs para entender:
    • em que momento o script [main] é executado. Você deve perceber que ele é executado sempre que o usuário digita um URL manualmente;
    • em que momentos as visualizações são criadas. Você deve perceber que elas são criadas sempre que vão ser exibidas;
    • o funcionamento do roteamento. Antes de cada roteamento, é gerado um log que indica:
      • a rota de onde você vem;
      • a rota para onde você está indo;

19.13. Implantação do aplicativo em um servidor local

Como exercício, siga o parágrafo |Implantação em um servidor local| para implantar o projeto [vuejs-21] no servidor Laragon local. Em seguida, teste-o.

19.14. Ajustes na versão móvel

Teoricamente, o uso do Bootstrap deveria nos permitir ter um aplicativo utilizável em diferentes dispositivos: smartphones, tablets, laptops e computadores de mesa. O que diferencia esses dispositivos é o tamanho de suas telas.

Se testarmos a versão [vuejs-21] em um celular, perceberemos que a exibição das visualizações fica um caos. A versão [vuejs-22] corrige esse problema. Todas as alterações ocorreram nos modelos das visualizações. Elas consistiram essencialmente em ajustar a exibição para a tela de um smartphone. Quando isso está ajustado, a exibição em telas maiores ocorre de maneira fluida graças ao Bootstrap.

Image

19.14.1. A visualização [Main]

A visualização [Main] evolui da seguinte forma:


<!-- definição HTML da visualização -->
<template>
  <div class="container">
    <b-card>
      <!-- jumbotron -->
      <b-jumbotron>
        <b-row>
          <b-col sm="4">
            <img src="../assets/logo.jpg" alt="Cerisier en fleurs" />
          </b-col>
          <b-col sm="8">
            <h1>Calculez votre impôt</h1>
          </b-col>
        </b-row>
      </b-jumbotron>
      ....
    </b-card>
  </div>
</template>

Comentários

  • linha 8: onde antes havia [cols=’4’], escreve-se [sm=’4’]. [sm] significa [small]. As telas dos smartphones se enquadram nessa categoria. As outras categorias são [xs=extra small, md=medium, lg=large, xl=extra large];
  • linha 11: o mesmo;

19.14.2. A visualização [Layout]

A visualização [Layout] evolui da seguinte forma:


<!-- definição HTML do layout da visualização roteada -->
<template>
  <!-- linha -->
  <div>
    <b-row>
      <!-- área de três colunas à esquerda -->
      <b-col sm="3" v-if="left">
        <slot name="left" />
      </b-col>
      <!-- área de nove colunas à direita -->
      <b-col sm="9" v-if="right">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

19.14.3. A visualização [Authentification]

A visualização [Authentification] passa a ter a seguinte forma:


<!-- definição HTML da visualização -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulário HTML — os valores são enviados com a ação [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- título -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1ª linha -->
        <b-form-group label="Nom d'utilisateur" label-for="user" description="Tapez admin">
          <!-- campo de entrada do usuário -->
          <b-col sm="6">
            <b-form-input type="text" id="user" placeholder="Nom d'utilisateur" v-model="user" />
          </b-col>
        </b-form-group>
        <!-- 2ª linha -->
        <b-form-group label="Mot de passe" label-for="password" description="Tapez admin">
          <!-- campo de digitação de senha -->
          <b-col sm="6">
            <b-input type="password" id="password" placeholder="Mot de passe" v-model="password" />
          </b-col>
        </b-form-group>
        <!-- 3ª linha -->
        <b-alert
          show
          variant="danger"
          v-if="showError"
          class="mt-3"
        >L'erreur suivante s'est produite : {{message}}</b-alert>
        <!-- botão do tipo [submit] na terceira linha -->
        <b-row>
          <b-col sm="2">
            <b-button variant="primary" type="submit" :disabled="!valid">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

Comentários

  • linhas 11 e 19: foi removido o atributo [label-cols], que definia o número de colunas do rótulo do campo de entrada. Na ausência desse atributo, o rótulo fica acima do campo de entrada. Isso se adapta melhor às telas dos smartphones;

19.14.4. A visualização [CalculImpot]

A visualização [CalculImpot] sofreu as seguintes alterações:


<!-- definição HTML da visualização -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulário de cálculo do imposto à direita -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- menu de navegação à esquerda -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- área de exibição dos resultados do cálculo do imposto abaixo do formulário -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- área vazia com três colunas -->
      <b-col sm="3" />
      <!-- área de nove colunas -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

19.14.5. A visualização [FormCalculImpot]

A visualização [FormCalculImpot] passa a ter a seguinte forma:


<!-- definição HTML da visualização -->
  <template>
  <!-- formulário HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- mensagem em 12 colunas com fundo azul -->
    <b-row>
      <b-col sm="12">
        <b-alert show variant="primary">
          <h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
        </b-alert>
      </b-col>
    </b-row>
    <!-- elementos do formulário -->
    <!-- primeira linha -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- botões de opção em 5 colunas-->
      <b-col sm="5">
        <b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
        <b-form-radio v-model="marié" value="non">Non</b-form-radio>
      </b-col>
    </b-form-group>
    <!-- segunda linha -->
    <b-form-group label="Nombre d'enfants à charge" label-for="enfants">
      <b-form-input
        type="text"
        id="enfants"
        placeholder="Indiquez votre nombre d'enfants"
        v-model="enfants"
        :state="enfantsValide"
      ></b-form-input>
      <!-- possível mensagem de erro -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- terceira linha -->
    <b-form-group
      label="Salaire annuel net imposable"
      label-for="salaire"
      description="Arrondissez à l'euro inférieur"
    >
      <b-form-input
        type="text"
        id="salaire"
        placeholder="Salaire annuel"
        v-model="salaire"
        :state="salaireValide"
      ></b-form-input>
      <!-- possível mensagem de erro -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- quarta linha, botão [submit] -->
    <b-col sm="3">
      <b-button type="submit" variant="primary" :disabled="formInvalide">Valider</b-button>
    </b-col>
  </b-form>
</template>

Comentários

  • linhas 15, 23, 35: o atributo [label-cols] foi removido;

Além disso, estamos atualizando os testes de validade:


...
// estado interno calculado
  computed: {
    // validação do formulário
    formInvalide() {
      return (
        // salário inválido
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // ou filhos inválidos
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // ou dados fiscais não obtidos
        !this.$métier.taxAdminData
      );
    },
    // validação do salário
    salaireValide() {
      // deve ser um número >=0
      return Boolean(
        this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/)
      );
    },
    // validação de filhos
    enfantsValide() {
      // deve ser um valor numérico >=0
      return Boolean(
        this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/)
      );
    }
},
...

Comentários

  • linha 19: quando nada foi inserido, a entrada é considerada válida. Isso permite que a entrada seja válida quando a visualização é exibida inicialmente. Na versão anterior, a entrada aparecia inicialmente como incorreta;
  • linha 26: o mesmo;
  • linhas 5-14: o botão de validação só fica ativo se ambas as entradas contiverem algum valor e forem válidas;

19.14.6. A tela [Menu]

A visualização [Menu] sofre as seguintes alterações:


<!-- definição HTML da visualização -->
<template>
  <b-card class="mb-3">
    <!-- menu Bootstrap vertical -->
    <b-nav vertical>
      <!-- opções do menu -->
      <b-nav-item
        v-for="(option,index) of options"
        :key="index"
        :to="option.path"
        exact
        exact-active-class="active"
      >{{option.text}}</b-nav-item>
    </b-nav>
  </b-card>
</template>

Comentários

  • linha 3: adiciona-se a tag <b-card> para envolver o menu com uma borda fina. Isso permite localizar melhor o menu no smartphone;

19.14.7. A visualização [ListeSimulations]

A visualização [ListeSimulations] permanece inalterada:


<!-- definição HTML da visualização -->
<template>
  <div>
    <!-- formatação -->
    <Layout :left="true" :right="true">
      <!-- simulações na coluna da direita -->
      <template slot="right">
        <template v-if="simulations.length==0">
          <!-- sem simulações -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length!=0">
          <!-- há simulações -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tabela de simulações -->
          <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 navegação na coluna da esquerda -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

Comentários

  • linha 20: observe o atributo [responsive], que faz com que a exibição da tabela se adapte ao tamanho da tela:

Image

  • em [2], em telas pequenas, uma barra de rolagem horizontal permite visualizar a tabela;

19.14.8. A visualização [NotFound]

Permanece inalterada.

19.14.9. As visualizações em dispositivos móveis

Image

Image

Observação: certamente é possível obter visualizações ainda mais adaptadas para dispositivos móveis. Estou pensando, em particular, no menu de navegação, que poderia ser aprimorado, mas há outros pontos a serem considerados. O objetivo principal deste documento não era a criação de um aplicativo móvel. Nesse caso, talvez tivéssemos recorrido a um framework como o Ionic |https://ionicframework.com/|.