Skip to content

17. Esempio [nuxt-20]: Porting dell'esempio [vuejs-22]

17.1. Introduzione

Qui proponiamo di trasferire l'esempio [vuejs-22], che era un'applicazione di tipo SPA [vue.js], in un contesto SSR [nuxt]. [vuejs-22] era un'applicazione client per il server di calcolo delle imposte che presentava le seguenti viste:

La prima vista è quella di autenticazione:

Image

La seconda schermata è quella del calcolo delle imposte:

Image

La terza schermata mostra l'elenco delle simulazioni eseguite dall'utente:

Image

La schermata sopra mostra che la simulazione n. 1 può essere eliminata. Ciò porta alla seguente schermata:

Image

Se ora eliminiamo l'ultima simulazione, otteniamo la seguente nuova schermata:

Image

Migreremo gradualmente l'applicazione [vuejs-22] all'applicazione [nuxt-20]. Non spiegheremo nuovamente il codice di [vuejs-22]. Invitiamo i lettori a consultare il documento |Introduzione al framework VUE.JS attraverso esempi|. I vari passaggi dovrebbero evidenziare le differenze tra un'applicazione [vuejs] e un'applicazione [nuxt].

17.2. Passaggio 1

Il progetto [nuxt-20] viene inizialmente creato clonando il progetto [nuxt-12]. Questo è infatti un buon punto di partenza:

  • può comunicare con il server di calcolo delle imposte;
  • gestisce correttamente gli errori inviati dal server;
  • il client e il server [Nuxt] possono comunicare tramite una sessione [Nuxt];

Abbiamo quindi una solida infrastruttura di partenza. Il nostro compito principale dovrebbe essere quello di modificare:

  • le pagine. Useremo quelle del progetto [vuejs-22], che dovranno essere adattate al nuovo ambiente;
  • gestione dello store. Dovrebbero apparire informazioni aggiuntive (elenco delle simulazioni), mentre altre potrebbero diventare superflue;
  • gestione del routing client e server [nuxt];

Quindi, per prima cosa, creiamo il progetto [nuxt-20] clonando il progetto [nuxt-12]:

Image

Quindi rimuoviamo le pagine e i componenti che non sono più necessari [2]:

  • il componente [components/navigation] scompare;
  • il layout [layout/default] scompare;
  • le pagine [index, authentication, get-admindata, end-session] vengono rimosse;

Quindi integriamo gli elementi da [vuejs-22] in [nuxt-20] [3]:

  • le tre pagine [Authentication, TaxCalculation, SimulationList] dell'applicazione [vuejs-22] vengono inserite nella cartella [pages];
  • i componenti [FormCalculImpot, Menu, Layout] dell'applicazione [vuejs-22] vengono inseriti nella cartella [components];
  • la pagina [Main] di [vuejs-22], che fungeva da [layout] per l'applicazione [vuejs-22], va nella cartella [layouts];

Rinominiamo gli elementi integrati [4]:

Image

  • in [layouts], [Main] è diventato [default] poiché questo è il nome predefinito per il layout di un'applicazione [nuxt];
  • in [pages], la pagina [Authentication] è diventata [index], poiché [Authentication] svolgeva questo ruolo nell'applicazione [vuejs-22];

A questo punto, possiamo compilare il progetto per vedere i primi errori. Modifichiamo il file [nuxt.config] dall'esempio [nuxt-12] per eseguire ora [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',
    // Doc: 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) {}
  },
  // source code directory
  srcDir: 'nuxt-20',
  // router
  router: {
    // application URL root
    base: '/nuxt-20/',
    // routing middleware
    middleware: ['routing']
  },
  // server
  server: {
    // service port, default 3000
    port: 81,
    // network addresses listened to, default localhost: 127.0.0.1
    // 0.0.0.0 = all the machine's network addresses
    host: 'localhost'
  },
  // environment
  env: {
    // axios configuration
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // session cookie configuration [nuxt]
    maxAge: 60 * 5
  }
}

Successivamente, [compiliamo] il progetto:

Image

Vengono segnalati i seguenti errori:

1
2
3
4
5
6
7
Module not found: Error: Can't resolve '../assets/logo.jpg' @ ./nuxt-20/layouts/default.vue?...
Module not found: Error: Can't resolve './FormCalculImpot' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/liste-des-simulations...
Module not found: Error: Can't resolve './Layout' @ ./nuxt-20/pages/calcul-impot.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/_.vue?...
Module not found: Error: Can't resolve './Menu' @ ./nuxt-20/pages/calcul-impot.vue
  • L'errore alla riga 1 indica che si sta facendo riferimento a un'immagine inesistente. La recupereremo in [vuejs-22];
  • L'errore alla riga 2 mostra che il componente [./FormCalculImpot] non esiste. In effetti, questo componente si trova ora in [@/components/form-calcul-impot];
  • Gli errori alle righe [3-5] indicano che il componente [./Layout] non esiste. In realtà, questo componente si trova ora in [@/components/layout];
  • gli errori alle righe [6-7] indicano che il componente [./Menu] non esiste. Infatti, ora si chiama [@/components/menu];

Aggiungiamo l'immagine [assets/logo.jpg] al progetto [nuxt-20]:

Image

Inoltre, correggeremo i percorsi dei componenti su tutte le pagine. Prendiamo come esempio la pagina [calcul-impot]:


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
// imports
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'
 
