Skip to content

17. Exemplo [nuxt-20]: adaptação do exemplo [vuejs-22]

17.1. Présentation

Propomos aqui portar o exemplo [vuejs-22], que era um aplicativo [vue.js] do tipo SPA, para um contexto [nuxt] SSR. [vuejs-22] era um aplicativo cliente do servidor de cálculo de impostos que apresentava as seguintes visualizações:

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

Image

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

Image

A terceira tela é aquela que exibe a lista de simulações realizadas pelo usuário:

Image

A tela acima mostra que é possível excluir a simulação nº 1. Obtém-se, então, a seguinte tela:

Image

Se excluirmos agora a última simulação, obtemos a seguinte nova tela:

Image

Vamos migrar o aplicativo [vuejs-22] para o aplicativo [nuxt-20] de forma gradual. Não explicaremos novamente os códigos do [vuejs-22]. Recomenda-se ao leitor que releia o documento |Introdução à estrutura VUE.JS por meio de um exemplo|. As diferentes etapas devem mostrar as diferenças entre um aplicativo [vuejs] e um aplicativo [nuxt].

17.2. Etapa 1

O projeto [nuxt-20] é obtido inicialmente por meio da cópia do projeto [nuxt-12]. Este, de fato, é um bom ponto de partida:

  • ele consegue se comunicar com o servidor de cálculo de impostos;
  • gerencia corretamente os erros que este envia;
  • o cliente e o servidor [nuxt] conseguem se comunicar por meio de uma sessão [nuxt];

Portanto, temos uma boa infraestrutura inicial. Nosso principal trabalho deve ser modificar:

  • as páginas. Usaremos as do projeto [vuejs-22], que precisarão ser adaptadas ao novo ambiente;
  • o gerenciamento do store. Informações adicionais devem aparecer (lista de simulações) e outras podem se tornar desnecessárias;
  • a gestão do roteamento do cliente e do servidor [nuxt];

Portanto, em primeiro lugar, criamos o projeto [nuxt-20] copiando o projeto [nuxt-12]:

Image

Em seguida, excluímos as páginas e os componentes que se tornaram desnecessários [2]:

  • o componente [components/navigation] desaparece;
  • o layout [layout/default] é removido;
  • as páginas [index, authentification, get-admindata, fin-session] desaparecem;

Em seguida, integramos no [nuxt-20] elementos do [vuejs-22] e do [3]:

  • as três páginas [Authentification, CalculImpot, ListeSimulations] do aplicativo [vuejs-22] vão para a pasta [pages];
  • os componentes [FormCalculImpot, Menu, Layout] do aplicativo [vuejs-22] vão para a pasta [components];
  • a página [Main] de [vuejs-22], que servia como [layout] para o aplicativo [vuejs-22], vai para a pasta [layouts];

Renomeamos os elementos integrados [4]:

Image

  • em [layouts], [Main] passou a ser [default], pois esse é o nome padrão do layout de um aplicativo [nuxt];
  • no [pages], a página [Authentification] passou a ser [index], pois [Authentification] desempenhava essa função no aplicativo [vuejs-22];

Nesse ponto, é possível compilar o projeto para identificar os primeiros erros. Modifica-se o arquivo [nuxt.config] do exemplo [nuxt-12] para que, a partir de agora, seja executado o [nuxt-20]:


export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: 'Introduction à [nuxt.js]',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: 'ssr routing loading asyncdata middleware plugins store'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: false,

  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    { src: '@/plugins/client/plgSession', mode: 'client' },
    { src: '@/plugins/server/plgSession', mode: 'server' },
    { src: '@/plugins/client/plgDao', mode: 'client' },
    { src: '@/plugins/server/plgDao', mode: 'server' },
    { src: '@/plugins/client/plgEventBus', mode: 'client' }
  ],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [
    // Doc: https://bootstrap-vue.js.org
    'bootstrap-vue/nuxt',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    // https://www.npmjs.com/package/cookie-universal-nuxt
    'cookie-universal-nuxt'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // diretório do código-fonte
  srcDir: 'nuxt-20',
  // roteador
  router: {
    // raiz dos URL do aplicativo
    base: '/nuxt-20/',
    // middleware de roteamento
    middleware: ['routing']
  },
  // servidor
  server: {
    // porta de serviço, 3000 por padrão
    port: 81,
    // endereços de rede monitorados, por padrão localhost: 127.0.0.1
    // 0.0.0.0 = todos os endereços de rede da máquina
    host: 'localhost'
  },
  // ambiente
  env: {
    // configuração do Axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/versão-14',
    // configuração do cookie de sessão [nuxt]
    maxAge: 60 * 5
  }
}

Em seguida, geramos um [build] do projeto:

Image

Os erros relatados são os seguintes:

1
2
3
4
5
6
7
Module not found: Error: Can't resolve '../assets/logo.jpg' @ ./nuxt-20/layouts/default.vue?...
Module not found: Error: Can't resolve './FormCalculImpot' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/liste-des-simulations...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/calcul-impot.vue
  • o erro na linha 1 indica que há uma referência a uma imagem inexistente. Ela será recuperada no [vuejs-22];
  • o erro na linha 2 mostra que o componente [./FormCalculImpot] não existe. De fato, esse componente agora está em [@/components/form-calcul-impot];
  • os erros nas linhas [3-5] mostram que o componente [./Layout] não existe. De fato, esse componente agora está em [@/components/layout];
  • os erros nas linhas [6-7] indicam que o componente [./Menu] não existe. De fato, ele agora se chama [@/components/menu];

Adicionamos a imagem [assets/logo.jpg] ao projeto [nuxt-20]:

Image

Além disso, em todas as páginas, corrigiremos o caminho dos componentes. Tomemos como exemplo a página [calcul-impot]:


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

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

