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]:

- 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;

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:

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

Os registros no navegador ficam então assim:

- 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:

- 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:

- 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;

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:

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

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

- 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:

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

Percebemos que, também aqui, a sessão tornou-se reativa: [2].
Vamos consultar o valor retornado pela função [this.$session()]:

- 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()]:

- 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.