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 uma aplicação [vue.js] do tipo SPA, para um contexto [nuxt] SSR. O [vuejs-22] era uma aplicação cliente do servidor de cálculo de impostos que apresentava as seguintes vistas:

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

Image

A segunda vista é a do cálculo do imposto:

Image

A terceira vista é a que apresenta a lista das simulações realizadas pelo utilizador:

Image

O ecrã acima mostra que é possível eliminar a simulação n.º 1. Obtém-se então a seguinte vista:

Image

Se eliminarmos agora a última simulação, obtemos a seguinte nova vista:

Image

Vamos migrar a aplicação [vuejs-22] para a aplicação [nuxt-20] de forma gradual. Não iremos explicar novamente os códigos da [vuejs-22]. Convidamos o leitor a reler o documento |Introdução ao framework VUE.JS através de um exemplo|. As diferentes etapas deverão mostrar as diferenças entre uma aplicação [vuejs] e uma aplicação [nuxt].

17.2. Etapa 1

O projeto [nuxt-20] é inicialmente obtido através da cópia do projeto [nuxt-12]. Este último constitui, de facto, um bom ponto de partida:

  • sabe comunicar com o servidor de cálculo de impostos;
  • gerencia corretamente os erros que este envia;
  • o cliente e o servidor [nuxt] conseguem comunicar através de uma sessão [nuxt];

Temos, portanto, uma boa infraestrutura inicial. O nosso principal trabalho deverá consistir em modificar:

  • as páginas. Iremos utilizar as do projeto [vuejs-22], que terão de ser adaptadas ao novo ambiente;
  • a gestão da loja. Deverão surgir informações adicionais (lista de simulações) e outras poderão tornar-se desnecessárias;
  • a gestão do encaminhamento do cliente e do servidor [nuxt];

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

Image

Em seguida, eliminam-se as páginas e os componentes que já não são necessários [2]:

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

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

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

Renomeia-se os elementos integrados [4]:

Image

  • em [layouts], [Main] passou a ser [default], uma vez que este é o nome predefinido do layout de uma aplicação [nuxt];
  • no [pages], a página [Authentification] passou a ser [index], uma vez que [Authentification] desempenhava essa função na aplicação [vuejs-22];

