Skip to content

17. Ejemplo [nuxt-20]: adaptación del ejemplo [vuejs-22]

17.1. Présentation

Aquí nos proponemos adaptar el ejemplo [vuejs-22], que era una aplicación [vue.js] del tipo SPA, a un contexto [nuxt] SSR. [vuejs-22] era una aplicación cliente del servidor de cálculo de impuestos que presentaba las siguientes vistas:

La primera vista es la de autenticación:

Image

La segunda vista es la del cálculo de impuestos:

Image

La tercera vista es la que muestra la lista de simulaciones realizadas por el usuario:

Image

La pantalla anterior muestra que se puede eliminar la simulación n.º 1. Al hacerlo, se obtiene la siguiente vista:

Image

Si ahora eliminamos la última simulación, obtenemos la siguiente vista:

Image

Vamos a migrar la aplicación [vuejs-22] a la aplicación [nuxt-20] de manera gradual. No volveremos a explicar los códigos de [vuejs-22]. Se invita al lector a volver a leer el documento |Introducción al marco de trabajo VUE.JS a través de un ejemplo|. Los distintos pasos deberían mostrar las diferencias entre una aplicación [vuejs] y una aplicación [nuxt].

17.2. paso 1

El proyecto [nuxt-20] se obtiene inicialmente al copiar el proyecto [nuxt-12]. Este último es, de hecho, un buen punto de partida:

  • sabe comunicarse con el servidor de cálculo de impuestos;
  • gestiona correctamente los errores que este envía;
  • el cliente y el servidor [nuxt] pueden comunicarse a través de una sesión [nuxt];

Así que contamos con una buena infraestructura inicial. Nuestra tarea principal debería consistir en modificar:

  • las páginas. Tomaremos las del proyecto [vuejs-22], que habrá que adaptar al nuevo entorno;
  • la gestión del almacén. Debería aparecer información adicional (lista de simulaciones) y otra podría volverse innecesaria;
  • la gestión del enrutamiento del cliente y del servidor [nuxt];

Así que, en primer lugar, creamos el proyecto [nuxt-20] copiando el proyecto [nuxt-12]:

Image

Luego se eliminan las páginas y los componentes que ya no son necesarios [2]:

  • el componente [components/navigation] desaparece;
  • el diseño [layout/default] desaparece;
  • las páginas [index, authentification, get-admindata, fin-session] desaparecen;

Luego se integran en [nuxt-20] elementos de [vuejs-22] y [3]:

  • las tres páginas [Authentification, CalculImpot, ListeSimulations] de la aplicación [vuejs-22] se trasladan a la carpeta [pages];
  • los componentes [FormCalculImpot, Menu, Layout] de la aplicación [vuejs-22] se colocan en la carpeta [components];
  • la página [Main] de [vuejs-22], que servía como [layout] para la aplicación [vuejs-22], se coloca en la carpeta [layouts];

Se renombran los elementos integrados [4]:

Image

  • en [layouts], [Main] pasó a ser [default], ya que ese es el nombre predeterminado del diseño de una aplicación [nuxt];
  • en [pages], la página [Authentification] pasó a ser [index], ya que [Authentification] desempeñaba esa función en la aplicación [vuejs-22];

En este punto, podemos compilar el proyecto para ver los primeros errores. Modificamos el archivo [nuxt.config] del ejemplo [nuxt-12] para que ahora ejecute [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) {}
  },
  // directorio del código fuente
  srcDir: 'nuxt-20',
  // enrutador
  router: {
    // raíz de los URL de la aplicación
    base: '/nuxt-20/',
    // middleware de enrutamiento
    middleware: ['routing']
  },
  // servidor
  server: {
    // puerto de servicio, 3000 por defecto
    port: 81,
    // direcciones de red en las que escucha, por defecto localhost: 127.0.0.1
    // 0.0.0.0 = todas las direcciones de red de la máquina
    host: 'localhost'
  },
  // entorno
  env: {
    // configuración de Axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/versión-14',
    // configuración de la cookie de sesión [nuxt]
    maxAge: 60 * 5
  }
}

A continuación, se genera un [build] del proyecto:

Image

Los errores reportados son los siguientes:

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
  • el error de la línea 1 indica que se hace referencia a una imagen que no existe. La recuperaremos en [vuejs-22];
  • el error de la línea 2 muestra que el componente [./FormCalculImpot] no existe. De hecho, este componente ahora se encuentra en [@/components/form-calcul-impot];
  • los errores de las líneas [3-5] indican que el componente [./Layout] no existe. De hecho, este componente ahora se encuentra en [@/components/layout];
  • los errores en las líneas [6-7] indican que el componente [./Menu] no existe. De hecho, ahora se llama [@/components/menu];

Agregamos la imagen [assets/logo.jpg] al proyecto [nuxt-20]:

Image

Además, en todas las páginas corregiremos la ruta de los componentes. Tomemos como ejemplo la página [calcul-impot]:


<!-- definición HTML de la vista -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulario de cálculo del impuesto a la derecha -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- menú de navegación a la izquierda -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- área de visualización de los resultados del cálculo del impuesto debajo del formulario -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- área vacía de tres columnas -->
      <b-col sm="3" />
      <!-- área de nueve columnas -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

<script>
// importaciones
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'

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

Los tres [import] de las líneas 26 a 28 se convierten en:


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

De esta manera, se revisan y, si es necesario, se corrigen los [import] de todos los componentes, diseños y páginas. Una vez realizadas estas correcciones, se puede intentar un nuevo [build]. Normalmente ya no habrá errores.

Entonces se puede intentar una ejecución:

Image

Aparecen errores:

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

Se observa que el comando [dev], combinado con el módulo [eslint], es más estricto, a nivel sintáctico, que el comando [build]. En este caso, exige que el operador de comparación [!=] se escriba como [!==], que es un operador más estricto (también verifica el tipo de los operandos). Estos errores se producen en la página [index.vue].

Corregimos los errores anteriores y volvemos a ejecutar el proyecto. Entonces aparece una advertencia del 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

Corregimos este error con el módulo [Quick fix] del módulo [eslint] [2].

