Skip to content

8. Exemplo [nuxt-05]: persistência do store com um cookie de sessão

Objetivo: pretendemos que o store [Vuex] não seja reinicializado a cada pedido ao servidor. Para tal, vamos utilizar um cookie de sessão:

  • o armazenamento será inicializado pelo servidor e este colocá-lo-á num cookie de sessão;
  • o navegador do cliente receberá esse cookie de sessão e enviá-lo-á automaticamente em cada nova solicitação ao servidor;
  • o servidor poderá então recuperar esse cookie de sessão e trabalhar com o armazenamento que este contém, um armazenamento atualizado pelo cliente;

8.1. Présentation

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

Image

Veremos que apenas o ficheiro [store / index.js] irá sofrer alterações.

Para utilizar cookies com o [nuxt], vamos utilizar o módulo [cookie-universal-nuxt], que instalamos juntamente com o [yarn] num terminal VSCode:

Image

  • no [4], introduzimos o comando [yarn add cookie-universal-nuxt];

Desta forma, é adicionado um novo módulo ao ficheiro [package.json] do projeto [dvp]:


...
},
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "bootstrap": "^4.1.3",
    "bootstrap-vue": "^2.0.0",
    "cookie-universal-nuxt": "^2.0.19",
    "nuxt": "^2.0.0"
},

8.2. O ficheiro de configuração [nuxt.config.js]

Para que o [nuxt] possa utilizar os cookies do [cookie-universal-nuxt], é necessário declarar este módulo no ficheiro de configuração [nuxt.config.js]:


...
],
  /*
   ** 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'
  ],
...

  • na linha 12, o módulo [cookie-universal-nuxt] é adicionado à tabela de módulos [6] de [nuxt];

O ficheiro [nuxt.config.js] fica, por fim, com o seguinte conteúdo:


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: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** 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',
    // Documentação: 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) {}
  },
  // diretório do código-fonte
  srcDir: 'nuxt-05',
  // router
  router: {
    // raiz da aplicação URL
    base: '/nuxt-05/'
  },
  // servidor
  server: {
    // porta de serviço, 3000 por predefinição
    port: 81,
    // endereços de rede em escuta, por predefinição localhost: 127.0.0.1
    // 0.0.0.0 = todos os endereços de rede do computador
    host: 'localhost'
  },
  // ambiente
  env: {
    maxAge: 60 * 5
  }
}
  • linha 79: foi adicionada a chave [env] ao ficheiro. Esta chave é uma palavra reservada. Os elementos declarados neste objeto estão disponíveis a partir do objeto [context.env] nos elementos da aplicação;
  • linha 80: o atributo [maxAge] corresponderá ao tempo de vida máximo do cookie de sessão, duração que é medida a partir da última vez que o cookie foi inicializado. Este tempo é expresso em segundos. Aqui, definimos um tempo de vida de 5 minutos;

8.3. O princípio da persistência do armazenamento

Os cookies trocados entre o cliente e o servidor estão disponíveis em ambos os lados (cliente e servidor) em:

  • [context.app.$cookies], onde o objeto [context] está disponível, ou seja, praticamente em todo o lado;
  • [this.$cookies] no interior de uma vista;

Obtenha-se um cookie específico com a expressão [...$cookies.get(‘nom_du_cookie’)]. Defina-se o valor de um cookie com a expressão [...$cookies.set(‘nom_du_cookie’, valeur_du_cookie)].

O princípio do cookie de persistência do armazenamento será o seguinte:

  • quando o servidor inicializar o armazenamento na função [nuxtServerInit], o estado do armazenamento será guardado num cookie denominado «session»;
  • o cookie «session» fará então parte da resposta HTTP do servidor. Sabe-se que um navegador devolve ao servidor os cookies que este lhe enviou. Faz-o em cada nova solicitação que envia ao servidor. Sabe-se também que o servidor envia o armazenamento dentro da página que envia ao cliente;
  • no navegador, a aplicação cliente recupera o «store» enviado pelo servidor e, em seguida, executa as suas tarefas. Asseguraremos que, sempre que a aplicação alterar o «store», o seu novo estado seja armazenado no cookie «session» registado pelo navegador;
  • se o utilizador forçar uma chamada ao servidor, o navegador do cliente reenviará automaticamente todos os cookies que o servidor lhe enviou anteriormente, nomeadamente o cookie denominado «session»;
  • quando, na sequência dessa chamada, o servidor reinicializar novamente o armazenamento, irá recuperar o cookie denominado «session» e inicializar o estado do armazenamento com o valor deste;
  • haverá, portanto, continuidade do «store» entre o cliente e o servidor;

8.4. Inicialização do armazenamento

O armazenamento está implementado no ficheiro [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
        initStore(store, context)
        // sucesso
        resolve()
      }, 1000)
    })
  }
}

function initStore(store, context) {
  // existe algum cookie de sessão na solicitação atual
  const cookies = context.app.$cookies
  const session = cookies.get('session')
  if (!session) {
    // não existe nenhuma sessão
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // inicializar o armazenamento
    store.commit('increment', 77)
  } else {
    console.log("nuxtServerInit, reprise d'une session existante")
    // atualização do armazenamento com o cookie de sessão
    store.commit('replace', session.store)
  }
  // colocamos o armazenamento no cookie de sessão
  cookies.set('session', { store: store.state }, { path: context.base, maxAge: context.env.maxAge })
  // registo
  console.log('initStore terminé, store=', store.state)
}

Comentários

  • linhas 2-5: o store será constituído por um contador;
  • linhas 9-11: este contador poderá ser incrementado;
  • linhas 13-17: o estado do registo poderá ser inicializado a partir de um novo estado. Esta função serve para mostrar uma possível inicialização do registo quando este não se limita apenas ao contador, como neste caso;
  • linhas 21-35: a função [nuxtServerInit] não sofreu alterações;
  • linha 30: quando o tempo de espera de um segundo terminar, o registo é inicializado com a função das linhas 38-56;
  • linhas 40-41: começamos por recuperar o cookie denominado «session»:
    1. na primeira execução da aplicação e na primeira solicitação enviada ao servidor, este cookie ainda não existirá. Será então criado (linha 53) e enviado ao navegador do cliente;
    2. durante a mesma execução da aplicação e nas solicitações n.º 2, 3, ... feitas ao servidor, este cookie já existirá, pois o navegador do cliente o reenviará com cada nova solicitação feita ao servidor;
    3. numa segunda execução da aplicação e na primeira solicitação feita ao servidor, este cookie também pode existir. Com efeito, no final da etapa 1, o cookie foi armazenado no navegador com um determinado período de validade. Se esse período de validade não tiver expirado, o cookie denominado «session» será enviado com a primeira solicitação feita ao servidor

Em resumo, para cada pedido feito ao servidor: se o cookie «session» já estiver armazenado no navegador do cliente, o servidor irá recebê-lo; caso contrário, não o receberá.

  • linhas 42-47: se o servidor não receber o cookie de sessão, então o armazenamento é inicializado pela linha 46;
    • depois, na linha 53, será criado um cookie denominado «session» e colocado na resposta HTTP do servidor. O valor do cookie é o objeto [{ store: store.state }]. É, portanto, o estado do armazenamento e não o próprio armazenamento que é colocado no cookie de sessão;
    • o terceiro parâmetro da função [set] é um objeto de opções:
      • [path] indica a que URL este cookie deverá ser reenviado. [context.base] é o URL de base da aplicação [nuxt-05]. Este está definido no ficheiro [nuxt.config.js]:

  // router
  router: {
    // raiz dos URL da aplicação
    base: '/nuxt-05/'
},
      • [maxAge] corresponde ao tempo de vida, em segundos, do cookie no navegador. Passado esse tempo, o navegador deixa de o reenviar ao servidor. [context.env.maxAge] reenvia, mais uma vez, um valor registado no ficheiro [nuxt.config.js]:
1
2
3
4
// ambiente
  env: {
    maxAge: 60 * 5
}

[env] é uma palavra-chave reservada do ficheiro de configuração. Aqui, define-se o tempo de vida para 5 minutos. Este período é medido a partir da última vez em que o navegador recebeu o cookie de sessão. Passado esse período, o cookie não será reenviado ao servidor, que terá então de iniciar uma nova sessão;

  • linhas 48-50: se o servidor receber o cookie de sessão, o estado do armazenamento é inicializado com o objeto [store] do cookie de sessão. Recorde-se que este objeto contém o estado guardado do armazenamento;
    • em seguida, na linha 53, o cookie de sessão será incluído na resposta enviada ao navegador do cliente:
      • a função [get] vai buscar o cookie de sessão na solicitação recebida pelo servidor;
      • a função [set] insere o cookie de sessão na resposta que o servidor envia ao navegador do cliente;

8.5. Incremento do contador da loja

O incremento do contador na página [index.vue] ocorre da seguinte forma:


  // gestão de eventos
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
       // incremento do contador em 1
      this.$store.commit('increment', 1)
       // alteração do valor apresentado
      this.value = this.$store.state.counter
       // gravação do estado no cookie de sessão
      this.$cookies.set('session', { store: this.$store.state }, { path: this.$nuxt.context.base, maxAge: this.$nuxt.context.env.maxAge })
    }
}

Do lado do cliente, sempre que se altera o store, é necessário guardá-lo no cookie de sessão. Com efeito, o utilizador pode solicitar manualmente uma URL a qualquer momento e, nesse caso, é necessário poder enviar ao servidor um store atualizado. É por isso que, na linha 10, após o incremento do contador do «store», guardamos o seu estado no cookie de sessão:

  • os cookies estão disponíveis na propriedade [this.$cookies];
  • o estado do store [this.$store.state] é guardado no cookie associado à chave [store];
  • o caminho do cookie é [context.base]. Numa vista, o contexto está disponível em [this.$nuxt.context];
  • o tempo de vida do cookie é [context.env.maxAge], disponível aqui na propriedade [this.$nuxt.context.env.maxAge];

8.6. Execução do exemplo [nuxt-05]

Iniciamos a aplicação [nuxt-05]:

Image

As capturas de ecrã que se seguem foram tiradas num navegador Chrome. Solicitamos o URL [http://localhost:81/nuxt-05/]. Não se esqueça do último / a seguir a /nuxt-05, caso contrário não obterá os resultados esperados:

Image

  • em [4], obtivemos o valor inicial do store (77);

Vamos analisar os registos do navegador (F12):

Image

  • em [5-6], os registos do servidor;
  • em [7], vemos que o servidor inicia uma nova sessão. Isto significa que não recebeu nenhum cookie de sessão;
  • em [8], inicialização do contador com o valor 77;
  • em [9], a página [index] do servidor (9) e a do cliente (10) apresentam, de facto, o mesmo valor do contador;

Agora, vejamos os cookies recebidos pelo navegador:

Image

  • em [1], selecione o separador [Application] e, em seguida, a opção [Cookies] [2]. Entre todos os cookies do seu navegador, selecione o do domínio [http://localhost:81];
  • em [4], o cookie denominado «session». Se não o tiver, atualize a página [F5]: talvez tenha excedido o seu tempo de validade, que é de 5 minutos;
  • em [5], o valor do cookie. Embora não seja muito legível devido à codificação dos caracteres { :, é possível distinguir o valor 77 do contador;
  • em [6], o URL do cookie: sempre que este URL for solicitado, o navegador enviará o cookie para o servidor;
  • em [7], a hora de validade do cookie. Quando essa hora for ultrapassada, o cookie será eliminado do navegador;

Certifique-se de que possui este cookie. Se não o tiver, atualize a página (F5). Quando tiver a página com o respetivo cookie, atualize novamente a página (F5). Os registos passam então a ser os seguintes:

Image

Desta vez, em [3], o servidor recuperou corretamente o cookie de sessão. Foi o navegador do cliente que lho enviou.

Agora, aumente o contador e, de vez em quando, atualize a página atual (F5), quer seja [index] ou [page1], deverá verificar que o contador não volta a 77, como no exemplo [nuxt-04], mas mantém o valor que tinha no navegador do cliente antes de a página ser recarregada:

Image

Image

Os registos do navegador são, então, os seguintes:

Image

Nota: para os testes, poderá ser necessário eliminar o cookie de sessão [5] armazenado no navegador para recomeçar com uma nova sessão, inicializada pelo servidor, na próxima solicitação enviada a este.

Por fim, vamos mostrar a influência da função [incrementCounter] da página [index] no cookie de sessão armazenado no navegador do cliente:


// gestão de eventos
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incremento do contador em 1
      this.$store.commit('increment', 1)
      // alteração do valor apresentado
      this.value = this.$store.state.counter
      // armazenamento do valor no cookie de sessão
      this.$cookies.set('session', { store: this.$store.state }, { path: this.$nuxt.context.base, maxAge: this.$nuxt.context.env.maxAge })
    }
  }
  • linha 10: a alteração do contador é refletida no cookie de sessão;

Vamos verificar este ponto. Partimos da seguinte situação:

Image

  • em [4], o contador do cookie de sessão reflete corretamente o valor apresentado [1];

Agora, vamos incrementar o contador uma vez: [5]. O cookie de sessão, que era [4], evolui da seguinte forma:

Image

  • para [7]; o contador do cookie de sessão passou efetivamente para 84. Para o verificar, é necessário atualizar a visualização [8]. Para tal, selecione outra opção do [Storage] ([9]) e, em seguida, volte a selecionar a opção [8]. O novo valor do cookie de sessão deverá então aparecer;