Skip to content

9. Ejemplo [nuxt-06]: inyección en el contexto de un administrador de sesión

9.1. Présentation

El ejemplo [nuxt-05] demostró que se puede mantener el almacén incluso cuando el usuario fuerza llamadas al servidor. Los elementos del almacén son reactivos, de modo que, si se integran en vistas, estas reaccionan a los cambios en el almacén. También puede ser conveniente conservar elementos a lo largo de los intercambios entre el cliente y el servidor sin que sean reactivos, simplemente porque no se muestran en las vistas. En ese caso, se pueden almacenar en la sesión sin que por ello estén en el almacén.

Se puede acceder fácilmente al almacén a través de propiedades como [context.app.$store] fuera de las vistas o [this.$store] dentro de las vistas. Nos gustaría tener algo similar para la sesión, algo como [context.app.$session] o [this.$session]. Veremos que esto es posible gracias al concepto de inyección. Sin embargo, no se pueden inyectar objetos en el contexto, solo funciones. Esta estará entonces disponible a través de las expresiones [context.app.$session()] o [this.$session()].

Por último, presentaremos el concepto [nuxt] de [plugin].

El ejemplo [nuxt-06] se obtiene inicialmente al copiar el proyecto [nuxt-05]:

Image

  • En [1], agregaremos una carpeta [plugins];

9.2. el concepto de complemento [nuxt]

[nuxt] denomina [plugin] a cualquier código que se ejecute al iniciar la aplicación, incluso antes de que el servidor ejecute la función [nuxtServerInit], que hasta ahora era la primera función de usuario en ejecutarse. Los complementos de la aplicación deben declararse en la clave [plugins] del archivo de configuración [nuxt.config.js]:


  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    { src: '~/plugins/client/session', mode: 'client' },
    { src: '~/plugins/server/session', mode: 'server' }
],
  • líneas 5-6: un complemento se identifica mediante su ruta [src] y su modo de ejecución [mode]. [mode] puede tener tres valores:
    • [client]: el complemento debe ejecutarse únicamente del lado del cliente;
    • [server]: el complemento debe ejecutarse únicamente del lado del servidor;
    • ausencia de la clave [mode]: en este caso, el complemento debe ejecutarse tanto del lado del cliente como del servidor;
  • líneas 5-6: hemos colocado nuestros dos complementos en una carpeta llamada [plugins]. No es obligatorio hacerlo así. Los complementos pueden colocarse en cualquier lugar del árbol de directorios del proyecto. Del mismo modo, los nombres de las subcarpetas [client, server] son arbitrarios en este caso;

Image

9.3. El complemento [session] del servidor

El complemento [server / session.js] es el siguiente:


/* eslint-disable no-console */
export default (context, inject) => {
  // gestión de la sesión del servidor

  // ¿Existe ya una sesión?
  let value = context.app.$cookies.get('session')
  if (!value) {
    // nueva sesión
    console.log("[plugin session server], démarrage d'une nouvelle session")
    value = initValue
  } else {
    // sesión existente
    console.log("[plugin session server], reprise d'une session existante")
  }
  // definición de la sesión
  const session = {
    // contenido de la sesión
    value,
    // almacenamiento de la sesión en una cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }
  // se inyecta una función en [context, Vue] que hará que la sesión sea la actual
  inject('session', () => session)
}

// valor inicial de la sesión
const initValue = {
  initSessionDone: false
}
  • línea 2: los complementos se ejecutan cada vez que hay una llamada al servidor: al inicio y cada vez que el usuario fuerza una llamada al servidor al escribir un URL manualmente:
    • primero se ejecutan el complemento o los complementos del servidor;
    • cuando el navegador del cliente recibe la respuesta del servidor, es el turno de que se ejecuten los complementos del cliente;
  • línea 2: cualquier complemento, ya sea del cliente o del servidor, recibe dos parámetros:
    • [context]: el contexto del servidor o del cliente, según quién ejecute el complemento;
    • [inject]: una función que permite inyectar una función en el contexto del servidor o del cliente;
  • el objetivo del complemento [server / session] es doble:
    • definir una sesión (líneas 16-23);
    • definir, dentro del contexto, una función [$session] que devolverá como resultado la sesión de la línea 16. Esto lo hace la línea 25;
  • líneas 16-23: la sesión encapsulará sus datos en el objeto [value] de la línea 18;
  • líneas 20-22: cuenta con una función [save] que recibe como parámetro un objeto [context]. Es el código que la invoca el que le proporciona este contexto. Con este, la función [save] guarda el valor de la sesión, el objeto [value], en la cookie de sesión;
  • línea 6: cuando se ejecuta el complemento [server / session], primero verifica si el servidor ha recibido una cookie de sesión;
    • si es así, el objeto [value] de la línea 6 representa el valor de la sesión, es decir, el conjunto de datos encapsulados en ella;
    • de lo contrario, en las líneas 7 a 11, se establece el valor inicial de la sesión. Este será el objeto [initValue] de las líneas 29 a 31. Los elementos de la sesión se definirán en la función [nuxtServerInit], que se ejecuta después del complemento del servidor;
  • línea 18: la notación [value] es un atajo para la notación [value:value]. El [value] de la izquierda es el nombre de una clave de objeto; el [value] de la derecha es el objeto [value] declarado en la línea 6;
  • línea 25: al llegar a esta línea, la sesión se ha creado porque no existía, o bien se ha recuperado de la solicitud HTTP del navegador del cliente;
  • línea 25: se inyecta una nueva función en el contexto del servidor:
    • el primer parámetro de [inject] es el nombre de la función que se crea, en este caso «session». [nuxt] le asignará, de hecho, el nombre «$session»;
    • el segundo parámetro es la definición de la función. Aquí, la función [$session]
      • no admitirá ningún parámetro;
      • devolverá el objeto [session] de la línea 16;
  • una vez ejecutado el complemento:
    • la función [$session] estará disponible en [context.app.$session], donde se encuentra el objeto [context], o [this.$session] en una vista o en el almacén [vuex];
    • la función [$session] devuelve un objeto [session] con una clave única [value];
    • al momento de la creación inicial de la sesión, el objeto [value] solo tiene una clave [initStoreDone] (líneas 29-31). La clave [initStoreDone:false] sirve para indicar que el almacén aún no se ha colocado en la sesión. Esto lo hará la función [nuxtServerInit];

9.4. Inicialización de la sesión

Una vez que el servidor haya ejecutado el complemento [session / server], este ejecutará el siguiente script [store / index.js]:


/* eslint-disable no-console */
export const state = () => ({
  // contador
  counter: 0
})

export const mutations = {
  // incremento del contador en un valor [inc]
  increment(state, inc) {
    state.counter += inc
  },
  // reemplazo del estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  }
}

export const actions = {
  async nuxtServerInit(store, context) {
    // ¿Quién ejecuta este código?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // se espera el final de una promesa
    await new Promise(function(resolve, reject) {
      // Normalmente aquí hay una función asíncrona
      // la simulamos con una espera de un segundo
      setTimeout(() => {
        // inicializar sesión
        initSession(store, context)
        // éxito
        resolve()
      }, 1000)
    })
  }
}

function initSession(store, context) {
  // store es el store que se debe inicializar

  // se recupera la sesión
  const session = context.app.$session()
  // ¿Ya se ha inicializado la sesión?
  if (!session.value.initSessionDone) {
    // Se inicia un nuevo almacén
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // se inicializa el almacén
    store.commit('increment', 77)
    // se coloca el almacén en la sesión
    session.value.store = store.state
    // Se está inicializando una nueva sesión
    session.value.somethingImportant = { x: 2, y: 4 }
    // la sesión ya está inicializada
    session.value.initSessionDone = 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('initSession terminé, store=', store.state, 'session=', session.value)
}

En comparación con el store del proyecto [nuxt-05], solo cambia la función [initSession] (anteriormente initStore) de las líneas 38 a 60:

  • línea 42: se recupera la sesión mediante la función [$session], que se ha inyectado en el contexto del servidor;
  • línea 44: se verifica si la sesión ya se ha inicializado;
  • líneas 45-54: si no es así:
    • línea 48: se inicializa el almacén;
    • línea 50: se guarda el estado del almacén en la sesión;
    • línea 52: se agrega otro objeto [somethingImportant] a la sesión. Este no formará parte del almacén;
    • línea 54: se registra que la sesión ya está inicializada;
  • líneas 55-59: si la sesión ya estaba inicializada:
    • línea 58: se inicializa el nuevo almacén con el contenido de la sesión;
  • línea 61: la sesión se guarda en la cookie de sesión. Recordemos que esto consiste en incluir la cookie en la respuesta HTTP que el servidor enviará al navegador del cliente;

9.5. El complemento [client / session] del cliente

Una vez que el servidor haya ejecutado los scripts [plugins / server / session] y [store / index], enviará una de las páginas [index, page1] al navegador del cliente. En la respuesta HTTP del servidor, se incluirá la cookie de sesión. Una vez que el navegador del cliente reciba la página, se ejecutarán los scripts del cliente incrustados en la página. A continuación, se ejecutará el complemento [client / session]:


/* eslint-disable no-console */
export default (context, inject) => {
  // gestión de la sesión del cliente

  // la sesión existe necesariamente, inicializada por el servidor
  console.log('[plugin session client], reprise de la session du serveur')

  // definición de la sesión
  const session = {
    // contenido de la sesión
    value: context.app.$cookies.get('session'),
    // almacenamiento de la sesión en una cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }

  // se inyecta una función en [context, Vue] que convertirá la sesión actual
  inject('session', () => session)
}
  • cuando se ejecuta el complemento de cliente, el navegador del cliente ya ha recibido la cookie de sesión;
  • el objetivo del complemento [client] es inyectar también una función [$session] en el contexto del cliente. Esta función devolvería la sesión enviada por el servidor;
  • línea 19: la función inyectada [$session] devolverá la sesión de las líneas 9 a 16;
  • líneas 9 a 16: el objeto [session] administrado por el cliente. Será una copia de la sesión enviada por el servidor;
  • línea 11: el valor de la sesión del cliente se toma de la cookie de sesión enviada por el servidor [nuxt];
  • líneas 13-15: al igual que en la sesión del servidor, la sesión del cliente cuenta con una función [save] que permite guardar el valor de la sesión, [this.value] (línea 14), en la cookie de sesión almacenada en el navegador;

9.6. La página [index]

La página [index] evoluciona de la siguiente manera:


<!-- página [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navegación -->
    <Navigation slot="left" />
    <!-- mensaje-->
    <template slot="right">
      <b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
      <!-- botón -->
      <b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
    </template>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-desactivar no-console */
/* eslint-desactivar nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
  name: 'Home',
  // componentes utilizados
  components: {
    Layout,
    Navigation
  },
  computed: {
    jsonSession() {
      return JSON.stringify(this.$session().value)
    }
  },
  // ciclo de vida
  beforeCreate() {
    // cliente y servidor
    console.log('[home beforeCreate]')
  },
  created() {
    // cliente y servidor
    console.log('[home created], session=', this.$session().value)
  },
  beforeMount() {
    // solo cliente
    console.log('[home beforeMount]')
  },
  mounted() {
    // solo cliente
    console.log('[home mounted]')
  },
  // gestión de eventos
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incremento del contador en 1
      this.$store.commit('increment', 1)
      // modificación de sesión
      const session = this.$session()
      session.value.store = this.$store.state
      session.value.somethingImportant.x++
      session.value.somethingImportant.y++
      // guardar la sesión en la cookie de sesión
      session.save(this.$nuxt.context)
    }
  }
}
</script>

Hay que recordar que esta página se ejecuta tanto del lado del servidor como del lado del cliente.

  • línea 8: ahora se muestran tanto la sesión como el almacén;
  • línea 30: [jsonSession] es una propiedad calculada que devuelve la cadena jSON con el valor de la sesión;
  • línea 41: se muestra el valor de la sesión mediante la función inyectada [this.$session]. Esta existe tanto en el contexto del servidor como en el del cliente;
  • línea 53: el método [incrementCounter] solo se ejecuta del lado del cliente;
  • línea 56: se incrementa el contador del store y se muestra como antes;
  • línea 58: se recupera la sesión mediante la función inyectada [this.$session];
  • línea 59: se actualiza el almacén de la sesión;
  • líneas 60-61: se incrementan los atributos [somethingImportant.x, somethingImportant.y] de la sesión. Esto es solo para mostrar que una sesión puede servir para transportar algo más que el almacén;
  • línea 63: la sesión se guarda en la cookie de sesión almacenada en el navegador. En una vista de cliente, el contexto de esta está disponible en [this.$nuxt.context];

El objetivo de la página [index] es mostrar que la sesión no es reactiva, mientras que el almacén sí lo es. Al incrementar los elementos de la sesión, se observará que la vista no se actualiza. La vista [page1] presenta una solución a este problema.

9.7. La página [page1]

La página [page1] se obtiene al copiar la página [index] y modificarla ligeramente:


<!-- página [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navegación -->
    <Navigation slot="left" />
    <!-- mensaje-->
    <template slot="right">
      <b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
      <!-- botón -->
      <b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
    </template>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-desactivar no-console */
/* eslint-desactivar nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
  name: 'Page1',
  // componentes utilizados
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      session: {}
    }
  },
  computed: {
    jsonSession() {
      return JSON.stringify(this.session.value)
    }
  },
  // ciclo de vida
  beforeCreate() {
    // cliente y servidor
    console.log('[page1 beforeCreate]')
  },
  created() {
    // cliente y servidor
    // se incluye la sesión en las propiedades reactivas de la página
    this.session = this.$session()
    // registro
    console.log('[page1 created], session=', this.session.value)
  },
  beforeMount() {
    // solo cliente
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // solo cliente
    console.log('[page1 mounted]')
  },
  // gestión de eventos
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incremento del contador en 1
      this.$store.commit('increment', 1)
      // modificación de sesión
      this.session.value.store = this.$store.state
      this.session.value.somethingImportant.x++
      this.session.value.somethingImportant.y++
      // guardar la sesión en la cookie de sesión
      this.session.save(this.$nuxt.context)
    }
  }
}
</script>
  • línea 47: la principal diferencia radica en que se incluye la sesión actual en las propiedades de la página (líneas 29-33). Esto hará que, a partir de ahora, la sesión sea reactiva. Cuando la función [incrementCounter] incremente los elementos de la sesión, se actualizará la vista [page1];

9.8. Ejecución del proyecto

Antes de ejecutar el proyecto, revisa la cookie de sesión de tu navegador y, si existe, elimínala para que el servidor cree una sesión nueva:

Image

Ahora solicitemos el URL y el [http://localhost:81/nuxt-06/]:

Image

Los registros en el navegador son entonces los siguientes:

Image

  • en [2], el servidor inicia una nueva sesión en el complemento [session] del servidor;
  • en [3], esta nueva sesión se inicializa en [nuxtServerInit];
  • en [4], la nueva sesión tal como se conoce en el servidor;
  • en [5], el cliente ha recuperado correctamente esta sesión;

Ahora incrementemos el contador tres veces:

Image

  • en [3], el contador se ha incrementado correctamente, pero no la sesión en [2]. Mientras que [3] muestra el almacén, que está activo, [2] muestra la sesión, que no está activa:

Ahora recarguemos la página (F5). Los registros son los siguientes tras esta recarga:

Image

  • en [2], vemos que el servidor recibió una cookie de sesión enviada por el navegador del cliente;
  • en [4], se observa que el almacén no se reinicia, sino que se retoma de la sesión recibida;
  • en [4-5]: se observa que todos los atributos de la sesión se han incrementado tres veces;

La página enviada por el servidor es entonces la siguiente;

Image

La conclusión que se desprende de esta página es que la sesión puede transportar otros elementos además del almacén, pero estos no son interactivos.

Ahora hagamos clic en el enlace [Page 1] [4]. La nueva página que se muestra es la siguiente:

Image

Luego, utilicemos el botón [Incrémenter] tres veces. La página se vuelve la siguiente:

Image

Esta vez, la sesión se muestra correctamente en [2]. Aquí sí responde. Esto se puede ver en los registros:

Image

  • en [1-3], los valores de la sesión;
  • en [4-6], los getters y setters reactivos de los elementos de la sesión;

Ahora hagamos clic en el enlace [Home] [4]. Aparece la siguiente página:

Image

Luego, hagamos doble clic en el botón [Incrémenter] [4]. La página se vuelve la siguiente:

Image

Observamos que, en este caso también, la sesión se ha vuelto reactiva: [2].

Solicitemos el valor devuelto por la función [this.$session()]:

Image

  • en la pestaña [Vue], seleccionamos la página actual [Home] para obtener su referencia [$vm0] [3];

Luego, en la pestaña [Console] [4], solicitamos el valor de la función [$vm0.$session()]:

Image

  • en [5], vemos que la sesión se ha vuelto activa, aunque inicialmente no lo estaba;
  • en [6], solicitamos ver el valor de la sesión;
  • en [7-8], se descubre que este valor también se ha vuelto reactivo;

Por lo tanto, nos encontramos con un resultado inesperado: si un elemento se vuelve reactivo en una página porque se ha incluido en las propiedades de la página, entonces también se vuelve reactivo en las páginas donde no forma parte de las propiedades.

9.9. Conclusion

El ejemplo [nuxt-05] demostró que se podía mantener el almacén a lo largo de las solicitudes realizadas al servidor. El ejemplo [nuxt-06] hace lo mismo con un objeto al que llamamos [session] por analogía con la sesión web. Hemos visto que esta sesión podía tener las mismas propiedades que el almacén [Vuex] y volverse reactiva también, aunque de forma nativa no lo era.

Entonces, ¿cuál es el propósito del almacén [Vuex]? Debo admitir que, por el momento, no lo he entendido. Es probable que se me haya escapado algo. Así que, en caso de duda, recomendaría usar:

  • un almacén [Vuex] para guardar todo lo que deba compartirse entre las páginas del cliente, y lo que eventualmente deba compartirse entre el cliente y el servidor;
  • una cookie de sesión si el almacén debe persistir durante una llamada del cliente al servidor, en cuyo caso la sesión solo contendrá el almacén;

Los ejemplos [nuxt-05] y [nuxt-06] tenían como objetivo mostrar cómo se podía garantizar la continuidad de la aplicación cuando el usuario fuerza la llamada al servidor al escribir manualmente URL. Cabe recordar que el comportamiento predeterminado en este caso es un reinicio de la aplicación, con lo que se pierde su estado actual.