Se vuelve a ejecutar el proyecto. Ya no hay errores de compilación. A continuación, se abre el archivo URL [http://localhost:81/nuxt-20/] en un navegador. Se produce un error de ejecución:

Image

El error se encuentra en [index.vue] [2]. El error [1] se debe a que en [vuejs-22], la capa [dao] estaba disponible en [this.$dao], mientras que en [nuxt-12], cuya infraestructura hemos adoptado, está disponible en la función [this.$dao()].

El error se encuentra en la función [created] del ciclo de vida de la página [index]:

Image

Por el momento, nos limitamos a cambiar el nombre de [created] a [created2] para que no se ejecute la función del ciclo de vida [created] [3].

Guardamos el cambio y volvemos a cargar la página [index] en el navegador. Esta vez sí funciona:

Image

17.3. paso 2

Las páginas del proyecto [vuejs-22] utilizaban los siguientes elementos inyectados:

  • $dao: para la capa [dao] del cliente [vue.js];
  • $session: para una sesión almacenada en el [localStorage] del navegador;

Estos elementos ya no existen en la infraestructura del proyecto [nuxt-12] que hemos copiado:

  • ahora hay dos capas [dao], una para el cliente [nuxt] y otra para el servidor [nuxt]. Ambas están disponibles a través de una función inyectada llamada [$dao]. Esto significa que, en las páginas de la aplicación, [this.$dao] debe sustituirse por [this.$dao()];
  • la sesión [nuxt], administrada por la aplicación [nuxt-20], ya no tiene nada que ver con el objeto [$session] de laaplicación [vuejs-22], donde no existía el concepto de cookie de sesión. Sin embargo, ambas tienen una funcionalidad similar: almacenar información persistente a medida que el usuario realiza acciones. La sesión [nuxt] almacena la información en el almacén en lugar de hacerlo directamente en la sesión. En las páginas de la aplicación, [this.$session] debe sustituirse por [this.$store] cuando se trate de almacenar información en la sesión y por [this.$session()] cuando se trate de manipular la sesión misma;
  • para conocer el estado de una propiedad P del almacén, se deberá escribir [this.$store.state.P];
  • para cambiar la propiedad P del almacén, hay que escribir [this.$store.commit(‘replace’, {P:value}];

Realizamos estas modificaciones en la página [index]:


<!-- definición HTML de la vista -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulario HTML: se envían sus valores con la acción [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>
        <!-- primera línea -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- campo de entrada de usuario -->
          <b-col cols="6">
            <b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
          </b-col>
        </b-form-group>
        <!-- segunda línea -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- campo de entrada de contraseña -->
          <b-col cols="6">
            <b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
          </b-col>
        </b-form-group>
        <!-- tercera línea -->
        <b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
        <!-- botón de tipo [submit] en una tercera línea -->
        <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 de la vista -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // componentes utilizados
  components: {
    Layout
  },
  // estado del componente
  data() {
    return {
      // usuario
      user: '',
      // su contraseña
      password: '',
      // controla la visualización de un mensaje de error
      showError: false,
      // el mensaje de error
      message: ''
    }
  },

  // propiedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // ciclo de vida: el componente acaba de crearse
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // ¿Puede el usuario realizar simulaciones?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
      // entonces el usuario puede realizar simulaciones
      this.$router.push({ name: 'calculImpot' })
      // regreso al bucle de eventos
      return
    }
    // si la sesión jSON ya se ha iniciado, no se vuelve a iniciar
    if (!this.$store.state.started) {
      // comienza la espera
      this.$emit('loading', true)
      // se inicializa la sesión con el servidor: solicitud asíncrona
      // se utiliza la promesa devuelta por los métodos de la capa [dao]
      this.$dao()
        // se inicia una sesión jSON
        .initSession()
        // se ha obtenido la respuesta
        .then((response) => {
          // fin de la espera
          this.$emit('loading', false)
          // análisis de la respuesta
          if (response.état !== 700) {
            // se muestra el error
            this.message = response.réponse
            this.showError = true
            // regreso al bucle de eventos
            return
          }
          // la sesión ha comenzado
          this.$store.commit('replace', { started: true })
          console.log('[authentification], session=', this.$session())
        })
        // en caso de error
        .catch((error) => {
          // se remite el error a la vista [Main]
          this.$emit('error', error)
        })
        // en todos los casos
        .finally(() => {
          // se guarda la sesión
          this.$session().save()
        })
    }
  },

  // gestores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      try {
        // inicio de espera
        this.$emit('loading', true)
        // aún no se ha autenticado
        this.$store.commit('replace', { authenticated: false })
        // autenticación bloqueante en el servidor
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // fin de la carga
        this.$emit('loading', false)
        // análisis de la respuesta del servidor
        if (response.état !== 200) {
          // se muestra el error
          this.message = response.réponse
          this.showError = true
          // regreso al bucle de eventos
          return
        }
        // sin error
        this.showError = false
        // se ha autenticado
        this.$store.commit('replace', { authenticated: true })
        // --------- ahora se solicitan los datos de la administración tributaria
        // al inicio, no hay datos
        this.$métier.setTaxAdminData(null)
        // comienza la espera
        this.$emit('loading', true)
        // solicitud bloqueada en el servidor
        const response2 = await this.$dao().getAdminData()
        // fin de la carga
        this.$emit('loading', false)
        // análisis de la respuesta
        if (response2.état !== 1000) {
          // se muestra el error
          this.message = response2.réponse
          this.showError = true
          // regreso al bucle de eventos
          return
        }
        // sin error
        this.showError = false
        // se almacena el dato recibido en la capa [métier]
        this.$métier.setTaxAdminData(response2.réponse)
        // se puede pasar al cálculo del impuesto
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // se reporta el error al componente principal
        this.$emit('error', error)
      } finally {
        // actualización del almacén
        this.$store.commit('replace', { métier: this.$métier })
        // se guarda la sesión
        this.$session().save()
      }
    }
  }
}
</script>

Tenga en cuenta lo siguiente:

  • línea 69: la función [created2] ha sido renombrada como [mounted], para que el servidor [nuxt] no la ejecute (este no ejecuta ni [beforeMount] ni [mounted]). Solo el cliente [nuxt] lo ejecutará, tal como sucedía con el ejemplo [vuejs-22];
  • línea 73: se hace referencia a [this.$métier], que por el momento no existe;
  • línea 75: nunca hemos utilizado este método en una aplicación [nuxt]. Habrá que ver si funciona en un contexto [nuxt];
  • líneas 112 y 172: en [vuejs-22], la sesión del proyecto se guardaba de esta manera. Con el proyecto [nuxt-20], el método [save] debe recibir el contexto actual. Se sabe que en una página [nuxt], el objeto [context] está disponible en [this.$nuxt.context];

