Skip to content

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

9.1. Visão geral

O exemplo [nuxt-05] mostrou que o store pode ser mantido mesmo quando o utilizador força chamadas ao servidor. Os elementos do store são reativos, pelo que, se forem integrados nas vistas, essas vistas reagem às alterações no store. Também poderá querer manter elementos durante as trocas cliente/servidor sem que estes sejam reativos, simplesmente porque não são apresentados pelas vistas. Pode então armazená-los na sessão sem que estejam no store.

O store é facilmente acessível através de propriedades como [context.app.$store] fora das vistas ou [this.$store] dentro das vistas. Gostaríamos de 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. No entanto, não podemos injetar objetos no contexto, apenas funções. Esta função estará 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] é inicialmente criado através da clonagem do projeto [nuxt-05]:

Image

  • Em [1], vamos adicionar uma pasta [plugins];

9.2. O conceito de um plugin [nuxt]

[nuxt] refere-se a [plugin] como qualquer código executado quando a aplicação é iniciada, mesmo antes da função [nuxtServerInit] ser executada pelo servidor, que até agora era a primeira função do 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 ter 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;
    • A chave [mode] está em falta: 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]. Isto não é obrigatório. Os plugins podem ser colocados em qualquer local da estrutura de diretórios do projeto. Da mesma forma, os nomes das subpastas [client, server] são arbitrários neste caso;

Image

9.3. O plugin [session] do servidor

O plugin [server / session.js] é o seguinte:


/* eslint-disable no-console */
export default (context, inject) => {
  // server session management
 
  // is there an existing session?
  let value = context.app.$cookies.get('session')
  if (!value) {
    // new session
    console.log("[plugin session server], démarrage d'une nouvelle session")
    value = initValue
  } else {
    // existing session
    console.log("[plugin session server], reprise d'une session existante")
  }
  // session definition
  const session = {
    // session content
    value,
    // save the session in a cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }
  // we inject a function into [context, Vue] that will render the current session
  inject('session', () => session)
}
 
// initial session value
const initValue = {
  initSessionDone: false
}
  • Linha 2: Os plugins são executados sempre que há um pedido ao servidor: no arranque e sempre que o utilizador força um pedido ao servidor ao introduzir manualmente um URL:
    • primeiro, o(s) plugin(s) do servidor são executados;
    • assim que o navegador do cliente receber a resposta do servidor, é a vez do(s) plugin(s) do cliente serem executados;
  • Linha 2: Todos os plugins, sejam do lado do cliente ou do servidor, recebem dois parâmetros:
    • [context]: o contexto do servidor ou do cliente, dependendo de qual está a executar o plugin;
    • [inject]: uma função que permite que uma função seja injetada no contexto do servidor ou do cliente;
  • O objetivo do plugin [server / session] é duplo:
    • definir uma sessão (linhas 16–23);
    • Definir uma função [$session] dentro do contexto que retorna a sessão da linha 16. A linha 25 faz isso;
  • linhas 16–23: a sessão irá encapsular os seus dados no objeto [value] na linha 18;
  • linhas 20–22: possui uma função [save] que recebe um objeto [context] como parâmetro. O código de chamada fornece este contexto. Com ele, a função [save] guarda o valor da sessão, o objeto [value], no cookie da sessão;
  • linha 6: quando o plugin [server / session] é executado, ele verifica primeiro se o servidor recebeu um cookie de sessão;
    • se sim, o objeto [value] da linha 6 representa o valor da sessão, o conjunto de dados encapsulados nele;
    • caso contrário, nas linhas 7–11, definimos o valor inicial da sessão. Este será o objeto [initValue] nas 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] é uma abreviatura da 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 esta linha é alcançada, a sessão foi criada porque não existia, ou recuperada da solicitação HTTP do navegador do cliente;
  • linha 25: injetamos uma nova função no contexto do servidor:
    • o primeiro parâmetro de [inject] é o nome da função que está a ser criada, neste caso «session». O [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á quaisquer parâmetros;
      • retorna o objeto [session] da linha 16;
  • assim que o plugin for executado:
    • a função [$session] fica disponível em [context.app.$session] sempre que o objeto [context] estiver disponível, ou em [this.$session] numa vista ou no armazenamento [vuex];
    • a função [$session] retorna um objeto [session] com uma única chave [value];
    • Após a criação inicial da sessão, o objeto [value] tem apenas uma chave [initStoreDone] (linhas 29–31). A chave [initStoreDone:false] indica que o store ainda não foi adicionado à sessão. Isto será feito pela função [nuxtServerInit];

9.4. Inicialização da sessão

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


/* eslint-disable no-console */
export const state = () => ({
  // meter
  counter: 0
})
 
export const mutations = {
  // increment counter by one [inc] value
  increment(state, inc) {
    state.counter += inc
  },
  // state replacement
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  }
}
 