export default {
  // componentes utilizados
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  

Os três [import] das linhas 26 a 28 passam a ser:


// importações
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'

Verifica-se e, se necessário, corrige-se assim os [import] de todos os componentes, layouts e páginas. Após essas correções, pode-se tentar um novo [build]. Normalmente, não há mais erros.

É possível, então, tentar uma execução:

Image

Aparecem erros:

1
2
3
4
5
6
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

c:\Data\st-2019\dev\nuxtjs\dvp\nuxt-20\pages\index.vue
   92:29  error  Expected '!==' and instead saw '!='  eqeqeq
  129:27  error  Expected '!==' and instead saw '!='  eqeqeq
150:28  error  Expected '!==' and instead saw '!='  eqeqeq

Percebe-se que o comando [dev], combinado com o módulo [eslint], é mais rigoroso, do ponto de vista sintático, do que o comando [build]. Nesse caso, ela exige que o operador de comparação [!=] seja escrito como [!==], que é um operador mais restritivo (ele também verifica o tipo dos operandos). Esses erros ocorrem na página [index.vue].

Corrigimos os erros acima e reiniciamos a execução do projeto. Recebemos então um aviso do módulo [eslint]:

c:\Data\st-2019\dev\nuxtjs\dvp\nuxt-20\components\menu.vue
14:5  warning  Prop 'options' requires default value to be set  vue/require-default-prop

Image

Corrigimos esse erro com o [Quick fix] do módulo [eslint] [2].

Reiniciamos a execução do projeto. Não há mais erros de compilação. Em seguida, acessamos o URL e o [http://localhost:81/nuxt-20/] em um navegador. Recebemos um erro de execução:

Image

O erro está em [index.vue] [2]. O erro [1] ocorre porque, em [vuejs-22], a camada [dao] estava disponível em [this.$dao], enquanto que em [nuxt-12], cuja infraestrutura adotamos, ela está disponível na função [this.$dao()].

O erro está na função [created] do ciclo de vida da página [index]:

Image

Por enquanto, basta renomear [created] para [created2], para que a função do ciclo de vida [created] não seja executada ([3]).

Salvamos a alteração e recarregamos a página [index] no navegador. Desta vez deu certo:

Image

17.3. etapa 2

As páginas do projeto [vuejs-22] utilizavam os seguintes elementos injetados:

  • $dao: para a camada [dao] do cliente [vue.js];
  • $session: para uma sessão armazenada no [localStorage] do navegador;

Esses elementos não existem mais na infraestrutura do projeto [nuxt-12] que copiamos:

  • agora há duas camadas [dao], uma para o cliente [nuxt] e outra para o servidor [nuxt]. Ambas estão disponíveis por meio de uma função injetada chamada [$dao]. Isso significa que, nas páginas do aplicativo, [this.$dao] deve ser substituído por [this.$dao()];
  • a sessão [nuxt] gerenciada pelo aplicativo [nuxt-20] não tem mais nada a ver com o objeto [$session] doaplicativo [vuejs-22], no qual não havia o conceito de cookie de sessão. No entanto, elas têm uma funcionalidade semelhante: armazenar informações persistentes ao longo das ações do usuário. A sessão [nuxt] armazena as informações no store, em vez de diretamente na sessão. Nas páginas do aplicativo, [this.$session] deve ser substituído por [this.$store] quando se trata de armazenar informações na sessão e por [this.$session()] quando se trata de manipular a própria sessão;
  • para verificar o estado de uma propriedade P do store, será necessário escrever [this.$store.state.P];
  • para alterar a propriedade P do store, será necessário escrever [this.$store.commit(‘replace’, {P:value}];

Fazemos essas modificações na página [index]:


<!-- definição HTML da visualização -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulário HTML — os valores são lançados 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" label-cols="3">
          <!-- campo de entrada do usuário -->
          <b-col cols="6">
            <b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
          </b-col>
        </b-form-group>
        <!-- 2ª linha -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- campo de digitação de senha -->
          <b-col cols="6">
            <b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
          </b-col>
        </b-form-group>
        <!-- 3ª linha -->
        <b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
        <!-- botão do tipo [submit] na terceira linha -->
        <b-row>
          <b-col cols="2">
            <b-button :disabled="!valid" variant="primary" type="submit">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>

<!-- dinâmica da visualização -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // componentes utilizados
  components: {
    Layout
  },
  // 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: ''
    }
  },

  // propriedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // ciclo de vida: o componente acaba de ser criado
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // o usuário pode realizar simulações?
    if (this.$store.state.started && this.$store.state.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.$store.state.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.$store.commit('replace', { started: true })
          console.log('[authentification], session=', this.$session())
        })
        // 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()
        })
    }
  },

  // gerenciadores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      try {
        // início da espera
        this.$emit('loading', true)
        // ainda não estamos autenticados
        this.$store.commit('replace', { authenticated: false })
        // autenticação bloqueante no 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
        // autentificado
        this.$store.commit('replace', { 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 do armazenamento
        this.$store.commit('replace', { métier: this.$métier })
        // a sessão é salva
        this.$session().save()
      }
    }
  }
}
</script>

Observe os seguintes pontos:

  • linha 69: a função [created2] foi renomeada para [mounted], para que o servidor [nuxt] não a execute (ele não executa nem [beforeMount] nem [mounted]). Apenas o cliente [nuxt] irá executá-lo, assim como acontecia com o exemplo [vuejs-22];
  • linha 73: há uma referência a [this.$métier], que, por enquanto, não existe;
  • linha 75: nunca utilizamos esse método em um aplicativo [nuxt]. Será preciso verificar se ele funciona no contexto [nuxt];
  • linhas 112 e 172: no [vuejs-22], a sessão do projeto era salva dessa forma. No projeto [nuxt-20], o método [save] deve receber o contexto atual. Sabe-se que, em uma página [nuxt], o objeto [context] está disponível em [this.$nuxt.context];