Por lo tanto, las líneas 112 y 172 se reescriben de la siguiente manera:


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

Cabe señalar que este código no está optimizado. En lugar de utilizar varias veces la función [this.$session()], sería preferible escribir:


const session=this.$session()

y luego utilizar la variable [session]. Se puede aplicar el mismo razonamiento a la función [this.$dao()].

Una vez realizadas estas correcciones, podemos volver a cargar URL y [http://localhost:81/nuxt-20/] en un navegador. Seguimos obteniendo la misma página que antes:

Image

Veamos los registros del navegador:

Image

El registro [1] es el último registro generado por el cliente [nuxt]. En [2], vemos que la propiedad [started] está en [vrai], lo que significa que la función [mounted] logró iniciar una sesión jSON con el servidor de cálculo de impuestos. También vemos que el almacén tiene propiedades que habrá que eliminar o renombrar. Recordemos que estamos utilizando el almacén del ejemplo [nuxt-12].

Ahora volvamos a solicitar el URL [http://localhost:81/nuxt-20/] mientras el servidor de cálculo de impuestos no está en ejecución. Primero nos aseguramos de eliminar la cookie de la sesión [nuxt]:

Image

La captura de pantalla anterior es de Chrome. Una vez hecho esto, al ejecutar URL [http://localhost:81/nuxt-20/] se obtiene el siguiente resultado:

Image

El error fue manejado correctamente por el proyecto [vuejs-22]. Sigue siendo manejado correctamente por el proyecto [nuxt-20].

17.4. Paso 3

Ahora que tenemos la página de autenticación, debemos revisar el código que se ejecuta cuando el usuario hace clic en el botón [Valider]:


// gestores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      try {
        // Inicio de espera
        this.$emit('loading', true)
        // aún no se ha autenticado
        this.$store.commit('replace', { authenticated: false })
        // autenticación bloqueante en el servidor
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // fin de la carga
        this.$emit('loading', false)
        // análisis de la respuesta del servidor
        if (response.état !== 200) {
          // se muestra el error
          this.message = response.réponse
          this.showError = true
          // regreso al bucle de eventos
          return
        }
        // sin error
        this.showError = false
        // se ha autenticado
        this.$store.commit('replace', { authenticated: true })
        // --------- ahora se solicitan los datos de la administración tributaria
        // al inicio, no hay datos
        this.$métier.setTaxAdminData(null)
        // comienza la espera
        this.$emit('loading', true)
        // solicitud bloqueada en el servidor
        const response2 = await this.$dao().getAdminData()
        // fin de la carga
        this.$emit('loading', false)
        // análisis de la respuesta
        if (response2.état !== 1000) {
          // se muestra el error
          this.message = response2.réponse
          this.showError = true
          // regreso al bucle de eventos
          return
        }
        // sin error
        this.showError = false
        // se almacena el dato recibido en la capa [métier]
        this.$métier.setTaxAdminData(response2.réponse)
        // se puede pasar al cálculo del impuesto
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // se remite el error al componente principal
        this.$emit('error', error)
      } finally {
        // actualización de la memoria
        this.$store.commit('replace', { métier: this.$métier })
        // se guarda la sesión
        this.$session().save(this.$nuxt.context)
      }
    }
  }

El principal problema aquí parece ser la falta del dato [this.$métier]. Para solucionarlo, vamos a:

  • incluir la clase [Métier] del ejemplo [vuejs-22]. La colocaremos en la carpeta [api];
  • insertar una función [$métier] en el contexto del cliente [nuxt] que dará acceso a esta clase;

En primer lugar, copiaremos la clase [Métier] en la carpeta [api]:

Image

Una vez que la clase [Métier] se encuentra en el proyecto, se crea un nuevo complemento para el cliente [nuxt]. Este complemento, llamado [pluginMétier], inyectará una función [$métier] que dará acceso a la clase [Métier]:


/* eslint-disable no-console */
// se crea un punto de acceso a la capa [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
  // instanciación de la capa [métier]
  const métier = new Métier()
  // Inyección de una función [$métier] en el contexto
  inject('métier', () => métier)
  // registro
  console.log('[fonction client $métier créée]')
}

Una vez hecho esto, podemos corregir la página [index]:


// ciclo de vida: el componente acaba de crearse
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // ¿Puede el usuario realizar simulaciones?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // entonces el usuario puede realizar simulaciones
      this.$router.push({ name: 'calcul-impot' })
      // regreso al bucle de eventos
      return
    }
    // si la sesión jSON ya se ha iniciado, no se vuelve a iniciar
    ...
  },

  // gestores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      try {
        // Inicio de la espera
        this.$emit('loading', true)
        // Aún no se ha autenticado
        this.$store.commit('replace', { authenticated: false })
        // autenticación bloqueante en el servidor
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // fin de la carga
        this.$emit('loading', false)
        // análisis de la respuesta del servidor
        if (response.état !== 200) {
          // se muestra el error
          this.message = response.réponse
          this.showError = true
          // regreso al bucle de eventos
          return
        }
        // sin error
        this.showError = false
        // se ha autenticado
        this.$store.commit('replace', { authenticated: true })
        // --------- ahora se solicitan los datos de la administración tributaria
        // al inicio, no hay datos
        this.$métier().setTaxAdminData(null)
        // comienza la espera
        this.$emit('loading', true)
        // solicitud bloqueada en el servidor
        const response2 = await this.$dao().getAdminData()
        // fin de la carga
        this.$emit('loading', false)
        // análisis de la respuesta
        if (response2.état !== 1000) {
          // se muestra el error
          this.message = response2.réponse
          this.showError = true
          // regreso al bucle de eventos
          return
        }
        // sin error
        this.showError = false
        // se almacena el dato recibido en la capa [métier]
        this.$métier().setTaxAdminData(response2.réponse)
        // se puede pasar al cálculo del impuesto
        this.$router.push({ name: 'calcul-impot' })
      } catch (error) {
        // se reporta el error al componente principal
        this.$emit('error', error)
      } finally {
        // actualización de la memoria
        this.$store.commit('replace', { métier: this.$métier() })
        // se guarda la sesión
        this.$session().save(this.$nuxt.context)
      }
    }
  }
  • líneas 43, 61, 69: [this.$métier] se ha reemplazado por [this.$métier()];
  • líneas 8 y 63: el nombre de la página [CalculImpot] del proyecto [vuejs-22] pasó a ser la página [calcul-impot] en el proyecto [nuxt-20];