export default {
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
 

Le tre istruzioni [import] alle righe 26–28 diventano:


// imports
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'

Controlla e, se necessario, correggi le istruzioni [import] per tutti i componenti, i layout e le pagine. Una volta apportate queste correzioni, puoi provare a eseguire una nuova [build]. Normalmente, non dovrebbero esserci più errori.

È quindi possibile provare a eseguire l'applicazione:

Image

Vengono visualizzati degli errori:

1
2
3
4
5
6
Module Error (from ./node_modules/eslint-loader/dist/cjs.js):

c:\Data\st-2019\dev\nuxtjs\dvp\nuxt-20\pages\index.vue
   92:29  error  Expected '!==' and instead saw '!='  eqeqeq
  129:27  error  Expected '!==' and instead saw '!='  eqeqeq
150:28  error  Expected '!==' and instead saw '!='  eqeqeq

Possiamo notare che il comando [dev], combinato con il modulo [eslint], è più rigoroso, dal punto di vista sintattico, rispetto al comando [build]. In questo caso, richiede che l'operatore di confronto [!=] sia scritto come [!==], che è un operatore più rigoroso (controlla anche il tipo degli operandi). Questi errori si verificano nella pagina [index.vue].

Correggiamo gli errori sopra indicati e rieseguiamo il progetto. Riceviamo quindi un avviso dal modulo [eslint]:

c:\Data\st-2019\dev\nuxtjs\dvp\nuxt-20\components\menu.vue
14:5  warning  Prop 'options' requires default value to be set  vue/require-default-prop

Image

Risolviamo questo errore utilizzando la [Correzione rapida] dal modulo [eslint] [2].

Riavviamo il progetto. Non ci sono più errori di compilazione. Richiediamo quindi l'URL [http://localhost:81/nuxt-20/] in un browser. Otteniamo un errore di runtime:

Image

L'errore si trova in [index.vue] [2]. L'errore [1] deriva dal fatto che in [vuejs-22] il livello [dao] era disponibile in [this.$dao], mentre in [nuxt-12], la cui infrastruttura abbiamo adottato, è disponibile nella funzione [this.$dao()].

L'errore si trova nella funzione [created] del ciclo di vita della pagina [index]:

Image

Per ora, rinomineremo semplicemente [created] in [created2] in modo che la funzione del ciclo di vita [created] non venga eseguita [3].

Salviamo la modifica e ricarichiamo la pagina [index] nel browser. Questa volta funziona:

Image

17.3. Passaggio 2

Le pagine del progetto [vuejs-22] utilizzavano i seguenti elementi iniettati:

  • $dao: per il livello [dao] del client [vue.js];
  • $session: per una sessione memorizzata nel [localStorage] del browser;

Questi elementi non esistono più nell'infrastruttura del progetto [nuxt-12] che abbiamo copiato:

  • ora ci sono due livelli [dao], uno per il client [nuxt] e l'altro per il server [nuxt]. Entrambi sono disponibili tramite una funzione iniettata chiamata [$dao]. Ciò significa che nelle pagine dell'applicazione, [this.$dao] deve essere sostituito con [this.$dao()];
  • la sessione [nuxt] gestita dall'applicazione [nuxt-20] non ha più nulla a che vedere con l'oggetto [$session] dell'applicazione [vuejs-22], dove non esisteva il concetto di cookie di sessione. Tuttavia, hanno uno scopo simile: memorizzare informazioni persistenti mentre l'utente interagisce con l'applicazione. La sessione [nuxt] memorizza le informazioni nello store piuttosto che direttamente nella sessione. Nelle pagine dell'applicazione, [this.$session] deve essere sostituito con [this.$store] quando si memorizzano informazioni nella sessione, e con [this.$session()] quando si manipola la sessione stessa;
  • per verificare lo stato di una proprietà P nello store, è necessario scrivere [this.$store.state.P];
  • per modificare la proprietà P dello store, è necessario scrivere [this.$store.commit('replace', {P:value}]

Apportiamo queste modifiche nella pagina [index]:


<!-- définition HTML de la vue -->
<template>
  <Layout :left="false" :right="true">
    <template slot="right">
      <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
      <b-form @submit.prevent="login">
        <!-- titre -->
        <b-alert show variant="primary">
          <h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
        </b-alert>
        <!-- 1ère ligne -->
        <b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
          <!-- zone de saisie user -->
          <b-col cols="6">
            <b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
          </b-col>
        </b-form-group>
        <!-- 2ième ligne -->
        <b-form-group label="Mot de passe" label-for="password" label-cols="3">
          <!-- zone de saisie password -->
          <b-col cols="6">
            <b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
          </b-col>
        </b-form-group>
        <!-- 3ième ligne -->
        <b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
        <!-- bouton de type [submit] sur une 3ième ligne -->
        <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>
 
<!-- dynamique de la vue -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // components used
  components: {
    Layout
  },
  // component status
  data() {
    return {
      // user
      user: '',
      // password
      password: '',
      // controls the display of an error msg
      showError: false,
      // the error message
      message: ''
    }
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // life cycle: the component has just been created
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // can the user run simulations?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
      // then the user can run simulations
      this.$router.push({ name: 'calculImpot' })
      // return to event loop
      return
    }
    // if the jSON session has already been started, it is not restarted again
    if (!this.$store.state.started) {
      // start waiting
      this.$emit('loading', true)
      // initialize the session with the server - asynchronous request
      // we use the promise rendered by the [dao] layer methods
      this.$dao()
        // initialize a jSON session
        .initSession()
        // we got the answer
        .then((response) => {
          // end waiting
          this.$emit('loading', false)
          // response analysis
          if (response.état !== 700) {
            // error is displayed
            this.message = response.réponse
            this.showError = true
            // return to event loop
            return
          }
          // the session has started
          this.$store.commit('replace', { started: true })
          console.log('[authentification], session=', this.$session())
        })
        // in case of error
        .catch((error) => {
          // the error is traced back to the [Main] view
          this.$emit('error', error)
        })
        // in all cases
        .finally(() => {
          // save the session
          this.$session().save()
        })
    }
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated
        this.$store.commit('replace', { authenticated: false })
        // blocking server authentication
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // end of loading
        this.$emit('loading', false)
        // server response analysis
        if (response.état !== 200) {
          // error is displayed
          this.message = response.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // you are authenticated
        this.$store.commit('replace', { authenticated: true })
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null)
        // start waiting
        this.$emit('loading', true)
        // blocking request to the server
        const response2 = await this.$dao().getAdminData()
        // end of loading
        this.$emit('loading', false)
        // response analysis
        if (response2.état !== 1000) {
          // error is displayed
          this.message = response2.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse)
        // we can move on to tax calculation
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // the error is traced back to the main component
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier })
        // save the session
        this.$session().save()
      }
    }
  }
}
</script>

Si prega di tenere presente quanto segue:

  • riga 69: la funzione [created2] è stata rinominata [mounted] in modo che il server [nuxt] non la esegua (non esegue né [beforeMount] né [mounted]). Solo il client [nuxt] la eseguirà, come nel caso dell'esempio [vuejs-22];
  • riga 73: facciamo riferimento a [this.$business], che al momento non esiste;
  • riga 75: non abbiamo mai utilizzato questo metodo in un'applicazione [nuxt]. Dovremo verificare se funziona in un contesto [nuxt];
  • righe 112, 172: in [vuejs-22], la sessione del progetto veniva salvata in questo modo. Con il progetto [nuxt-20], il metodo [save] deve ricevere il contesto corrente. Sappiamo che in una pagina [nuxt], l'oggetto [context] è disponibile in [this.$nuxt.context];

