Skip to content

17. Exemplo [nuxt-20]: Portar o exemplo [vuejs-22]

17.1. Introdução

Aqui, propomos portar o exemplo [vuejs-22], que era uma aplicação do tipo SPA [vue.js], para um contexto SSR [nuxt]. [vuejs-22] era uma aplicação cliente para o servidor de cálculo de impostos que apresentava as seguintes vistas:

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

Image

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

Image

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

Image

O ecrã acima mostra que a simulação n.º 1 pode ser eliminada. Isto resulta na seguinte vista:

Image

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

Image

Iremos migrar gradualmente a aplicação [vuejs-22] para a aplicação [nuxt-20]. Não iremos explicar novamente o código da [vuejs-22]. Recomendamos aos leitores que consultem o documento |Introdução ao Framework VUE.JS através de exemplos|. Os vários passos devem destacar as diferenças entre uma aplicação [vuejs] e uma aplicação [nuxt].

17.2. Passo 1

O projeto [nuxt-20] é inicialmente criado através da clonagem do projeto [nuxt-12]. Este é, de facto, um bom ponto de partida:

  • consegue comunicar com o servidor de cálculo de impostos;
  • lida corretamente com os erros que o servidor envia;
  • o cliente e o servidor [Nuxt] podem comunicar através de uma sessão [Nuxt];

Temos, portanto, uma infraestrutura inicial sólida. A nossa principal tarefa deve ser modificar:

  • as páginas. Vamos utilizar as do projeto [vuejs-22], que terão de ser adaptadas ao novo ambiente;
  • gestão do armazenamento. Devem aparecer informações adicionais (lista de simulações), e outras informações podem tornar-se desnecessárias;
  • gestão de encaminhamento [nuxt] no cliente e no servidor;

Então, primeiro, criamos o projeto [nuxt-20] clonando o projeto [nuxt-12]:

Image

Em seguida, removemos 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, authentication, get-admindata, end-session] são removidas;

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

  • as três páginas [Authentication, TaxCalculation, SimulationList] 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] do [vuejs-22], que serviu como [layout] para a aplicação [vuejs-22], vai para a pasta [layouts];

Renomeamos os elementos integrados [4]:

Image

  • em [layouts], [Main] passou a ser [default], uma vez que esse é o nome padrão para o layout de uma aplicação [nuxt];
  • em [pages], a página [Authentication] passou a ser [index], uma vez que [Authentication] desempenhava esta função na aplicação [vuejs-22];

Nesta altura, podemos compilar o projeto para ver os primeiros erros. Modificamos o ficheiro [nuxt.config] do exemplo [nuxt-12] para agora 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',
    // 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) {}
  },
  // source code directory
  srcDir: 'nuxt-20',
  // router
  router: {
    // application URL root
    base: '/nuxt-20/',
    // routing middleware
    middleware: ['routing']
  },
  // server
  server: {
    // service port, default 3000
    port: 81,
    // network addresses listened to, default localhost: 127.0.0.1
    // 0.0.0.0 = all the machine's network addresses
    host: 'localhost'
  },
  // environment
  env: {
    // axios configuration
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // session cookie configuration [nuxt]
    maxAge: 60 * 5
  }
}

Em seguida, [compilamos] o projeto:

Image

São apresentados os seguintes erros:

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 em [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] mostram que o componente [./Layout] não existe. Na verdade, este componente encontra-se agora em [@/components/layout];
  • os erros nas linhas [6-7] indicam que o componente [./Menu] não existe. De facto, agora chama-se [@/components/menu];

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

Image

Além disso, vamos corrigir os caminhos dos componentes em todas as páginas. Vamos tomar a página [calcul-impot] como exemplo:


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
// imports
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'
 