Una vez realizadas estas correcciones, podemos intentar validar la página de autenticación:

Image

La página obtenida es la siguiente:

Image

Sí, hemos obtenido la página de cálculo del impuesto. Ahora revisemos los registros:

Image

En [2], vemos que el hecho de estar autenticado se ha registrado correctamente. En [3-4], vemos que se ha recuperado el dato [taxAdminData], que permite el cálculo del impuesto mediante la clase [Métier].

17.5. paso 4

Analicemos la página [calcul-impot] que obtuvimos:


<!-- definición HTML de la vista -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulario de cálculo del impuesto a la derecha -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- menú de navegación a la izquierda -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- área de visualización de los resultados del cálculo del impuesto debajo del formulario -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- área vacía de tres columnas -->
      <b-col sm="3" />
      <!-- área de nueve columnas -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>

<script>
// importaciones
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
  },
  // informe interno
  data() {
    return {
      // opciones del menú
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // resultado del cálculo del impuesto
      résultat: '',
      résultatObtenu: false
    }
  },
  // ciclo de vida
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  },
  // métodos de gestión de eventos
  methods: {
    // resultado del cálculo del impuesto
    handleResultatObtenu(résultat) {
      // se construye el resultado en una cadena 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
      // visualización del resultado
      this.résultatObtenu = true
      // ---- actualización del almacén [Vuex]
      // una simulación de +
      this.$store.commit('addSimulation', résultat)
      // se guarda la sesión
      this.$session.save()
    }
  }
}
</script>
  • líneas 44 y 48: los enlaces del menú de navegación son correctos. La página [/fin-session] no existe. El proyecto [vuejs-22] resolvía este problema mediante el enrutamiento. Haremos lo mismo con el proyecto [nuxt-20];
  • línea 76: se hace referencia a una modificación [addSimulation] que por el momento no existe. La crearemos;
  • línea 78: al igual que en la página [index], hay que escribir [this.$session().save(this.$nuxt.context)];

Modifiquemos el almacén [store/index]. Heredado del proyecto [nuxt-12], por el momento tiene el siguiente contenido:


/* eslint-disable no-console */

// estado de la tienda
export const state = () => ({
  // sesión jSON iniciada
  jsonSessionStarted: false,
  // usuario autenticado
  userAuthenticated: false,
  // cookie de sesión PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})

// cambios en el almacén
export const mutations = {
  // reemplazo del estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reinicio del almacén
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}

// acciones del almacén
export const actions = {
  nuxtServerInit(store, context) {
    // ¿Quién ejecuta este código?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // inicialización de sesión
    initStore(store, context)
  }
}

function initStore(store, context) {
  // store es el almacén que se debe inicializar
  // se recupera la sesión
  const session = context.app.$session()
  // ¿Ya se ha inicializado la sesión?
  if (!session.value.initStoreDone) {
    // se inicia un nuevo almacén
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // se agrega el almacén a la sesión
    session.value.store = store.state
    // el almacén ya está inicializado
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // Se actualiza el almacén con el de la sesión
    store.commit('replace', session.value.store)
  }
  // se guarda la sesión
  session.save(context)
  // registro
  console.log('initStore terminé, store=', store.state)
}
  • líneas 3-27: vamos a retomar el estado y las modificaciones de la aplicación [vuejs-22] (véase el documento [3]):

// estado de la persiana
export const state = () => ({
  // sesión jSON iniciada
  started: false,
  // usuario autenticado
  authenticated: false,
  // cookie de sesión PHP
  phpSessionCookie: '',
  // lista de simulaciones
  simulations: [],
  // el número de la última simulación
  idSimulation: 0,
  // capa [métier]
  métier: null
})

// cambios en la persiana
export const mutations = {
  // reemplazo del estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reinicio del estado
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
  },
  // eliminación de la línea n.º índice
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // se elimina la línea n.º [index]
    state.simulations.splice(index, 1)
    console.log('store simulations', state.simulations)
  },
  // se agrega una simulación
  addSimulation(state, simulation) {
    // eslint-disable-next-line no-console
    console.log('mutation addSimulation')
    // N.º de la simulación
    state.idSimulation++
    simulation.id = state.idSimulation
    // se agrega la simulación a la tabla de simulaciones
    state.simulations.push(simulation)
  }
}
  • líneas 4 y 6: introducimos las propiedades ya utilizadas;
  • línea 8: conservamos la cookie de sesión PHP. Es fundamental para que el cliente y el servidor [nuxt] tengan la misma sesión PHP con el servidor de cálculo de impuestos;
  • línea 10: la lista de simulaciones realizadas por el usuario;
  • línea 12: el número de la última simulación realizada por el usuario;
  • línea 14: la capa [métier];
  • líneas 30-47: las mutaciones presentes en el almacén del proyecto [vuejs-22] y a las que hacen referencia las páginas de la aplicación. El proyecto [vuejs-22] tenía una mutación llamada [clear] que vaciaba la lista de simulaciones. No la incluimos porque la mutación [reset], que ya está presente, debería ser suficiente;
  • líneas 26-28: se modifica la mutación [reset] para tener en cuenta el nuevo contenido del estado;

La página [calcul-impot] utiliza el siguiente componente [form-calcul-impot]:


<!-- definición HTML de la vista -->
<template>
  <!-- formulario HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- mensaje en 12 columnas sobre fondo 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 del formulario -->
    <!-- primera línea -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- botones de opción en 5 columnas-->
      <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 línea -->
    <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>
      <!-- posible mensaje de error -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- tercera línea -->
    <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>
      <!-- posible mensaje de error -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- cuarta línea, botón [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 o soltero
      marié: 'non',
      // número de hijos
      enfants: '',
      // salario anual
      salaire: ''
    }
  },
  // estado interno calculado
  computed: {
    // validación del formulario
    formInvalide() {
      return (
        // salario no válido
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // o hijos no válidos
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // o datos fiscales no obtenidos
        !this.$métier.taxAdminData
      )
    },
    // validación del salario
    salaireValide() {
      // debe ser un número >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
    },
    // validación de los hijos
    enfantsValide() {
      // debe ser un número >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/))
    }
  },
  // ciclo de vida
  created() {
    // registro
    // eslint-desactivar-línea-siguiente
    console.log("FormCalculImpot created");
  },
  // gestor de eventos
  methods: {
    calculerImpot() {
      // se calcula el impuesto utilizando la capa [métier]
      const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
      // eslint-desactivar-línea-siguiente
      console.log("résultat=", résultat);
      // se completa el resultado
      résultat.marié = this.marié
      résultat.enfants = this.enfants
      résultat.salaire = this.salaire
      // se emite el evento [resultatObtenu]
      this.$emit('resultatObtenu', résultat)
    }
  }
}
</script>
  • líneas 65, 89: la referencia [this.$métier] debe cambiarse a [this.$métier()];