Nesta altura, é possível compilar o projeto para ver os primeiros erros. Altera-se o ficheiro [nuxt.config] do exemplo [nuxt-12] para que passe a executar [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',
    // Documentação: 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',
  // router
  router: {
    // raiz dos URL da aplicação
    base: '/nuxt-20/',
    // middleware de encaminhamento
    middleware: ['routing']
  },
  // servidor
  server: {
    // porta de serviço, 3000 por predefinição
    port: 81,
    // endereços de rede em escuta, por predefiniçã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, executa-se um [build] do projeto:

Image

Os erros detetados 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 está a ser feita referência a uma imagem inexistente. Iremos recuperá-la no [vuejs-22];
  • o erro na linha 2 mostra que o componente [./FormCalculImpot] não existe. De facto, este componente encontra-se agora em [@/components/form-calcul-impot];
  • os erros nas linhas [3-5] indicam que o componente [./Layout] não existe. De facto, esse componente encontra-se agora em [@/components/layout];
  • os erros nas linhas [6-7] indicam que o componente [./Menu] não existe. De facto, chama-se agora [@/components/menu];

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

Image

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


<!-- definição HTML da vista -->
<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 de três colunas vazia -->
      <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-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. Depois de efetuadas estas correções, pode-se tentar um novo [build]. Normalmente, já não há erros.

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

Verifica-se que o comando [dev], em conjunto com o módulo [eslint], é mais rigoroso, do ponto de vista sintático, do que o comando [build]. Neste caso, exige que o operador de comparação [!=] seja escrito como [!==], que é um operador mais restritivo (verifica também o tipo dos operandos). Estes 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 este erro com o [Quick fix] do módulo [eslint] [2].

Reiniciamos a execução do projeto. Já não há erros de compilação. Em seguida, acedemos ao URL e ao [http://localhost:81/nuxt-20/] através de um navegador. Obtemos um erro de execução:

Image

O erro ocorre em [index.vue] [2]. O erro [1] deve-se ao facto de, em [vuejs-22], a camada [dao] estava disponível em [this.$dao], enquanto que em [nuxt-12], cuja infraestrutura adotámos, está disponível na função [this.$dao()].

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

Image

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

Guardamos a alteração e recarregamos a página [index] no navegador. Desta vez está tudo bem:

Image

17.3. passo 2

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

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

Estes elementos já não existem na infraestrutura do projeto [nuxt-12] que copiámos:

  • existem agora duas camadas [dao], uma para o cliente [nuxt] e outra para o servidor [nuxt]. Ambas estão disponíveis através de uma função injetada denominada [$dao]. Isto significa que, nas páginas da aplicação, [this.$dao] deve ser substituído por [this.$dao()];
  • a sessão [nuxt] gerida pela aplicação [nuxt-20] já não tem qualquer relação com o objeto [$session] daaplicação [vuejs-22], onde não existia o conceito de cookie de sessão. No entanto, têm uma funcionalidade semelhante: armazenar informações persistentes ao longo das ações do utilizador. A sessão [nuxt] armazena as informações no store, em vez de diretamente na sessão. Nas páginas da aplicação, [this.$session] deve ser substituído por [this.$store] quando se trata de memorizar informações na sessão e por [this.$session()] quando se trata de manipular a própria sessão;
  • para saber o estado de uma propriedade P do armazenamento, deverá escrever-se [this.$store.state.P];
  • para alterar a propriedade P do store, é necessário escrever [this.$store.commit(‘replace’, {P:value}];

Efetua-se estas alterações na página [index]:


<!-- definição HTML da vista -->
<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">
          <!-- área de introdução de dados do utilizador -->
          <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 introdução da palavra-passe -->
          <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 3.ª 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 vista -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // componentes utilizados
  components: {
    Layout
  },
  // estado do componente
  data() {
    return {
      // utilizador
      user: '',
      // a sua palavra-passe
      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 utilizador pode realizar simulações?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
      // então o utilizador pode realizar simulações
      this.$router.push({ name: 'calculImpot' })
      // regresso ao ciclo de eventos
      return
    }
    // se a sessão jSON já tiver sido iniciada, não a reiniciamos novamente
    if (!this.$store.state.started) {
      // início da espera
      this.$emit('loading', true)
      // inicializa-se a sessão com o servidor — pedido assíncrono
      // utiliza-se a promessa devolvida pelos métodos da camada [dao]
      this.$dao()
        // inicialização de uma sessão jSON
        .initSession()
        // obteve-se a resposta
        .then((response) => {
          // fim da espera
          this.$emit('loading', false)
          // análise da resposta
          if (response.état !== 700) {
            // exibe-se o erro
            this.message = response.réponse
            this.showError = true
            // regresso 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 é reportado à vista [Main]
          this.$emit('error', error)
        })
        // em todos os casos
        .finally(() => {
          // a sessão é guardada
          this.$session().save()
        })
    }
  },

  // gestores 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 junto do 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) {
          // exibe-se o erro
          this.message = response.réponse
          this.showError = true
          // regresso ao ciclo de eventos
          return
        }
        // sem erro
        this.showError = false
        // autenticação bem-sucedida
        this.$store.commit('replace', { authenticated: true })
        // --------- solicita-se agora os dados da administração fiscal
        // inicialmente, sem dados
        this.$métier.setTaxAdminData(null)
        // início da espera
        this.$emit('loading', true)
        // pedido em espera junto do servidor
        const response2 = await this.$dao().getAdminData()
        // fim do carregamento
        this.$emit('loading', false)
        // análise da resposta
        if (response2.état !== 1000) {
          // exibe-se o erro
          this.message = response2.réponse
          this.showError = true
          // regresso 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 avançar para o cálculo do imposto
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // o erro é reportado ao componente principal
        this.$emit('error', error)
      } finally {
        // atualização da memória
        this.$store.commit('replace', { métier: this.$métier })
        // a sessão é guardada
        this.$session().save()
      }
    }
  }
}
</script>

É importante destacar os seguintes pontos:

  • linha 69: a função [created2] foi renomeada para [mounted], para que o servidor [nuxt] não a execute (este não executa nem a [beforeMount] nem a [mounted]). Apenas o cliente [nuxt] irá executá-lo, tal como acontecia com o exemplo [vuejs-22];
  • linha 73: é feita uma referência a [this.$métier], que, por enquanto, não existe;
  • linha 75: nunca utilizámos este método numa aplicação [nuxt]. Será necessário verificar se funciona num contexto [nuxt];
  • linhas 112 e 172: no [vuejs-22], a sessão do projeto era guardada desta forma. No projeto [nuxt-20], o método [save] deve receber o contexto atual. Sabe-se que, numa 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)