export default {
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
 

As três instruções [import] nas linhas 26–28 passam a ser:


// imports
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'

Verifique e, se necessário, corrija as instruções [import] para todos os componentes, layouts e páginas. Depois de efetuar estas correções, pode tentar uma nova [compilação]. Normalmente, não deverá haver mais erros.

Pode então tentar executar a aplicaçã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

Podemos ver que o comando [dev], combinado com o módulo [eslint], é mais rigoroso, do ponto de vista sintático, do que o comando [build]. Aqui, exige que o operador de comparação [!=] seja escrito como [!==], que é um operador mais rigoroso (também verifica o tipo dos operandos). Estes erros ocorrem na página [index.vue].

Corrigimos os erros acima e executamos novamente o projeto. Em seguida, recebemos 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 utilizando a [Correção rápida] do módulo [eslint] [2].

Reiniciamos o projeto. Não há mais erros de compilação. Em seguida, acedemos à URL [http://localhost:81/nuxt-20/] num navegador. Recebemos um erro de tempo de execução:

Image

O erro está em [index.vue] [2]. O erro [1] deve-se ao facto de, no [vuejs-22], a camada [dao] estar disponível em [this.$dao], enquanto no [nuxt-12], cuja infraestrutura adotámos, 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, vamos simplesmente renomear [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, funciona:

Image

17.3. Passo 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;

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 chamada [$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 nada a ver com o objeto [$session] da aplicação [vuejs-22], onde não existia o conceito de cookies de sessão. No entanto, servem um propósito semelhante: armazenar informações persistentes à medida que o utilizador interage com a aplicação. A sessão [nuxt] armazena 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] ao armazenar informações na sessão, e por [this.$session()] ao manipular a própria sessão;
  • para verificar o estado de uma propriedade P no store, deve escrever [this.$store.state.P];
  • Para alterar a propriedade P do store, deve escrever [this.$store.commit('replace', {P:value}]

Fazemos estas alterações na página [index]:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- titre -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1ère ligne -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- zone de saisie user -->
          <b-col cols="6">
            <b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
          </b-col>
        </b-form-group>
        <!-- 2ième ligne -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- zone de saisie password -->
          <b-col cols="6">
            <b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
          </b-col>
        </b-form-group>
        <!-- 3ième ligne -->
        <b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
        <!-- bouton de type [submit] sur une 3ième ligne -->
        <b-row>
          <b-col cols="2">
            <b-button :disabled="!valid" variant="primary" type="submit">Valider</b-button>
          </b-col>
        </b-row>
      </b-form>
    </template>
  </Layout>
</template>
 
<!-- dynamique de la vue -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // components used
  components: {
    Layout
  },
  // component status
  data() {
    return {
      // user
      user: '',
      // password
      password: '',
      // controls the display of an error msg
      showError: false,
      // the error message
      message: ''
    }
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // life cycle: the component has just been created
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // can the user run simulations?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
      // then the user can run simulations
      this.$router.push({ name: 'calculImpot' })
      // return to event loop
      return
    }
    // if the jSON session has already been started, it is not restarted again
    if (!this.$store.state.started) {
      // start waiting
      this.$emit('loading', true)
      // initialize the session with the server - asynchronous request
      // we use the promise rendered by the [dao] layer methods
      this.$dao()
        // initialize a jSON session
        .initSession()
        // we got the answer
        .then((response) => {
          // end waiting
          this.$emit('loading', false)
          // response analysis
          if (response.état !== 700) {
            // error is displayed
            this.message = response.réponse
            this.showError = true
            // return to event loop
            return
          }
          // the session has started
          this.$store.commit('replace', { started: true })
          console.log('[authentification], session=', this.$session())
        })
        // in case of error
        .catch((error) => {
          // the error is traced back to the [Main] view
          this.$emit('error', error)
        })
        // in all cases
        .finally(() => {
          // save the session
          this.$session().save()
        })
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated
        this.$store.commit('replace', { authenticated: false })
        // blocking server authentication
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // end of loading
        this.$emit('loading', false)
        // server response analysis
        if (response.état !== 200) {
          // error is displayed
          this.message = response.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // you are authenticated
        this.$store.commit('replace', { authenticated: true })
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null)
        // start waiting
        this.$emit('loading', true)
        // blocking request to the server
        const response2 = await this.$dao().getAdminData()
        // end of loading
        this.$emit('loading', false)
        // response analysis
        if (response2.état !== 1000) {
          // error is displayed
          this.message = response2.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse)
        // we can move on to tax calculation
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // the error is traced back to the main component
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier })
        // save the session
        this.$session().save()
      }
    }
  }
}
</script>

Tenha em atenção 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á-la, tal como aconteceu no exemplo [vuejs-22];
  • linha 73: fazemos referência a [this.$business], que atualmente não existe;
  • linha 75: nunca utilizámos este método numa aplicação [nuxt]. Teremos de verificar se funciona num contexto [nuxt];
  • linhas 112, 172: no [vuejs-22], a sessão do projeto foi guardada desta forma. Com o projeto [nuxt-20], o método [save] deve receber o contexto atual. Sabemos 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 que este código não está otimizado. Em vez de utilizar a função [this.$session()] várias vezes, seria melhor escrever:


const session=this.$session()

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