As linhas 112 e 172 são, portanto, reescritas da seguinte forma:


this.$session().save(this.$nuxt.context)

Observe-se que esse código não está otimizado. Em vez de utilizar várias vezes a função [this.$session()], seria preferível escrever:


const session=this.$session()

e, em seguida, utilizar a variável [session]. O mesmo raciocínio se aplica à função [this.$dao()].

Feitas essas correções, podemos recarregar o URL [http://localhost:81/nuxt-20/] em um navegador. Obtemos a mesma página de antes:

Image

Vamos examinar os logs do navegador:

Image

O log [1] é o último log gerado pelo cliente [nuxt]. No [2], vemos que a propriedade [started] está em [vrai], o que significa que a função [mounted] conseguiu iniciar uma sessão jSON com o servidor de cálculo de impostos. Também vemos que o store possui propriedades que precisarão ser descartadas ou renomeadas. Vale lembrar que estamos usando o store do exemplo [nuxt-12].

Agora, solicitemos novamente o URL [http://localhost:81/nuxt-20/] enquanto o servidor de cálculo de impostos não está em execução. Primeiro, devemos nos certificar de excluir o cookie da sessão [nuxt]:

Image

A captura de tela acima foi feita no Chrome. Feito isso, o URL [http://localhost:81/nuxt-20/] apresenta o seguinte resultado:

Image

O erro foi tratado corretamente pelo projeto [vuejs-22]. Ele continua sendo tratado corretamente pelo projeto [nuxt-20].

17.4. Etapa 3

Agora que temos a página de autenticação, precisamos examinar o código executado quando o usuário clica no botão [Valider]:


// gerenciadores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      try {
        // início da espera
        this.$emit('loading', true)
        // ainda não estamos autenticados
        this.$store.commit('replace', { authenticated: false })
        // autenticação bloqueante no 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
        // autentificado
        this.$store.commit('replace', { 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) {
          // é exibido o 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 com 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 do armazenamento
        this.$store.commit('replace', { métier: this.$métier })
        // a sessão é salva
        this.$session().save(this.$nuxt.context)
      }
    }
  }

O principal problema aqui parece ser a ausência do dado [this.$métier]. Para resolver isso, vamos:

  • incluir a classe [Métier] do exemplo [vuejs-22]. Vamos colocá-la na pasta [api];
  • injetar uma função [$métier] no contexto do cliente [nuxt], que dará acesso a essa classe;

Primeiro, copiar a classe [Métier] para a pasta [api]:

Image

Assim que a classe [Métier] estiver presente no projeto, crie-se um novo plug-in para o cliente [nuxt]. Esse plug-in, chamado [pluginMétier], irá injetar uma função [$métier] que dará acesso à classe [Métier]:


/* eslint-disable no-console */
// cria-se um ponto de acesso à camada [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
  // instanciação da camada [métier]
  const métier = new Métier()
  // injeção de uma função [$métier] no contexto
  inject('métier', () => métier)
  // registro
  console.log('[fonction client $métier créée]')
}

Feito isso, podemos corrigir a página [index]:


// ciclo de vida: o componente acaba de ser criado
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // o usuário pode realizar simulações?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // então o usuário pode realizar simulações
      this.$router.push({ name: 'calcul-impot' })
      // retorno ao ciclo de eventos
      return
    }
    // se a sessão jSON já tiver sido iniciada, ela não será reiniciada
    ...
  },

  // managers de eventos
  methods: {
    // ----------- autenticação
    async login() {
      try {
        // início da espera
        this.$emit('loading', true)
        // ainda não estamos autenticados
        this.$store.commit('replace', { authenticated: false })
        // autenticação bloqueada no 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.$store.commit('replace', { 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: 'calcul-impot' })
      } catch (error) {
        // o erro é repassado ao componente principal
        this.$emit('error', error)
      } finally {
        // atualização do armazenamento
        this.$store.commit('replace', { métier: this.$métier() })
        // a sessão é salva
        this.$session().save(this.$nuxt.context)
      }
    }
  }
  • linhas 43, 61, 69: [this.$métier] foi substituído por [this.$métier()];
  • linhas 8 e 63: o nome da página [CalculImpot] do projeto [vuejs-22] passou a ser a página [calcul-impot] no projeto [nuxt-20];

Após essas correções, é possível tentar validar a página de autenticação:

Image

A página exibida é a seguinte:

Image

Conseguimos, de fato, a página de cálculo do imposto. Agora, vamos examinar os logs:

Image

Em [2], vemos que o fato de estarmos autenticados foi registrado corretamente. Em [3-4], vemos que foi recuperado o dado [taxAdminData], que permite o cálculo do imposto pela classe [Métier].

17.5. Etapa 4

Vamos examinar a página [calcul-impot] que obtivemos:


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

<script>
// importações
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'

export default {
  // componentes utilizados
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // relatório 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
    }
  },
  // ciclo de vida
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  },
  // métodos de gerenciamento de eventos
  methods: {
    // resultado do cálculo do imposto
    handleResultatObtenu(résultat) {
      // construímos o resultado em cadeia 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
      // exibição do resultado
      this.résultatObtenu = true
      // ---- atualização do armazenamento [Vuex]
      // uma simulação de +
      this.$store.commit('addSimulation', résultat)
      // salvando a sessão
      this.$session.save()
    }
  }
}
</script>
  • linhas 44 e 48: os links do menu de navegação estão corretos. A página [/fin-session] não existe. O projeto [vuejs-22] resolvia esse problema por meio de roteamento. Faremos o mesmo com o projeto [nuxt-20];
  • linha 76: há uma referência a uma modificação [addSimulation] que ainda não existe. Vamos criá-la;
  • linha 78: assim como na página [index], é preciso escrever [this.$session().save(this.$nuxt.context)];