Note-se que este 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 aplica-se à função [this.$dao()].

Feitas estas correções, podemos recarregar o URL [http://localhost:81/nuxt-20/] num navegador. Obtemos sempre a mesma página que anteriormente:

Image

Vejamos os registos do navegador:

Image

O registo [1] é o último registo efetuado 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. Vemos também que o «store» tem propriedades que terão de ser eliminadas ou renomeadas. Recorde-se que estamos a utilizar o «store» do exemplo [nuxt-12].

Agora, vamos solicitar novamente o URL [http://localhost:81/nuxt-20/] enquanto o servidor de cálculo de impostos não está em execução. Em primeiro lugar, certificamo-nos de eliminar o cookie da sessão [nuxt]:

Image

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

Image

O erro foi corretamente tratado pelo projeto [vuejs-22]. Continua a ser corretamente tratado pelo projeto [nuxt-20].

17.4. Etapa 3

Agora que temos a página de autenticação, é necessário analisar o código executado quando o utilizador clica no botão [Valider]:


// gestores 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 junto do 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) {
          // exibe-se o erro
          this.message = response.réponse
          this.showError = true
          // regresso ao ciclo de eventos
          return
        }
        // sem erro
        this.showError = false
        // autenticação bem-sucedida
        this.$store.commit('replace', { authenticated: true })
        // --------- agora solicitam-se os dados da administração fiscal
        // inicialmente, sem dados
        this.$métier.setTaxAdminData(null)
        // início da espera
        this.$emit('loading', true)
        // pedido em espera junto do servidor
        const response2 = await this.$dao().getAdminData()
        // fim do carregamento
        this.$emit('loading', false)
        // análise da resposta
        if (response2.état !== 1000) {
          // é apresentado o erro
          this.message = response2.réponse
          this.showError = true
          // regresso 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 avançar para o cálculo do imposto
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // o erro é reportado ao componente principal
        this.$emit('error', error)
      } finally {
        // atualização da memória
        this.$store.commit('replace', { métier: this.$métier })
        // guardamos a sessão
        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 esta classe;

Primeiro, a cópia da classe [Métier] para a pasta [api]:

Image

Assim que a classe [Métier] estiver presente no projeto, cria-se um novo plugin para o cliente [nuxt]. Este plugin, denominado [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)
  // registo
  console.log('[fonction client $métier créée]')
}

Feito isto, 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 utilizador pode realizar simulações?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // então o utilizador pode realizar simulações
      this.$router.push({ name: 'calcul-impot' })
      // regresso ao ciclo de eventos
      return
    }
    // se a sessão jSON já tiver sido iniciada, não a reiniciamos novamente
    ...
  },

  // gestores 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 junto do 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) {
          // exibe-se o erro
          this.message = response.réponse
          this.showError = true
          // regresso ao ciclo de eventos
          return
        }
        // sem erro
        this.showError = false
        // autenticação bem-sucedida
        this.$store.commit('replace', { authenticated: true })
        // --------- solicita-se agora os dados da administração fiscal
        // inicialmente, sem dados
        this.$métier().setTaxAdminData(null)
        // início da espera
        this.$emit('loading', true)
        // pedido em espera junto do servidor
        const response2 = await this.$dao().getAdminData()
        // fim do carregamento
        this.$emit('loading', false)
        // análise da resposta
        if (response2.état !== 1000) {
          // exibe-se o erro
          this.message = response2.réponse
          this.showError = true
          // regresso 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 avançar para o cálculo do imposto
        this.$router.push({ name: 'calcul-impot' })
      } catch (error) {
        // o erro é reportado ao componente principal
        this.$emit('error', error)
      } finally {
        // atualização da memória
        this.$store.commit('replace', { métier: this.$métier() })
        // a sessão é guardada
        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];

