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] de 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. A continuación, aparece la siguiente vista:

Image

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

Image

Vamos a migrar la aplicación [vuejs-22] a la aplicación [nuxt-20] de forma progresiva. No volveremos a explicar los códigos de [vuejs-22]. Se recomienda al lector que vuelva a leer el documento |Introducción al marco 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 copiando 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] se comunican a través de una sesión [nuxt];

Así pues, 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 resultar innecesaria;
  • la gestión del enrutamiento del cliente y del servidor [nuxt];

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

Image

A continuación, 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;

A continuación, 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 guardan en la carpeta [components];
  • la página [Main] de [vuejs-22], que servía como [layout] para la aplicación [vuejs-22], se traslada a la carpeta [layouts];

Se renombran los elementos integrados [4]:

Image

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

Llegados a este punto, podemos compilar el proyecto para ver los primeros errores. Modificamos el archivo [nuxt.config] del ejemplo [nuxt-12] para que, a partir de ahora, se 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 a las que se escucha, por defecto localhost: 127.0.0.1
    // 0.0.0.0 = todas las direcciones de red del equipo
    host: 'localhost'
  },
  // entorno
  env: {
    // configuración de Axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://«localhost/php7/scripts-web/impots/version-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 detectados 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 inexistente. La recuperaremos en [vuejs-22];
  • el error de la línea 2 muestra que el componente [./FormCalculImpot] no existe. Efectivamente, este componente se encuentra ahora en [@/components/form-calcul-impot];
  • los errores de las líneas [3-5] indican que el componente [./Layout] no existe. Efectivamente, este componente se encuentra ahora en [@/components/layout];
  • los errores de las líneas [6-7] indican que el componente [./Menu] no existe. Efectivamente, ahora se llama [@/components/menu];

Añadimos la imagen [assets/logo.jpg] al proyecto [nuxt-20]:

Image

Por otra parte, 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 de impuestos 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-28 pasan a ser:


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

De este modo, se comprueban y, en su caso, se corrigen los [import] de todos los componentes, diseños y páginas. Una vez realizadas estas correcciones, se puede intentar ejecutar de nuevo el [build]. Normalmente, ya no debería haber errores.

A continuación, 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 comprueba 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 ningún error de compilación. A continuación, se solicita el archivo URL y [http://localhost:81/nuxt-20/] con 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 ya está bien:

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 denominada [$dao]. Esto significa que, en las páginas de la aplicación, [this.$dao] debe sustituirse por [this.$dao()];
  • la sesión [nuxt] gestionada por la aplicación [nuxt-20] ya no tiene nada que ver con el objeto [$session] de laaplicación [vuejs-22], en la que no existía el concepto de cookie de sesión. No obstante, ambas tienen una funcionalidad similar: almacenar información persistente a lo largo de las acciones del usuario. 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 propia sesión;
  • para conocer el estado de una propiedad P del almacén, habrá que escribir [this.$store.state.P];
  • para modificar la propiedad P del almacén, habrá que escribir [this.$store.commit(‘replace’, {P:value}];

Realizamos estos cambios en la página [index]:


<!-- definición HTML de la vista -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulario HTML: los valores se envían mediante 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 introducción 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 introducción 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' })
      // Vuelta al bucle de eventos
      return
    }
    // si la sesión jSON ya se ha iniciado, no se vuelve a iniciar
    if (!this.$store.state.started) {
      // comienzo de 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 inicializa 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
            // vuelta al bucle de eventos
            return
          }
          // la sesión se ha iniciado
          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 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
          // vuelta 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 principio, no hay datos
        this.$métier.setTaxAdminData(null)
        // Inicio de 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
          // vuelta 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 del almacén
        this.$store.commit('replace', { métier: this.$métier })
        // se guarda la sesión
        this.$session().save()
      }
    }
  }
}
</script>

Cabe destacar los siguientes puntos:

  • línea 69: la función [created2] ha pasado a llamarse [mounted], para que el servidor [nuxt] no la ejecute (este no ejecuta ni [beforeMount] ni [mounted]). Solo el cliente [nuxt] lo ejecutará, tal y como ocurrí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 comprobar si funciona en un contexto [nuxt];
  • líneas 112 y 172: en [vuejs-22], la sesión del proyecto se guardaba de esta forma. 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, a continuación, 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

Echemos un vistazo a los registros del navegador:

Image

El registro [1] es el último registro realizado por el cliente [nuxt]. En [2], se observa que la propiedad [started] está en [vrai], lo que significa que la función [mounted] ha conseguido iniciar una sesión jSON con el servidor de cálculo de impuestos. También se observa 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 marcha. En primer lugar, nos aseguramos de eliminar la cookie de la sesión [nuxt]:

Image

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

Image

El error se gestionó correctamente en el proyecto [vuejs-22]. Sigue gestionándose correctamente en el proyecto [nuxt-20].

17.4. Paso 3