Vamos modificar o store [store/index]. Herdado do projeto [nuxt-12], ele está, por enquanto, da seguinte forma:


/* eslint-disable no-console */

// estado da loja
export const state = () => ({
  // sessão jSON iniciada
  jsonSessionStarted: false,
  // usuário autenticado
  userAuthenticated: false,
  // cookie de sessão PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})

// alterações no store
export const mutations = {
  // substituição do estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reinicialização do armazenamento
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}

// ações do armazenamento
export const actions = {
  nuxtServerInit(store, context) {
    // quem executa esse código?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // inicialização da sessão
    initStore(store, context)
  }
}

function initStore(store, context) {
  // o store é o que deve ser inicializado
  // recuperamos a sessão
  const session = context.app.$session()
  // a sessão já foi inicializada?
  if (!session.value.initStoreDone) {
    // inicia-se um novo store
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // colocamos o store na sessão
    session.value.store = store.state
    // o store agora está inicializado
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // atualizando o armazenamento com o armazenamento da sessão
    store.commit('replace', session.value.store)
  }
  // a sessão é salva
  session.save(context)
  // registro
  console.log('initStore terminé, store=', store.state)
}
  • linhas 3-27: vamos retomar o estado e as alterações do aplicativo [vuejs-22] (ver documento [3]):

// estado da persiana
export const state = () => ({
  // sessão jSON iniciada
  started: false,
  // usuário autenticado
  authenticated: false,
  // cookie de sessão PHP
  phpSessionCookie: '',
  // lista de simulações
  simulations: [],
  // o número da última simulação
  idSimulation: 0,
  // camada [métier]
  métier: null
})

// alterações na persiana
export const mutations = {
  // substituição do estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reinicialização do armazenamento
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
  },
  // remoção da linha nº índice
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // exclusão da linha nº [index]
    state.simulations.splice(index, 1)
    console.log('store simulations', state.simulations)
  },
  // adição de uma simulação
  addSimulation(state, simulation) {
    // eslint-disable-next-line no-console
    console.log('mutation addSimulation')
    // nº da simulação
    state.idSimulation++
    simulation.id = state.idSimulation
    // a simulação é adicionada à tabela de simulações
    state.simulations.push(simulation)
  }
}
  • linhas 4 e 6: inserimos as propriedades já utilizadas;
  • linha 8: mantemos o cookie de sessão PHP. Ele é fundamental para que o cliente e o servidor [nuxt] tenham a mesma sessão PHP com o servidor de cálculo de impostos;
  • linha 10: a lista das simulações realizadas pelo usuário;
  • linha 12: o número da última simulação realizada pelo usuário;
  • linha 14: a camada [métier];
  • linhas 30-47: as alterações presentes no armazenamento do projeto [vuejs-22] e referenciadas pelas páginas do aplicativo. O projeto [vuejs-22] possuía uma alteração chamada [clear] que esvaziava a lista de simulações. Não a incluímos porque a mutação [reset], já presente, deve ser suficiente;
  • linhas 26-28: a mutação [reset] é modificada para levar em conta o novo conteúdo do state;

A página [calcul-impot] utiliza o componente [form-calcul-impot] a seguir:


<!-- 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 id="enfants" v-model="enfants" :state="enfantsValide" type="text" placeholder="Indiquez votre nombre d'enfants"></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 id="salaire" v-model="salaire" :state="salaireValide" type="text" placeholder="Salaire annuel"></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 :disabled="formInvalide" type="submit" variant="primary">Valider</b-button>
    </b-col>
  </b-form>
</template>

<!-- script -->
<script>
export default {
  // estado interno
  data() {
    return {
      // casado ou solteiro
      marié: 'non',
      // número de filhos
      enfants: '',
      // salário anual
      salaire: ''
    }
  },
  // status interno calculado
  computed: {
    // validação do formulário
    formInvalide() {
      return (
        // salário inválido
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // ou número de filhos inválido
        !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*$/))
    }
  },
  // ciclo de vida
  created() {
    // log
    // eslint-disable-next-line
    console.log("FormCalculImpot created");
  },
  // gerenciador de eventos
  methods: {
    calculerImpot() {
      // o imposto é calculado com a camada [métier]
      const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // completa-se o resultado
      résultat.marié = this.marié
      résultat.enfants = this.enfants
      résultat.salaire = this.salaire
      // emite-se o evento [resultatObtenu]
      this.$emit('resultatObtenu', résultat)
    }
  }
}
</script>
  • linhas 65, 89: a referência [this.$métier] deve ser alterada para [this.$métier()];

Após essas correções, é possível tentar uma simulação:

Image

Obtém-se a seguinte resposta:

Image

Se analisarmos os logs:

Image

  • em [9-10], percebe-se que a primeira simulação está, de fato, no [store];
  • em [5], o número da última simulação foi incrementado corretamente;

17.6. etapa 5

Agora que realizamos uma simulação, vamos clicar no link [Liste des simulations]. Aparece a seguinte página:

Image

O roteamento do cliente [nuxt] foi realizado corretamente. Vamos examinar o código da página [liste-des-simulations]:


<!-- 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 :items="simulations" :fields="fields" striped hover responsive>
            <template v-slot:cell(action)="data">
              <b-button @click="supprimerSimulation(data.index)" variant="link">Supprimer</b-button>
            </template>
          </b-table>
        </template>
      </template>
      <!-- menu de navegação na coluna da esquerda -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