Feitas estas correções, podemos tentar validar a página de autenticação:

Image

A página obtida é a seguinte:

Image

Conseguimos, de facto, aceder à página de cálculo do imposto. Agora, vamos analisar os registos:

Image

Em [2], verifica-se que o facto de estarmos autenticados foi corretamente registado. Em [3-4], verifica-se que foram recuperados os dados [taxAdminData], que permitem o cálculo do imposto através da classe [Métier].

17.5. Etapa 4

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


<!-- definição HTML da vista -->
<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 gestão de eventos
  methods: {
    // resultado do cálculo do imposto
    handleResultatObtenu(résultat) {
      // construímos o resultado numa 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 da loja [Vuex]
      // uma simulação de +
      this.$store.commit('addSimulation', résultat)
      // guardamos 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 este problema através do encaminhamento. Faremos o mesmo com o projeto [nuxt-20];
  • linha 76: é feita referência a uma alteração [addSimulation] que ainda não existe. Vamos criá-la;
  • linha 78: tal como na página [index], é necessário escrever [this.$session().save(this.$nuxt.context)];

Vamos alterar o store [store/index]. Herdado do projeto [nuxt-12], apresenta-se, de momento, da seguinte forma:


/* eslint-disable no-console */

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

// alterações no armazenamento
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 este 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 store a 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")
    // coloca-se o armazenamento na sessão
    session.value.store = store.state
    // o armazenamento está agora inicializado
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // o armazenamento está a ser atualizado com o armazenamento da sessão
    store.commit('replace', session.value.store)
  }
  // a sessão é guardada
  session.save(context)
  // registo
  console.log('initStore terminé, store=', store.state)
}
  • linhas 3-27: vamos retomar o estado e as alterações da aplicação [vuejs-22] (ver documento [3]):

// estado da persiana
export const state = () => ({
  // sessão jSON iniciada
  started: false,
  // utilizador autenticado
  authenticated: false,
  // cookie de sessão PHP
  phpSessionCookie: '',
  // lista de simulações
  simulations: [],
  // o n.º 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 })
  },
  // eliminação da linha n.º índice
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // elimina-se a 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: introduzimos as propriedades já utilizadas;
  • linha 8: mantemos o cookie de sessão PHP. É fundamental para que o cliente e o servidor [nuxt] tenham a mesma sessão PHP com o servidor de cálculo do imposto;
  • linha 10: a lista das simulações realizadas pelo utilizador;
  • linha 12: o número da última simulação efetuada pelo utilizador;
  • linha 14: a camada [métier];
  • linhas 30-47: as alterações presentes no armazenamento do projeto [vuejs-22] e referenciadas pelas páginas da aplicação. O projeto [vuejs-22] tinha uma alteração denominada [clear] que esvaziava a lista de simulações. Não a incluímos porque a mutação [reset], já presente, deverá ser suficiente;
  • linhas 26-28: a mutação [reset] é alterada para ter em conta o novo conteúdo do estado;

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


<!-- definição HTML da vista -->
<template>
  <!-- formulário HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- mensagem em 12 colunas sobre 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>
      <!-- eventual 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>
      <!-- eventual 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: ''
    }
  },
  // estado 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 valor numérico >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
    },
    // validação dos 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() {
    // registo
    // eslint-disable-next-line
    console.log("FormCalculImpot created");
  },
  // gestor de eventos
  methods: {
    calculerImpot() {
      // o imposto é calculado utilizando 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()];

Depois de efetuadas estas correções, pode-se tentar uma simulação:

Image

Obtém-se a seguinte resposta:

Image

Se analisarmos os registos:

Image

  • em [9-10], verifica-se que a primeira simulação se encontra, de facto, no [store];
  • em [5], o número da última simulação foi efetivamente incrementado;

17.6. passo 5

Agora que já realizámos uma simulação, cliquemos na ligação [Liste des simulations]. Obtemos a seguinte página:

Image

O encaminhamento do cliente [nuxt] foi efetuado corretamente. Vejamos o código da página [liste-des-simulations]:


<!-- definição HTML da vista -->
<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">
          <!-- existem 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: '#', chave: '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);
      // eliminação da simulação n.º [index]
      this.$store.commit('deleteSimulation', index)
      // a sessão é guardada
      this.$session.save()
    }
  }
}
</script>
  • linhas 47-56: os destinos do menu de navegação estão corretos;
  • linha 75: a loja está corretamente referenciada;
  • linha 89: está a ser utilizada uma mutação [deleteSimulation] que integrámos na etapa anterior;
  • linha 91: esta linha deve ser reescrita como [this.$session().save(this.$nuxt.context)];

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

