17. Exemplo [nuxt-20]: adaptação do exemplo [vuejs-22]
17.1. Présentation
Propomos aqui portar o exemplo [vuejs-22], que era uma aplicação [vue.js] do tipo SPA, para um contexto [nuxt] SSR. O [vuejs-22] era uma aplicação cliente do servidor de cálculo de impostos que apresentava as seguintes vistas:
A primeira vista é a vista de autenticação:

A segunda vista é a do cálculo do imposto:

A terceira vista é a que apresenta a lista das simulações realizadas pelo utilizador:

O ecrã acima mostra que é possível eliminar a simulação n.º 1. Obtém-se então a seguinte vista:

Se eliminarmos agora a última simulação, obtemos a seguinte nova vista:

Vamos migrar a aplicação [vuejs-22] para a aplicação [nuxt-20] de forma gradual. Não iremos explicar novamente os códigos da [vuejs-22]. Convidamos o leitor a reler o documento |Introdução ao framework VUE.JS através de um exemplo|. As diferentes etapas deverão mostrar as diferenças entre uma aplicação [vuejs] e uma aplicação [nuxt].
17.2. Etapa 1
O projeto [nuxt-20] é inicialmente obtido através da cópia do projeto [nuxt-12]. Este último constitui, de facto, um bom ponto de partida:
- sabe comunicar com o servidor de cálculo de impostos;
- gerencia corretamente os erros que este envia;
- o cliente e o servidor [nuxt] conseguem comunicar através de uma sessão [nuxt];
Temos, portanto, uma boa infraestrutura inicial. O nosso principal trabalho deverá consistir em modificar:
- as páginas. Iremos utilizar as do projeto [vuejs-22], que terão de ser adaptadas ao novo ambiente;
- a gestão da loja. Deverão surgir informações adicionais (lista de simulações) e outras poderão tornar-se desnecessárias;
- a gestão do encaminhamento do cliente e do servidor [nuxt];
Portanto, em primeiro lugar, criamos o projeto [nuxt-20], copiando o projeto [nuxt-12]:

Em seguida, eliminam-se as páginas e os componentes que já não são necessários [2]:
- o componente [components/navigation] desaparece;
- o layout [layout/default] desaparece;
- as páginas [index, authentification, get-admindata, fin-session] desaparecem;
Em seguida, integram-se no [nuxt-20] elementos do [vuejs-22] e do [3]:
- as três páginas [Authentification, CalculImpot, ListeSimulations] da aplicação [vuejs-22] vão para a pasta [pages];
- os componentes [FormCalculImpot, Menu, Layout] da aplicação [vuejs-22] vão para a pasta [components];
- a página [Main] de [vuejs-22], que servia como [layout] para a aplicação [vuejs-22], vai para a pasta [layouts];
Renomeia-se os elementos integrados [4]:

- em [layouts], [Main] passou a ser [default], uma vez que este é o nome predefinido do layout de uma aplicação [nuxt];
- no [pages], a página [Authentification] passou a ser [index], uma vez que [Authentification] desempenhava essa função na aplicação [vuejs-22];
Nesta altura, é possível compilar o projeto para ver os primeiros erros. Altera-se o ficheiro [nuxt.config] do exemplo [nuxt-12] para que passe a executar [nuxt-20]:
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: false,
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '@/plugins/client/plgSession', mode: 'client' },
{ src: '@/plugins/server/plgSession', mode: 'server' },
{ src: '@/plugins/client/plgDao', mode: 'client' },
{ src: '@/plugins/server/plgDao', mode: 'server' },
{ src: '@/plugins/client/plgEventBus', mode: 'client' }
],
/*
** 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-20',
// router
router: {
// raiz dos URL da aplicação
base: '/nuxt-20/',
// middleware de encaminhamento
middleware: ['routing']
},
// 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 da máquina
host: 'localhost'
},
// ambiente
env: {
// configuração do Axios
timeout: 2000,
withCredentials: true,
baseURL: 'http://localhost/php7/scripts-web/impots/versão-14',
// configuração do cookie de sessão [nuxt]
maxAge: 60 * 5
}
}
Em seguida, executa-se um [build] do projeto:

Os erros detetados são os seguintes:
- o erro na linha 1 indica que está a ser feita referência a uma imagem inexistente. Iremos recuperá-la no [vuejs-22];
- o erro na linha 2 mostra que o componente [./FormCalculImpot] não existe. De facto, este componente encontra-se agora em [@/components/form-calcul-impot];
- os erros nas linhas [3-5] indicam que o componente [./Layout] não existe. De facto, esse componente encontra-se agora em [@/components/layout];
- os erros nas linhas [6-7] indicam que o componente [./Menu] não existe. De facto, chama-se agora [@/components/menu];
Adicionamos a imagem [assets/logo.jpg] ao projeto [nuxt-20]:

Além disso, em todas as páginas, vamos corrigir o caminho dos componentes. Tomemos como exemplo a página [calcul-impot]:
<!-- definição HTML da vista -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formulário de cálculo do imposto à direita -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menu de navegação à esquerda -->
<Menu slot="left" :options="options" />
</Layout>
<!-- área de exibição dos resultados do cálculo do imposto abaixo do formulário -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- área de três colunas vazia -->
<b-col sm="3" />
<!-- área com nove colunas -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
<script>
// importações
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'
export default {
// componentes utilizados
components: {
Layout,
FormCalculImpot,
Menu
},
Os três [import] das linhas 26-28 passam a ser:
// importações
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
Verifica-se e, se necessário, corrige-se assim os [import] de todos os componentes, layouts e páginas. Depois de efetuadas estas correções, pode-se tentar um novo [build]. Normalmente, já não há erros.
Pode-se então tentar uma execução:

Aparecem erros:
Verifica-se que o comando [dev], em conjunto com o módulo [eslint], é mais rigoroso, do ponto de vista sintático, do que o comando [build]. Neste caso, exige que o operador de comparação [!=] seja escrito como [!==], que é um operador mais restritivo (verifica também o tipo dos operandos). Estes erros ocorrem na página [index.vue].
Corrigimos os erros acima e reiniciamos a execução do projeto. Recebemos então um aviso do módulo [eslint]:

Corrigimos este erro com o [Quick fix] do módulo [eslint] [2].
Reiniciamos a execução do projeto. Já não há erros de compilação. Em seguida, acedemos ao URL e ao [http://localhost:81/nuxt-20/] através de um navegador. Obtemos um erro de execução:

O erro ocorre em [index.vue] [2]. O erro [1] deve-se ao facto de, em [vuejs-22], a camada [dao] estava disponível em [this.$dao], enquanto que em [nuxt-12], cuja infraestrutura adotámos, está disponível na função [this.$dao()].
O erro encontra-se na função [created] do ciclo de vida da página [index]:

Por enquanto, limitamo-nos a renomear a função [created] para [created2], para que a função do ciclo de vida [created] não seja executada ([3]).
Guardamos a alteração e recarregamos a página [index] no navegador. Desta vez está tudo bem:

17.3. passo 2
As páginas do projeto [vuejs-22] utilizavam os seguintes elementos inseridos:
- $dao: para a camada [dao] do cliente [vue.js];
- $session: para uma sessão armazenada no [localStorage] do navegador;
Estes elementos já não existem na infraestrutura do projeto [nuxt-12] que copiámos:
- existem agora duas camadas [dao], uma para o cliente [nuxt] e outra para o servidor [nuxt]. Ambas estão disponíveis através de uma função injetada denominada [$dao]. Isto significa que, nas páginas da aplicação, [this.$dao] deve ser substituído por [this.$dao()];
- a sessão [nuxt] gerida pela aplicação [nuxt-20] já não tem qualquer relação com o objeto [$session] daaplicação [vuejs-22], onde não existia o conceito de cookie de sessão. No entanto, têm uma funcionalidade semelhante: armazenar informações persistentes ao longo das ações do utilizador. A sessão [nuxt] armazena as informações no store, em vez de diretamente na sessão. Nas páginas da aplicação, [this.$session] deve ser substituído por [this.$store] quando se trata de memorizar informações na sessão e por [this.$session()] quando se trata de manipular a própria sessão;
- para saber o estado de uma propriedade P do armazenamento, deverá escrever-se [this.$store.state.P];
- para alterar a propriedade P do store, é necessário escrever [this.$store.commit(‘replace’, {P:value}];
Efetua-se estas alterações na página [index]:
<!-- definição HTML da vista -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- formulário HTML — os valores são lançados com a ação [authentifier-utilisateur] -->
<b-form @submit.prevent="login">
<!-- título -->
<b-alert show variant="primary">
<h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
</b-alert>
<!-- 1.ª linha -->
<b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
<!-- área de introdução de dados do utilizador -->
<b-col cols="6">
<b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
</b-col>
</b-form-group>
<!-- 2.ª linha -->
<b-form-group label="Mot de passe" label-for="password" label-cols="3">
<!-- campo de introdução da palavra-passe -->
<b-col cols="6">
<b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
</b-col>
</b-form-group>
<!-- 3.ª linha -->
<b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
<!-- botão do tipo [submit] na 3.ª linha -->
<b-row>
<b-col cols="2">
<b-button :disabled="!valid" variant="primary" type="submit">Valider</b-button>
</b-col>
</b-row>
</b-form>
</template>
</Layout>
</template>
<!-- dinâmica da vista -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// componentes utilizados
components: {
Layout
},
// estado do componente
data() {
return {
// utilizador
user: '',
// a sua palavra-passe
password: '',
// controla a exibição de uma mensagem de erro
showError: false,
// a mensagem de erro
message: ''
}
},
// propriedades calculadas
computed: {
// entradas válidas
valid() {
return this.user && this.password && this.$store.state.started
}
},
// ciclo de vida: o componente acaba de ser criado
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
// o utilizador pode realizar simulações?
if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
// então o utilizador pode realizar simulações
this.$router.push({ name: 'calculImpot' })
// regresso ao ciclo de eventos
return
}
// se a sessão jSON já tiver sido iniciada, não a reiniciamos novamente
if (!this.$store.state.started) {
// início da espera
this.$emit('loading', true)
// inicializa-se a sessão com o servidor — pedido assíncrono
// utiliza-se a promessa devolvida pelos métodos da camada [dao]
this.$dao()
// inicialização de uma sessão jSON
.initSession()
// obteve-se a resposta
.then((response) => {
// fim da espera
this.$emit('loading', false)
// análise da resposta
if (response.état !== 700) {
// exibe-se o erro
this.message = response.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// a sessão foi iniciada
this.$store.commit('replace', { started: true })
console.log('[authentification], session=', this.$session())
})
// em caso de erro
.catch((error) => {
// o erro é reportado à vista [Main]
this.$emit('error', error)
})
// em todos os casos
.finally(() => {
// a sessão é guardada
this.$session().save()
})
}
},
// gestores de eventos
methods: {
// ----------- autenticação
async login() {
try {
// início da espera
this.$emit('loading', true)
// ainda não estamos autenticados
this.$store.commit('replace', { authenticated: false })
// autenticação bloqueante junto do servidor
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// fim do carregamento
this.$emit('loading', false)
// análise da resposta do servidor
if (response.état !== 200) {
// exibe-se o erro
this.message = response.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// sem erro
this.showError = false
// autenticação bem-sucedida
this.$store.commit('replace', { authenticated: true })
// --------- solicita-se agora os dados da administração fiscal
// inicialmente, sem dados
this.$métier.setTaxAdminData(null)
// início da espera
this.$emit('loading', true)
// pedido em espera junto do servidor
const response2 = await this.$dao().getAdminData()
// fim do carregamento
this.$emit('loading', false)
// análise da resposta
if (response2.état !== 1000) {
// exibe-se o erro
this.message = response2.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// sem erro
this.showError = false
// os dados recebidos são armazenados na camada [métier]
this.$métier.setTaxAdminData(response2.réponse)
// é possível avançar para o cálculo do imposto
this.$router.push({ name: 'calculImpot' })
} catch (error) {
// o erro é reportado ao componente principal
this.$emit('error', error)
} finally {
// atualização da memória
this.$store.commit('replace', { métier: this.$métier })
// a sessão é guardada
this.$session().save()
}
}
}
}
</script>
É importante destacar os seguintes pontos:
- linha 69: a função [created2] foi renomeada para [mounted], para que o servidor [nuxt] não a execute (este não executa nem a [beforeMount] nem a [mounted]). Apenas o cliente [nuxt] irá executá-lo, tal como acontecia com o exemplo [vuejs-22];
- linha 73: é feita uma referência a [this.$métier], que, por enquanto, não existe;
- linha 75: nunca utilizámos este método numa aplicação [nuxt]. Será necessário verificar se funciona num contexto [nuxt];
- linhas 112 e 172: no [vuejs-22], a sessão do projeto era guardada desta forma. No projeto [nuxt-20], o método [save] deve receber o contexto atual. Sabe-se que, numa página [nuxt], o objeto [context] está disponível em [this.$nuxt.context];
As linhas 112 e 172 são, portanto, reescritas da seguinte forma:
this.$session().save(this.$nuxt.context)
Note-se que este código não está otimizado. Em vez de utilizar várias vezes a função [this.$session()], seria preferível escrever:
const session=this.$session()
e, em seguida, utilizar a variável [session]. O mesmo raciocínio aplica-se à função [this.$dao()].
Feitas estas correções, podemos recarregar o URL [http://localhost:81/nuxt-20/] num navegador. Obtemos sempre a mesma página que anteriormente:

Vejamos os registos do navegador:

O registo [1] é o último registo efetuado pelo cliente [nuxt]. No [2], vemos que a propriedade [started] está em [vrai], o que significa que a função [mounted] conseguiu iniciar uma sessão jSON com o servidor de cálculo de impostos. Vemos também que o «store» tem propriedades que terão de ser eliminadas ou renomeadas. Recorde-se que estamos a utilizar o «store» do exemplo [nuxt-12].
Agora, vamos solicitar novamente o URL [http://localhost:81/nuxt-20/] enquanto o servidor de cálculo de impostos não está em execução. Em primeiro lugar, certificamo-nos de eliminar o cookie da sessão [nuxt]:

A captura de ecrã acima foi feita no Chrome. Feito isto, o URL [http://localhost:81/nuxt-20/] apresenta o seguinte resultado:

O erro foi corretamente tratado pelo projeto [vuejs-22]. Continua a ser corretamente tratado pelo projeto [nuxt-20].
17.4. Etapa 3
Agora que temos a página de autenticação, é necessário analisar o código executado quando o utilizador clica no botão [Valider]:
// gestores de eventos
methods: {
// ----------- autenticação
async login() {
try {
// início da espera
this.$emit('loading', true)
// ainda não estamos autenticados
this.$store.commit('replace', { authenticated: false })
// autenticação bloqueante junto do servidor
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// fim do carregamento
this.$emit('loading', false)
// análise da resposta do servidor
if (response.état !== 200) {
// exibe-se o erro
this.message = response.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// sem erro
this.showError = false
// autenticação bem-sucedida
this.$store.commit('replace', { authenticated: true })
// --------- agora solicitam-se os dados da administração fiscal
// inicialmente, sem dados
this.$métier.setTaxAdminData(null)
// início da espera
this.$emit('loading', true)
// pedido em espera junto do servidor
const response2 = await this.$dao().getAdminData()
// fim do carregamento
this.$emit('loading', false)
// análise da resposta
if (response2.état !== 1000) {
// é apresentado o erro
this.message = response2.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// sem erro
this.showError = false
// os dados recebidos são armazenados na camada [métier]
this.$métier.setTaxAdminData(response2.réponse)
// é possível avançar para o cálculo do imposto
this.$router.push({ name: 'calculImpot' })
} catch (error) {
// o erro é reportado ao componente principal
this.$emit('error', error)
} finally {
// atualização da memória
this.$store.commit('replace', { métier: this.$métier })
// guardamos a sessão
this.$session().save(this.$nuxt.context)
}
}
}
O principal problema aqui parece ser a ausência do dado [this.$métier]. Para resolver isso, vamos:
- incluir a classe [Métier] do exemplo [vuejs-22]. Vamos colocá-la na pasta [api];
- injetar uma função [$métier] no contexto do cliente [nuxt], que dará acesso a esta classe;
Primeiro, a cópia da classe [Métier] para a pasta [api]:

Assim que a classe [Métier] estiver presente no projeto, cria-se um novo plugin para o cliente [nuxt]. Este plugin, denominado [pluginMétier], irá injetar uma função [$métier] que dará acesso à classe [Métier]:
/* eslint-disable no-console */
// cria-se um ponto de acesso à camada [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
// instanciação da camada [métier]
const métier = new Métier()
// injeção de uma função [$métier] no contexto
inject('métier', () => métier)
// registo
console.log('[fonction client $métier créée]')
}
Feito isto, podemos corrigir a página [index]:
// ciclo de vida: o componente acaba de ser criado
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
// o utilizador pode realizar simulações?
if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
// então o utilizador pode realizar simulações
this.$router.push({ name: 'calcul-impot' })
// regresso ao ciclo de eventos
return
}
// se a sessão jSON já tiver sido iniciada, não a reiniciamos novamente
...
},
// gestores de eventos
methods: {
// ----------- autenticação
async login() {
try {
// início da espera
this.$emit('loading', true)
// ainda não estamos autenticados
this.$store.commit('replace', { authenticated: false })
// autenticação bloqueante junto do servidor
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// fim do carregamento
this.$emit('loading', false)
// análise da resposta do servidor
if (response.état !== 200) {
// exibe-se o erro
this.message = response.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// sem erro
this.showError = false
// autenticação bem-sucedida
this.$store.commit('replace', { authenticated: true })
// --------- solicita-se agora os dados da administração fiscal
// inicialmente, sem dados
this.$métier().setTaxAdminData(null)
// início da espera
this.$emit('loading', true)
// pedido em espera junto do servidor
const response2 = await this.$dao().getAdminData()
// fim do carregamento
this.$emit('loading', false)
// análise da resposta
if (response2.état !== 1000) {
// exibe-se o erro
this.message = response2.réponse
this.showError = true
// regresso ao ciclo de eventos
return
}
// sem erro
this.showError = false
// os dados recebidos são armazenados na camada [métier]
this.$métier().setTaxAdminData(response2.réponse)
// é possível avançar para o cálculo do imposto
this.$router.push({ name: 'calcul-impot' })
} catch (error) {
// o erro é reportado ao componente principal
this.$emit('error', error)
} finally {
// atualização da memória
this.$store.commit('replace', { métier: this.$métier() })
// a sessão é guardada
this.$session().save(this.$nuxt.context)
}
}
}
- linhas 43, 61, 69: [this.$métier] foi substituído por [this.$métier()];
- linhas 8 e 63: o nome da página [CalculImpot] do projeto [vuejs-22] passou a ser a página [calcul-impot] no projeto [nuxt-20];
Feitas estas correções, podemos tentar validar a página de autenticação:

A página obtida é a seguinte:

Conseguimos, de facto, aceder à página de cálculo do imposto. Agora, vamos analisar os registos:

Em [2], verifica-se que o facto de estarmos autenticados foi corretamente registado. Em [3-4], verifica-se que foram recuperados os dados [taxAdminData], que permitem o cálculo do imposto através da classe [Métier].
17.5. Etapa 4
Analisemos a página [calcul-impot] que obtivemos:
<!-- definição HTML da vista -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- formulário de cálculo do imposto à direita -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- menu de navegação à esquerda -->
<Menu slot="left" :options="options" />
</Layout>
<!-- área de exibição dos resultados do cálculo do imposto abaixo do formulário -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- área vazia com três colunas -->
<b-col sm="3" />
<!-- área com nove colunas -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
<script>
// importações
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
export default {
// componentes utilizados
components: {
Layout,
FormCalculImpot,
Menu
},
// relatório interno
data() {
return {
// opções do menu
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// resultado do cálculo do imposto
résultat: '',
résultatObtenu: false
}
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
},
// métodos de gestão de eventos
methods: {
// resultado do cálculo do imposto
handleResultatObtenu(résultat) {
// construímos o resultado numa cadeia HTML
const impôt = "Montant de l'impôt : " + résultat.impôt + ' euro(s)'
const décôte = 'Décôte : ' + résultat.décôte + ' euro(s)'
const réduction = 'Réduction : ' + résultat.réduction + ' euro(s)'
const surcôte = 'Surcôte : ' + résultat.surcôte + ' euro(s)'
const taux = "Taux d'imposition : " + résultat.taux
this.résultat = impôt + '<br/>' + décôte + '<br/>' + réduction + '<br/>' + surcôte + '<br/>' + taux
// exibição do resultado
this.résultatObtenu = true
// ---- atualização da loja [Vuex]
// uma simulação de +
this.$store.commit('addSimulation', résultat)
// guardamos a sessão
this.$session.save()
}
}
}
</script>
- linhas 44 e 48: os links do menu de navegação estão corretos. A página [/fin-session] não existe. O projeto [vuejs-22] resolvia este problema através do encaminhamento. Faremos o mesmo com o projeto [nuxt-20];
- linha 76: é feita referência a uma alteração [addSimulation] que ainda não existe. Vamos criá-la;
- linha 78: tal como na página [index], é necessário escrever [this.$session().save(this.$nuxt.context)];
Vamos alterar o store [store/index]. Herdado do projeto [nuxt-12], apresenta-se, de momento, da seguinte forma:
/* eslint-disable no-console */
// estado da loja
export const state = () => ({
// sessão jSON iniciada
jsonSessionStarted: false,
// utilizador autenticado
userAuthenticated: false,
// cookie de sessão PHP
phpSessionCookie: '',
// adminData
adminData: ''
})
// alterações no armazenamento
export const mutations = {
// substituição do estado
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// reinicialização do armazenamento
reset() {
this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
}
}
// ações do armazenamento
export const actions = {
nuxtServerInit(store, context) {
// quem executa este código?
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// inicialização da sessão
initStore(store, context)
}
}
function initStore(store, context) {
// o store é o store a ser inicializado
// recuperamos a sessão
const session = context.app.$session()
// A sessão já foi inicializada?
if (!session.value.initStoreDone) {
// inicia-se um novo «store»
console.log("nuxtServerInit, initialisation d'un nouveau store")
// coloca-se o armazenamento na sessão
session.value.store = store.state
// o armazenamento está agora inicializado
session.value.initStoreDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// o armazenamento está a ser atualizado com o armazenamento da sessão
store.commit('replace', session.value.store)
}
// a sessão é guardada
session.save(context)
// registo
console.log('initStore terminé, store=', store.state)
}
- linhas 3-27: vamos retomar o estado e as alterações da aplicação [vuejs-22] (ver documento [3]):
// estado da persiana
export const state = () => ({
// sessão jSON iniciada
started: false,
// utilizador autenticado
authenticated: false,
// cookie de sessão PHP
phpSessionCookie: '',
// lista de simulações
simulations: [],
// o n.º da última simulação
idSimulation: 0,
// camada [métier]
métier: null
})
// alterações na persiana
export const mutations = {
// substituição do estado
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// reinicialização do armazenamento
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
},
// eliminação da linha n.º índice
deleteSimulation(state, index) {
// eslint-disable-next-line no-console
console.log('mutation deleteSimulation')
// elimina-se a linha n.º [index]
state.simulations.splice(index, 1)
console.log('store simulations', state.simulations)
},
// adição de uma simulação
addSimulation(state, simulation) {
// eslint-disable-next-line no-console
console.log('mutation addSimulation')
// n.º da simulação
state.idSimulation++
simulation.id = state.idSimulation
// a simulação é adicionada à tabela de simulações
state.simulations.push(simulation)
}
}
- linhas 4 e 6: introduzimos as propriedades já utilizadas;
- linha 8: mantemos o cookie de sessão PHP. É fundamental para que o cliente e o servidor [nuxt] tenham a mesma sessão PHP com o servidor de cálculo do imposto;
- linha 10: a lista das simulações realizadas pelo utilizador;
- linha 12: o número da última simulação efetuada pelo utilizador;
- linha 14: a camada [métier];
- linhas 30-47: as alterações presentes no armazenamento do projeto [vuejs-22] e referenciadas pelas páginas da aplicação. O projeto [vuejs-22] tinha uma alteração denominada [clear] que esvaziava a lista de simulações. Não a incluímos porque a mutação [reset], já presente, deverá ser suficiente;
- linhas 26-28: a mutação [reset] é alterada para ter em conta o novo conteúdo do estado;
A página [calcul-impot] utiliza o seguinte componente [form-calcul-impot]:
<!-- definição HTML da vista -->
<template>
<!-- formulário HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- mensagem em 12 colunas sobre fundo azul -->
<b-row>
<b-col sm="12">
<b-alert show variant="primary">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</b-alert>
</b-col>
</b-row>
<!-- elementos do formulário -->
<!-- primeira linha -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- botões de opção em 5 colunas-->
<b-col sm="5">
<b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
<b-form-radio v-model="marié" value="non">Non</b-form-radio>
</b-col>
</b-form-group>
<!-- segunda linha -->
<b-form-group label="Nombre d'enfants à charge" label-for="enfants">
<b-form-input id="enfants" v-model="enfants" :state="enfantsValide" type="text" placeholder="Indiquez votre nombre d'enfants"></b-form-input>
<!-- eventual mensagem de erro -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- terceira linha -->
<b-form-group label="Salaire annuel net imposable" label-for="salaire" description="Arrondissez à l'euro inférieur">
<b-form-input id="salaire" v-model="salaire" :state="salaireValide" type="text" placeholder="Salaire annuel"></b-form-input>
<!-- eventual mensagem de erro -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- quarta linha, botão [submit] -->
<b-col sm="3">
<b-button :disabled="formInvalide" type="submit" variant="primary">Valider</b-button>
</b-col>
</b-form>
</template>
<!-- script -->
<script>
export default {
// estado interno
data() {
return {
// casado ou solteiro
marié: 'non',
// número de filhos
enfants: '',
// salário anual
salaire: ''
}
},
// estado interno calculado
computed: {
// validação do formulário
formInvalide() {
return (
// salário inválido
!this.salaire.match(/^\s*\d+\s*$/) ||
// ou número de filhos inválido
!this.enfants.match(/^\s*\d+\s*$/) ||
// ou dados fiscais não obtidos
!this.$métier.taxAdminData
)
},
// validação do salário
salaireValide() {
// deve ser um valor numérico >=0
return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
},
// validação dos filhos
enfantsValide() {
// deve ser um valor numérico >=0
return Boolean(this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/))
}
},
// ciclo de vida
created() {
// registo
// eslint-disable-next-line
console.log("FormCalculImpot created");
},
// gestor de eventos
methods: {
calculerImpot() {
// o imposto é calculado utilizando a camada [métier]
const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
// eslint-disable-next-line
console.log("résultat=", résultat);
// completa-se o resultado
résultat.marié = this.marié
résultat.enfants = this.enfants
résultat.salaire = this.salaire
// emite-se o evento [resultatObtenu]
this.$emit('resultatObtenu', résultat)
}
}
}
</script>
- linhas 65, 89: a referência [this.$métier] deve ser alterada para [this.$métier()];
Depois de efetuadas estas correções, pode-se tentar uma simulação:

Obtém-se a seguinte resposta:

Se analisarmos os registos:

- em [9-10], verifica-se que a primeira simulação se encontra, de facto, no [store];
- em [5], o número da última simulação foi efetivamente incrementado;
17.6. passo 5
Agora que já realizámos uma simulação, cliquemos na ligação [Liste des simulations]. Obtemos a seguinte página:

O encaminhamento do cliente [nuxt] foi efetuado corretamente. Vejamos o código da página [liste-des-simulations]:
<!-- definição HTML da vista -->
<template>
<div>
<!-- formatação -->
<Layout :left="true" :right="true">
<!-- simulações na coluna da direita -->
<template slot="right">
<template v-if="simulations.length == 0">
<!-- sem simulações -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length != 0">
<!-- existem simulações -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- tabela de simulações -->
<b-table :items="simulations" :fields="fields" striped hover responsive>
<template v-slot:cell(action)="data">
<b-button @click="supprimerSimulation(data.index)" variant="link">Supprimer</b-button>
</template>
</b-table>
</template>
</template>
<!-- menu de navegação na coluna da esquerda -->
<Menu slot="left" :options="options" />
</Layout>
</div>
</template>
<script>
// importações
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
// componentes
components: {
Layout,
Menu
},
// estado interno
data() {
return {
// opções do menu de navegação
options: [
{
text: "Calcul de l'impôt",
path: '/calcul-impot'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// parâmetros da tabela HTML
fields: [
{ label: '#', chave: 'id' },
{ label: 'Marié', key: 'marié' },
{ label: "Nombre d'enfants", key: 'enfants' },
{ label: 'Salaire', key: 'salaire' },
{ label: 'Impôt', key: 'impôt' },
{ label: 'Décôte', key: 'décôte' },
{ label: 'Réduction', key: 'réduction' },
{ label: 'Surcôte', key: 'surcôte' },
{ label: '', key: 'action' }
]
}
},
// estado interno calculado
computed: {
// lista de simulações obtida no armazenamento Vuex
simulations() {
return this.$store.state.simulations
}
},
// ciclo de vida
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
},
// métodos
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// eliminação da simulação n.º [index]
this.$store.commit('deleteSimulation', index)
// a sessão é guardada
this.$session.save()
}
}
}
</script>
- linhas 47-56: os destinos do menu de navegação estão corretos;
- linha 75: a loja está corretamente referenciada;
- linha 89: está a ser utilizada uma mutação [deleteSimulation] que integrámos na etapa anterior;
- linha 91: esta linha deve ser reescrita como [this.$session().save(this.$nuxt.context)];
Fazemos as alterações necessárias e, em seguida, tentamos eliminar a simulação apresentada:

Obtenha-se então a seguinte página:

Vamos analisar os registos:

- em [6], vemos que a tabela de simulações está vazia;
Agora, voltemos ao formulário de cálculo do imposto:

Obtemos a seguinte página:

Portanto, o encaminhamento funcionou.
17.7. Passo 6
Resta-nos ainda gerir a opção de navegação [Fin de session] do menu de navegação:
// opções do menu
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
]
- na linha 9, a página [/fin-session] não existe. O projeto [vuejs-22] tratava este caso com regras de encaminhamento num ficheiro [router.js]:
// importações
import Vue from 'vue'
import VueRouter from 'vue-router'
// as visualizações
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// a sessão
import session from './session'
// plug-in de encaminhamento
Vue.use(VueRouter)
// as rotas da aplicação
const routes = [
// autenticação
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification2', component: Authentification },
// cálculo do imposto
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// lista de simulações
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
// fim da sessão
{
path: '/fin-session', name: 'finSession'
},
// página desconhecida
{
path: '*', name: 'notFound', component: NotFound,
},
]
// o router
const router = new VueRouter({
// as rotas
routes,
// o modo de visualização do URL
mode: 'history',
// o URL básico da aplicação
base: '/client-vuejs-impot/'
})
// verificação de rotas
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
// rota reservada a utilizadores autenticados?
if (to.meta.authenticated && !session.authenticated) {
next({
// passamos à autenticação
name: 'authentification',
})
// regresso ao ciclo de eventos
return;
}
// caso específico do fim da sessão
if (to.name === "finSession") {
// limpa-se a sessão
session.clear();
// avançamos para a vista [authentification]
next({
name: 'authentification',
})
// regresso ao ciclo de eventos
return;
}
// outros casos — vista seguinte normal do encaminhamento
next();
})
// exportação do router
export default router
- as linhas 64-76 geriam o caso específico do encaminhamento para o caminho [/fin-session];
- linha 66: esvazia-se a sessão atual;
- linhas 68-70: exibe-se a vista [authentification];
Vamos tentar fazer algo semelhante no ficheiro de encaminhamento do cliente [nuxt]:

O script [client/routing.js] passa a ser o seguinte:
/* eslint-disable no-console */
export default function(context) {
// quem executa este código?
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// gestão do cookie de sessão PHP no navegador
// o cookie de sessão PHP do navegador deve ser idêntico ao encontrado na sessão Nuxt
// a ação [fin-session] recebe um novo cookie PHP (tanto no servidor como no cliente Nuxt)
// se for o servidor a recebê-lo, o cliente deve transmiti-lo ao navegador
// para as suas próprias comunicações com o servidor PHP
// estamos aqui num encaminhamento do cliente
// recuperamos o cookie da sessão PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// se existir, atribui-se o cookie de sessão PHP ao navegador
document.cookie = phpSessionCookie
}
// para onde vamos?
const to = context.route.path
if (to === '/fin-session') {
// limpa-se a sessão
const session = context.app.$session()
session.reset(context)
// redireciona-se para a página inicial
context.redirect({ name: 'index' })
}
}
- Adicionámos as linhas [19-27] ao código existente;
- linha 20: recupera-se o [path] do destino do percurso atual;
- linha 21: verifica-se se é [/fin-session]. Se for:
- linhas 23-24: a sessão é reiniciada;
- linha 26: redireciona-se o cliente [nuxt] para a página inicial;
O método [session.reset(context)] (linha 24) da sessão é o seguinte:
// reinicialização da sessão
reset(context) {
console.log('nuxt-session reset')
// reinicialização do armazenamento
context.store.commit('reset')
// guardar o novo armazenamento na sessão e guardar a sessão
this.save(context)
}
O método [context.store.commit('reset')] (linha 5) é o seguinte:
// reinicialização do armazenamento
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}
Ao utilizar agora o link [Fin de session], a página inicial é apresentada com os seguintes registos:

- em [3], verifica-se que já não estamos autenticados;
- em [4], verifica-se que a sessão jSON foi iniciada;
- em [6], a camada [métier] já não está presente no armazenamento (continua presente nas páginas com [this.$métier()]);
- em [5, 7], já não existem simulações;
É importante compreender bem o que acontece no final de uma sessão:
- a sessão [nuxt] é reiniciada: a propriedade [started] do armazenamento passa para [false];
- ocorre um redirecionamento para a página [index];
- o método [mounted] da página [index] é executado. Este inicia uma nova sessão jSON com o servidor de cálculo de impostos. Se a operação for bem-sucedida, a propriedade [started] do armazenamento passa a ser [true];
17.8. etapa 7
Nesta fase, a aplicação [nuxt-20] dispõe de todas as funcionalidades da aplicação [vuejs-22]. A migração parece estar concluída.
Vamos avançar um pouco mais, seguindo a lógica de [nuxt]. O método [mounted] da página [index] apresenta um problema. Este método inicia uma operação assíncrona cuja conclusão um motor de busca não irá aguardar. Sabemos que, neste caso, é necessário colocar a operação assíncrona numa função [asyncData], pois assim o servidor [nuxt] que a executa aguarda que ela termine antes de entregar a página ao motor de busca.
Recorremos aqui à função [asyncData], escrita na aplicação [nuxt-12] para a página [index]:
export default {
name: 'InitSession',
// componentes utilizados
components: {
Layout,
Navigation
},
// dados assíncronos
async asyncData(context) {
// registo
console.log('[index asyncData started]')
// não se repetem as operações se a página já tiver sido solicitada
if (process.server && context.store.state.jsonSessionStarted) {
console.log('[index asyncData canceled]')
return { result: '[succès]' }
}
try {
// inicia-se uma sessão jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// registo
console.log('[index asyncData response=]', response)
// recupera-se o cookie de sessão PHP para as próximas solicitações
const phpSessionCookie = dao.getPhpSessionCookie()
// o cookie de sessão PHP é guardado na sessão [nuxt]
context.store.commit('replace', { phpSessionCookie })
// Houve algum erro?
if (response.état !== 700) {
// o erro encontra-se em response.réponse
throw new Error(response.réponse)
}
// regista-se que a sessão jSON foi iniciada
context.store.commit('replace', { jsonSessionStarted: true })
// apresenta-se o resultado
return { result: '[succès]' }
} catch (e) {
// registo
console.log('[index asyncData error=]', e)
// regista-se que a sessão jSON não foi iniciada
context.store.commit('replace', { jsonSessionStarted: false })
// é sinalizado o erro
return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
// guarda-se o armazenamento
const session = context.app.$session()
session.save(context)
// registo
console.log('[index asyncData finished]')
}
},
// ciclo de vida
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
console.log('[index beforeMount]')
},
mounted() {
console.log('[index mounted]')
// apenas cliente
if (this.showErrorLoading) {
console.log('[index mounted, showErrorLoading=true]')
this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
}
}
- linhas 13, 33, 40: é necessário alterar a propriedade [jsonSessionStarted] para [started];
- linha 13: na aplicação [nuxt-12], apenas o servidor [nuxt] executava a página [index] e a sua função [asyncData]. O cliente [nuxt] só executava a página [index] depois de a ter recebido do servidor [nuxt] e, nessa altura, não executava a função [asyncData]. Em [nuxt-20], a situação é diferente: o link [Fin de session] irá apresentar a página [index] no ambiente do cliente [nuxt]. A função [asyncData] será então executada. No entanto, quando se acede à página [index] desta forma, a sessão [nuxt] foi reiniciada entretanto e a propriedade [started] do armazenamento tem o valor [false], pelo que a condição da linha 13 será inevitavelmente falsa. Podemos, portanto, manter [process.server] e, assim, o cliente [nuxt] não realizará este teste;
- linhas 15, 35, 42: uma propriedade [result] é inserida nas propriedades [data] da página [index]. Em [nuxt-20], esta propriedade não será utilizada e iremos removê-la do resultado devolvido pela função;
- linhas 61-67: este método [mounted] deve ser mantido, pois é ele que permite ao cliente [nuxt] apresentar a mensagem de erro. No entanto, a forma de gerir o erro será alterada;
Na página [index] atual, integramos a função [asyncData] acima, em substituição da antiga função [mounted], e adicionamos uma nova função [mounted]. O código da página [index] do exemplo [nuxt-20] passa então a ser o seguinte:
...
<!-- dinâmica da vista -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// componentes utilizados
components: {
Layout
},
// estado do componente
data() {
return {
// utilizador
user: '',
// a sua palavra-passe
password: '',
// exibição de erros
showError: false
}
},
// propriedades calculadas
computed: {
// entradas válidas
valid() {
return this.user && this.password && this.$store.state.started
}
},
// dados assíncronos
async asyncData(context) {
// registo
console.log('[index asyncData started]')
// não se repete o processo se a página já tiver sido solicitada
if (process.server && context.store.state.started) {
console.log('[index asyncData canceled]')
return
}
try {
// inicia-se uma sessão jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// registo
console.log('[index asyncData response=]', response)
// recupera-se o cookie de sessão PHP para as próximas solicitações
const phpSessionCookie = dao.getPhpSessionCookie()
// o cookie de sessão PHP é guardado na sessão [nuxt]
context.store.commit('replace', { phpSessionCookie })
// Houve algum erro?
if (response.état !== 700) {
// o erro encontra-se em response.réponse
throw new Error(response.réponse)
}
// regista-se que a sessão jSON foi iniciada
context.store.commit('replace', { started: true })
// sem resultados
return
} catch (e) {
// registo
console.log('[index asyncData error=]', e.message)
// regista-se que a sessão jSON não foi iniciada
context.store.commit('replace', { started: false })
// o erro é sinalizado
return { showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
// o armazenamento é guardado
const session = context.app.$session()
session.save(context)
// registo
console.log('[index asyncData finished]')
}
},
// ciclo de vida
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
// apenas para o cliente
console.log('[index beforeMount]')
// gestão de eventuais erros
if (this.showErrorLoading) {
// registo
console.log('[index beforeMount, showErrorLoading=true]')
// o erro é reportado ao componente principal [default]
this.$emit('error', new Error(this.errorLoadingMessage))
}
},
mounted() {
console.log('[index mounted]')
},
// gestores de eventos
methods: {
// ----------- autenticação
async login() {
...
}
</script>
- linhas 58, 65: a função [asyncData] já não torna a propriedade [result] obsoleta neste contexto;
- linha 81: o método [beforeMount] do cliente [nuxt]. Foi preferido ao método [mounted] para gerir um eventual erro do [asyncData];
- linha 85: verifica-se se a propriedade [errorLoading] foi definida. Esta só pode ser definida pela função [asyncData];
- linhas 85-90: se a função [asyncData] tiver sinalizado um erro, este é encaminhado para a página [default] através do evento [error]. Era assim que a antiga função [created], que acabámos de substituir, geria um eventual erro;
Vamos fazer alguns testes.
Primeiro, eliminamos o cookie de sessão [nuxt] e o cookie de sessão PHP, caso existam. Em seguida, acedemos à página [http://localhost:81/nuxt-20/] enquanto o servidor de cálculo do imposto não está em execução. Obtemos a seguinte página:

Recarregamos a mesma página após ter iniciado o servidor de cálculo do imposto:

Vejamos os registos:

- em [2-3], vemos que a sessão jSON foi iniciada;
- em [4], vemos o cookie de sessão PHP que o servidor [nuxt] recuperou durante a sua troca de dados com o servidor de cálculo do imposto. O cliente [nuxt] irá agora utilizá-lo;
Agora, vamos identificar-nos:

Obtemos a seguinte página:

Em [1], recebemos uma mensagem de erro. Isto significa que o navegador não enviou o cookie correto da sessão PHP iniciada pelo servidor [nuxt] na etapa anterior. Em [nuxt-12], a transferência do cookie de sessão PHP do servidor [nuxt] para o cliente [nuxt] ocorreu no encaminhamento do cliente [nuxt] do script [middleware/client/routing] :
/* eslint-disable no-console */
export default function(context) { // quem executa este código?
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// gestão do cookie de sessão PHP no navegador
// o cookie de sessão PHP do navegador deve ser idêntico ao encontrado na sessão Nuxt
// a ação [fin-session] recebe um novo cookie PHP (tanto no servidor como no cliente Nuxt)
// se for o servidor a recebê-lo, o cliente deve transmiti-lo ao navegador
// para as suas próprias comunicações com o servidor PHP
// estamos aqui num encaminhamento do cliente
// recuperamos o cookie da sessão PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// se existir, atribui-se o cookie de sessão PHP ao navegador
document.cookie = phpSessionCookie
}
// para onde vamos?
const to = context.route.path
if (to === '/fin-session') {
// limpa-se a sessão
const session = context.app.$session()
session.reset(context)
// redireciona-se para a página inicial
context.redirect({ name: 'index' })
}
}
São as linhas 13 a 17 que permitem ao cliente [nuxt] recuperar o cookie da sessão PHP do servidor [nuxt].
O problema aqui é que, quando se clica no botão [Valider], não ocorre o encaminhamento do cliente [nuxt]. A sua função de encaminhamento não é, portanto, chamada. Resolvemos o problema duplicando as linhas 12 a 17 no início do método de autenticação da página [index]:
// gestores de eventos
methods: {
// ----------- autenticação
async login() {
// recupera-se o cookie de sessão PHP do armazenamento
const phpSessionCookie = this.$store.state.phpSessionCookie
if (phpSessionCookie) {
// se existir, atribui-se o cookie de sessão PHP ao navegador
document.cookie = phpSessionCookie
}
try {
// início da espera
this.$emit('loading', true)
// ainda não estamos autenticados
Nas linhas 5 a 10, recupera-se da memória o cookie da sessão PHP iniciada pelo servidor [nuxt]. Após esta alteração, a página de cálculo do imposto é recuperada com sucesso, o que significa que a autenticação funcionou.
17.9. Passo 8
Temos uma aplicação funcional que funciona de acordo com a lógica [nuxt]. Tal como fizemos com a aplicação [nuxt-13], vamos analisar a navegação do servidor [nuxt]. Como já foi referido, não se espera que o utilizador introduza manualmente os URL da aplicação. Deve utilizar os links que lhe são apresentados e que são executados pelo cliente [nuxt], que funciona então no modo SPA. No entanto, vamos garantir que a navegação no servidor [nuxt] mantenha sempre a aplicação num estado estável.
A partir do estudo realizado para o [nuxt-13] (ver parágrafo sobre o link), sabemos que é necessário:
- modificar o script [midleware/routing];
- adicionar um script [middleware/server/routing];

O script [middleware/routing] é alterado da seguinte forma:
/* eslint-disable no-console */
// importam-se os middlewares do servidor e do cliente
import serverRouting from './server/routing'
import clientRouting from './client/routing'
export default function(context) {
// quem executa este código?
console.log('[middleware], process.server', process.server, ', process.client=', process.client)
if (process.server) {
// roteamento do servidor
serverRouting(context)
} else {
// roteamento do cliente
clientRouting(context)
}
}
- linha 4: importa-se o script de encaminhamento do servidor [nuxt];
- linhas 10-12: se for o servidor [nuxt] a executar o código, utiliza-se a sua função de encaminhamento;
O script [middleware/server/routing] é o seguinte:
/* eslint-disable no-console */
export default function(context) {
// quem executa este código?
console.log('[middleware server], process.server', process.server, ', process.client=', process.client)
// recolhemos algumas informações aqui e ali
const store = context.store
// de onde viemos?
const from = store.state.from || 'nowhere'
// para onde vamos?
let to = context.route.name
// caso específico de /fin-session que não tem o atributo [name]
if (context.route.path === '/fin-session') {
to = 'fin-session'
}
// eventual redirecionamento
let redirection = ''
// gestão do encaminhamento concluída
let done = false
// já estamos numa redireção do servidor [nuxt]?
if (store.state.serverRedirection) {
// não há nada a fazer
done = true
}
// trata-se de uma atualização da página?
if (!done && from === to) {
// não há nada a fazer
done = true
}
// controlo da navegação do servidor [nuxt]
// segue-se o menu de navegação do cliente
// tratamos primeiro o caso de fim de sessão
if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
// limpa-se a sessão
const session = context.app.$session()
session.reset(context)
// redireciona para a página inicial
redirection = 'index'
// trabalho concluído
done = true
}
// caso em que a sessão PHP não tenha sido iniciada
if (!done && !store.state.started && to !== 'index') {
// redirecionamento para [index]
redirection = 'index'
// trabalho concluído
done = true
}
// caso em que o utilizador não esteja autenticado
if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
redirection = 'index'
// trabalho concluído
done = true
}
// caso em que [adminData] não foi obtido
if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
// redirecionamento para [index]
redirection = 'index'
// trabalho concluído
done = true
}
// caso em que [adminData] tenha sido obtido
if (
!done &&
store.state.started &&
store.state.authenticated &&
store.state.métier.taxAdminData &&
to !== 'calcul-impot' &&
to !== 'liste-des-simulations'
) {
// permanece-se na mesma página
redirection = from
// trabalho concluído
done = true
}
// normalmente, foram efetuadas todas as verificações ---------------------
// redirecionamento?
if (redirection) {
// regista-se o redirecionamento na loja
store.commit('replace', { serverRedirection: true })
} else {
// sem redirecionamento
store.commit('replace', { serverRedirection: false, from: to })
}
// guardamos o armazenamento na sessão [nuxt]
const session = context.app.$session()
session.value.store = store.state
session.save(context)
// é efetuado o eventual redirecionamento do servidor [nuxt]
if (redirection) {
context.redirect({ name: redirection })
}
}
- neste script retomamos as ideias já desenvolvidas e utilizadas no encaminhamento do servidor [nuxt] da aplicação [nuxt-13];
- adicionamos duas propriedades ao armazenamento da aplicação:
- [from]: o nome da última página apresentada. Sabemos que o cliente [nuxt] dispõe desta informação, mas o servidor [nuxt] não. Vamos adicionar-lhe esta informação, armazenando no store, a cada encaminhamento do servidor [nuxt], o nome da página que vai ser apresentada. Faremos o mesmo a cada encaminhamento do cliente [nuxt]. Assim, no encaminhamento seguinte do servidor [nuxt], este encontrará no armazenamento o nome da última página apresentada pela aplicação;
- [serverRedirection]: quando um destino de encaminhamento for recusado pelo servidor [nuxt], este efetuará um redirecionamento. Indicará então no armazenamento que o próximo destino do servidor [nuxt] é uma página de redirecionamento. Este redirecionamento provocará uma nova execução do router do servidor [nuxt]. Se este detetar que o destino atual resulta de um redirecionamento, deixará o processo prosseguir;
- linhas 6-11: recuperam-se as informações úteis para o encaminhamento;
- linhas 13-16: o destino [/fin-session] não está associado a uma página chamada [fin-session]. Por isso, não tem nome. Atribui-se-lhe um;
- linha 19: o destino de um eventual redirecionamento;
- linha 21: [done=true] quando os testes de encaminhamento estiverem concluídos;
- linhas 23-27: como já foi referido, se o encaminhamento em curso resultar de um redirecionamento, não há nada a fazer. Com efeito, durante o encaminhamento anterior, o router decidiu que era necessário redirecionar o navegador do cliente. Não há motivo para reconsiderar essa decisão;
- linhas 29-33: se se tratar de uma atualização da página, deixa-se que o processo decorra normalmente. Este não é um axioma válido para todas as aplicações [nuxt]: é necessário analisar, para cada página, os efeitos de uma atualização. Neste caso, verifica-se que a atualização das páginas [index, calcul-impot, liste-des-simulations] não provoca efeitos indesejáveis;
- linhas 35-85: o encaminhamento do servidor [nuxt] retoma o encaminhamento do cliente [nuxt]. Quando se está numa página, o encaminhamento do servidor [nuxt] deve refletir o menu de navegação oferecido pelo cliente [nuxt] quando se está nessa página;
- linhas 38-47: trata-se, em primeiro lugar, do caso do destino [fin-session] que não corresponde a uma página existente. Se as condições estiverem reunidas (sessão iniciada, utilizador autenticado), limpa-se a sessão e redireciona-se o utilizador para a página [index];
- linhas 49-55: se a sessão jSON com o servidor de cálculo do imposto não tiver sido iniciada, então o único destino possível é a página [index];
- linhas 57-62: se a sessão jSON tiver sido iniciada e o utilizador não estiver autenticado nem tiver solicitado a página de autenticação, então o utilizador é redirecionado para a página de autenticação, que é a página [index];
- linhas 64-70: se o utilizador estiver autenticado, mas os dados [adminData] não tiverem sido obtidos, então o utilizador é redirecionado para a página de autenticação. A autenticação realiza duas ações: autentica e, caso a autenticação seja bem-sucedida, solicita adicionalmente o dado [adminData]. Se este último não tiver sido obtido, é necessário reiniciar a autenticação;
- linhas 72-85: se o dado [adminData] tiver sido obtido, então os únicos destinos possíveis são [calcul-impot] e [liste-des-simulations]. Caso contrário, o encaminhamento é recusado;
- linhas 88-95: atualiza-se o armazenamento consoante haja ou não redirecionamento;
- linha 94: não há redirecionamento. Assim, o atual [to] passará a ser o [from] do próximo encaminhamento;
- linhas 96-99: as informações do armazenamento são guardadas no cookie da sessão [nuxt];
- linhas 100-103: se for necessário efetuar um redirecionamento, faz-se;
Para realizar os testes, é necessário certificar-se de que se parte de uma situação inicial, eliminando o cookie da sessão [nuxt] e o cookie da sessão PHP no servidor de cálculo do imposto:

Para testar o encaminhamento do servidor [nuxt], em cada página experimente todos os URL possíveis [/, /calcul-impot, /liste-des-simulations]. Em cada caso, a aplicação deve permanecer num estado coerente.
17.10. Etapa 9
A etapa 9 consiste na implementação da aplicação [nuxt-20]. Esta requer um alojamento que ofereça um ambiente [node.js] para executar o servidor [nuxt]. Não disponho desse ambiente. O leitor poderá seguir os procedimentos descritos no parágrafo «ligação» para implementar a aplicação [nuxt-20] na sua máquina de desenvolvimento e protegê-la com um protocolo HTTPS.
17.11. Conclusion
A migração da aplicação [vuejs-22] para a aplicação [nuxt-20] está agora concluída. Destacamos alguns pontos desta migração:
- as páginas de [vuejs-22] foram mantidas;
- as operações assíncronas que existiam nas páginas de [vuejs-22] foram migradas para uma função [asyncData];
- no [nuxt-20] foi necessário gerir duas entidades: o cliente [nuxt] e o servidor [nuxt]. Esta última entidade não existia no [vuejs-22]. Para manter a coerência entre as duas entidades, precisámos de uma sessão [nuxt];
- tivemos de gerir o encaminhamento do servidor [nuxt];
Na prática, é sem dúvida preferível começar diretamente com uma arquitetura [nuxt] do que criar uma arquitetura [vue.js] que seja posteriormente transferida para um ambiente [nuxt].