Skip to content

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

9.1. Présentation

O exemplo [nuxt-05] demonstrou que é possível manter o store mesmo quando o utilizador força chamadas ao servidor. Os elementos do armazenamento são dinâmicos, de modo que, se forem integrados em vistas, estas reajam às alterações no armazenamento. Também se pode querer manter elementos ao longo das trocas cliente/servidor sem, no entanto, querer que sejam dinâmicos, simplesmente porque não são apresentados pelas vistas. É então possível armazená-los na sessão sem que, por isso, estejam no armazenamento.

O armazenamento é facilmente acessível através de propriedades como [context.app.$store] fora das vistas ou [this.$store] nas vistas. Gostaríamos de ter algo semelhante para a sessão, algo como [context.app.$session] ou [this.$session]. Vamos ver que isso é possível graças ao conceito de injeção. No entanto, não é possível injetar objetos no contexto, apenas funções. Esta ficará então disponível através das expressões [context.app.$session()] ou [this.$session()].

Por fim, vamos apresentar o conceito [nuxt] de [plugin].

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

Image

  • no [1], iremos adicionar uma pasta [plugins];

9.2. o conceito de plugin [nuxt]

[nuxt] designa como [plugin] todo o código executado no arranque da aplicação, antes mesmo da execução da função [nuxtServerInit] pelo servidor, que até agora era a primeira função de utilizador a ser executada. Os plugins da aplicação devem ser declarados na chave [plugins] do ficheiro 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 plugin é identificado pelo seu caminho [src] e pelo seu modo de execução [mode]. [mode] pode assumir três valores:
    • [client]: o plugin deve ser executado apenas no lado do cliente;
    • [server]: o plugin deve ser executado apenas no lado do servidor;
    • ausência da chave [mode]: neste caso, o plugin deve ser executado tanto no lado do cliente como no lado do servidor;
  • linhas 5-6: colocámos os nossos dois plugins numa pasta [plugins]. Não há qualquer obrigação de o fazer. Os plugins podem ser colocados em qualquer local da árvore de pastas 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 plugin [server / session.js] é o seguinte:


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

  // existe alguma sessão?
  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 num cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }
  // injecta-se 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 plugins são executados sempre que há uma chamada ao servidor: no arranque e sempre que o utilizador força uma chamada ao servidor digitando manualmente um URL:
    • 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) plugin(s) do cliente ser(em) executado(s);
  • linha 2: qualquer plugin, seja do cliente ou do servidor, recebe dois parâmetros:
    • [context]: o contexto do servidor ou do cliente, consoante quem executa o plugin;
    • [inject]: uma função que permite injetar uma função no contexto do servidor ou do cliente;
  • o objetivo do plugin [server / session] é duplo:
    • definir uma sessão (linhas 16-23);
    • definir, dentro desse contexto, uma função [$session] que irá devolver, como resultado, a sessão da linha 16. É a linha 25 que faz isso;
  • linhas 16-23: a sessão encapsulará os seus dados no objeto [value] da linha 18;
  • linhas 20-22: dispõe de uma função [save] que recebe como parâmetro um objeto [context]. É o código chamador que lhe fornece esse contexto. Com este, a função [save] guarda o valor da sessão, o objeto [value], no cookie de sessão;
  • linha 6: quando o plugin [server / session] é executado, começa por verificar 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-11, define-se o valor inicial da sessão. Este será o objeto [initValue] das linhas 29-31. Os elementos da sessão serão definidos na função [nuxtServerInit], que é executada após o plugin 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: quando se chega a esta linha, a sessão foi criada porque não existia, ou foi recuperada a partir da solicitação HTTP do navegador do cliente;
  • linha 25: insere-se no contexto do servidor uma nova função:
    • o primeiro parâmetro de [inject] é o nome da função que se está a criar, neste caso «session». [nuxt] irá, na verdade, atribuir-lhe o nome «$session»;
    • o segundo parâmetro é a definição da função. Aqui, a função [$session]
      • não aceitará nenhum parâmetro;
      • devolverá o objeto [session] da linha 16;
  • assim que o plugin for executado:
    • a função [$session] fica disponível em [context.app.$session], onde o objeto [context] está disponível, ou o [this.$session] numa vista ou no store [vuex];
    • a função [$session] devolve um objeto [session] com uma chave única [value];
    • na criação inicial da sessão, o objeto [value] tem apenas uma chave [initStoreDone] (linhas 29-31). A chave [initStoreDone:false] serve para indicar que o store ainda não foi colocado 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 irá 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 estado
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  }
}