Image

Obtenha-se então a seguinte página:

Image

Vamos analisar os registos:

Image

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

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

Image

Obtemos a seguinte página:

Image

Portanto, o encaminhamento funcionou.

17.7. Passo 6

Resta-nos ainda gerir 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 este caso com regras de encaminhamento num ficheiro [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 encaminhamento
Vue.use(VueRouter)

// as rotas da aplicação
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 router
const router = new VueRouter({
  // as rotas
  routes,
  // o modo de visualização do URL
  mode: 'history',
  // o URL básico da aplicação
  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 a utilizadores autenticados?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // passamos à autenticação
      name: 'authentification',
    })
    // regresso ao ciclo de eventos
    return;
  }
  // caso específico do fim da sessão
  if (to.name === "finSession") {
    // limpa-se a sessão
    session.clear();
    // avançamos para a vista [authentification]
    next({
      name: 'authentification',
    })
    // regresso ao ciclo de eventos
    return;
  }
  // outros casos — vista seguinte normal do encaminhamento
  next();
})

// exportação do router
export default router
  • as linhas 64-76 geriam o caso específico do encaminhamento para o caminho [/fin-session];
  • linha 66: esvazia-se a sessão atual;
  • linhas 68-70: exibe-se a vista [authentification];

Vamos tentar fazer algo semelhante no ficheiro de encaminhamento do cliente [nuxt]:

Image

O script [client/routing.js] passa a ser o seguinte:


/* eslint-disable no-console */
export default function(context) {
  // quem executa este código?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestão 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 Nuxt
  // a ação [fin-session] recebe um novo cookie PHP (tanto no servidor como no cliente Nuxt)
  // se for o servidor a recebê-lo, o cliente deve transmiti-lo ao navegador
  // para as suas próprias comunicações com o servidor PHP
  // estamos aqui num encaminhamento do cliente

  // recuperamos o cookie da sessão PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // se existir, atribui-se 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-se para a página inicial
    context.redirect({ name: 'index' })
  }
}
  • Adicionámos as linhas [19-27] ao código existente;
  • linha 20: recupera-se o [path] do destino do percurso atual;
  • linha 21: verifica-se se é [/fin-session]. Se for:
    • linhas 23-24: a sessão é reiniciada;
    • 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 armazenamento
    context.store.commit('reset')
    // guardar o novo armazenamento na sessão e guardar 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 é apresentada com os seguintes registos:

Image

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

É importante compreender bem o que acontece no final de uma sessão:

  • a sessão [nuxt] é reiniciada: 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. Este 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 armazenamento passa a ser [true];

17.8. etapa 7

Nesta fase, a aplicação [nuxt-20] dispõe de todas as funcionalidades da aplicação [vuejs-22]. A migração parece estar concluída.

Vamos avançar um pouco mais, seguindo a lógica de [nuxt]. O método [mounted] da página [index] apresenta um problema. Este método inicia uma operação assíncrona cuja conclusão um motor de busca não irá aguardar. Sabemos que, neste caso, é necessário colocar a operação assíncrona numa função [asyncData], pois assim o servidor [nuxt] que a executa aguarda que ela termine antes de entregar a página ao motor de busca.

Recorremos aqui à função [asyncData], escrita na aplicação [nuxt-12] para a página [index]:


export default {
  name: 'InitSession',
  // componentes utilizados
  components: {
    Layout,
    Navigation
  },
  // dados assíncronos
  async asyncData(context) {
    // registo
    console.log('[index asyncData started]')
    // não se repetem as operações 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 {
      // inicia-se uma sessão jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // registo
      console.log('[index asyncData response=]', response)
      // recupera-se o cookie de sessão PHP para as próximas solicitações
      const phpSessionCookie = dao.getPhpSessionCookie()
      // o cookie de sessão PHP é guardado na sessão [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // Houve algum erro?
      if (response.état !== 700) {
        // o erro encontra-se em response.réponse
        throw new Error(response.réponse)
      }
      // regista-se que a sessão jSON foi iniciada
      context.store.commit('replace', { jsonSessionStarted: true })
      // apresenta-se o resultado
      return { result: '[succès]' }
    } catch (e) {
      // registo
      console.log('[index asyncData error=]', e)
      // regista-se que a sessão jSON não foi iniciada
      context.store.commit('replace', { jsonSessionStarted: false })
      // é sinalizado o erro
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // guarda-se o armazenamento
      const session = context.app.$session()
      session.save(context)
      // registo
      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]')
    // apenas cliente
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • linhas 13, 33, 40: é necessário alterar a propriedade [jsonSessionStarted] para [started];
  • linha 13: na aplicação [nuxt-12], apenas o servidor [nuxt] executava a página [index] e a sua função [asyncData]. O cliente [nuxt] só executava a página [index] depois de a ter recebido do servidor [nuxt] e, nessa altura, não executava a função [asyncData]. Em [nuxt-20], a situação é diferente: o link [Fin de session] irá apresentar a página [index] no ambiente do cliente [nuxt]. A função [asyncData] será então executada. No entanto, quando se acede à página [index] desta forma, a sessão [nuxt] foi reiniciada entretanto e a propriedade [started] do armazenamento tem o valor [false], pelo que a condição da linha 13 será inevitavelmente falsa. Podemos, portanto, manter [process.server] e, assim, o cliente [nuxt] não realizará este teste;
  • linhas 15, 35, 42: uma propriedade [result] é inserida nas propriedades [data] da página [index]. Em [nuxt-20], esta propriedade não será utilizada e iremos removê-la do resultado devolvido pela função;
  • linhas 61-67: este método [mounted] deve ser mantido, pois é ele que permite ao cliente [nuxt] apresentar a mensagem de erro. No entanto, a forma de gerir o erro será alterada;

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


...

<!-- dinâmica da vista -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // componentes utilizados
  components: {
    Layout
  },
  // estado do componente
  data() {
    return {
      // utilizador
      user: '',
      // a sua palavra-passe
      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) {
    // registo
    console.log('[index asyncData started]')
    // não se repete o processo se a página já tiver sido solicitada
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // inicia-se uma sessão jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // registo
      console.log('[index asyncData response=]', response)
      // recupera-se o cookie de sessão PHP para as próximas solicitações
      const phpSessionCookie = dao.getPhpSessionCookie()
      // o cookie de sessão PHP é guardado na sessão [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // Houve algum erro?
      if (response.état !== 700) {
        // o erro encontra-se em response.réponse
        throw new Error(response.réponse)
      }
      // regista-se que a sessão jSON foi iniciada
      context.store.commit('replace', { started: true })
      // sem resultados
      return
    } catch (e) {
      // registo
      console.log('[index asyncData error=]', e.message)
      // regista-se que a sessão jSON não foi iniciada
      context.store.commit('replace', { started: false })
      // o erro é sinalizado
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // o armazenamento é guardado
      const session = context.app.$session()
      session.save(context)
      // registo
      console.log('[index asyncData finished]')
    }
  },
  // ciclo de vida
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // apenas para o cliente
    console.log('[index beforeMount]')
    // gestão de eventuais erros
    if (this.showErrorLoading) {
      // registo
      console.log('[index beforeMount, showErrorLoading=true]')
      // o erro é reportado ao componente principal [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },

  // gestores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      ...
}
</script>
  • linhas 58, 65: a função [asyncData] já não torna a propriedade [result] obsoleta neste contexto;
  • linha 81: o método [beforeMount] do cliente [nuxt]. Foi preferido ao método [mounted] para gerir um eventual erro do [asyncData];
  • linha 85: verifica-se se a propriedade [errorLoading] foi definida. Esta só pode ser definida pela função [asyncData];
  • linhas 85-90: se a função [asyncData] tiver sinalizado um erro, este é encaminhado para a página [default] através do evento [error]. Era assim que a antiga função [created], que acabámos de substituir, geria um eventual erro;

Vamos fazer alguns testes.

Primeiro, eliminamos o cookie de sessão [nuxt] e o cookie de sessão PHP, caso existam. Em seguida, acedemos à 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 ter iniciado o servidor de cálculo do imposto:

Image

Vejamos os registos:

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 a sua troca de dados com o servidor de cálculo do imposto. O cliente [nuxt] irá agora utilizá-lo;

Agora, vamos identificar-nos:

Image

Obtemos a seguinte página:

Image

Em [1], recebemos uma mensagem de erro. Isto 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 encaminhamento do cliente [nuxt] do script [middleware/client/routing] :


/* eslint-disable no-console */
export default function(context) {  // quem executa este código?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestão 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 Nuxt
  // a ação [fin-session] recebe um novo cookie PHP (tanto no servidor como no cliente Nuxt)
  // se for o servidor a recebê-lo, o cliente deve transmiti-lo ao navegador
  // para as suas próprias comunicações com o servidor PHP
  // estamos aqui num encaminhamento do cliente

  // recuperamos o cookie da sessão PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // se existir, atribui-se 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-se para a página inicial
    context.redirect({ name: 'index' })
  }
}

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

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


