Skip to content

9. Exemplo [nuxt-06]: injeção no contexto de um gerenciador de sessão

9.1. Présentation

O exemplo [nuxt-05] demonstrou que é possível manter o store mesmo quando o usuário força chamadas ao servidor. Os elementos do store são reativos para que, caso sejam integrados a visualizações, estas sejam reativas às alterações no store. Também pode ser desejável persistir elementos ao longo das trocas cliente/servidor sem, no entanto, querer que eles sejam reativos, simplesmente porque não são exibidos pelas visualizações. É possível, então, armazená-los na sessão sem que estejam necessariamente no store.

O store é facilmente acessível por meio de propriedades como [context.app.$store] fora das visualizações ou [this.$store] nas visualizações. Gostaríamos de ter algo semelhante para a sessão, algo como [context.app.$session] ou [this.$session]. Veremos que isso é possível graças ao conceito de injeção. Porém, não é possível injetar objetos no contexto, apenas funções. Essa função estará então disponível por meio das expressões [context.app.$session()] ou [this.$session()].

Por fim, apresentaremos o conceito [nuxt] de [plugin].

O exemplo [nuxt-06] é obtido inicialmente por meio da cópia do projeto [nuxt-05]:

Image

  • no [1], adicionaremos uma pasta [plugins];

9.2. o conceito de plugin [nuxt]

[nuxt] denomina [plugin] todo código executado na inicialização do aplicativo, antes mesmo da execução da função [nuxtServerInit] pelo servidor, que até agora era a primeira função do usuário a ser executada. Os plug-ins do aplicativo devem ser declarados na chave [plugins] do arquivo de configuração [nuxt.config.js]:


  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    { src: '~/plugins/client/session', mode: 'client' },
    { src: '~/plugins/server/session', mode: 'server' }
],
  • linhas 5-6: um plug-in é identificado por seu caminho [src] e seu modo de execução [mode]. [mode] pode ter três valores:
    • [client]: o plug-in deve ser executado apenas no lado do cliente;
    • [server]: o plug-in deve ser executado apenas no lado do servidor;
    • ausência da chave [mode]: nesse caso, o plug-in deve ser executado tanto no lado do cliente quanto no lado do servidor;
  • linhas 5-6: colocamos nossos dois plug-ins em uma pasta chamada [plugins]. Não há nenhuma obrigação quanto a isso. Os plug-ins podem ser colocados em qualquer lugar na árvore de diretórios do projeto. Da mesma forma, os nomes das subpastas [client, server] são, neste caso, arbitrários;

Image

9.3. O plugin [session] do servidor

O plug-in [server / session.js] é o seguinte:


/* eslint-disable no-console */
export default (context, inject) => {
  // gerenciamento da sessão do servidor

  // existe uma sessão ativa?
  let value = context.app.$cookies.get('session')
  if (!value) {
    // nova sessão
    console.log("[plugin session server], démarrage d'une nouvelle session")
    value = initValue
  } else {
    // sessão existente
    console.log("[plugin session server], reprise d'une session existante")
  }
  // definição da sessão
  const session = {
    // conteúdo da sessão
    value,
    // armazenamento da sessão em um cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }
  // injetamos uma função em [context, Vue] que tornará a sessão atual
  inject('session', () => session)
}

// valor inicial da sessão
const initValue = {
  initSessionDone: false
}
  • linha 2: os plug-ins são executados sempre que há uma chamada ao servidor: na inicialização e sempre que o usuário força uma chamada ao servidor digitando um URL manualmente:
    • primeiro, o(s) plugin(s) do servidor é(são) executado(s);
    • quando o navegador do cliente recebe a resposta do servidor, é a vez do(s) plug-in(s) do cliente ser(em) executado(s);
  • linha 2: todo plugin, seja do cliente ou do servidor, recebe dois parâmetros:
    • [context]: o contexto do servidor ou do cliente, dependendo de quem está executando o plug-in;
    • [inject]: uma função que permite injetar uma função no contexto do servidor ou do cliente;
  • o objetivo do plug-in [server / session] é duplo:
    • definir uma sessão (linhas 16-23);
    • definir, dentro desse contexto, uma função [$session] que retornará como resultado a sessão da linha 16. É a linha 25 que faz isso;
  • linhas 16-23: a sessão encapsulará seus dados no objeto [value] da linha 18;
  • linhas 20-22: ela dispõe de uma função [save] que recebe como parâmetro um objeto [context]. É o código chamador que fornece esse contexto a ela. Com ele, a função [save] salva o valor da sessão, o objeto [value], no cookie de sessão;
  • linha 6: quando o plug-in [server / session] é executado, ele começa verificando se o servidor recebeu um cookie de sessão;
    • se sim, o objeto [value] da linha 6 representa o valor da sessão, ou seja, o conjunto de dados nela encapsulados;
    • caso contrário, nas linhas 7 a 11, define-se o valor inicial da sessão. Esse valor será o objeto [initValue] das linhas 29 a 31. Os elementos da sessão serão definidos na função [nuxtServerInit], que é executada após o plug-in do servidor;
  • linha 18: a notação [value] é um atalho para a notação [value:value]. O [value] à esquerda é o nome de uma chave de objeto; o [value] à direita é o objeto [value] declarado na linha 6;
  • linha 25: ao chegar a esta linha, a sessão foi criada porque não existia ou foi recuperada da solicitação HTTP do navegador do cliente;
  • linha 25: injeta-se no contexto do servidor uma nova função:
    • o primeiro parâmetro de [inject] é o nome da função que estamos criando, neste caso, ‘session’. [nuxt], na verdade, atribuirá a ela o nome ‘$session’;
    • o segundo parâmetro é a definição da função. Aqui, a função [$session]
      • não aceitará nenhum parâmetro;
      • retornará o objeto [session] da linha 16;
  • assim que o plugin for executado:
    • a função [$session] estará disponível em [context.app.$session], onde o objeto [context] está disponível, ou [this.$session] em uma visualização ou no store [vuex];
    • a função [$session] retorna um objeto [session] com uma chave única [value];
    • na criação inicial da sessão, o objeto [value] possui apenas uma chave [initStoreDone] (linhas 29-31). A chave [initStoreDone:false] serve para indicar que o store ainda não foi inserido na sessão. Isso será feito pela função [nuxtServerInit];

9.4. Inicialização da sessão

Assim que o plugin [session / server] for executado pelo servidor, este executará o seguinte script [store / index.js]:


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

export const mutations = {
  // incremento do contador em um valor [inc]
  increment(state, inc) {
    state.counter += inc
  },
  // substituição do state
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  }
}

export const actions = {
  async nuxtServerInit(store, context) {
    // quem está executando esse código?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // aguardando o término de uma promessa
    await new Promise(function(resolve, reject) {
      // normalmente, temos aqui uma função assíncrona
      // simulamos isso com uma espera de um segundo
      setTimeout(() => {
        // inicialização da sessão
        initSession(store, context)
        // sucesso
        resolve()
      }, 1000)
    })
  }
}

function initSession(store, context) {
  // store é a variável a ser inicializada

  // recuperamos a sessão
  const session = context.app.$session()
  // a sessão já foi inicializada?
  if (!session.value.initSessionDone) {
    // iniciando um novo armazenamento
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // inicializando o store
    store.commit('increment', 77)
    // colocando o armazenamento na sessão
    session.value.store = store.state
    // inicializando uma nova sessão
    session.value.somethingImportant = { x: 2, y: 4 }
    // a sessão já está inicializada
    session.value.initSessionDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // atualiza-se o armazenamento com o armazenamento da sessão
    store.commit('replace', session.value.store)
  }
  // a sessão é salva
  session.save(context)
  // registro
  console.log('initSession terminé, store=', store.state, 'session=', session.value)
}

Em relação ao store do projeto [nuxt-05], apenas a função [initSession] (anteriormente initStore) das linhas 38 a 60 sofre alteração:

  • linha 42: recupera-se a sessão por meio da função [$session], que foi inserida no contexto do servidor;
  • linha 44: verifica-se se a sessão já foi inicializada;
  • linhas 45-54: se não for o caso:
    • linha 48: o store é inicializado;
    • linha 50: o estado do store é inserido na sessão;
    • linha 52: adiciona-se outro objeto [somethingImportant] à sessão. Esse objeto não fará parte do store;
    • linha 54: observa-se que a sessão agora está inicializada;
  • linhas 55-59: se a sessão já estivesse inicializada:
    • linha 58: o novo armazenamento é inicializado com o conteúdo da sessão;
  • linha 61: a sessão é salva no cookie de sessão. Vale lembrar que isso consiste em inserir o cookie na resposta HTTP que o servidor enviará ao navegador do cliente;

9.5. O plugin [client / session] do cliente