<script>
// importações
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
  // componentes
  components: {
    Layout,
    Menu
  },
  // estado interno
  data() {
    return {
      // opções do menu de navegação
      options: [
        {
          text: "Calcul de l'impôt",
          path: '/calcul-impot'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // parâmetros da tabela HTML
      fields: [
        { label: '#', key: 'id' },
        { label: 'Marié', key: 'marié' },
        { label: "Nombre d'enfants", key: 'enfants' },
        { label: 'Salaire', key: 'salaire' },
        { label: 'Impôt', key: 'impôt' },
        { label: 'Décôte', key: 'décôte' },
        { label: 'Réduction', key: 'réduction' },
        { label: 'Surcôte', key: 'surcôte' },
        { label: '', key: 'action' }
      ]
    }
  },
  // estado interno calculado
  computed: {
    // lista de simulações obtida no armazenamento Vuex
    simulations() {
      return this.$store.state.simulations
    }
  },
  // ciclo de vida
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  },
  // métodos
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // remoção da simulação n.º [index]
      this.$store.commit('deleteSimulation', index)
      // a sessão é salva
      this.$session.save()
    }
  }
}
</script>
  • linhas 47-56: os destinos do menu de navegação estão corretos;
  • linha 75: o store está corretamente referenciado;
  • linha 89: está sendo utilizada uma mutação [deleteSimulation] que integramos na etapa anterior;
  • linha 91: essa linha deve ser reescrita como [this.$session().save(this.$nuxt.context)];

Fazemos as alterações necessárias e, em seguida, tentamos excluir a simulação exibida:

Image

Obtemos então a seguinte página:

Image

Vamos verificar os logs:

Image

  • em [6], vemos que a tabela de simulações está vazia;

Agora, voltemos ao formulário de cálculo do imposto:

Image

Aparece a seguinte página:

Image

Portanto, o encaminhamento funcionou.

17.7. Etapa 6

Resta-nos configurar a opção de navegação [Fin de session] do menu de navegação:


// opções do menu
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
]
  • na linha 9, a página [/fin-session] não existe. O projeto [vuejs-22] tratava desse caso com regras de roteamento em um arquivo [router.js]:

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

// rotas do aplicativo
const routes = [
  // autenticação
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification2', 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 de 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({
      // passamos 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
  • as linhas 64-76 tratavam do caso específico do roteamento para o caminho [/fin-session];
  • linha 66: esvazia-se a sessão atual;
  • linhas 68 a 70: exibe-se a visualização [authentification];

Vamos tentar fazer algo semelhante no arquivo de roteamento do cliente [nuxt]:

Image

O script [client/routing.js] fica da seguinte forma:


/* eslint-disable no-console */
export default function(context) {
  // quem executa esse código?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gerenciamento do cookie de sessão PHP no navegador
  // o cookie de sessão PHP do navegador deve ser idêntico ao encontrado na sessão do Nuxt
  // a ação [fin-session] recebe um novo cookie PHP (tanto no servidor quanto no cliente Nuxt)
  // se for o servidor que o receber, o cliente deve transmiti-lo ao navegador
  // para suas próprias comunicações com o servidor PHP
  // estamos aqui em um roteamento do cliente

  // recuperamos o cookie da sessão PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // se existir, atribuímos o cookie de sessão PHP ao navegador
    document.cookie = phpSessionCookie
  }

  // para onde vamos?
  const to = context.route.path
  if (to === '/fin-session') {
    // limpa-se a sessão
    const session = context.app.$session()
    session.reset(context)
    // redireciona para a página inicial
    context.redirect({ name: 'index' })
  }
}
  • adicionamos as linhas [19-27] ao código existente;
  • linha 20: recuperamos o [path] do destino da rota atual;
  • linha 21: verifica-se se é [/fin-session]. Se for:
    • linhas 23-24: a sessão é reinicializada;
    • linha 26: redireciona-se o cliente [nuxt] para a página inicial;

O método [session.reset(context)] (linha 24) da sessão é o seguinte:


// reinicialização da sessão
  reset(context) {
    console.log('nuxt-session reset')
    // reinicialização do cache
    context.store.commit('reset')
    // salva o novo cache na sessão e salva a sessão
    this.save(context)
}

O método [context.store.commit('reset')] (linha 5) é o seguinte:


// reinicialização do armazenamento
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}

Ao utilizar agora o link [Fin de session], a página inicial é exibida com os seguintes logs:

Image

  • em [3], percebe-se que não estamos mais autenticados;
  • em [4], percebe-se que a sessão jSON foi iniciada;
  • em [6], a camada [métier] não está mais presente no armazenamento (ela ainda está presente nas páginas com [this.$métier()]);
  • em [5, 7], não há mais simulações;

É preciso entender bem o que acontece no encerramento de uma sessão:

  • a sessão [nuxt] é reinicializada: a propriedade [started] do armazenamento passa para [false];
  • ocorre um redirecionamento para a página [index];
  • o método [mounted] da página [index] é executado. Esse método inicia uma nova sessão jSON com o servidor de cálculo de impostos. Se a operação for bem-sucedida, a propriedade [started] do store passa para [true];

17.8. etapa 7

Nesta fase, o aplicativo [nuxt-20] possui todas as funcionalidades do aplicativo [vuejs-22]. A migração parece estar concluída.

Vamos um pouco além, no espírito do [nuxt]. O método [mounted] da página [index] apresenta um problema. Ele inicia uma operação assíncrona cuja conclusão não será aguardada por um mecanismo de busca. Sabemos que, nesse caso, é preciso colocar a operação assíncrona em uma função [asyncData], pois, assim, o servidor [nuxt] que a executa aguarda sua conclusão antes de entregar a página ao mecanismo de busca.

Aqui, utilizamos a função [asyncData], escrita no aplicativo [nuxt-12] para a página [index]:


export default {
  name: 'InitSession',
  // componentes utilizados
  components: {
    Layout,
    Navigation
  },
  // dados assíncronos
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // não se repete a operação se a página já tiver sido solicitada
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
      // iniciamos uma sessão jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // recuperamos o cookie de sessão PHP para as próximas solicitações
      const phpSessionCookie = dao.getPhpSessionCookie()
      // o cookie de sessão PHP é armazenado na sessão [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // houve algum erro?
      if (response.état !== 700) {
        // o erro está em response.réponse
        throw new Error(response.réponse)
      }
      // observa-se que a sessão jSON foi iniciada
      context.store.commit('replace', { jsonSessionStarted: true })
      // retornamos o resultado
      return { result: '[succès]' }
    } catch (e) {
      // registro
      console.log('[index asyncData error=]', e)
      // registra-se que a sessão jSON não foi iniciada
      context.store.commit('replace', { jsonSessionStarted: false })
      // o erro é relatado
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // salva-se o store
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // ciclo de vida
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    console.log('[index beforeMount]')
  },
  mounted() {
    console.log('[index mounted]')
    // somente cliente
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • linhas 13, 33, 40: é preciso alterar a propriedade [jsonSessionStarted] para [started];
  • linha 13: no aplicativo [nuxt-12], apenas o servidor [nuxt] executava a página [index] e sua função [asyncData]. O cliente [nuxt] só executava a página [index] após recebê-la do servidor [nuxt] e, nesse caso, não executava a função [asyncData]. Em [nuxt-20], a situação é diferente: o link [Fin de session] exibirá a página [index] no ambiente do cliente [nuxt]. A função [asyncData] será então executada. No entanto, quando se acessa a página [index] dessa forma, a sessão [nuxt] foi reinicializada nesse intervalo, e a propriedade [started] do store vale [false], de modo que a condição da linha 13 será necessariamente falsa. Portanto, podemos deixar [process.server] e, assim, o cliente [nuxt] não realizará esse teste;
  • linhas 15, 35, 42: uma propriedade [result] é inserida nas propriedades [data] da página [index]. Em [nuxt-20], essa propriedade não será utilizada e nós a removeremos do resultado retornado pela função;
  • linhas 61-67: esse método [mounted] deve ser mantido, pois é ele que permite que o cliente [nuxt] exiba a mensagem de erro. No entanto, a forma de lidar com o erro será alterada;

Na página [index] atual, integramos a função [asyncData] acima no lugar da antiga função [mounted] e adicionamos uma nova função [mounted]. O código da página [index] do exemplo [nuxt-20] passa a ser o seguinte:


...

<!-- dinâmica da visualização -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // componentes utilizados
  components: {
    Layout
  },
  // estado do componente
  data() {
    return {
      // usuário
      user: '',
      // sua senha
      password: '',
      // exibição de erros
      showError: false
    }
  },

  // propriedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // dados assíncronos
  async asyncData(context) {
    // registro
    console.log('[index asyncData started]')
    // não se faz a mesma coisa duas vezes se a página já tiver sido solicitada
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // iniciando uma sessão jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // registro
      console.log('[index asyncData response=]', response)
      // recuperamos o cookie de sessão PHP para as próximas solicitações
      const phpSessionCookie = dao.getPhpSessionCookie()
      // o cookie de sessão PHP é armazenado na sessão [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // houve algum erro?
      if (response.état !== 700) {
        // o erro está em response.réponse
        throw new Error(response.réponse)
      }
      // observa-se que a sessão jSON foi iniciada
      context.store.commit('replace', { started: true })
      // nenhum resultado
      return
    } catch (e) {
      // registro
      console.log('[index asyncData error=]', e.message)
      // observa-se que a sessão jSON não foi iniciada
      context.store.commit('replace', { started: false })
      // o erro é relatado
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // o armazenamento está sendo salvo
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // ciclo de vida
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // somente cliente
    console.log('[index beforeMount]')
    // gestão de possíveis erros
    if (this.showErrorLoading) {
      // registro
      console.log('[index beforeMount, showErrorLoading=true]')
      // o erro é encaminhado ao componente principal [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },

  // gerenciadores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      ...
}
</script>
  • linhas 58, 65: a função [asyncData] não torna mais a propriedade [result] desutilizada aqui;
  • linha 81: o método [beforeMount] do cliente [nuxt]. Ele foi preferido ao método [mounted] para lidar com um possível erro do [asyncData];
  • linha 85: verifica-se se a propriedade [errorLoading] foi definida. Ela só pode ser definida pela função [asyncData];
  • linhas 85-90: se a função [asyncData] sinalizou um erro, ele é encaminhado para a página [default] por meio do evento [error]. Era assim que a antiga função [created], que acabamos de substituir, lidava com um possível erro;

Vamos fazer alguns testes.

Primeiro, excluímos o cookie de sessão [nuxt] e o cookie de sessão PHP, caso existam. Em seguida, acessamos a página [http://localhost:81/nuxt-20/] enquanto o servidor de cálculo do imposto não está em execução. Obtemos a seguinte página:

Image

Recarregamos a mesma página após iniciar o servidor de cálculo de impostos:

Image

Vamos examinar os logs:

Image

  • em [2-3], vemos que a sessão jSON foi iniciada;
  • em [4], vemos o cookie de sessão PHP que o servidor [nuxt] recuperou durante sua troca de dados com o servidor de cálculo de impostos. O cliente [nuxt] passará a utilizá-lo;

Agora vamos nos identificar:

Image

Obtemos a seguinte página:

Image

Em [1], recebemos uma mensagem de erro. Isso significa que o navegador não enviou o cookie correto da sessão PHP iniciada pelo servidor [nuxt] na etapa anterior. Em [nuxt-12], a transferência do cookie de sessão PHP do servidor [nuxt] para o cliente [nuxt] ocorreu no roteamento do cliente [nuxt] do script [middleware/client/routing] :


/* eslint-disable no-console */
export default function(context) {  // quem executa esse código?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gerenciamento do cookie de sessão PHP no navegador
  // o cookie de sessão PHP do navegador deve ser idêntico ao encontrado na sessão do Nuxt
  // a ação [fin-session] recebe um novo cookie PHP (tanto no servidor quanto no cliente Nuxt)
  // se for o servidor que o receber, o cliente deve transmiti-lo ao navegador
  // para suas próprias comunicações com o servidor PHP
  // estamos aqui em um roteamento do cliente

  // recuperamos o cookie da sessão PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // se existir, atribuímos o cookie de sessão PHP ao navegador
    document.cookie = phpSessionCookie
  }

  // para onde vamos?
  const to = context.route.path
  if (to === '/fin-session') {
    // limpa-se a sessão
    const session = context.app.$session()
    session.reset(context)
    // redirecionamos para a página inicial
    context.redirect({ name: 'index' })
  }
}

São as linhas 13 a 17 que permitem que o cliente [nuxt] recupere o cookie da sessão PHP do servidor [nuxt].

O problema aqui é que, ao clicar no botão [Valider], não ocorre o roteamento do cliente [nuxt]. Sua função de roteamento, portanto, não é chamada. Resolvemos o problema duplicando as linhas 12 a 17 no início do método de autenticação da página [index]:


// gerenciadores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      // recuperamos o cookie de sessão PHP do armazenamento
      const phpSessionCookie = this.$store.state.phpSessionCookie
      if (phpSessionCookie) {
        // se existir, atribui-se o cookie de sessão PHP ao navegador
        document.cookie = phpSessionCookie
      }
      try {
        // início da espera
        this.$emit('loading', true)
        // ainda não estamos autenticados

Nas linhas 5 a 10, recupera-se do store o cookie da sessão PHP iniciada pelo servidor [nuxt]. Após essa modificação, a página de cálculo do imposto é carregada corretamente, o que significa que a autenticação funcionou.

17.9. Etapa 8

Temos um aplicativo funcional que opera de acordo com a lógica [nuxt]. Assim como fizemos com o aplicativo [nuxt-13], vamos nos concentrar na navegação do servidor [nuxt]. Como já foi mencionado, o usuário não deve digitar manualmente os URL do aplicativo. Ele deve utilizar os links que lhe são apresentados e que são executados pelo cliente [nuxt], que passa a funcionar no modo SPA. No entanto, vamos garantir que a navegação no servidor [nuxt] mantenha a aplicação sempre em um estado estável.

A partir do estudo realizado para o [nuxt-13] (ver parágrafo sobre o link), sabemos que é necessário:

  • modificar o script [midleware/routing];
  • adicionar um script [middleware/server/routing];

Image

O script [middleware/routing] é modificado da seguinte forma:


/* eslint-disable no-console */

// importamos os middlewares do servidor e do cliente
import serverRouting from './server/routing'
import clientRouting from './client/routing'

export default function(context) {
  // quem está executando esse código?
  console.log('[middleware], process.server', process.server, ', process.client=', process.client)
  if (process.server) {
    // roteamento do servidor
    serverRouting(context)
  } else {
    // roteamento do cliente
    clientRouting(context)
  }
}
  • linha 4: importa-se o script de roteamento do servidor [nuxt];
  • linhas 10-12: se for o servidor [nuxt] que executar o código, utiliza-se sua função de roteamento;

O script [middleware/server/routing] é o seguinte:


/* eslint-disable no-console */
export default function(context) {
  // quem executa esse código?
  console.log('[middleware server], process.server', process.server, ', process.client=', process.client)

  // recolhemos algumas informações aqui e ali
  const store = context.store
  // de onde viemos?
  const from = store.state.from || 'nowhere'
  // para onde estamos indo?
  let to = context.route.name
  
  // caso específico de /fin-session que não possui o atributo [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
  
  // possível redirecionamento
  let redirection = ''
  // gerenciamento do roteamento concluído
  let done = false
  
  // já estamos em um redirecionamento do servidor [nuxt]?
  if (store.state.serverRedirection) {
    // nada a fazer
    done = true
  }
  
  // trata-se de uma atualização da página?
  if (!done && from === to) {
    // não há nada a fazer
    done = true
  }
  
  // controle da navegação do servidor [nuxt]
  // segue o menu de navegação do cliente

  // tratamos primeiro o caso de fim de sessão
  if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
    // limpa-se a sessão
    const session = context.app.$session()
    session.reset(context)
    // redireciona para a página inicial
    redirection = 'index'
    // trabalho concluído
    done = true
  }

  // caso em que a sessão PHP não tenha sido iniciada
  if (!done && !store.state.started && to !== 'index') {
    // redirecionamento para [index]
    redirection = 'index'
    // trabalho concluído
    done = true
  }

  // caso em que o usuário não esteja autenticado
  if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
    redirection = 'index'
    // trabalho concluído
    done = true
  }

  // caso em que [adminData] não foi obtido
  if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
    // redirecionamento para [index]
    redirection = 'index'
    // trabalho concluído
    done = true
  }

  // caso em que [adminData] foi obtido
  if (
    !done &&
    store.state.started &&
    store.state.authenticated &&
    store.state.métier.taxAdminData &&
    to !== 'calcul-impot' &&
    to !== 'liste-des-simulations'
  ) {
    // permanece-se na mesma página
    redirection = from
    // trabalho concluído
    done = true
  }

  // normalmente, todas as verificações foram feitas ---------------------
  // redirecionamento?
  if (redirection) {
    // registramos o redirecionamento no armazenamento
    store.commit('replace', { serverRedirection: true })
  } else {
    // sem redirecionamento
    store.commit('replace', { serverRedirection: false, from: to })
  }
  // o armazenamento é salvo na sessão [nuxt]
  const session = context.app.$session()
  session.value.store = store.state
  session.save(context)
  // é feita a eventual redirecionamento do servidor [nuxt]
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • neste script, retomamos as ideias já desenvolvidas e utilizadas no roteamento do servidor [nuxt] do aplicativo [nuxt-13];
  • adicionamos duas propriedades ao store do aplicativo:
    • [from]: o nome da última página exibida. Sabemos que o cliente [nuxt] possui essa informação, mas o servidor [nuxt] não. Vamos adicionar essa informação ao armazenamento, gravando nele, a cada roteamento do servidor [nuxt], o nome da página que será exibida. Faremos o mesmo a cada roteamento do cliente [nuxt]. Assim, no próximo roteamento do servidor [nuxt], este encontrará no armazenamento o nome da última página exibida pelo aplicativo;
    • [serverRedirection]: quando um destino de roteamento for recusado pelo servidor [nuxt], este realizará um redirecionamento. Ele indicará então no armazenamento que o próximo destino do servidor [nuxt] é uma página de redirecionamento. Esse redirecionamento provocará uma nova execução do roteador do servidor [nuxt]. Se este detectar que o destino atual é resultado de um redirecionamento, ele permitirá que o processo continue;
  • linhas 6-11: recuperam-se as informações úteis para o roteamento;
  • linhas 13-16: o destino [/fin-session] não está associado a uma página chamada [fin-session]. Portanto, ele não tem nome. Atribuímos um nome a ele;
  • linha 19: o destino de um possível redirecionamento;
  • linha 21: [done=true] quando os testes de roteamento estiverem concluídos;
  • linhas 23-27: como já foi dito, se o roteamento em andamento for resultado de um redirecionamento, não há nada a ser feito. De fato, durante o roteamento anterior, o roteador decidiu que era necessário redirecionar o navegador do cliente. Não há motivo para reconsiderar essa decisão;
  • linhas 29-33: se for uma recarga de página, deixa-se que ocorra. Esse não é um axioma válido para todas as aplicações [nuxt]: é preciso verificar, para cada página, os efeitos de uma recarga. Neste caso, verifica-se que a atualização das páginas [index, calcul-impot, liste-des-simulations] não provoca efeitos indesejáveis;
  • linhas 35-85: o roteamento do servidor [nuxt] retoma o roteamento do cliente [nuxt]. Quando se está em uma página, o roteamento do servidor [nuxt] deve refletir o menu de navegação oferecido pelo cliente [nuxt] quando se está nessa página;
  • linhas 38-47: trata-se, primeiro, do caso do destino [fin-session], que não corresponde a uma página existente. Se as condições forem atendidas (sessão iniciada, usuário autenticado), a sessão é reiniciada e o usuário é redirecionado para a página [index];
  • linhas 49-55: se a sessão jSON com o servidor de cálculo de impostos não tiver sido iniciada, então o único destino possível é a página [index];
  • linhas 57-62: se a sessão jSON tiver sido iniciada e o usuário não estiver autenticado nem tiver solicitado a página de autenticação, então ele é redirecionado para a página de autenticação, que é a página [index];
  • linhas 64-70: se o usuário estiver autenticado, mas o dado [adminData] não tiver sido obtido, o usuário é redirecionado para a página de autenticação. A autenticação realiza duas ações: autentica e, caso a autenticação seja bem-sucedida, solicita adicionalmente o dado [adminData]. Se este último não tiver sido obtido, é necessário reiniciar a autenticação;
  • linhas 72-85: se o dado [adminData] tiver sido obtido, os únicos destinos possíveis são [calcul-impot] e [liste-des-simulations]. Caso contrário, o roteamento é recusado;
  • linhas 88-95: atualiza-se o armazenamento dependendo se haverá redirecionamento ou não;
  • linha 94: não há redirecionamento. Portanto, o [to] atual se tornará o [from] do próximo roteamento;
  • linhas 96-99: as informações do store são salvas no cookie da sessão [nuxt];
  • linhas 100-103: se for necessário fazer um redirecionamento, ele é feito;

Para realizar os testes, é preciso certificar-se de partir de uma situação inicial, excluindo o cookie da sessão [nuxt] e o cookie da sessão PHP do servidor de cálculo de impostos:

Image

Para testar o roteamento do servidor [nuxt], em cada página, tente todos os URL possíveis, como [/, /calcul-impot, /liste-des-simulations]. Em cada tentativa, o aplicativo deve permanecer em um estado coerente.

17.10. Etapa 9

A etapa 9 consiste na implantação da aplicação [nuxt-20]. Para isso, é necessário um serviço de hospedagem que ofereça um ambiente [node.js] para executar o servidor [nuxt]. Eu não tenho isso. O leitor poderá seguir os procedimentos descritos no parágrafo com o link para implantar o aplicativo [nuxt-20] em sua máquina de desenvolvimento e protegê-lo com um protocolo HTTPS.

17.11. Conclusion

A migração do aplicativo [vuejs-22] para o aplicativo [nuxt-20] está concluída. Destacamos alguns pontos dessa migração:

  • as páginas do [vuejs-22] foram mantidas;
  • as operações assíncronas que existiam nas páginas do [vuejs-22] foram migradas para uma função [asyncData];
  • no [nuxt-20], foi necessário gerenciar duas entidades: o cliente [nuxt] e o servidor [nuxt]. Esta última entidade não existia no [vuejs-22]. Para manter a coerência entre as duas entidades, precisamos de uma sessão [nuxt];
  • tivemos que gerenciar o roteamento do servidor [nuxt];

Na prática, é sem dúvida preferível iniciar diretamente com uma arquitetura [nuxt] do que criar uma arquitetura [vue.js] que seja posteriormente migrada para um ambiente [nuxt].