// gestores de eventos
  methods: {
    // ----------- autenticação
    async login() {
      // recupera-se 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 da memória o cookie da sessão PHP iniciada pelo servidor [nuxt]. Após esta alteração, a página de cálculo do imposto é recuperada com sucesso, o que significa que a autenticação funcionou.

17.9. Passo 8

Temos uma aplicação funcional que funciona de acordo com a lógica [nuxt]. Tal como fizemos com a aplicação [nuxt-13], vamos analisar a navegação do servidor [nuxt]. Como já foi referido, não se espera que o utilizador introduza manualmente os URL da aplicação. Deve utilizar os links que lhe são apresentados e que são executados pelo cliente [nuxt], que funciona então no modo SPA. No entanto, vamos garantir que a navegação no servidor [nuxt] mantenha sempre a aplicação num 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] é alterado da seguinte forma:


/* eslint-disable no-console */

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

export default function(context) {
  // quem executa este 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 encaminhamento do servidor [nuxt];
  • linhas 10-12: se for o servidor [nuxt] a executar o código, utiliza-se a sua função de encaminhamento;

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


/* eslint-disable no-console */
export default function(context) {
  // quem executa este 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 vamos?
  let to = context.route.name
  
  // caso específico de /fin-session que não tem o atributo [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
  
  // eventual redirecionamento
  let redirection = ''
  // gestão do encaminhamento concluída
  let done = false
  
  // já estamos numa redireção do servidor [nuxt]?
  if (store.state.serverRedirection) {
    // não há 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
  }
  
  // controlo da navegação do servidor [nuxt]
  // segue-se 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 utilizador 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] tenha sido 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, foram efetuadas todas as verificações ---------------------
  // redirecionamento?
  if (redirection) {
    // regista-se o redirecionamento na loja
    store.commit('replace', { serverRedirection: true })
  } else {
    // sem redirecionamento
    store.commit('replace', { serverRedirection: false, from: to })
  }
  // guardamos o armazenamento na sessão [nuxt]
  const session = context.app.$session()
  session.value.store = store.state
  session.save(context)
  // é efetuado o eventual redirecionamento do servidor [nuxt]
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • neste script retomamos as ideias já desenvolvidas e utilizadas no encaminhamento do servidor [nuxt] da aplicação [nuxt-13];
  • adicionamos duas propriedades ao armazenamento da aplicação:
    • [from]: o nome da última página apresentada. Sabemos que o cliente [nuxt] dispõe desta informação, mas o servidor [nuxt] não. Vamos adicionar-lhe esta informação, armazenando no store, a cada encaminhamento do servidor [nuxt], o nome da página que vai ser apresentada. Faremos o mesmo a cada encaminhamento do cliente [nuxt]. Assim, no encaminhamento seguinte do servidor [nuxt], este encontrará no armazenamento o nome da última página apresentada pela aplicação;
    • [serverRedirection]: quando um destino de encaminhamento for recusado pelo servidor [nuxt], este efetuará um redirecionamento. Indicará então no armazenamento que o próximo destino do servidor [nuxt] é uma página de redirecionamento. Este redirecionamento provocará uma nova execução do router do servidor [nuxt]. Se este detetar que o destino atual resulta de um redirecionamento, deixará o processo prosseguir;
  • linhas 6-11: recuperam-se as informações úteis para o encaminhamento;
  • linhas 13-16: o destino [/fin-session] não está associado a uma página chamada [fin-session]. Por isso, não tem nome. Atribui-se-lhe um;
  • linha 19: o destino de um eventual redirecionamento;
  • linha 21: [done=true] quando os testes de encaminhamento estiverem concluídos;
  • linhas 23-27: como já foi referido, se o encaminhamento em curso resultar de um redirecionamento, não há nada a fazer. Com efeito, durante o encaminhamento anterior, o router decidiu que era necessário redirecionar o navegador do cliente. Não há motivo para reconsiderar essa decisão;
  • linhas 29-33: se se tratar de uma atualização da página, deixa-se que o processo decorra normalmente. Este não é um axioma válido para todas as aplicações [nuxt]: é necessário analisar, para cada página, os efeitos de uma atualização. 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 encaminhamento do servidor [nuxt] retoma o encaminhamento do cliente [nuxt]. Quando se está numa página, o encaminhamento 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, em primeiro lugar, do caso do destino [fin-session] que não corresponde a uma página existente. Se as condições estiverem reunidas (sessão iniciada, utilizador autenticado), limpa-se a sessão e redireciona-se o utilizador para a página [index];
  • linhas 49-55: se a sessão jSON com o servidor de cálculo do imposto 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 utilizador não estiver autenticado nem tiver solicitado a página de autenticação, então o utilizador é redirecionado para a página de autenticação, que é a página [index];
  • linhas 64-70: se o utilizador estiver autenticado, mas os dados [adminData] não tiverem sido obtidos, então o utilizador é 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, então os únicos destinos possíveis são [calcul-impot] e [liste-des-simulations]. Caso contrário, o encaminhamento é recusado;
  • linhas 88-95: atualiza-se o armazenamento consoante haja ou não redirecionamento;
  • linha 94: não há redirecionamento. Assim, o atual [to] passará a ser o [from] do próximo encaminhamento;
  • linhas 96-99: as informações do armazenamento são guardadas no cookie da sessão [nuxt];
  • linhas 100-103: se for necessário efetuar um redirecionamento, faz-se;

Para realizar os testes, é necessário certificar-se de que se parte de uma situação inicial, eliminando o cookie da sessão [nuxt] e o cookie da sessão PHP no servidor de cálculo do imposto:

Image

Para testar o encaminhamento do servidor [nuxt], em cada página experimente todos os URL possíveis [/, /calcul-impot, /liste-des-simulations]. Em cada caso, a aplicação deve permanecer num estado coerente.

17.10. Etapa 9

A etapa 9 consiste na implementação da aplicação [nuxt-20]. Esta requer um alojamento que ofereça um ambiente [node.js] para executar o servidor [nuxt]. Não disponho desse ambiente. O leitor poderá seguir os procedimentos descritos no parágrafo «ligação» para implementar a aplicação [nuxt-20] na sua máquina de desenvolvimento e protegê-la com um protocolo HTTPS.

17.11. Conclusion

A migração da aplicação [vuejs-22] para a aplicação [nuxt-20] está agora concluída. Destacamos alguns pontos desta migração:

  • as páginas de [vuejs-22] foram mantidas;
  • as operações assíncronas que existiam nas páginas de [vuejs-22] foram migradas para uma função [asyncData];
  • no [nuxt-20] foi necessário gerir 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, precisámos de uma sessão [nuxt];
  • tivemos de gerir o encaminhamento do servidor [nuxt];

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