export const actions = {
  async nuxtServerInit(store, context) {
    // quem executa este código?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // aguarda-se o fim de uma promessa
    await new Promise(function(resolve, reject) {
      // normalmente, aqui temos uma função assíncrona
      // simulamo-la com uma espera de um segundo
      setTimeout(() => {
        // inicializar 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) {
    // inicia-se um novo armazenamento
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // inicializa-se o armazenamento
    store.commit('increment', 77)
    // coloca-se o armazenamento na sessão
    session.value.store = store.state
    // está a ser inicializada uma nova sessão
    session.value.somethingImportant = { x: 2, y: 4 }
    // a sessão está agora 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 é guardada
  session.save(context)
  // registo
  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ções:

  • linha 42: recupera-se a sessão através 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 armazenamento é colocado na sessão;
    • linha 52: adiciona-se outro objeto [somethingImportant] à sessão. Este não fará parte do armazenamento;
    • linha 54: regista-se que a sessão está agora 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 é guardada no cookie de sessão. Recorde-se que isto consiste em colocar o cookie na resposta HTTP que o servidor irá enviar ao navegador do cliente;

9.5. O plugin [client / session] do cliente

Assim que o servidor tiver executado os scripts [plugins / server / session] e [store / index], enviará uma das páginas [index, page1] para o 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 plugin [client / session] será então executado:


/* eslint-disable no-console */
export default (context, inject) => {
  // gestão 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 num cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }

  // injecta-se uma função em [context, Vue] que tornará a sessão atual
  inject('session', () => session)
}
  • quando o plugin do cliente é executado, o cookie de sessão já foi recebido pelo navegador do cliente;
  • o objetivo do plugin [client] é também injetar uma função [$session] no contexto do cliente. Esta função renderia a sessão enviada pelo servidor;
  • linha 19: a função injetada [$session] irá restituir a sessão das linhas 9-16;
  • linhas 9 a 16: o objeto [session] gerido pelo cliente. Será uma cópia da sessão enviada pelo servidor;
  • linha 11: o valor da sessão do cliente é obtido a partir do cookie de sessão enviado pelo servidor [nuxt];
  • linhas 13-15: tal como na sessão do servidor, a sessão do cliente possui uma função [save] que permite guardar 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() {
    // apenas cliente
    console.log('[home beforeMount]')
  },
  mounted() {
    // apenas cliente
    console.log('[home mounted]')
  },
  // gestão 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++
      // gravação da sessão no cookie de sessão
      session.save(this.$nuxt.context)
    }
  }
}
</script>

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

  • linha 8: agora são apresentados tanto a sessão como o armazenamento;
  • linha 30: [jsonSession] é uma propriedade calculada que transforma a cadeia jSON no valor da sessão;
  • linha 41: exibe-se o valor da sessão utilizando a função injetada [this.$session]. Esta existe tanto no contexto do servidor como no do cliente;
  • linha 53: o método [incrementCounter] só é executado no lado do cliente;
  • linha 56: o contador da loja é incrementado e apresentado como anteriormente;
  • linha 58: recupera-se a sessão através da função injetada [this.$session];
  • linha 59: o armazenamento da sessão é atualizado;
  • linhas 60-61: incrementam-se os atributos [somethingImportant.x, somethingImportant.y] da sessão. Isto apenas para mostrar que uma sessão pode servir para transportar outros dados além do armazenamento;
  • linha 63: a sessão é guardada no cookie de sessão armazenado no navegador. Numa perspetiva 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 armazenamento (store) o é. Ao incrementarmos os elementos da sessão, verificaremos que a vista não é atualizada. A vista [page1] apresenta uma solução para este problema.

9.7. A página [page1]

A página [page1] é obtida através da cópia da página [index] e, posteriormente, ligeiramente modificada:


<!-- 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()
    // registo
    console.log('[page1 created], session=', this.session.value)
  },
  beforeMount() {
    // apenas cliente
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // apenas cliente
    console.log('[page1 mounted]')
  },
  // gestão 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++
      // gravação da sessão no cookie de sessão
      this.session.save(this.$nuxt.context)
    }
  }
}
</script>
  • linha 47: a principal diferença reside no facto de se colocar a sessão atual nas propriedades da página (linhas 29-33). Isto fará com que, a partir de agora, a sessão passe a ser reativa. Quando a função [incrementCounter] incrementar os elementos da sessão, a vista [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 exista, elimine-o para que o servidor crie uma nova sessão:

Image

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

Image

Os registos no navegador são então os seguintes:

Image

  • em [2], o servidor inicia uma nova sessão no plugin [session] do servidor;
  • em [3], esta nova sessão é inicializada em [nuxtServerInit];
  • em [4], a nova sessão tal como é conhecida no servidor;
  • no [5], o cliente recuperou corretamente esta 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] apresenta o armazenamento, que está ativo, [2] apresenta a sessão, que não está ativa:

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

Image

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

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

Image

A conclusão que se retira desta página é que a sessão pode transportar outros elementos além do «store», mas estes não são reativos.

Agora, cliquemos na ligação [Page 1] [4]. A nova página apresentada é então a seguinte:

Image

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

Image

Desta vez, a sessão é apresentada corretamente em [2]. Aqui, está ativa. Isso é visível nos registos:

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 na ligação [Home] [4]. Obtemos 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

Verifica-se que, também aqui, a sessão ficou ativa: [2].

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

Image

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

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

Image

  • em [5], verifica-se que a sessão passou a estar ativa, quando inicialmente não o estava;
  • em [6], solicitamos o valor da sessão;
  • em [7-8], descobre-se que este valor também se tornou reativo;

Temos, portanto, um resultado inesperado: se um elemento se torna reativo numa página por ter sido colocado nas propriedades da página, então torna-se igualmente reativo nas páginas onde não faz parte das propriedades.

9.9. Conclusion

O exemplo [nuxt-05] demonstrou 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 a que chamámos [session], por analogia com a sessão web. Vimos que esta sessão podia ter as mesmas propriedades que o armazenamento [Vuex] e tornar-se também reativa, apesar de, por natureza, não o ser.

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

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

Os exemplos [nuxt-05] e [nuxt-06] tinham como objetivo mostrar como se poderia garantir a continuidade da aplicação quando o utilizador força a chamada ao servidor digitando manualmente URL. Recorde-se que o comportamento por predefinição neste caso é o reinício da aplicação, perdendo-se assim o seu estado atual.