Depois que o servidor executar os scripts [plugins / server / session] e [store / index], ele enviará uma das páginas [index, page1] ao navegador do cliente. Na resposta HTTP do servidor, estará presente o cookie de sessão. Assim que a página for recebida pelo navegador do cliente, os scripts do cliente incorporados na página serão executados. O plug-in [client / session] será então executado:


/* eslint-disable no-console */
export default (context, inject) => {
  // gerenciamento da sessão do cliente

  // a sessão existe necessariamente, inicializada pelo servidor
  console.log('[plugin session client], reprise de la session du serveur')

  // definição da sessão
  const session = {
    // conteúdo da sessão
    value: context.app.$cookies.get('session'),
    // armazenamento da sessão em um cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }

  // injetamos uma função em [context, Vue] que tornará a sessão atual
  inject('session', () => session)
}
  • quando o plug-in do cliente é executado, o cookie de sessão já foi recebido pelo navegador do cliente;
  • o objetivo do plug-in [client] é também injetar uma função [$session] no contexto do cliente. Essa função renderia a sessão enviada pelo servidor;
  • linha 19: a função injetada [$session] renderá a sessão das linhas 9 a 16;
  • linhas 9 a 16: o objeto [session] gerenciado pelo cliente. Será uma cópia da sessão enviada pelo servidor;
  • linha 11: o valor da sessão do cliente é obtido do cookie de sessão enviado pelo servidor [nuxt];
  • linhas 13-15: assim como na sessão do servidor, a sessão do cliente possui uma função [save] que permite salvar o valor da sessão, [this.value] (linha 14), no cookie de sessão armazenado no navegador;

9.6. A página [index]

A página [index] evolui da seguinte forma:


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

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable 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 e servidor
    console.log('[home beforeCreate]')
  },
  created() {
    // cliente e servidor
    console.log('[home created], session=', this.$session().value)
  },
  beforeMount() {
    // somente cliente
    console.log('[home beforeMount]')
  },
  mounted() {
    // somente cliente
    console.log('[home mounted]')
  },
  // gerenciamento de eventos
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incremento do contador em 1
      this.$store.commit('increment', 1)
      // alteração da sessão
      const session = this.$session()
      session.value.store = this.$store.state
      session.value.somethingImportant.x++
      session.value.somethingImportant.y++
      // salvamento da sessão no cookie de sessão
      session.save(this.$nuxt.context)
    }
  }
}
</script>

É importante lembrar que essa página é executada tanto no lado do servidor quanto no lado do cliente.

  • linha 8: agora são exibidos tanto a sessão quanto o store;
  • linha 30: [jsonSession] é uma propriedade calculada que define a string jSON com o valor da sessão;
  • linha 41: exibe-se o valor da sessão por meio da função injetada [this.$session]. Essa função existe tanto no contexto do servidor quanto no do cliente;
  • linha 53: o método [incrementCounter] é executado apenas no lado do cliente;
  • linha 56: o contador da loja é incrementado e exibido como antes;
  • linha 58: a sessão é recuperada por meio da função injetada [this.$session];
  • linha 59: o store da sessão é atualizado;
  • linhas 60-61: incrementam-se os atributos [somethingImportant.x, somethingImportant.y] da sessão. Isso serve apenas para mostrar que uma sessão pode ser usada para transportar outros dados além do armazenamento;
  • linha 63: a sessão é salva no cookie de sessão armazenado no navegador. Em uma visão do cliente, o contexto deste está disponível em [this.$nuxt.context];

O objetivo da página [index] é mostrar que a sessão não é reativa, ao passo que o store é. Ao incrementarmos os elementos da sessão, perceberemos que a visualização não é atualizada. A visualização [page1] apresenta uma solução para esse problema.

9.7. A página [page1]

A página [page1] é obtida a partir da página [index], com algumas pequenas modificações:


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

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable 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 e servidor
    console.log('[page1 beforeCreate]')
  },
  created() {
    // cliente e servidor
    // colocamos a sessão nas propriedades reativas da página
    this.session = this.$session()
    // log
    console.log('[page1 created], session=', this.session.value)
  },
  beforeMount() {
    // somente cliente
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // somente cliente
    console.log('[page1 mounted]')
  },
  // gerenciamento de eventos
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incremento do contador em 1
      this.$store.commit('increment', 1)
      // alteração da sessão
      this.session.value.store = this.$store.state
      this.session.value.somethingImportant.x++
      this.session.value.somethingImportant.y++
      // salvamento da sessão no cookie de sessão
      this.session.save(this.$nuxt.context)
    }
  }
}
</script>
  • linha 47: a principal diferença reside no fato de que a sessão atual é inserida nas propriedades da página (linhas 29-33). Isso fará com que, a partir de agora, a sessão se torne reativa. Quando a função [incrementCounter] incrementar os elementos da sessão, a visualização [page1] será atualizada;