export const actions = {
  async nuxtServerInit(store, context) {
    // who executes this code?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // waiting for a promise to be fulfilled
    await new Promise(function(resolve, reject) {
      // this is normally an asynchronous function
      // we simulate it with a one-second wait
      setTimeout(() => {
        // init session
        initSession(store, context)
        // success
        resolve()
      }, 1000)
    })
  }
}
 
function initSession(store, context) {
  // store is the blind to be initialized
 
  // retrieve the session
  const session = context.app.$session()
  // has the session already been initiated?
  if (!session.value.initSessionDone) {
    // start a new blind
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // initialize the blind
    store.commit('increment', 77)
    // put the blind in the session
    session.value.store = store.state
    // initialize a new session
    session.value.somethingImportant = { x: 2, y: 4 }
    // the session is now initialized
    session.value.initSessionDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // update the store with the session store
    store.commit('replace', session.value.store)
  }
  // save the session
  session.save(context)
  // log
  console.log('initSession terminé, store=', store.state, 'session=', session.value)
}

Em comparação com o store no projeto [nuxt-05], apenas a função [initSession] (anteriormente initStore) nas linhas 38–60 foi alterada:

  • linha 42: recuperamos a sessão utilizando a função [$session], que foi injetada no contexto do servidor;
  • linha 44: verificamos se a sessão já foi inicializada;
  • linhas 45–54: se não:
    • linha 48: o store é inicializado;
    • linha 50: o estado do armazenamento é colocado na sessão;
    • linha 52: adicionamos outro objeto [somethingImportant] à sessão. Este objeto não fará parte do armazenamento;
    • linha 54: observamos que a sessão está agora inicializada;
  • linhas 55–59: se a sessão já estava 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. Note-se que isto implica incluir o cookie na resposta HTTP que o servidor 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. A resposta HTTP do servidor incluirá o cookie de sessão. Assim que a página for recebida pelo navegador do cliente, os scripts do lado 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) => {
  // customer session management
 
  // the session necessarily exists, initialized by the server
  console.log('[plugin session client], reprise de la session du serveur')
 
  // session definition
  const session = {
    // session content
    value: context.app.$cookies.get('session'),
    // save the session in a cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }
 
  // we inject a function into [context, Vue] that will render the current session
  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 retornaria a sessão enviada pelo servidor;
  • linha 19: a função injetada [$session] irá devolver a sessão das linhas 9–16;
  • Linhas 9–16: O objeto [session] gerido pelo cliente. Esta será uma cópia da sessão enviada pelo servidor;
  • linha 11: o valor da sessão do cliente é recuperado 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 que o valor da sessão, [this.value] na linha 14, seja guardado no cookie de sessão armazenado no navegador;

9.6. A página [index]

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


<!-- page [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <template slot="right">
      <b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
      <!-- bouton -->
      <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',
  // components used
  components: {
    Layout,
    Navigation
  },
  computed: {
    jsonSession() {
      return JSON.stringify(this.$session().value)
    }
  },
  // life cycle
  beforeCreate() {
    // client and server
    console.log('[home beforeCreate]')
  },
  created() {
    // client and server
    console.log('[home created], session=', this.$session().value)
  },
  beforeMount() {
    // customer only
    console.log('[home beforeMount]')
  },
  mounted() {
    // customer only
    console.log('[home mounted]')
  },
  // event management
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // counter increment of 1
      this.$store.commit('increment', 1)
      // session modification
      const session = this.$session()
      session.value.store = this.$store.state
      session.value.somethingImportant.x++
      session.value.somethingImportant.y++
      // save session in session cookie
      session.save(this.$nuxt.context)
    }
  }
}
</script>

Tenha em mente que esta página é executada tanto no servidor como no cliente.

  • Linha 8: Agora exibimos tanto a sessão como a loja;
  • linha 30: [jsonSession] é uma propriedade calculada que devolve a cadeia JSON do valor da sessão;
  • linha 41: exibimos 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] é executado apenas no lado do cliente;
  • Linha 56: O contador cego é incrementado e exibido como antes;
  • linha 58: a sessão é recuperada utilizando a função injetada [this.$session];
  • linha 59: o armazenamento da sessão é atualizado;
  • linhas 60–61: incrementamos os atributos da sessão [somethingImportant.x, somethingImportant.y]. Isto serve apenas para mostrar que uma sessão pode ser usada para transportar dados além do armazenamento;
  • linha 63: a sessão é guardada no cookie de sessão armazenado no navegador. Numa vista do cliente, o contexto da sessão está disponível em [this.$nuxt.context];