Com estas correções feitas, podemos recarregar a URL [http://localhost:81/nuxt-20/] num navegador. Continuamos a obter a mesma página de antes:

Image

Vamos dar uma olhada nos registos do navegador:

Image

O registo [1] é o último registo gerado pelo cliente [nuxt]. Em [2], vemos que a propriedade [started] está definida como [true], o que significa que a função [mounted] iniciou com sucesso 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 descartadas ou renomeadas. Lembre-se de que estamos a utilizar o store do exemplo [nuxt-12].

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

Image

A captura de ecrã acima é do Chrome. Depois de fazer isto, a URL [http://localhost:81/nuxt-20/] devolve o seguinte resultado:

Image

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

17.4. Passo 3

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


// event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated
        this.$store.commit('replace', { authenticated: false })
        // blocking server authentication
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // end of loading
        this.$emit('loading', false)
        // server response analysis
        if (response.état !== 200) {
          // error is displayed
          this.message = response.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // you are authenticated
        this.$store.commit('replace', { authenticated: true })
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null)
        // start waiting
        this.$emit('loading', true)
        // blocking request to the server
        const response2 = await this.$dao().getAdminData()
        // end of loading
        this.$emit('loading', false)
        // response analysis
        if (response2.état !== 1000) {
          // error is displayed
          this.message = response2.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse)
        // we can move on to tax calculation
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // the error is traced back to the main component
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier })
        // save the session
        this.$session().save(this.$nuxt.context)
      }
    }
  }

O principal problema aqui parece ser a ausência dos dados [this.$métier]. Para resolver isto, 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], o que dará acesso a esta classe;

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

Image

Assim que a classe [Métier] estiver no projeto, vamos criar um novo plugin para o cliente [nuxt]. Este plugin, chamado [pluginMétier], irá injetar uma função [$métier] que fornece acesso à classe [Métier]:


/* eslint-disable no-console */
// on crée un point d'accès à la couche [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
  // instanciation de la couche [métier]
  const métier = new Métier()
  // injection d'une fonction [$métier] dans le contexte
  inject('métier', () => métier)
  // log
  console.log('[fonction client $métier créée]')
}

Agora que isto está feito, podemos atualizar a página [index]:


// life cycle: the component has just been created
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // can the user run simulations?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // then the user can run simulations
      this.$router.push({ name: 'calcul-impot' })
      // return to event loop
      return
    }
    // if the jSON session has already been started, it is not restarted again
    ...
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated
        this.$store.commit('replace', { authenticated: false })
        // blocking server authentication
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // end of loading
        this.$emit('loading', false)
        // server response analysis
        if (response.état !== 200) {
          // error is displayed
          this.message = response.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // you are authenticated
        this.$store.commit('replace', { authenticated: true })
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier().setTaxAdminData(null)
        // start waiting
        this.$emit('loading', true)
        // blocking request to the server
        const response2 = await this.$dao().getAdminData()
        // end of loading
        this.$emit('loading', false)
        // response analysis
        if (response2.état !== 1000) {
          // error is displayed
          this.message = response2.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // the received data is stored in the [business] layer
        this.$métier().setTaxAdminData(response2.réponse)
        // we can move on to tax calculation
        this.$router.push({ name: 'calcul-impot' })
      } catch (error) {
        // the error is traced back to the main component
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier() })
        // save the session
        this.$session().save(this.$nuxt.context)
      }
    }
  }
  • linhas 43, 61, 69: [this.$métier] foi substituído por [this.$métier()];
  • linhas 8, 63: o nome da página [CalculImpot] no projeto [vuejs-22] passou a ser a página [calcul-impot] no projeto [nuxt-20];

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

Image

A página resultante é a seguinte:

Image

Chegámos com sucesso à página de cálculo de impostos. Agora, vamos ver os registos:

Image

Em [2], vemos que o estado da autenticação foi corretamente guardado. Em [3-4], vemos que os dados [taxAdminData] foram recuperados, o que permite que o imposto seja calculado pela classe [Métier].

17.5. Passo 4

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


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
// imports
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
 