9.8. Execução do projeto

Antes de executar o projeto, verifique o cookie de sessão do seu navegador e, caso ele exista, exclua-o para que o servidor crie uma nova sessão:

Image

Agora, solicitemos o URL e o [http://localhost:81/nuxt-06/]:

Image

Os registros no navegador ficam então assim:

Image

  • em [2], o servidor inicia uma nova sessão no plug-in [session] do servidor;
  • em [3], essa nova sessão é inicializada em [nuxtServerInit];
  • em [4], a nova sessão tal como é reconhecida no servidor;
  • no [5], o cliente recuperou corretamente essa sessão;

Agora, vamos incrementar o contador três vezes:

Image

  • em [3], o contador foi incrementado corretamente, mas não a sessão em [2]. Enquanto [3] exibe o store, que está ativo, [2] exibe a sessão, que não está ativa:

Agora, vamos recarregar a página (F5). Os registros após essa recarga são os seguintes:

Image

  • em [2], vemos que o servidor recebeu um cookie de sessão enviado pelo navegador do cliente;
  • em [4], vemos que o store não foi reinicializado, mas foi recuperado da sessão recebida;
  • em [4-5]: percebe-se que todos os atributos da sessão foram incrementados três vezes;

A página enviada pelo servidor é, então, a seguinte;

Image

A conclusão que se tira dessa página é que a sessão pode transportar outros elementos além do store, mas esses elementos não são reativos.

Agora, vamos clicar no link [Page 1] [4]. A nova página exibida é a seguinte:

Image

Em seguida, vamos usar o botão [Incrémenter] três vezes. A página passa a ser a seguinte:

Image

Desta vez, a sessão é exibida corretamente como [2]. Ela está ativa aqui. Isso pode ser observado nos logs:

Image

  • em [1-3], os valores da sessão;
  • em [4-6], os getters e setters reativos dos elementos da sessão;

Agora, vamos clicar no link [Home] [4]. Aparece a seguinte página:

Image

Em seguida, cliquemos duas vezes no botão [Incrémenter] [4]. A página passa a ser a seguinte:

Image

Percebemos que, também aqui, a sessão tornou-se reativa: [2].

Vamos consultar o valor retornado pela função [this.$session()]:

Image

  • na aba [Vue], selecionamos a página atual [Home] para obter sua referência [$vm0] [3];

Em seguida, na guia [Console] [4], solicitamos o valor da função [$vm0.$session()]:

Image

  • em [5], vemos que a sessão tornou-se ativa, embora inicialmente não estivesse;
  • em [6], solicitamos o valor da sessão;
  • em [7-8], descobrimos que esse valor também se tornou reativo;

Portanto, temos aqui um resultado inesperado: se um elemento se torna reativo em uma página porque foi incluído nas propriedades da página, ele também se torna reativo nas páginas em que não faz parte das propriedades.

9.9. Conclusion

O exemplo [nuxt-05] mostrou que era possível manter o store ao longo das solicitações feitas ao servidor. O exemplo [nuxt-06] faz o mesmo com um objeto que chamamos de [session], por analogia com a sessão da web. Vimos que essa sessão poderia ter as mesmas propriedades que o store [Vuex] e também se tornar reativa, embora, por padrão, não o fosse.

Então, qual é a utilidade do store [Vuex]? Devo admitir que, por enquanto, isso não me ficou claro. É provável que algo tenha me escapado. Portanto, na dúvida, aconselho usar:

  • um store [Vuex] para armazenar tudo o que precisa ser compartilhado entre as páginas do cliente e o que, eventualmente, precise ser compartilhado entre o cliente e o servidor;
  • um cookie de sessão, caso o armazenamento precise ser mantido durante uma chamada do cliente para o servidor, sendo que a sessão conterá apenas o armazenamento;

Os exemplos [nuxt-05] e [nuxt-06] tinham como objetivo mostrar como garantir a continuidade do aplicativo quando o usuário força a chamada ao servidor digitando manualmente URL. Vale lembrar que o comportamento padrão nesse caso é a reinicialização da aplicação, perdendo-se, assim, seu estado atual.