Ahora que tenemos la página de autenticación, hay que fijarse en 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 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
          // vuelta 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 principio, no hay datos
        this.$métier.setTaxAdminData(null)
        // Inicio de 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
          // vuelta 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 del almacén
        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 ausencia 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, denominado [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' })
      // Vuelta 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
          // vuelta 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 principio, no hay datos
        this.$métier().setTaxAdminData(null)
        // Inicio de 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
          // vuelta 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 remite 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(this.$nuxt.context)
      }
    }
  }
  • líneas 43, 61 y 69: [this.$métier] se ha sustituido por [this.$métier()];
  • líneas 8 y 63: el nombre de la página [CalculImpot] del proyecto [vuejs-22] ha pasado 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

Efectivamente, hemos obtenido la página de cálculo del impuesto. Ahora echemos un vistazo a los registros:

Image

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

17.5. Paso 4

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


<!-- definición HTML de la vista -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulario de cálculo de impuestos 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] solucionaba 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 = {
  // sustitución 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) {
  // el almacén es el que hay que inicializar
  // se recupera la sesión
  const session = context.app.$session()
  // ¿Se ha inicializado ya la sesión?
  if (!session.value.initStoreDone) {
    // se inicia un nuevo «store»
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // se añade 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: [],
  // N.º de la última simulación
  idSimulation: 0,
  // capa [métier]
  métier: null
})