Le righe 112 e 172 vengono quindi riscritte come segue:


this.$session().save(this.$nuxt.context)

Si noti che questo codice non è ottimizzato. Anziché utilizzare più volte la funzione [this.$session()], sarebbe meglio scrivere:


const session=this.$session()

e poi utilizzare la variabile [session]. Lo stesso ragionamento vale per la funzione [this.$dao()].

Una volta apportate queste correzioni, possiamo ricaricare l'URL [http://localhost:81/nuxt-20/] in un browser. Otteniamo ancora la stessa pagina di prima:

Image

Diamo un'occhiata ai log del browser:

Image

Il log [1] è l'ultimo log generato dal client [nuxt]. In [2], vediamo che la proprietà [started] è impostata su [true], il che significa che la funzione [mounted] ha avviato con successo una sessione JSON con il server di calcolo delle imposte. Vediamo anche che lo store ha proprietà che dovranno essere scartate o rinominate. Ricordiamo che stiamo utilizzando lo store dell'esempio [nuxt-12].

Ora richiediamo nuovamente l'URL [http://localhost:81/nuxt-20/] mentre il server di calcolo delle imposte non è in esecuzione. Per prima cosa, ci assicuriamo di eliminare il cookie di sessione [nuxt]:

Image

Lo screenshot qui sopra è stato fatto su Chrome. Una volta fatto questo, l'URL [http://localhost:81/nuxt-20/] restituisce il seguente risultato:

Image

L'errore è stato gestito correttamente dal progetto [vuejs-22]. Continua a essere gestito correttamente dal progetto [nuxt-20].

17.4. Passaggio 3

Ora che abbiamo la pagina di autenticazione, dobbiamo esaminare il codice che viene eseguito quando l'utente fa clic sul pulsante [Convalida]:


// event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated
        this.$store.commit('replace', { authenticated: false })
        // blocking server authentication
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // end of loading
        this.$emit('loading', false)
        // server response analysis
        if (response.état !== 200) {
          // error is displayed
          this.message = response.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // you are authenticated
        this.$store.commit('replace', { authenticated: true })
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier.setTaxAdminData(null)
        // start waiting
        this.$emit('loading', true)
        // blocking request to the server
        const response2 = await this.$dao().getAdminData()
        // end of loading
        this.$emit('loading', false)
        // response analysis
        if (response2.état !== 1000) {
          // error is displayed
          this.message = response2.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // the received data is stored in the [business] layer
        this.$métier.setTaxAdminData(response2.réponse)
        // we can move on to tax calculation
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // the error is traced back to the main component
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier })
        // save the session
        this.$session().save(this.$nuxt.context)
      }
    }
  }

Il problema principale sembra essere l'assenza dei dati [this.$métier]. Per risolvere questo problema, faremo quanto segue:

  • includere la classe [Métier] dall'esempio [vuejs-22]. La inseriremo nella cartella [api];
  • inseriremo una funzione [$métier] nel contesto client [nuxt], che fornirà l'accesso a questa classe;

Per prima cosa, copia la classe [Métier] nella cartella [api]:

Image

Una volta che la classe [Métier] è nel progetto, creeremo un nuovo plugin per il client [nuxt]. Questo plugin, chiamato [pluginMétier], inietterà una funzione [$métier] che fornisce l'accesso alla classe [Métier]:


/* eslint-disable no-console */
// on crée un point d'accès à la couche [métier]
import Métier from '@/api/client/Métier'
export default (context, inject) => {
  // instanciation de la couche [métier]
  const métier = new Métier()
  // injection d'une fonction [$métier] dans le contexte
  inject('métier', () => métier)
  // log
  console.log('[fonction client $métier créée]')
}

Ora che questo è fatto, possiamo aggiornare la pagina [index]:


// life cycle: the component has just been created
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // can the user run simulations?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // then the user can run simulations
      this.$router.push({ name: 'calcul-impot' })
      // return to event loop
      return
    }
    // if the jSON session has already been started, it is not restarted again
    ...
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated
        this.$store.commit('replace', { authenticated: false })
        // blocking server authentication
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // end of loading
        this.$emit('loading', false)
        // server response analysis
        if (response.état !== 200) {
          // error is displayed
          this.message = response.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // you are authenticated
        this.$store.commit('replace', { authenticated: true })
        // --------- we now request data from the tax authorities
        // initially, no data
        this.$métier().setTaxAdminData(null)
        // start waiting
        this.$emit('loading', true)
        // blocking request to the server
        const response2 = await this.$dao().getAdminData()
        // end of loading
        this.$emit('loading', false)
        // response analysis
        if (response2.état !== 1000) {
          // error is displayed
          this.message = response2.réponse
          this.showError = true
          // return to event loop
          return
        }
        // no error
        this.showError = false
        // the received data is stored in the [business] layer
        this.$métier().setTaxAdminData(response2.réponse)
        // we can move on to tax calculation
        this.$router.push({ name: 'calcul-impot' })
      } catch (error) {
        // the error is traced back to the main component
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier() })
        // save the session
        this.$session().save(this.$nuxt.context)
      }
    }
  }
  • righe 43, 61, 69: [this.$métier] è stato sostituito da [this.$métier()];
  • righe 8, 63: il nome della pagina [CalculImpot] nel progetto [vuejs-22] è diventato la pagina [calcul-impot] nel progetto [nuxt-20];

Una volta apportate queste correzioni, possiamo provare a convalidare la pagina di autenticazione:

Image

La pagina risultante è la seguente:

Image

Siamo arrivati alla pagina di calcolo delle imposte. Ora diamo un'occhiata ai log:

Image

In [2] vediamo che lo stato di autenticazione è stato memorizzato correttamente. In [3-4] vediamo che i dati [taxAdminData] sono stati recuperati, il che consente alla classe [Métier] di calcolare l'imposta.

17.5. Passaggio 4