export default {
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // état interne
  data() {
    return {
      // options du menu
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // résultat du calcul de l'impôt
      résultat: '',
      résultatObtenu: false
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  },
  // méthodes de gestion des évts
  methods: {
    // résultat du calcul de l'impôt
    handleResultatObtenu(résultat) {
      // on construit le résultat en chaîne HTML
      const impôt = "Montant de l'impôt : " + résultat.impôt + ' euro(s)'
      const décôte = 'Décôte : ' + résultat.décôte + ' euro(s)'
      const réduction = 'Réduction : ' + résultat.réduction + ' euro(s)'
      const surcôte = 'Surcôte : ' + résultat.surcôte + ' euro(s)'
      const taux = "Taux d'imposition : " + résultat.taux
      this.résultat = impôt + '<br/>' + décôte + '<br/>' + réduction + '<br/>' + surcôte + '<br/>' + taux
      // affichage du résultat
      this.résultatObtenu = true
      // ---- maj du store [Vuex]
      // une simulation de +
      this.$store.commit('addSimulation', résultat)
      // on sauvegarde la session
      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] resolveu este problema com o roteamento. Faremos o mesmo com o projeto [nuxt-20];
  • linha 76: estamos a referenciar um método [addSimulation] que ainda não existe. Vamos criá-lo;
  • linha 78: tal como na página [index], precisamos de escrever [this.$session().save(this.$nuxt.context)];

Vamos modificar o store [store/index]. Herdado do projeto [nuxt-12], atualmente tem este aspeto:


/* eslint-disable no-console */
 
// awning status
export const state = () => ({
  // session jSON started
  jsonSessionStarted: false,
  // authenticated user
  userAuthenticated: false,
  // session cookie PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})
 
// changes in the awning
export const mutations = {
  // state replacement
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // awning reset
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}
 
// awning actions
export const actions = {
  nuxtServerInit(store, context) {
    // who executes this code?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // init session
    initStore(store, context)
  }
}
 
function initStore(store, context) {
  // store is the blind to be initialized
  // retrieve the session
  const session = context.app.$session()
  // has the session already been initiated?
  if (!session.value.initStoreDone) {
    // start a new blind
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // put the blind in the session
    session.value.store = store.state
    // the blind is now initialized
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // update the store with the session store
    store.commit('replace', session.value.store)
  }
  // save the session
  session.save(context)
  // log
  console.log('initStore terminé, store=', store.state)
}
  • linhas 3–27: vamos retomar o estado e as mutações da aplicação [vuejs-22] (ver documento [3]):

// awning status
export const state = () => ({
  // session jSON started
  started: false,
  // authenticated user
  authenticated: false,
  // session cookie PHP
  phpSessionCookie: '',
  // list of simulations
  simulations: [],
  // last simulation number
  idSimulation: 0,
  // business] layer
  métier: null
})
 
// changes in the awning
export const mutations = {
  // state replacement
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // awning reset
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
  },
  // delete line n° index
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // delete line no. [index]
    state.simulations.splice(index, 1)
    console.log('store simulations', state.simulations)
  },
  // add a simulation
  addSimulation(state, simulation) {
    // eslint-disable-next-line no-console
    console.log('mutation addSimulation')
    // simulation no
    state.idSimulation++
    simulation.id = state.idSimulation
    // add the simulation to the simulation table
    state.simulations.push(simulation)
  }
}
  • linhas 4 e 6: apresentamos as propriedades já utilizadas;
  • linha 8: armazenamos o cookie de sessão PHP. Isto é essencial para que o cliente e o servidor [nuxt] partilhem a mesma sessão PHP com o servidor de cálculo de impostos;
  • linha 10: a lista de simulações realizadas pelo utilizador;
  • linha 12: o número da última simulação realizada pelo utilizador;
  • linha 14: a camada [business];
  • linhas 30–47: as mutações presentes no store do projeto [vuejs-22] e referenciadas pelas páginas da aplicação. O projeto [vuejs-22] tinha uma mutação chamada [clear] que limpava a lista de simulações. Não a estamos a incluir porque a mutação [reset] já presente deve ser suficiente;
  • linhas 26–28: a mutação [reset] é modificada para ter em conta o novo conteúdo do estado;

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


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

Depois de feitas estas correções, podemos tentar executar uma simulação:

Image

Recebemos a seguinte resposta:

Image

Se analisarmos os registos:

Image

  • em [9-10], vemos que a primeira simulação está, de facto, na [loja];
  • em [5], o número da última simulação foi efetivamente incrementado;

17.6. Passo 5

Agora que executámos uma simulação, vamos clicar na ligação [Lista de Simulações]. Obtemos a seguinte página:

Image

O encaminhamento para o cliente [nuxt] foi bem-sucedido. Vamos ver o código da página [simulation-list]:


<!-- définition HTML de la vue -->
<template>
  <div>
    <!-- mise en page -->
    <Layout :left="true" :right="true">
      <!-- simulations dans colonne de droite -->
      <template slot="right">
        <template v-if="simulations.length == 0">
          <!-- pas de simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length != 0">
          <!-- il y a des simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tableau des simulations -->
          <b-table :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 navigation dans colonne de gauche -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>
 
<script>
// imports
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne
  data() {
    return {
      // options du menu de navigation
      options: [
        {
          text: "Calcul de l'impôt",
          path: '/calcul-impot'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // paramètres de la table HTML
      fields: [
        { label: '#', key: 'id' },
        { label: 'Marié', key: 'marié' },
        { label: "Nombre d'enfants", key: 'enfants' },
        { label: 'Salaire', key: 'salaire' },
        { label: 'Impôt', key: 'impôt' },
        { label: 'Décôte', key: 'décôte' },
        { label: 'Réduction', key: 'réduction' },
        { label: 'Surcôte', key: 'surcôte' },
        { label: '', key: 'action' }
      ]
    }
  },
  // état interne calculé
  computed: {
    // liste des simulations prise dans le store Vuex
    simulations() {
      return this.$store.state.simulations
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  },
  // méthodes
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // suppression de la simulation n° [index]
      this.$store.commit('deleteSimulation', index)
      // on sauvegarde la session
      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: estamos a utilizar uma mutação [deleteSimulation] que adicionámos no passo 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

Em seguida, obtemos a seguinte página:

Image

Vamos dar uma olhada nos registos:

Image

  • Em [6], vemos que a tabela de simulação está vazia;

Agora, voltemos ao formulário de cálculo de impostos:

Image

Aparece a seguinte página:

Image

Portanto, o encaminhamento funcionou.

17.7. Passo 6

Ainda precisamos de tratar da opção de navegação [Terminar sessão] no menu de navegação:

// options du menu
      options: [
        {
          text: 'List of simulations',
          path: '/list-of-simulations
        },
        {
          text: 'End of session',
          path: '/end-session'
        }
]
  • na linha 9, a página [/end-session] não existe. O projeto [vuejs-22] tratou este caso com regras de roteamento num ficheiro [router.js]:

// imports
import Vue from 'vue'
import VueRouter from 'vue-router'
// les vues
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// la session
import session from './session'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification2', component: Authentification },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession'
  },
  // page inconnue
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des URL
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// vérification des routes
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // route réservée aux utilisateurs authentifiés ?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // on passe à l'authentification
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // cas particulier de la fin de session
  if (to.name === "finSession") {
    // on nettoie la session
    session.clear();
    // on va sur la vue [authentification]
    next({
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // autres cas - vue suivante normale du routage
  next();
})
 
// export du router
export default router
  • As linhas 64–76 tratavam do caso especial do caminho para o caminho [/end-session];
  • linha 66: limpa a sessão atual;
  • linhas 68–70: exibem a vista [authentication];

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

Image

O script [client/routing.js] fica assim:


/* eslint-disable no-console */
export default function(context) {
  // who executes this code?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // management of the PHP session cookie in the browser
  // the browser's PHP session cookie must be identical to the one found in the nuxt session
  // the [end-session] action receives a new PHP cookie (server as nuxt client)
  // if the server receives it, the client must pass it on to the browser
  // for its own exchanges with the PHP server
  // this is customer routing
 
  // retrieve the session cookie PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // if it exists, we assign the PHP session cookie to the browser
    document.cookie = phpSessionCookie
  }
 
  // where are we going?
  const to = context.route.path
  if (to === '/fin-session') {
    // clean the session
    const session = context.app.$session()
    session.reset(context)
    // redirects to the index page
    context.redirect({ name: 'index' })
  }
}
  • Adicionámos as linhas [19-27] ao código existente;
  • linha 20: recuperamos o [path] do destino da rota atual;
  • linha 21: verificamos se é [/end-session]. Se for:
    • linhas 23-24: a sessão é reiniciada;
    • linha 26: redirecionamos o cliente [nuxt] para a página inicial;

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


// reset de la session
  reset(context) {
    console.log('nuxt-session reset')
    // reset du store
    context.store.commit('reset')
    // sauvegarde du nouveau store en session et sauvegarde de la session
    this.save(context)
}

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


// reset du store
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}

Quando clicamos agora no link [Terminar sessão], a página inicial é apresentada com os seguintes registos:

Image

  • em [3], vemos que já não estamos autenticados;
  • em [4], vemos que a sessão JSON foi iniciada;
  • em [6], a camada [business] já não está presente na loja (continua presente nas páginas através de [this.$business()]);
  • em [5, 7], já não há simulações;

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

  • a sessão [nuxt] é reiniciada: a propriedade [started] do store muda para [false];
  • ocorre um redirecionamento para a página [index];
  • o método [mounted] da página [index] é executado. Isto 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 a [true];

17.8. Passo 7

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

Vamos dar mais um passo no espírito do [nuxt]. O método [mounted] da página [index] coloca um problema. Ele inicia uma operação assíncrona que um motor de busca não vai esperar que termine. Sabemos que, neste caso, devemos colocar a operação assíncrona dentro de uma função [asyncData], pois assim o servidor [nuxt] que a executa irá esperar que ela termine antes de entregar a página ao motor de busca.

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


export default {
  name: 'InitSession',
  // components used
  components: {
    Layout,
    Navigation
  },
  // asynchronous data
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // don't do things twice if the page has already been requested
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
      // start a jSON session
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // retrieve session cookie PHP for future requests
      const phpSessionCookie = dao.getPhpSessionCookie()
      // we store the PHP session cookie in the [nuxt] session
      context.store.commit('replace', { phpSessionCookie })
      // was there a mistake?
      if (response.état !== 700) {
        // the error is in response.réponse
        throw new Error(response.réponse)
      }
      // note that the jSON session has started
      context.store.commit('replace', { jsonSessionStarted: true })
      // we return the result
      return { result: '[succès]' }
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e)
      // note that session jSON has not started
      context.store.commit('replace', { jsonSessionStarted: false })
      // we report the error
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // save the blind
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // life cycle
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    console.log('[index beforeMount]')
  },
  mounted() {
    console.log('[index mounted]')
    // customer only
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • linhas 13, 33, 40: altere a propriedade [jsonSessionStarted] para [started];
  • linha 13: na aplicação [nuxt-12], apenas o servidor [nuxt] executou a página [index] e a sua função [asyncData]. O cliente [nuxt] só executou a página [index] depois de a receber do servidor [nuxt] e, por isso, não executou a função [asyncData]. No [nuxt-20], é diferente: o link [End Session] irá apresentar a página [index] no ambiente do cliente [nuxt]. A função [asyncData] será então executada. No entanto, ao chegar à página [index] desta forma, a sessão [nuxt] foi reiniciada entretanto, e a propriedade [started] do store é [false], pelo que a condição na linha 13 será necessariamente falsa. Podemos, portanto, omitir [process.server], e o cliente [nuxt] não realizará esta verificação;
  • linhas 15, 35, 42: uma propriedade [result] é adicionada às propriedades [data] da página [index]. No [nuxt-20], esta propriedade não será utilizada, pelo que a removeremos do resultado devolvido pela função;
  • Linhas 61–67: Este método [mounted] deve ser mantido, pois é ele que permite ao cliente [nuxt] exibir a mensagem de erro. No entanto, a forma como o erro é tratado será modificada;

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 para a página [index] no exemplo [nuxt-20] passa então a ser o seguinte:


...
 
<!-- dynamique de la vue -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // components used
  components: {
    Layout
  },
  // component status
  data() {
    return {
      // user
      user: '',
      // password
      password: '',
      // error display
      showError: false
    }
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // asynchronous data
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // don't do things twice if the page has already been requested
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // start a jSON session
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // retrieve session cookie PHP for future requests
      const phpSessionCookie = dao.getPhpSessionCookie()
      // we store the PHP session cookie in the [nuxt] session
      context.store.commit('replace', { phpSessionCookie })
      // was there a mistake?
      if (response.état !== 700) {
        // the error is in response.réponse
        throw new Error(response.réponse)
      }
      // note that the jSON session has started
      context.store.commit('replace', { started: true })
      // no result
      return
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e.message)
      // note that session jSON has not started
      context.store.commit('replace', { started: false })
      // we report the error
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // save the blind
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // life cycle
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // customer only
    console.log('[index beforeMount]')
    // error handling
    if (this.showErrorLoading) {
      // log
      console.log('[index beforeMount, showErrorLoading=true]')
      // the error is traced back to the main component [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      ...
}
</script>
  • linhas 58, 65: a função [asyncData] já não renderiza a propriedade [result] aqui;
  • linha 81: o método [beforeMount] do cliente [nuxt]. Foi escolhido em vez do método [mounted] para lidar com quaisquer erros potenciais do [asyncData];
  • linha 85: verificamos se a propriedade [errorLoading] foi definida. Ela só pode ser definida pela função [asyncData];
  • linhas 85–90: se a função [asyncData] tiver reportado um erro, passamo-lo para a página [default] através do evento [error]. Era assim que a antiga função [created], que acabámos de substituir, tratava potenciais erros;

Vamos realizar alguns testes.

Primeiro, eliminamos tanto o cookie de sessão [nuxt] como o cookie de sessão PHP, caso existam. Em seguida, solicitamos a página [http://localhost:81/nuxt-20/] enquanto o servidor de cálculo de impostos não está a funcionar. Obtemos a seguinte página:

Image

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

Image

Vamos ver 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 de impostos. O cliente [nuxt] irá agora utilizá-lo;

Agora vamos iniciar sessão:

Image

Aparece a seguinte página:

Image

Em [1], recebemos uma mensagem de erro. Isto significa que o navegador não enviou o cookie de sessão correto da sessão PHP iniciada pelo servidor [nuxt] no passo anterior. Em [nuxt-12], o cookie de sessão PHP foi passado do servidor [nuxt] para o cliente [nuxt] no roteamento do cliente [nuxt] do script [middleware/client/routing]:


/* eslint-disable no-console */
export default function(context) {  // who executes this code?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // management of the PHP session cookie in the browser
  // the browser's PHP session cookie must be identical to the one found in the nuxt session
  // acion [fin-session] receives a new cookie PHP (server as nuxt client)
  // if the server receives it, the client must pass it on to the browser
  // for its own exchanges with the PHP server
  // this is customer routing
 
  // retrieve the session cookie PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // if it exists, we assign the PHP session cookie to the browser
    document.cookie = phpSessionCookie
  }
 
  // where are we going?
  const to = context.route.path
  if (to === '/fin-session') {
    // clean the session
    const session = context.app.$session()
    session.reset(context)
    // redirects to the index page
    context.redirect({ name: 'index' })
  }
}

As linhas 13–17 permitem que o cliente [nuxt] recupere o cookie de sessão PHP do servidor [nuxt].

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


// event managers
  methods: {
    // ----------- authentication
    async login() {
      // retrieve the PHP session cookie from the store
      const phpSessionCookie = this.$store.state.phpSessionCookie
      if (phpSessionCookie) {
        // if it exists, we assign the PHP session cookie to the browser
        document.cookie = phpSessionCookie
      }
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated

Linhas 5–10: Recuperamos o cookie de sessão PHP iniciado pelo servidor [nuxt] a partir do store. Assim que esta alteração é feita, a página de cálculo de impostos carrega com sucesso, indicando que a autenticação funcionou.

17.9. Passo 8

Temos uma aplicação funcional que funciona no espírito [nuxt]. Tal como fizemos para a aplicação [nuxt-13], vamos concentrar-nos na navegação no servidor [nuxt]. Conforme mencionado anteriormente, não se espera que o utilizador digite manualmente os URLs da aplicação. Espera-se que utilize os links que lhe são apresentados, os quais são executados pelo cliente [nuxt], que passa então a operar 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 em link), sabemos que precisamos de:

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

Image

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


/* eslint-disable no-console */
 
// on importe les middleware du serveur et du client
import serverRouting from './server/routing'
import clientRouting from './client/routing'
 
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware], process.server', process.server, ', process.client=', process.client)
  if (process.server) {
    // routage serveur
    serverRouting(context)
  } else {
    // routage client
    clientRouting(context)
  }
}
  • linha 4: importamos o script de roteamento do servidor [nuxt];
  • linhas 10–12: se o servidor [nuxt] estiver a executar o código, usamos a sua função de roteamento;

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


/* eslint-disable no-console */
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware server], process.server', process.server, ', process.client=', process.client)
 
  // on récupère quelques informations ici et là
  const store = context.store
  // d'où vient-on ?
  const from = store.state.from || 'nowhere'
  // où va-t-on ?
  let to = context.route.name
 
  // cas particulier de /fin-session qui n'a pas d'attribut [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
 
  // éventuelle redirection
  let redirection = ''
  // gestion du routage terminé
  let done = false
 
  // est-on déjà dans une redirection du serveur [nuxt]?
  if (store.state.serverRedirection) {
    // rien à faire
    done = true
  }
 
  // s'agit-il d'un rechargement de page ?
  if (!done && from === to) {
    // rien à faire
    done = true
  }
 
  // contrôle de la navigation du serveur [nuxt]
  // on se calque sur le menu de navigation du client
 
  // on traite d'abord le cas de fin-session
  if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
    // on nettoie la session
    const session = context.app.$session()
    session.reset(context)
    // on redirige vers la page index
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où la session PHP n'a pas démarré
  if (!done && !store.state.started && to !== 'index') {
    // redirection vers [index]
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où l'utilisateur n'est pas authentifié
  if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où [adminData] n'a pas été obtenu
  if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
    // redirection vers [index]
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où [adminData] a été obtenu
  if (
    !done &&
    store.state.started &&
    store.state.authenticated &&
    store.state.métier.taxAdminData &&
    to !== 'calcul-impot' &&
    to !== 'liste-des-simulations'
  ) {
    // on reste sur la même page
    redirection = from
    // travail terminé
    done = true
  }
 
  // on a normalement fait tous les contrôles ---------------------
  // redirection ?
  if (redirection) {
    // on note la redirection dans le store
    store.commit('replace', { serverRedirection: true })
  } else {
    // pas de redirection
    store.commit('replace', { serverRedirection: false, from: to })
  }
  // on sauvegarde le store dans la session [nuxt]
  const session = context.app.$session()
  session.value.store = store.state
  session.save(context)
  // on fait l'éventuelle redirection du serveur [nuxt]
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • Neste script, reutilizamos os conceitos já desenvolvidos e utilizados no roteamento do servidor [nuxt] da aplicação [nuxt-13];
  • Adicionamos duas propriedades ao armazenamento da aplicação:
    • [from]: o nome da última página exibida. Sabemos que o cliente [nuxt] tem esta informação, mas o servidor [nuxt] não. Iremos adicionar esta informação, armazenando o nome da página a ser exibida no store cada vez que o servidor [nuxt] efetuar o roteamento. Faremos o mesmo para cada roteamento no cliente [nuxt]. Assim, no próximo roteamento pelo servidor [nuxt], este encontrará na loja o nome da última página exibida pela aplicação;
    • [serverRedirection]: Quando um destino de encaminhamento é rejeitado pelo servidor [nuxt], este executará um redirecionamento. Indicará então no armazenamento que o próximo destino do servidor [nuxt] é uma página de redirecionamento. Este redirecionamento desencadeará uma nova execução do encaminhador do servidor [nuxt]. Se o encaminhador detetar que o destino atual é o resultado de um redirecionamento, permitirá que este prossiga;
  • linhas 6–11: recuperamos as informações necessárias para o encaminhamento;
  • linhas 13–16: o destino [/end-session] não está associado a uma página chamada [end-session]. Por isso, não tem nome. Atribuímos-lhe um;
  • linha 19: o destino de um possível redirecionamento;
  • linha 21: [done=true] quando os testes de encaminhamento estiverem concluídos;
  • linhas 23–27: como mencionado, se o encaminhamento atual resultar de um redirecionamento, não há nada a fazer. De facto, durante o encaminhamento anterior, o router decidiu que o navegador do cliente deveria ser redirecionado. Não há necessidade de reconsiderar esta decisão;
  • linhas 29–33: se for uma recarga da página, deixamos que isso aconteça. Esta não é uma regra universal para todas as aplicações [Nuxt]: deve examinar os efeitos de uma recarga para cada página. Aqui, verifica-se que recarregar as páginas [index, tax-calculation, simulation-list] não causa quaisquer efeitos indesejáveis;
  • linhas 35–85: o encaminhamento do servidor [nuxt] espelha o encaminhamento do cliente [nuxt]. Quando numa página, o encaminhamento do servidor [nuxt] deve refletir o menu de navegação fornecido pelo cliente [nuxt] enquanto estiver nessa página;
  • linhas 38–47: tratamos primeiro o caso em que o destino [end-session] não corresponde a uma página existente. Se as condições forem satisfeitas (sessão iniciada, utilizador autenticado), limpamos a sessão e redirecionamos o utilizador 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 utilizador não estiver autenticado e não tiver solicitado a página de autenticação, redirecionamos para a página de autenticação, que é a página [index];
  • linhas 64–70: se o utilizador estiver autenticado, mas os [adminData] não tiverem sido obtidos, o utilizador é redirecionado para a página de autenticação. A autenticação faz duas coisas: autentica o utilizador e, se a autenticação for bem-sucedida, também solicita os dados [adminData]. Se estes dados não tiverem sido obtidos, a autenticação deve ser reiniciada;
  • linhas 72–85: se os dados [adminData] tiverem sido obtidos, então os únicos destinos possíveis são [tax-calculation] e [simulation-list]. Se não for esse o caso, o encaminhamento é negado;
  • linhas 88–95: o armazenamento é atualizado dependendo de haver ou não um redirecionamento;
  • linha 94: não há redirecionamento. Portanto, o atual [to] torna-se o [from] para o próximo encaminhamento;
  • linhas 96–99: as informações da loja são guardadas no cookie de sessão [nuxt];
  • linhas 100–103: se for necessário um redirecionamento, este é executado;

Para executar os testes, certifique-se de começar do zero, eliminando o cookie de sessão [nuxt] e o cookie de sessão PHP com o servidor de cálculo de impostos:

Image

Para testar o roteamento do servidor [nuxt], experimente todos os URLs possíveis [/, /tax-calculation, /simulation-list] em cada página. Em cada ocasião, a aplicação deve permanecer num estado consistente.

17.10. Passo 9

O Passo 9 envolve a implementação da aplicação [nuxt-20]. Isto requer um alojamento que forneça um ambiente [node.js] para executar o servidor [nuxt]. Eu não disponho disso. O leitor pode seguir os procedimentos descritos na secção indicada para implementar a aplicação [nuxt-20] na sua máquina de desenvolvimento e protegê-la com um protocolo HTTPS.

17.11. Conclusão

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

  • as páginas [vuejs-22] foram mantidas;
  • as operações assíncronas que existiam nas páginas [vuejs-22] foram migradas para uma função [asyncData];
  • no [nuxt-20], tivemos de gerir duas entidades: o cliente [nuxt] e o servidor [nuxt]. A última entidade não existia no [vuejs-22]. Para manter a consistência entre as duas entidades, precisámos de uma sessão [nuxt];
  • tínhamos de gerir o encaminhamento do servidor [nuxt];

Na prática, é provavelmente preferível começar diretamente com uma arquitetura [nuxt] em vez de construir uma arquitetura [vue.js] e depois portá-la para um ambiente [nuxt].