O objetivo da página [index] é demonstrar que a sessão não é reativa, ao passo que o armazenamento é. Quando incrementamos os elementos da sessão, vemos 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] é criada copiando a página [index] e, em seguida, modificando-a ligeiramente:


<!-- page [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <template slot="right">
      <b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
      <!-- bouton -->
      <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',
  // components used
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      session: {}
    }
  },
  computed: {
    jsonSession() {
      return JSON.stringify(this.session.value)
    }
  },
  // life cycle
  beforeCreate() {
    // client and server
    console.log('[page1 beforeCreate]')
  },
  created() {
    // client and server
    // set the session in the page's reactive properties
    this.session = this.$session()
    // log
    console.log('[page1 created], session=', this.session.value)
  },
  beforeMount() {
    // customer only
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // customer only
    console.log('[page1 mounted]')
  },
  // event management
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // counter increment of 1
      this.$store.commit('increment', 1)
      // session modification
      this.session.value.store = this.$store.state
      this.session.value.somethingImportant.x++
      this.session.value.somethingImportant.y++
      // save session in session cookie
      this.session.save(this.$nuxt.context)
    }
  }
}
</script>
  • linha 47: a principal diferença é que estamos a adicionar a sessão atual às propriedades da página (linhas 29–33). Isto tornará a sessão reativa. Quando a função [incrementCounter] incrementar os elementos da sessão, a vista [page1] será atualizada;

9.8. Executar o projeto

Antes de executar o projeto, verifique o cookie de sessão do seu navegador e, se existir, elimine-o para que o servidor crie uma nova sessão:

Image

Agora vamos solicitar a URL [http://localhost:81/nuxt-06/]:

Image

Os registos do navegador ficam então assim:

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;
  • em [5], o cliente recuperou com sucesso esta sessão;

Agora, vamos incrementar o contador três vezes:

Image

  • em [3], o contador foi incrementado, mas não a sessão em [2]. Enquanto [3] exibe a loja, que é reativa, [2] exibe a sessão, que não é reativa:

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], vemos que o armazenamento não é reiniciado, mas é transferido da sessão recebida;
  • em [4-5]: vemos que os atributos da sessão foram, de facto, todos incrementados três vezes;

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

Image

A conclusão tirada desta página é que a sessão pode conter outros elementos além do store, mas estes não são reativos.

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

Image

Em seguida, vamos utilizar o botão [Incrementar] três vezes. A página fica assim:

Image

Desta vez, a sessão é apresentada corretamente em [2]. Aqui, é reativa. Isto pode ser verificado nos registos:

Image

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

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

Image

Em seguida, clique duas vezes no botão [Increment] [4]. A página muda para o seguinte:

Image

Podemos ver que, também aqui, a sessão se tornou reativa [2].

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

Image

  • No separador [View], selecione a página atual [Home] para obter a sua referência [$vm0] [3];

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

Image

  • em [5], vemos que a sessão se tornou reativa, ao passo que inicialmente não o era;
  • em [6], solicitamos o valor da sessão;
  • em [7-8], descobrimos que este valor também se tornou reativo;

Temos, portanto, um resultado inesperado aqui: se um elemento se torna reativo numa página porque foi colocado nas propriedades da página, então também se torna reativo em páginas onde não faz parte das propriedades.

9.9. Conclusão

O exemplo [nuxt-05] mostrou que podíamos manter o store entre pedidos feitos 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 store [Vuex] e tornar-se reativa também, mesmo que não o fosse nativamente.

Então, qual é o objetivo do store [Vuex]? Devo admitir que, por enquanto, isso ainda não ficou claro para mim. É provável que me tenha escapado alguma coisa. Por isso, em caso de dúvida, recomendo usar:

  • um store [Vuex] para guardar tudo o que precisa de ser partilhado entre páginas do lado do cliente e tudo o que possa precisar de ser partilhado entre o cliente e o servidor;
  • um cookie de sessão se o store precisar de ser mantido durante um pedido cliente-servidor, com a sessão a conter apenas o store;

Os exemplos [nuxt-05] e [nuxt-06] tinham como objetivo mostrar como garantir a continuidade da aplicação quando o utilizador força uma chamada ao servidor digitando manualmente URLs. Note-se que o comportamento padrão neste caso é o reinício da aplicação, fazendo com que o seu estado atual se perca.