Una vez realizadas estas correcciones, se puede intentar una simulación:

Image

Se obtiene la siguiente respuesta:

Image

Si revisamos los registros:

Image

  • en [9-10], se observa que la primera simulación se encuentra efectivamente en el [store];
  • En [5], el número de la última simulación sí se incrementó;

17.6. paso 5

Ahora que hemos realizado una simulación, hagamos clic en el enlace [Liste des simulations]. Aparece la siguiente página:

Image

El enrutamiento del cliente [nuxt] se realizó correctamente. Veamos el código de la página [liste-des-simulations]:


<!-- definición HTML de la vista -->
<template>
  <div>
    <!-- diseño de página -->
    <Layout :left="true" :right="true">
      <!-- simulaciones en la columna de la derecha -->
      <template slot="right">
        <template v-if="simulations.length == 0">
          <!-- sin simulaciones -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length != 0">
          <!-- hay simulaciones -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tabla de simulaciones -->
          <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>
      <!-- menú de navegación en la columna de la izquierda -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>

<script>
// importaciones
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
  // componentes
  components: {
    Layout,
    Menu
  },
  // estado interno
  data() {
    return {
      // opciones del menú de navegación
      options: [
        {
          text: "Calcul de l'impôt",
          path: '/calcul-impot'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // parámetros de la tabla HTML
      fields: [
        { label: '#', clave: '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 simulaciones tomadas del almacén 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);
      // eliminación de la simulación n.º [index]
      this.$store.commit('deleteSimulation', index)
      // se guarda la sesión
      this.$session.save()
    }
  }
}
</script>
  • líneas 47-56: los destinos del menú de navegación son correctos;
  • línea 75: el menú desplegable está correctamente referenciado;
  • línea 89: se utiliza una mutación [deleteSimulation] que integramos en el paso anterior;
  • línea 91: esta línea debe reescribirse como [this.$session().save(this.$nuxt.context)];

Realizamos las modificaciones necesarias y luego intentamos eliminar la simulación que se muestra:

Image

A continuación, se obtiene la siguiente página:

Image

Revisemos los registros:

Image

  • en [6], vemos que la tabla de simulaciones está vacía;

Ahora regresemos al formulario de cálculo del impuesto:

Image

Aparece la siguiente página:

Image

Así que el enrutamiento funcionó.

17.7. Paso 6

Ahora nos queda configurar la opción de navegación [Fin de session] del menú de navegación:


// opciones del menú
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
]
  • En la línea 9, la página [/fin-session] no existe. El proyecto [vuejs-22] manejaba este caso con reglas de enrutamiento en un archivo [router.js]:

// importaciones
import Vue from 'vue'
import VueRouter from 'vue-router'
// las vistas
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// la sesión
import session from './session'

// complemento de enrutamiento
Vue.use(VueRouter)

// las rutas de la aplicación
const routes = [
  // autenticación
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification2', component: Authentification },
  // cálculo del impuesto
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // lista de simulaciones
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fin de sesión
  {
    path: '/fin-session', name: 'finSession'
  },
  // página desconocida
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]

// el enrutador
const router = new VueRouter({
  // las rutas
  routes,
  // el modo de visualización de URL
  mode: 'history',
  // la aplicación básica URL
  base: '/client-vuejs-impot/'
})

// verificación de rutas
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // ¿Ruta reservada para usuarios autenticados?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // pasamos a la autenticación
      name: 'authentification',
    })
    // regreso al bucle de eventos
    return;
  }
  // caso particular del fin de la sesión
  if (to.name === "finSession") {
    // se limpia la sesión
    session.clear();
    // se pasa a la vista [authentification]
    next({
      name: 'authentification',
    })
    // regreso al bucle de eventos
    return;
  }
  // otros casos: siguiente vista normal del enrutamiento
  next();
})

// exportación del enrutador
export default router
  • las líneas 64-76 manejaban el caso particular de la ruta hacia la ruta [/fin-session];
  • línea 66: se vacía la sesión actual;
  • líneas 68 a 70: se muestra la vista [authentification];

Vamos a intentar hacer algo similar en el archivo de enrutamiento del cliente [nuxt]:

Image

El script [client/routing.js] queda así:


/* eslint-disable no-console */
export default function(context) {
  // ¿Quién ejecuta este código?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestión de la cookie de sesión PHP en el navegador
  // la cookie de sesión PHP del navegador debe ser idéntica a la que se encuentra en la sesión de Nuxt
  // la acción [fin-session] recibe una nueva cookie PHP (tanto del servidor como del cliente Nuxt)
  // si es el servidor quien la recibe, el cliente debe transmitirla al navegador
  // para sus propias comunicaciones con el servidor PHP
  //: aquí se trata de un enrutamiento del cliente

  // se recupera la cookie de sesión PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // si existe, se asigna la cookie de sesión PHP al navegador
    document.cookie = phpSessionCookie
  }

  // ¿A dónde vamos?
  const to = context.route.path
  if (to === '/fin-session') {
    // se borra la sesión
    const session = context.app.$session()
    session.reset(context)
    // se redirige a la página de inicio
    context.redirect({ name: 'index' })
  }
}
  • Hemos agregado las líneas [19-27] al código existente;
  • línea 20: se recupera el [path] del destino de la ruta actual;
  • línea 21: se verifica si es [/fin-session]. Si es así:
    • líneas 23-24: se reinicia la sesión;
    • línea 26: se redirige al cliente [nuxt] a la página de inicio;

El método [session.reset(context)] (línea 24) de la sesión es el siguiente:


// Se reinicia la sesión
  reset(context) {
    console.log('nuxt-session reset')
    // restablecimiento del almacén
    context.store.commit('reset')
    // se guarda el nuevo almacén en la sesión y se guarda la sesión
    this.save(context)
}

El método [context.store.commit('reset')] (línea 5) es el siguiente:


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