// cambios en la persiana
export const mutations = {
  // sustitución del estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reinicio del almacén
  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 añade 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 añade 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-disable-next-line
    console.log("FormCalculImpot created");
  },
  // gestor de eventos
  methods: {
    calculerImpot() {
      // se calcula el impuesto mediante la capa [métier]
      const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
      // eslint-disable-next-line
      console.log("résultat=", résultat);
      // 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 y 89: la referencia [this.$métier] debe cambiarse por [this.$métier()];

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

Image

Se obtiene la siguiente respuesta:

Image

Si consultamos los registros:

Image

  • en [9-10], se ve que la primera simulación se encuentra efectivamente en el [store];
  • En [5], el número de la última simulación se ha incrementado correctamente;

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 ha realizado 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 extraída 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 hemos integrado en el paso anterior;
  • línea 91: esta línea debe reescribirse como [this.$session().save(this.$nuxt.context)];

Realizamos los cambios necesarios y, a continuación, intentamos eliminar la simulación que se muestra:

Image

A continuación, aparece la siguiente página:

Image

Echemos un vistazo a los registros:

Image

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

Ahora volvamos al formulario de cálculo de impuestos:

Image

Aparece la siguiente página:

Image

Por lo tanto, el enrutamiento ha funcionado.

17.7. Paso 6

Ahora nos queda gestionar 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] gestionaba 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 de impuestos
  {
    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 router
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',
    })
    // Vuelta 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 accede a la vista [authentification]
    next({
      name: 'authentification',
    })
    // vuelta 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 gestionaban el caso particular de la ruta hacia la ruta [/fin-session];
  • línea 66: se vacía la sesión actual;
  • líneas 68-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 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í nos encontramos en 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 añadido 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 comprueba 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 ve que se ha iniciado la sesión jSON;
  • en [6], la capa [métier] ya no está presente en el almacén (sigue estando 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 pasa a ser [false];
  • se produce una redirección 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 pasa a [true];

17.8. paso 7

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

Vamos a ir un poco más allá con un enfoque [nuxt]. El método [mounted] de la página [index] plantea 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 finalice antes de entregar la página al motor de búsqueda.

Para ello, 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 almacena la cookie de sesión PHP en la sesión [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // ¿Se ha producido 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 indica que la sesión jSON no se ha iniciado
      context.store.commit('replace', { jsonSessionStarted: false })
      // se notifica 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 cliente
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • líneas 13, 33 y 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 haberla recibido 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]. A continuación, se ejecutará la función [asyncData]. Sin embargo, cuando se accede a la página [index] de esta forma, la sesión [nuxt] se ha reiniciado entretanto 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, de este modo, el cliente [nuxt] no realizará esta comprobación;
  • líneas 15, 35 y 42: se incluye una propiedad [result] en las propiedades [data] de la página [index]. En [nuxt-20], esta propiedad no se utilizará, por lo que 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. No obstante, se modificará la forma de gestionar el error;

En la página [index] actual, integramos la función [asyncData] anterior en lugar de la antigua función [mounted] y añadimos 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-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: '',
      // visualización de errores
      showError: false
    }
  },

  // propiedades calculadas
  computed: {
    // entradas válidas
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // datos asíncronos
  async asyncData(context) {
    // registro
    console.log('[index asyncData started]')
    // no se repiten las operaciones 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 almacena la cookie de sesión PHP en la sesión [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // ¿Se ha producido 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', { started: true })
      // sin resultados
      return
    } catch (e) {
      // registro
      console.log('[index asyncData error=]', e.message)
      // se indica que la sesión jSON no se ha iniciado
      context.store.commit('replace', { started: false })
      // se notifica 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 clientes
    console.log('[index beforeMount]')
    // gestión de posibles errores
    if (this.showErrorLoading) {
      // registro
      console.log('[index beforeMount, showErrorLoading=true]')
      // se remite 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 y 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 ha preferido este método al [mounted] para gestionar el 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 señalado un error, se pasa a la página [default] a través del evento [error]. Así era como la antigua función [created], que acabamos de sustituir, gestionaba los posibles errores;

Hagamos algunas pruebas.

En primer lugar, eliminamos la cookie de sesión [nuxt] y la cookie de sesión PHP, si existen. A continuación, solicitamos la página [http://localhost:81/nuxt-20/] sin que el servidor de cálculo de impuestos esté en marcha. Obtenemos la siguiente página:

Image

Recargamos la misma página tras haber iniciado el servidor de cálculo de impuestos:

Image

Echemos un vistazo a los registros:

Image

  • En [2-3], vemos que se ha iniciado la sesión jSON;
  • en [4], vemos la cookie de sesión PHP que el servidor [nuxt] ha recuperado 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], hemos recibido un mensaje de error. Esto significa que el navegador no ha enviado la cookie correcta de la sesión PHP iniciada por el servidor [nuxt] en el paso anterior. En [nuxt-12], el traspaso 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í nos encontramos en 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-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 produce el enrutamiento del cliente [nuxt]. Por lo tanto, no se invoca su función de enrutamiento. El problema se soluciona duplicando las líneas 12-17 al principio 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-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 de impuestos, lo que significa que la autenticación ha funcionado.

17.9. Paso 8

Tenemos una aplicación funcional que funciona según el modelo [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 introduzca manualmente los URL de la aplicación. Se supone que debe utilizar los enlaces que se le presentan y que son ejecutados por el cliente [nuxt], que entonces funciona en modo SPA. No obstante, nos aseguraremos de que la navegación del servidor [nuxt] mantenga siempre la aplicación en un estado estable.

A partir del estudio realizado para [nuxt-13] (véase el apartado del enlace), sabemos que hay que:

  • modificar el script [midleware/routing];
  • añadir un script [middleware/server/routing];

Image

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


/* eslint-disable no-console */

// se importan los middleware 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-disable 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'
  // ¿Adónde vamos?
  let to = context.route.name
  
  // caso particular de /fin-session que no tiene ningún atributo [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
  
  // posible redirección
  let redirection = ''
  // Gestión del enrutamiento finalizada
  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

  // se trata primero 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 finalizado
    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 finalizado
    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 ha obtenido [adminData]
  if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
    // redirección a [index]
    redirection = 'index'
    // trabajo finalizado
    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 finalizado
    done = true
  }

  // Normalmente se han realizado todas las comprobaciones ---------------------
  // ¿redirección?
  if (redirection) {
    // se anota 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];
  • Añadimos dos propiedades al almacén de la aplicación:
    • [from]: el nombre de la última página mostrada. Sabemos que el cliente [nuxt] dispone de esta información, pero no el servidor [nuxt]. Le añadiremos 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 este modo, 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 llevará a cabo una redirección. A continuación, indicará en el almacén que el siguiente 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 procede de una redirección, no intervendrá;
  • líneas 6-11: se recopila la información necesaria para el enrutamiento;
  • líneas 13-16: el destino [/fin-session] no está asociado a ninguna 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 se ha dicho, si el enrutamiento en curso 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 esta decisión;
  • líneas 29-33: si se trata de una recarga de página, se deja que se realice. No es un axioma válido para todas las aplicaciones [nuxt]: hay que analizar los efectos de una recarga en 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] cuando se está en dicha página;
  • líneas 38-47: primero se trata el caso del destino [fin-session] que no se corresponde con ninguna 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 se ha realizado con éxito, solicita además el dato [adminData]. Si este último no se ha obtenido, hay que volver a iniciar la autenticación;
  • líneas 72-85: si se ha obtenido el dato [adminData], 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 en función de si va a haber redirección o no;
  • línea 94: no hay redirección. Por lo tanto, el [to] actual pasará a ser 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 realizar una redirección, se lleva a cabo;

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], prueba en cada página 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 necesita un alojamiento que ofrezca un entorno [node.js] en el que ejecutar el servidor [nuxt]. Yo no lo tengo. El lector podrá seguir los procedimientos descritos en el apartado «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 finalizado. Destacamos algunos puntos de esta migración:

  • se han conservado las páginas de [vuejs-22];
  • las operaciones asíncronas que existían en las páginas de [vuejs-22] se han migrado a una función [asyncData];
  • en [nuxt-20] ha sido 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 ambas entidades, necesitamos una sesión [nuxt];
  • tuvimos que gestionar el enrutamiento del servidor [nuxt];

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