Esaminiamo la pagina [calcul-impot] che abbiamo ottenuto:


<!-- definition HTML of the view -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- tax calculation form on the right -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- left-hand navigation menu -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- display area for tax calculation results under the form -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- empty three-column zone -->
      <b-col sm="3" />
      <!-- nine-column zone -->
      <b-col sm="9">
        <b-alert show variant="success">
          <span v-html="résultat"></span>
        </b-alert>
      </b-col>
    </b-row>
  </div>
</template>
 
<script>
// imports
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
 
export default {
  // composants utilisés
  components: {
    Layout,
    FormCalculImpot,
    Menu
  },
  // état interne
  data() {
    return {
      // options du menu
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // résultat du calcul de l'impôt
      résultat: '',
      résultatObtenu: false
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("CalculImpot created");
  },
  // méthodes de gestion des évts
  methods: {
    // résultat du calcul de l'impôt
    handleResultatObtenu(résultat) {
      // on construit le résultat en chaîne 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
      // affichage du résultat
      this.résultatObtenu = true
      // ---- maj du store [Vuex]
      // une simulation de +
      this.$store.commit('addSimulation', résultat)
      // on sauvegarde la session
      this.$session.save()
    }
  }
}
</script>
  • righe 44 e 48: i link del menu di navigazione sono corretti. La pagina [/fin-session] non esiste. Il progetto [vuejs-22] ha risolto questo problema con il routing. Faremo lo stesso con il progetto [nuxt-20];
  • riga 76: stiamo facendo riferimento a un metodo [addSimulation] che al momento non esiste. Lo creeremo;
  • riga 78: come nella pagina [index], dobbiamo scrivere [this.$session().save(this.$nuxt.context)];

Modifichiamo lo store [store/index]. Ereditato dal progetto [nuxt-12], attualmente si presenta così:


/* eslint-disable no-console */
 
// awning status
export const state = () => ({
  // session jSON started
  jsonSessionStarted: false,
  // authenticated user
  userAuthenticated: false,
  // session cookie PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})
 
// changes in the awning
export const mutations = {
  // state replacement
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // awning reset
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}
 
// awning actions
export const actions = {
  nuxtServerInit(store, context) {
    // who executes this code?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // init session
    initStore(store, context)
  }
}
 
function initStore(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.initStoreDone) {
    // start a new blind
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // put the blind in the session
    session.value.store = store.state
    // the blind is now initialized
    session.value.initStoreDone = 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('initStore terminé, store=', store.state)
}
  • righe 3–27: riprenderemo lo stato e le mutazioni dell'applicazione [vuejs-22] (vedi documento [3]):

// awning status
export const state = () => ({
  // session jSON started
  started: false,
  // authenticated user
  authenticated: false,
  // session cookie PHP
  phpSessionCookie: '',
  // list of simulations
  simulations: [],
  // last simulation number
  idSimulation: 0,
  // business] layer
  métier: null
})
 
// changes in the awning
export const mutations = {
  // state replacement
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // awning reset
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
  },
  // delete line n° index
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // delete line no. [index]
    state.simulations.splice(index, 1)
    console.log('store simulations', state.simulations)
  },
  // add a simulation
  addSimulation(state, simulation) {
    // eslint-disable-next-line no-console
    console.log('mutation addSimulation')
    // simulation no
    state.idSimulation++
    simulation.id = state.idSimulation
    // add the simulation to the simulation table
    state.simulations.push(simulation)
  }
}
  • righe 4 e 6: introduciamo le proprietà già utilizzate;
  • riga 8: memorizziamo il cookie di sessione PHP. Questo è essenziale affinché il client e il server [nuxt] condividano la stessa sessione PHP con il server di calcolo delle imposte;
  • riga 10: l'elenco delle simulazioni eseguite dall'utente;
  • riga 12: il numero dell'ultima simulazione eseguita dall'utente;
  • riga 14: il livello [business];
  • righe 30–47: le mutazioni presenti nello store del progetto [vuejs-22] e a cui fanno riferimento le pagine dell'applicazione. Il progetto [vuejs-22] aveva una mutazione chiamata [clear] che cancellava l'elenco delle simulazioni. Non la includiamo perché la mutazione [reset] già presente dovrebbe essere sufficiente;
  • righe 26–28: la mutazione [reset] viene modificata per tenere conto del nuovo contenuto dello stato;

La pagina [calcul-impot] utilizza il seguente componente [form-calcul-impot]:


<!-- définition HTML de la vue -->
<template>
  <!-- formulaire HTML -->
  <b-form @submit.prevent="calculerImpot" class="mb-3">
    <!-- message sur 12 colonnes sur fond bleu -->
    <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>
    <!-- éléments du formulaire -->
    <!-- première ligne -->
    <b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
      <!-- boutons radio sur 5 colonnes-->
      <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>
    <!-- deuxième ligne -->
    <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>
      <!-- message d'erreur éventuel -->
      <b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- troisème ligne -->
    <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>
      <!-- message d'erreur éventuel -->
      <b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
    </b-form-group>
    <!-- quatrième ligne, bouton [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 {
  // inner state
  data() {
    return {
      // married or not
      marié: 'non',
      // number of children
      enfants: '',
      // annual salary
      salaire: ''
    }
  },
  // calculated internal state
  computed: {
    // form validation
    formInvalide() {
      return (
        // disabled salary
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // or disabled children
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // or tax data not obtained
        !this.$métier.taxAdminData
      )
    },
    // salary validation
    salaireValide() {
      // must be numeric >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
    },
    // child validation
    enfantsValide() {
      // must be numeric >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/))
    }
  },
  // life cycle
  created() {
    // log
    // eslint-disable-next-line
    console.log("FormCalculImpot created");
  },
  // event manager
  methods: {
    calculerImpot() {
      // tax is calculated using the [business] layer
      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);
      // complete the result
      résultat.marié = this.marié
      résultat.enfants = this.enfants
      résultat.salaire = this.salaire
      // the [resultatObtenu] event is issued
      this.$emit('resultatObtenu', résultat)
    }
  }
}
</script>
  • righe 65, 89: il riferimento [this.$métier] deve essere modificato in [this.$métier()];

Una volta apportate queste correzioni, possiamo provare a eseguire una simulazione:

Image

Otteniamo la seguente risposta:

Image

Se diamo un'occhiata ai log:

Image

  • in [9-10], vediamo che la prima simulazione si trova effettivamente nello [store];
  • in [5], il numero dell'ultima simulazione è stato effettivamente incrementato;

17.6. Passaggio 5

Ora che abbiamo eseguito una simulazione, clicchiamo sul link [Elenco delle simulazioni]. Otteniamo la seguente pagina:

Image

L'instradamento per il client [nuxt] è andato a buon fine. Diamo un'occhiata al codice della pagina [simulation-list]:


<!-- définition HTML de la vue -->
<template>
  <div>
    <!-- mise en page -->
    <Layout :left="true" :right="true">
      <!-- simulations dans colonne de droite -->
      <template slot="right">
        <template v-if="simulations.length == 0">
          <!-- pas de simulations -->
          <b-alert show variant="primary">
            <h4>Votre liste de simulations est vide</h4>
          </b-alert>
        </template>
        <template v-if="simulations.length != 0">
          <!-- il y a des simulations -->
          <b-alert show variant="primary">
            <h4>Liste de vos simulations</h4>
          </b-alert>
          <!-- tableau des simulations -->
          <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 navigation dans colonne de gauche -->
      <Menu slot="left" :options="options" />
    </Layout>
  </div>
</template>
 
<script>
// imports
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
  // composants
  components: {
    Layout,
    Menu
  },
  // état interne
  data() {
    return {
      // options du menu de navigation
      options: [
        {
          text: "Calcul de l'impôt",
          path: '/calcul-impot'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
      ],
      // paramètres de la table HTML
      fields: [
        { label: '#', key: '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' }
      ]
    }
  },
  // état interne calculé
  computed: {
    // liste des simulations prise dans le store Vuex
    simulations() {
      return this.$store.state.simulations
    }
  },
  // cycle de vie
  created() {
    // eslint-disable-next-line
    console.log("ListeSimulations created");
  },
  // méthodes
  methods: {
    supprimerSimulation(index) {
      // eslint-disable-next-line
      console.log("supprimerSimulation", index);
      // suppression de la simulation n° [index]
      this.$store.commit('deleteSimulation', index)
      // on sauvegarde la session
      this.$session.save()
    }
  }
}
</script>
  • righe 47–56: i collegamenti del menu di navigazione sono corretti;
  • riga 75: il negozio è correttamente referenziato;
  • riga 89: stiamo utilizzando una mutazione [deleteSimulation] che abbiamo aggiunto nel passaggio precedente;
  • riga 91: questa riga deve essere riscritta come [this.$session().save(this.$nuxt.context)];

Apportiamo le modifiche necessarie, quindi proviamo a eliminare la simulazione visualizzata:

Image

Otteniamo quindi la seguente pagina:

Image

Diamo un'occhiata ai log:

Image

  • In [6], vediamo che la tabella di simulazione è vuota;

Ora torniamo al modulo di calcolo delle imposte:

Image

Otteniamo la seguente pagina:

Image

Quindi il routing ha funzionato.

17.7. Passaggio 6

Dobbiamo ancora gestire l'opzione di navigazione [Fine sessione] nel menu di navigazione:

// options du menu
      options: [
        {
          text: 'List of simulations',
          path: '/list-of-simulations
        },
        {
          text: 'End of session',
          path: '/end-session'
        }
]
  • alla riga 9, la pagina [/end-session] non esiste. Il progetto [vuejs-22] ha gestito questo caso con regole di routing in un file [router.js]:

// imports
import Vue from 'vue'
import VueRouter from 'vue-router'
// les vues
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// la session
import session from './session'
 
// plugin de routage
Vue.use(VueRouter)
 
// les routes de l'application
const routes = [
  // authentification
  { path: '/', name: 'authentification', component: Authentification },
  { path: '/authentification', name: 'authentification2', component: Authentification },
  // calcul de l'impôt
  {
    path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
    meta: { authenticated: true }
  },
  // liste des simulations
  {
    path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
    meta: { authenticated: true }
  },
  // fin de session
  {
    path: '/fin-session', name: 'finSession'
  },
  // page inconnue
  {
    path: '*', name: 'notFound', component: NotFound,
  },
]
 
// le routeur
const router = new VueRouter({
  // les routes
  routes,
  // le mode d'affichage des URL
  mode: 'history',
  // l'URL de base de l'application
  base: '/client-vuejs-impot/'
})
 
// vérification des routes
router.beforeEach((to, from, next) => {
  // eslint-disable-next-line no-console
  console.log("router to=", to, "from=", from);
  // route réservée aux utilisateurs authentifiés ?
  if (to.meta.authenticated && !session.authenticated) {
    next({
      // on passe à l'authentification
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // cas particulier de la fin de session
  if (to.name === "finSession") {
    // on nettoie la session
    session.clear();
    // on va sur la vue [authentification]
    next({
      name: 'authentification',
    })
    // retour à la boucle événementielle
    return;
  }
  // autres cas - vue suivante normale du routage
  next();
})
 
// export du router
export default router
  • Le righe 64–76 gestiscono il caso particolare del percorso verso il percorso [/end-session];
  • riga 66: cancella la sessione corrente;
  • righe 68–70: visualizza la vista [authentication];

Proveremo a fare qualcosa di simile nel file di routing del client [nuxt]:

Image

Lo script [client/routing.js] diventa il seguente:


/* eslint-disable no-console */
export default function(context) {
  // who executes this code?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // management of the PHP session cookie in the browser
  // the browser's PHP session cookie must be identical to the one found in the nuxt session
  // the [end-session] action receives a new PHP cookie (server as nuxt client)
  // if the server receives it, the client must pass it on to the browser
  // for its own exchanges with the PHP server
  // this is customer routing
 
  // retrieve the session cookie PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // if it exists, we assign the PHP session cookie to the browser
    document.cookie = phpSessionCookie
  }
 
  // where are we going?
  const to = context.route.path
  if (to === '/fin-session') {
    // clean the session
    const session = context.app.$session()
    session.reset(context)
    // redirects to the index page
    context.redirect({ name: 'index' })
  }
}
  • Abbiamo aggiunto le righe [19-27] al codice esistente;
  • riga 20: recuperiamo il [path] della destinazione del percorso corrente;
  • riga 21: controlliamo se è [/end-session]. Se lo è:
    • righe 23-24: la sessione viene resettata;
    • riga 26: reindirizziamo il client [nuxt] alla home page;

Il metodo [session.reset(context)] della sessione (riga 24) è il seguente:


// reset de la session
  reset(context) {
    console.log('nuxt-session reset')
    // reset du store
    context.store.commit('reset')
    // sauvegarde du nouveau store en session et sauvegarde de la session
    this.save(context)
}

Il metodo [context.store.commit('reset')] (riga 5) è il seguente:


// reset du store
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}

Quando ora clicchiamo sul link [End Session], viene visualizzata la home page con i seguenti log:

Image

  • in [3], vediamo che non siamo più autenticati;
  • in [4], vediamo che la sessione JSON è stata avviata;
  • in [6], il livello [business] non è più presente nello store (è ancora presente nelle pagine tramite [this.$business()]);
  • in [5, 7], non ci sono più simulazioni;

È importante capire cosa succede alla fine di una sessione:

  • la sessione [nuxt] viene resettata: la proprietà [started] dello store passa a [false];
  • c'è un reindirizzamento alla pagina [index];
  • viene eseguito il metodo [mounted] della pagina [index]. Questo avvia una nuova sessione JSON con il server di calcolo delle imposte. Se l'operazione ha esito positivo, la proprietà [started] dello store diventa [true];

17.8. Passaggio 7

A questo punto, l'applicazione [nuxt-20] presenta tutte le funzionalità dell'applicazione [vuejs-22]. Il porting sembra completato.

Faremo un ulteriore passo avanti nello spirito [nuxt]. Il metodo [mounted] della pagina [index] pone un problema. Avvia un'operazione asincrona che un motore di ricerca non aspetterà che finisca. Sappiamo che in questo caso dobbiamo inserire l'operazione asincrona all'interno di una funzione [asyncData] perché, in questo modo, il server [nuxt] che la esegue attenderà che sia terminata prima di consegnare la pagina al motore di ricerca.

Qui, usiamo la funzione [asyncData] scritta nell'applicazione [nuxt-12] per la pagina [index]:


export default {
  name: 'InitSession',
  // components used
  components: {
    Layout,
    Navigation
  },
  // asynchronous data
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // don't do things twice if the page has already been requested
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
      // start a jSON session
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // retrieve session cookie PHP for future requests
      const phpSessionCookie = dao.getPhpSessionCookie()
      // we store the PHP session cookie in the [nuxt] session
      context.store.commit('replace', { phpSessionCookie })
      // was there a mistake?
      if (response.état !== 700) {
        // the error is in response.réponse
        throw new Error(response.réponse)
      }
      // note that the jSON session has started
      context.store.commit('replace', { jsonSessionStarted: true })
      // we return the result
      return { result: '[succès]' }
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e)
      // note that session jSON has not started
      context.store.commit('replace', { jsonSessionStarted: false })
      // we report the error
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // save the blind
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // life cycle
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    console.log('[index beforeMount]')
  },
  mounted() {
    console.log('[index mounted]')
    // customer only
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • righe 13, 33, 40: modificare la proprietà [jsonSessionStarted] in [started];
  • riga 13: nell'applicazione [nuxt-12], solo il server [nuxt] ha eseguito la pagina [index] e la sua funzione [asyncData]. Il client [nuxt] ha eseguito la pagina [index] solo dopo averla ricevuta dal server [nuxt] e, di conseguenza, non ha eseguito la funzione [asyncData]. In [nuxt-20] è diverso: il link [End Session] visualizzerà la pagina [index] nell'ambiente client [nuxt]. La funzione [asyncData] verrà quindi eseguita. Tuttavia, quando si arriva alla pagina [index] in questo modo, la sessione [nuxt] è stata nel frattempo resettata e la proprietà [started] dello store è [false], quindi la condizione alla riga 13 sarà necessariamente falsa. Possiamo quindi omettere [process.server] e il client [nuxt] non eseguirà questo controllo;
  • righe 15, 35, 42: una proprietà [result] viene aggiunta alle proprietà [data] della pagina [index]. In [nuxt-20], questa proprietà non verrà utilizzata, quindi la rimuoveremo dal risultato restituito dalla funzione;
  • Righe 61–67: questo metodo [mounted] deve essere mantenuto perché è ciò che permette al client [nuxt] di visualizzare il messaggio di errore. Tuttavia, il modo in cui l'errore viene gestito verrà modificato;

Nella pagina [index] attuale, integriamo la funzione [asyncData] sopra riportata al posto della vecchia funzione [mounted] e aggiungiamo una nuova funzione [mounted]. Il codice per la pagina [index] nell'esempio [nuxt-20] diventa quindi il seguente:


...
 
<!-- dynamique de la vue -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // components used
  components: {
    Layout
  },
  // component status
  data() {
    return {
      // user
      user: '',
      // password
      password: '',
      // error display
      showError: false
    }
  },
 
  // calculated properties
  computed: {
    // valid entries
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // asynchronous data
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // don't do things twice if the page has already been requested
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // start a jSON session
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // retrieve session cookie PHP for future requests
      const phpSessionCookie = dao.getPhpSessionCookie()
      // we store the PHP session cookie in the [nuxt] session
      context.store.commit('replace', { phpSessionCookie })
      // was there a mistake?
      if (response.état !== 700) {
        // the error is in response.réponse
        throw new Error(response.réponse)
      }
      // note that the jSON session has started
      context.store.commit('replace', { started: true })
      // no result
      return
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e.message)
      // note that session jSON has not started
      context.store.commit('replace', { started: false })
      // we report the error
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // save the blind
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // life cycle
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // customer only
    console.log('[index beforeMount]')
    // error handling
    if (this.showErrorLoading) {
      // log
      console.log('[index beforeMount, showErrorLoading=true]')
      // the error is traced back to the main component [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },
 
  // event managers
  methods: {
    // ----------- authentication
    async login() {
      ...
}
</script>
  • righe 58, 65: la funzione [asyncData] non rende più inutilizzata la proprietà [result] in questo punto;
  • riga 81: il metodo [beforeMount] del client [nuxt]. È stato preferito al metodo [mounted] per gestire eventuali errori provenienti da [asyncData];
  • riga 85: controlliamo se la proprietà [errorLoading] è stata impostata. Può essere impostata solo dalla funzione [asyncData];
  • righe 85–90: se la funzione [asyncData] ha segnalato un errore, lo passiamo alla pagina [default] tramite l'evento [error]. È così che la vecchia funzione [created], che abbiamo appena sostituito, gestiva i potenziali errori;

Eseguiamo alcuni test.

Per prima cosa, eliminiamo sia il cookie di sessione [nuxt] che quello PHP, se presenti. Quindi richiediamo la pagina [http://localhost:81/nuxt-20/] mentre il server di calcolo delle imposte non è in esecuzione. Otteniamo la seguente pagina:

Image

Ricarichiamo la stessa pagina dopo aver avviato il server di calcolo delle imposte:

Image

Diamo un'occhiata ai log:

Image

  • In [2-3] vediamo che la sessione JSON è stata avviata;
  • in [4], vediamo il cookie di sessione PHP che il server [nuxt] ha recuperato durante lo scambio con il server di calcolo delle imposte. Il client [nuxt] lo utilizzerà ora;

Ora effettuiamo l'accesso:

Image

Otteniamo la seguente pagina:

Image

In [1], abbiamo ricevuto un messaggio di errore. Ciò significa che il browser non ha inviato il cookie di sessione corretto dalla sessione PHP avviata dal server [nuxt] nel passaggio precedente. In [nuxt-12], il cookie di sessione PHP è stato passato dal server [nuxt] al client [nuxt] nel routing client [nuxt] dello script [middleware/client/routing]:


/* eslint-disable no-console */
export default function(context) {  // who executes this code?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // management of the PHP session cookie in the browser
  // the browser's PHP session cookie must be identical to the one found in the nuxt session
  // acion [fin-session] receives a new cookie PHP (server as nuxt client)
  // if the server receives it, the client must pass it on to the browser
  // for its own exchanges with the PHP server
  // this is customer routing
 
  // retrieve the session cookie PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // if it exists, we assign the PHP session cookie to the browser
    document.cookie = phpSessionCookie
  }
 
  // where are we going?
  const to = context.route.path
  if (to === '/fin-session') {
    // clean the session
    const session = context.app.$session()
    session.reset(context)
    // redirects to the index page
    context.redirect({ name: 'index' })
  }
}

Le righe 13–17 consentono al client [nuxt] di recuperare il cookie di sessione PHP dal server [nuxt].

Il problema qui è che quando si fa clic sul pulsante [Validate], non c'è alcun routing dal client [nuxt]. La sua funzione di routing non viene quindi chiamata. Risolviamo il problema duplicando le righe 12–17 all'inizio del metodo di autenticazione nella pagina [index]:


// event managers
  methods: {
    // ----------- authentication
    async login() {
      // retrieve the PHP session cookie from the store
      const phpSessionCookie = this.$store.state.phpSessionCookie
      if (phpSessionCookie) {
        // if it exists, we assign the PHP session cookie to the browser
        document.cookie = phpSessionCookie
      }
      try {
        // start waiting
        this.$emit('loading', true)
        // you are not yet authenticated

Righe 5–10: recuperiamo dallo store il cookie di sessione PHP generato dal server [nuxt]. Una volta apportata questa modifica, la pagina di calcolo delle imposte si carica correttamente, indicando che l'autenticazione ha funzionato.

17.9. Passaggio 8

Abbiamo un'applicazione funzionante che opera secondo lo spirito [nuxt]. Come abbiamo fatto per l'applicazione [nuxt-13], ci concentreremo sulla navigazione sul server [nuxt]. Come accennato in precedenza, l'utente non dovrebbe digitare manualmente gli URL dell'applicazione. Dovrebbe invece utilizzare i link che gli vengono presentati, che vengono eseguiti dal client [nuxt], il quale opera quindi in modalità SPA. Ciononostante, ci assicureremo che la navigazione sul server [nuxt] mantenga sempre l'applicazione in uno stato stabile.

Dallo studio condotto per [nuxt-13] (vedi paragrafo collegato), sappiamo che dobbiamo:

  • modificare lo script [middleware/routing];
  • aggiungere uno script [middleware/server/routing];

Image

Lo script [middleware/routing] viene modificato come segue:


/* eslint-disable no-console */
 
// on importe les middleware du serveur et du client
import serverRouting from './server/routing'
import clientRouting from './client/routing'
 
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware], process.server', process.server, ', process.client=', process.client)
  if (process.server) {
    // routage serveur
    serverRouting(context)
  } else {
    // routage client
    clientRouting(context)
  }
}
  • riga 4: importiamo lo script di routing dal server [nuxt];
  • righe 10–12: se il server [nuxt] sta eseguendo il codice, utilizziamo la sua funzione di routing;

Lo script [middleware/server/routing] è il seguente:


/* eslint-disable no-console */
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware server], process.server', process.server, ', process.client=', process.client)
 
  // on récupère quelques informations ici et là
  const store = context.store
  // d'où vient-on ?
  const from = store.state.from || 'nowhere'
  // où va-t-on ?
  let to = context.route.name
 
  // cas particulier de /fin-session qui n'a pas d'attribut [name]
  if (context.route.path === '/fin-session') {
    to = 'fin-session'
  }
 
  // éventuelle redirection
  let redirection = ''
  // gestion du routage terminé
  let done = false
 
  // est-on déjà dans une redirection du serveur [nuxt]?
  if (store.state.serverRedirection) {
    // rien à faire
    done = true
  }
 
  // s'agit-il d'un rechargement de page ?
  if (!done && from === to) {
    // rien à faire
    done = true
  }
 
  // contrôle de la navigation du serveur [nuxt]
  // on se calque sur le menu de navigation du client
 
  // on traite d'abord le cas de fin-session
  if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
    // on nettoie la session
    const session = context.app.$session()
    session.reset(context)
    // on redirige vers la page index
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où la session PHP n'a pas démarré
  if (!done && !store.state.started && to !== 'index') {
    // redirection vers [index]
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où l'utilisateur n'est pas authentifié
  if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où [adminData] n'a pas été obtenu
  if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
    // redirection vers [index]
    redirection = 'index'
    // travail terminé
    done = true
  }
 
  // cas où [adminData] a été obtenu
  if (
    !done &&
    store.state.started &&
    store.state.authenticated &&
    store.state.métier.taxAdminData &&
    to !== 'calcul-impot' &&
    to !== 'liste-des-simulations'
  ) {
    // on reste sur la même page
    redirection = from
    // travail terminé
    done = true
  }
 
  // on a normalement fait tous les contrôles ---------------------
  // redirection ?
  if (redirection) {
    // on note la redirection dans le store
    store.commit('replace', { serverRedirection: true })
  } else {
    // pas de redirection
    store.commit('replace', { serverRedirection: false, from: to })
  }
  // on sauvegarde le store dans la session [nuxt]
  const session = context.app.$session()
  session.value.store = store.state
  session.save(context)
  // on fait l'éventuelle redirection du serveur [nuxt]
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • In questo script riutilizziamo i concetti già sviluppati e utilizzati nel routing del server [nuxt] dell'applicazione [nuxt-13];
  • Aggiungiamo due proprietà all'application store:
    • [from]: il nome dell'ultima pagina visualizzata. Sappiamo che il client [nuxt] dispone di questa informazione, ma il server [nuxt] no. Aggiungeremo questa informazione memorizzando il nome della pagina da visualizzare nello store ogni volta che il server [nuxt] esegue il routing. Faremo lo stesso per ogni routing sul client [nuxt]. In questo modo, al successivo routing da parte del server [nuxt], questo troverà nello store il nome dell'ultima pagina visualizzata dall'applicazione;
    • [serverRedirection]: quando una destinazione di routing viene rifiutata dal server [nuxt], questo eseguirà un reindirizzamento. Indicherà quindi nello store che la destinazione successiva del server [nuxt] è una pagina di reindirizzamento. Questo reindirizzamento attiverà una nuova esecuzione del router del server [nuxt]. Se il router rileva che la destinazione corrente è il risultato di un reindirizzamento, ne consentirà il proseguimento;
  • righe 6–11: recuperiamo le informazioni necessarie per il routing;
  • righe 13–16: la destinazione [/end-session] non è associata a una pagina denominata [end-session]. Non ha quindi alcun nome. Ne assegniamo uno;
  • riga 19: la destinazione di un possibile reindirizzamento;
  • riga 21: [done=true] quando i test di routing sono completati;
  • righe 23–27: come accennato, se l'instradamento corrente deriva da un reindirizzamento, non c'è nulla da fare. Infatti, durante l'instradamento precedente, il router ha deciso che il browser del client dovesse essere reindirizzato. Non c'è bisogno di riconsiderare questa decisione;
  • righe 29–33: se si tratta di un ricaricamento della pagina, lo lasciamo avvenire. Questa non è una regola universale per tutte le applicazioni [Nuxt]: è necessario esaminare gli effetti di un ricaricamento per ogni pagina. In questo caso, risulta che il ricaricamento delle pagine [index, tax-calculation, simulation-list] non causi effetti indesiderati;
  • righe 35–85: il routing del server [nuxt] rispecchia il routing del client [nuxt]. Quando ci si trova su una pagina, il routing del server [nuxt] deve riflettere il menu di navigazione fornito dal client [nuxt] mentre ci si trova su quella pagina;
  • righe 38–47: gestiamo innanzitutto il caso in cui la destinazione [end-session] non corrisponda a una pagina esistente. Se le condizioni sono soddisfatte (sessione avviata, utente autenticato), cancelliamo la sessione e reindirizziamo l'utente alla pagina [index];
  • righe 49–55: se la sessione JSON con il server di calcolo delle imposte non è stata avviata, l'unica destinazione possibile è la pagina [index];
  • righe 57–62: se la sessione JSON è stata avviata e l'utente non è autenticato e non ha richiesto la pagina di autenticazione, allora reindirizziamo alla pagina di autenticazione, che è la pagina [index];
  • righe 64–70: se l'utente è autenticato ma i dati [adminData] non sono stati ottenuti, l'utente viene reindirizzato alla pagina di autenticazione. L'autenticazione svolge due funzioni: autentica l'utente e, se l'autenticazione ha esito positivo, richiede anche i dati [adminData]. Se questi dati non sono stati ottenuti, l'autenticazione deve essere riavviata;
  • righe 72–85: se i dati [adminData] sono stati ottenuti, gli unici destinatari possibili sono [tax-calculation] e [simulation-list]. In caso contrario, l'instradamento viene negato;
  • righe 88–95: l'archivio viene aggiornato a seconda che ci sia o meno un reindirizzamento;
  • riga 94: non c'è reindirizzamento. Pertanto, l'attuale [to] diventa il [from] per il routing successivo;
  • righe 96–99: le informazioni dell'archivio vengono salvate nel cookie di sessione [nuxt];
  • righe 100–103: se è richiesto un reindirizzamento, questo viene eseguito;

Per eseguire i test, assicurati di partire da zero eliminando il cookie di sessione [nuxt] e il cookie di sessione PHP con il server di calcolo delle imposte:

Image

Per testare il routing del server [nuxt], prova tutti gli URL possibili [/, /tax-calculation, /simulation-list] su ogni pagina. Ogni volta, l'applicazione deve rimanere in uno stato coerente.

17.10. Passaggio 9

Il passaggio 9 prevede la distribuzione dell'applicazione [nuxt-20]. Ciò richiede un hosting che fornisca un ambiente [node.js] per eseguire il server [nuxt]. Io non ne dispongo. Il lettore può seguire le procedure descritte nella sezione collegata per distribuire l'applicazione [nuxt-20] sul proprio computer di sviluppo e proteggerla con un protocollo HTTPS.

17.11. Conclusione

Il porting dell'applicazione [vuejs-22] all'applicazione [nuxt-20] è ora completo. Rivediamo alcuni punti chiave di questo porting:

  • le pagine [vuejs-22] sono state mantenute;
  • le operazioni asincrone presenti nelle pagine [vuejs-22] sono state migrate in una funzione [asyncData];
  • in [nuxt-20], abbiamo dovuto gestire due entità: il client [nuxt] e il server [nuxt]. Quest'ultima entità non esisteva in [vuejs-22]. Per mantenere la coerenza tra le due entità, avevamo bisogno di una sessione [nuxt];
  • dovevamo gestire il routing del server [nuxt];

In pratica, è probabilmente preferibile iniziare direttamente con un'architettura [nuxt] piuttosto che costruire un'architettura [vue.js] e poi portarla in un ambiente [nuxt].