Al utilizar ahora el enlace [Fin de session], se muestra la página de inicio con los siguientes registros:

Image

  • en [3], se observa que ya no estamos autenticados;
  • en [4], se observa que se inicia la sesión jSON;
  • en [6], la capa [métier] ya no está presente en el almacén (sigue presente en las páginas con [this.$métier()]);
  • en [5, 7], ya no hay simulaciones;

Es importante comprender bien lo que ocurre al finalizar una sesión:

  • la sesión [nuxt] se reinicia: la propiedad [started] del almacén cambia a [false];
  • se redirige a la página [index];
  • se ejecuta el método [mounted] de la página [index]. Este inicia una nueva sesión jSON con el servidor de cálculo de impuestos. Si la operación se realiza con éxito, la propiedad [started] del almacén cambia a [true];

17.8. paso 7

En esta etapa, la aplicación [nuxt-20] cuenta con todas las funcionalidades de la aplicación [vuejs-22]. La migración parece haber concluido.

Vamos a ir un poco más allá con un enfoque [nuxt]. El método [mounted] de la página [index] presenta un problema. Inicia una operación asíncrona cuya finalización no esperará un motor de búsqueda. Sabemos que, en este caso, hay que colocar la operación asíncrona dentro de una función [asyncData], ya que así el servidor [nuxt] que la ejecuta esperará a que termine antes de entregar la página al motor de búsqueda.

Aquí utilizamos la función [asyncData], escrita en la aplicación [nuxt-12] para la página [index]:


export default {
  name: 'InitSession',
  // componentes utilizados
  components: {
    Layout,
    Navigation
  },
  // datos asíncronos
  async asyncData(context) {
    // registro
    console.log('[index asyncData started]')
    // no se repiten las acciones si la página ya se ha solicitado
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
      // se inicia una sesión jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // registro
      console.log('[index asyncData response=]', response)
      // se recupera la cookie de sesión PHP para las próximas solicitudes
      const phpSessionCookie = dao.getPhpSessionCookie()
      // se guarda la cookie de sesión PHP en la sesión [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // ¿Hubo algún error?
      if (response.état !== 700) {
        //: el error se encuentra en response.réponse
        throw new Error(response.réponse)
      }
      // se observa que la sesión jSON se ha iniciado
      context.store.commit('replace', { jsonSessionStarted: true })
      // se devuelve el resultado
      return { result: '[succès]' }
    } catch (e) {
      // registro
      console.log('[index asyncData error=]', e)
      // se registra que la sesión jSON no se inició
      context.store.commit('replace', { jsonSessionStarted: false })
      // se reporta el error
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // se guarda el almacén
      const session = context.app.$session()
      session.save(context)
      // registro
      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]')
    // solo para el cliente
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • líneas 13, 33, 40: hay que cambiar la propiedad [jsonSessionStarted] por [started];
  • línea 13: en la aplicación [nuxt-12], solo el servidor [nuxt] ejecutaba la página [index] y su función [asyncData]. El cliente [nuxt] solo ejecutaba la página [index] después de recibirla del servidor [nuxt] y, en ese momento, no ejecutaba la función [asyncData]. En [nuxt-20], la situación es diferente: el enlace [Fin de session] mostrará la página [index] en el entorno del cliente [nuxt]. La función [asyncData] se ejecutará entonces. Sin embargo, cuando se llega a la página [index] de esta manera, la sesión [nuxt] se ha reiniciado mientras tanto y la propiedad [started] del almacén tiene el valor [false], por lo que la condición de la línea 13 será necesariamente falsa. Por lo tanto, podemos dejar [process.server] y así el cliente [nuxt] no realizará esta prueba;
  • líneas 15, 35, 42: se incluye una propiedad [result] en las propiedades [data] de la página [index]. En [nuxt-20], esta propiedad no se utilizará y la eliminaremos del resultado devuelto por la función;
  • líneas 61-67: este método [mounted] debe conservarse, ya que es el que permite al cliente [nuxt] mostrar el mensaje de error. Sin embargo, se modificará la forma de manejar el error;

En la página [index] actual, integramos la función [asyncData] anterior en lugar de la antigua función [mounted] y agregamos una nueva función [mounted]. El código de la página [index] del ejemplo [nuxt-20] queda entonces así:


...

<!-- dinámica de la vista -->
<script>
/* eslint-desactivar no-console */
import Layout from '@/components/layout'
export default {
  // componentes utilizados
  components: {
    Layout
  },
  // estado del componente
  data() {
    return {
      // usuario
      user: '',
      // su contraseña
      password: '',
      // mensaje de error
      showError: false
    }
  },

  // propiedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // datos asincrónicos
  async asyncData(context) {
    // registro
    console.log('[index asyncData started]')
    // no se repiten las acciones si la página ya se ha solicitado
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // se inicia una sesión jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // registro
      console.log('[index asyncData response=]', response)
      // se recupera la cookie de sesión PHP para las próximas solicitudes
      const phpSessionCookie = dao.getPhpSessionCookie()
      // se guarda la cookie de sesión PHP en la sesión [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // ¿Hubo algún error?
      if (response.état !== 700) {
        // el error se encuentra en response.réponse
        throw new Error(response.réponse)
      }
      // se observa que la sesión jSON se inició
      context.store.commit('replace', { started: true })
      // sin resultados
      return
    } catch (e) {
      // registro
      console.log('[index asyncData error=]', e.message)
      // se registra que la sesión jSON no se inició
      context.store.commit('replace', { started: false })
      // se reporta el error
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // se guarda el almacén
      const session = context.app.$session()
      session.save(context)
      // registro
      console.log('[index asyncData finished]')
    }
  },
  // ciclo de vida
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // solo para el cliente
    console.log('[index beforeMount]')
    // gestión de posibles errores
    if (this.showErrorLoading) {
      // registro
      console.log('[index beforeMount, showErrorLoading=true]')
      // se reporta el error al componente principal [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },

  // gestores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      ...
}
</script>
  • líneas 58, 65: la función [asyncData] ya no hace que la propiedad [result] quede sin usar aquí;
  • línea 81: el método [beforeMount] del cliente [nuxt]. Se eligió este método en lugar del [mounted] para manejar un posible error de [asyncData];
  • línea 85: se comprueba si se ha establecido la propiedad [errorLoading]. Solo puede establecerla la función [asyncData];
  • líneas 85-90: si la función [asyncData] ha reportado un error, se la pasa a la página [default] a través del evento [error]. Así era como la antigua función [created], que acabamos de reemplazar, manejaba un posible error;

Hagamos algunas pruebas.

Primero eliminamos la cookie de sesión [nuxt] y la cookie de sesión PHP, si es que existen. Luego solicitamos la página [http://localhost:81/nuxt-20/] mientras el servidor de cálculo de impuestos no está en ejecución. Obtenemos la siguiente página:

Image

Recargamos la misma página después de haber iniciado el servidor de cálculo de impuestos:

Image

Veamos los registros:

Image

  • en [2-3], vemos que se inició la sesión jSON;
  • en [4], vemos la cookie de sesión PHP que el servidor [nuxt] recuperó durante su intercambio con el servidor de cálculo de impuestos. El cliente [nuxt] la utilizará a partir de ahora;

Ahora identifiquémonos:

Image

Obtenemos la siguiente página:

Image

En [1], recibimos un mensaje de error. Esto significa que el navegador no envió la cookie correcta de la sesión PHP iniciada por el servidor [nuxt] en el paso anterior. En [nuxt-12], la transferencia de la cookie de sesión PHP del servidor [nuxt] al cliente [nuxt] se realizaba en el enrutamiento del cliente [nuxt] del script [middleware/client/routing] :


/* eslint-disable no-console */
export default function(context) {  // ¿Quién ejecuta este código?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestión de la cookie de sesión PHP en el navegador
  // la cookie de sesión PHP del navegador debe ser idéntica a la que se encuentra en la sesión de Nuxt
  // la acción [fin-session] recibe una nueva cookie PHP (tanto el servidor como el cliente Nuxt)
  // si es el servidor quien la recibe, el cliente debe transmitirla al navegador
  // para sus propias comunicaciones con el servidor PHP
  //: aquí se trata de un enrutamiento del cliente

  // se recupera la cookie de sesión PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // si existe, se asigna la cookie de sesión PHP al navegador
    document.cookie = phpSessionCookie
  }

  // ¿A dónde vamos?
  const to = context.route.path
  if (to === '/fin-session') {
    // se borra la sesión
    const session = context.app.$session()
    session.reset(context)
    // se redirige a la página de inicio
    context.redirect({ name: 'index' })
  }
}

Son las líneas 13 a 17 las que permiten al cliente [nuxt] recuperar la cookie de la sesión PHP del servidor [nuxt].

El problema aquí es que, al hacer clic en el botón [Valider], no se realiza el enrutamiento del cliente [nuxt]. Por lo tanto, no se invoca su función de enrutamiento. Se resuelve el problema duplicando las líneas 12 a 17 al inicio del método de autenticación de la página [index]:


// gestores de eventos
  methods: {
    // ----------- autenticación
    async login() {
      // se recupera la cookie de sesión PHP del almacén
      const phpSessionCookie = this.$store.state.phpSessionCookie
      if (phpSessionCookie) {
        // si existe, se asigna la cookie de sesión PHP al navegador
        document.cookie = phpSessionCookie
      }
      try {
        // comienza la espera
        this.$emit('loading', true)
        // Aún no se ha autenticado

En las líneas 5 a 10, se recupera del almacén la cookie de la sesión PHP iniciada por el servidor [nuxt]. Una vez realizada esta modificación, se recupera correctamente la página de cálculo del impuesto, lo que significa que la autenticación funcionó.

17.9. Paso 8

Tenemos una aplicación funcional que opera en el entorno [nuxt]. Al igual que hicimos con la aplicación [nuxt-13], nos centraremos en la navegación del servidor [nuxt]. Como ya se ha mencionado, no se espera que el usuario escriba manualmente los URL de la aplicación. Se espera que utilice los enlaces que se le presentan y que son ejecutados por el cliente [nuxt], el cual opera entonces en modo SPA. No obstante, nos aseguraremos de que la navegación del servidor [nuxt] siempre mantenga la aplicación en un estado estable.

A partir del estudio realizado para [nuxt-13] (véase el párrafo del enlace), sabemos que es necesario:

  • modificar el script [midleware/routing];
  • agregar un script [middleware/server/routing];

Image

El script [middleware/routing] se modifica de la siguiente manera:


/* eslint-disable no-console */

// se importan los middlewares del servidor y del cliente
import serverRouting from './server/routing'
import clientRouting from './client/routing'

export default function(context) {
  // ¿Quién ejecuta este código?
  console.log('[middleware], process.server', process.server, ', process.client=', process.client)
  if (process.server) {
    // enrutamiento del servidor
    serverRouting(context)
  } else {
    // enrutamiento del cliente
    clientRouting(context)
  }
}
  • línea 4: se importa el script de enrutamiento del servidor [nuxt];
  • líneas 10-12: si es el servidor [nuxt] el que ejecuta el código, se utiliza su función de enrutamiento;

El script [middleware/server/routing] es el siguiente:


/* eslint-desactivar no-console */
export default function(context) {
  // ¿Quién ejecuta este código?
  console.log('[middleware server], process.server', process.server, ', process.client=', process.client)

  // recogemos algo de información aquí y allá
  const store = context.store
  // ¿De dónde venimos?
  const from = store.state.from || 'nowhere'
  // ¿A dónde vamos?
  let to = context.route.name
  
  // caso particular de /fin-session que no tiene el atributo [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
  
  // posible redirección
  let redirection = ''
  // Gestión del enrutamiento completada
  let done = false
  
  // ¿Ya estamos en una redirección del servidor [nuxt]?
  if (store.state.serverRedirection) {
    // no hay nada que hacer
    done = true
  }
  
  // ¿Se trata de una recarga de la página?
  if (!done && from === to) {
    // No hay nada que hacer
    done = true
  }
  
  // control de la navegación del servidor [nuxt]
  // se adapta al menú de navegación del cliente

  // primero se procesa el caso de fin de sesión
  if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
    // se limpia la sesión
    const session = context.app.$session()
    session.reset(context)
    // se redirige a la página de inicio
    redirection = 'index'
    // Trabajo completado
    done = true
  }

  // caso en el que la sesión PHP no se ha iniciado
  if (!done && !store.state.started && to !== 'index') {
    // redirección a [index]
    redirection = 'index'
    // trabajo completado
    done = true
  }

  // caso en el que el usuario no está autenticado
  if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
    redirection = 'index'
    // tarea completada
    done = true
  }

  // caso en el que no se obtuvo [adminData]
  if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
    // redirección a [index]
    redirection = 'index'
    // trabajo completado
    done = true
  }

  // en caso de que se haya obtenido [adminData]
  if (
    !done &&
    store.state.started &&
    store.state.authenticated &&
    store.state.métier.taxAdminData &&
    to !== 'calcul-impot' &&
    to !== 'liste-des-simulations'
  ) {
    // se permanece en la misma página
    redirection = from
    // trabajo terminado
    done = true
  }

  // Normalmente se han realizado todas las verificaciones ---------------------
  // ¿redirección?
  if (redirection) {
    // se registra la redirección en el almacén
    store.commit('replace', { serverRedirection: true })
  } else {
    // sin redirección
    store.commit('replace', { serverRedirection: false, from: to })
  }
  // se guarda el almacén en la sesión [nuxt]
  const session = context.app.$session()
  session.value.store = store.state
  session.save(context)
  // se realiza la posible redirección del servidor [nuxt]
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • En este script retomamos las ideas ya desarrolladas y utilizadas en el enrutamiento del servidor [nuxt] de la aplicación [nuxt-13];
  • Agregamos dos propiedades al almacén de la aplicación:
    • [from]: el nombre de la última página mostrada. Sabemos que el cliente [nuxt] cuenta con esta información, pero no el servidor [nuxt]. Le agregaremos esta información almacenando en el almacén, en cada enrutamiento del servidor [nuxt], el nombre de la página que se va a mostrar. Haremos lo mismo cada vez que el cliente [nuxt] realice un enrutamiento. De esta manera, en el siguiente enrutamiento del servidor [nuxt], este encontrará en el almacén el nombre de la última página mostrada por la aplicación;
    • [serverRedirection]: cuando el servidor [nuxt] rechace un destino de enrutamiento, este realizará una redirección. A continuación, indicará en el almacén que el próximo destino del servidor [nuxt] es una página de redirección. Esta redirección provocará una nueva ejecución del enrutador del servidor [nuxt]. Si este detecta que el destino actual proviene de una redirección, permitirá que el proceso continúe;
  • líneas 6-11: se recaba la información necesaria para el enrutamiento;
  • líneas 13-16: el destino [/fin-session] no está asociado a una página llamada [fin-session]. Por lo tanto, no tiene nombre. Se le asigna uno;
  • línea 19: el destino de una posible redirección;
  • línea 21: [done=true] una vez finalizadas las pruebas de enrutamiento;
  • líneas 23-27: como ya se mencionó, si el enrutamiento actual proviene de una redirección, no hay nada que hacer. De hecho, durante el enrutamiento anterior, el enrutador decidió que había que redirigir el navegador del cliente. No hay motivo para reconsiderar esa decisión;
  • líneas 29-33: si se trata de una recarga de página, se deja que suceda. Este no es un axioma válido para toda la aplicación [nuxt]: hay que analizar los efectos de una recarga para cada página. En este caso, resulta que la recarga de las páginas [index, calcul-impot, liste-des-simulations] no provoca efectos indeseados;
  • líneas 35-85: el enrutamiento del servidor [nuxt] retoma el enrutamiento del cliente [nuxt]. Cuando se está en una página, el enrutamiento del servidor [nuxt] debe reflejar el menú de navegación que ofrece el cliente [nuxt] al estar en esa página;
  • líneas 38-47: primero se trata el caso del destino [fin-session] que no corresponde a una página existente. Si se cumplen las condiciones (sesión iniciada, usuario autenticado), se borra la sesión y se redirige al usuario a la página [index];
  • líneas 49-55: si la sesión jSON con el servidor de cálculo de impuestos no se ha iniciado, entonces el único destino posible es la página [index];
  • líneas 57-62: si se ha iniciado la sesión jSON y el usuario no está autenticado ni ha solicitado la página de autenticación, se le redirige a la página de autenticación, que es la página [index];
  • líneas 64-70: si el usuario está autenticado, pero no se ha obtenido el dato [adminData], se redirige a la página de autenticación. La autenticación realiza dos acciones: autentica y, si la autenticación es exitosa, solicita además el dato [adminData]. Si este último no se ha obtenido, es necesario volver a iniciar la autenticación;
  • líneas 72-85: si se ha obtenido el dato [adminData], entonces los únicos destinos posibles son [calcul-impot] y [liste-des-simulations]. Si no es así, se rechaza el enrutamiento;
  • líneas 88-95: se actualiza el almacén dependiendo de si habrá redirección o no;
  • línea 94: no hay redirección. Por lo tanto, el [to] actual se convertirá en el [from] del próximo enrutamiento;
  • líneas 96-99: la información del almacén se guarda en la cookie de sesión [nuxt];
  • líneas 100-103: si hay que hacer una redirección, la hacemos;

Para realizar las pruebas, hay que asegurarse de partir de una situación inicial eliminando la cookie de sesión [nuxt] y la cookie de sesión PHP del servidor de cálculo de impuestos:

Image

Para probar el enrutamiento del servidor [nuxt], en cada página prueba todas las combinaciones posibles de URL y [/, /calcul-impot, /liste-des-simulations]. En cada caso, la aplicación debe permanecer en un estado coherente.

17.10. Paso 9

El paso 9 consiste en la implementación de la aplicación [nuxt-20]. Para ello, se requiere un servicio de alojamiento que ofrezca un entorno [node.js] para ejecutar el servidor [nuxt]. No lo tengo. El lector podrá seguir los procedimientos descritos en el párrafo del enlace para implementar la aplicación [nuxt-20] en su máquina de desarrollo y protegerla con un protocolo HTTPS.

17.11. Conclusion

La migración de la aplicación [vuejs-22] a la aplicación [nuxt-20] ya ha concluido. Recordemos algunos puntos de esta migración:

  • se conservaron las páginas de [vuejs-22];
  • las operaciones asincrónicas que existían en las páginas de [vuejs-22] se han migrado a una función [asyncData];
  • en [nuxt-20] fue necesario gestionar dos entidades: el cliente [nuxt] y el servidor [nuxt]. Esta última entidad no existía en [vuejs-22]. Para mantener la coherencia entre las dos entidades, necesitamos una sesión [nuxt];
  • tuvimos que gestionar el enrutamiento del servidor [nuxt];

En la práctica, sin duda es preferible comenzar directamente con una arquitectura [nuxt] en lugar de crear una arquitectura [vue.js] que luego se traslade a un entorno [nuxt].