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

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

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:

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

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

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

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

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

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:

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

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

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

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

Verifica-se que, também aqui, a sessão ficou ativa: [2].
Vamos consultar o valor devolvido pela função [this.$session()]:

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

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