Introduction au framework NUXT.JS par l’exemple

Auteur

Serge Tahé, octobre 2019, https://sergetahe.com

Téléchargements

Téléchargement du PDF du cours

Téléchargement des exemples du cours (rar)

Présentation du cours

Ce document fait partie d’une série de quatre articles :

  1. [Introduction au langage PHP7 par l’exemple] ;
  2. [Introduction au langage ECMASCRIPT 6 par l’exemple] ;
  3. [Introduction au framework VUE.JS par l’exemple] ;
  4. [Introduction au framework NUXT.JS par l’exemple]. C’est le document présent ;

Ce sont tous des documents pour débutants. Les articles ont une suite logique mais sont faiblement couplés :

  • le document [1] présente le langage PHP 7. Le lecteur seulement intéressé par le langage PHP et pas par le langage Javascript des articles suivants s’arrêtera là ;
  • les documents [2-4] visent à construire un client Javascript au serveur de calcul de l’impôt développé dans le document [1] ;
  • les frameworks Javascript [vue.js] et [nuxt.js] des articles 3 et 4 nécessitent de connaître le Javascript des dernières versions d’ECMASCRIPT, celles de la version 6. Le document [2] est donc destiné à ceux qui ne connaissent pas cette version de Javascript. Il fait référence au serveur de calcul de l’impôt construit dans le document [1]. Le lecteur de [2] aura alors parfois besoin de se référer au document [1] ;
  • une fois ECMASCRIPT 6 maîtrisé, on peut aborder le framework VUE.JS qui permet de construire des clients Javascript s’exécutant dans un navigateur en mode SPA (Single Page Application). C’est le document [3]. Il fait référence à la fois au serveur de calcul de l’impôt construit dans le document [1] et au code du client Javascript autonome construit en [2]. Le lecteur de [3] aura alors parfois besoin de se référer aux documents [1] et [2] ;
  • une fois VUE.JS maîtrisé, on peut aborder le framework NUXT.JS qui permet de construire des clients Javascript s’exécutant dans un navigateur en mode SSR (Server Side Rendered). Il fait référence à la fois au serveur de calcul de l’impôt construit dans le document [1], au code du client Javascript autonome construit en [2] ainsi qu’à l’application [vue.js] développée dans le document [3]. Le lecteur de [4] aura alors parfois besoin de se référer aux documents [1] [2] et [3] ;

Ce document poursuit le travail fait dans le document [3] avec le framework VUE.JS.

Cette section s’intéresse à l’architecture suivante :

image0

  • en [1], un navigateur web affiche des pages web [5, 7] isues d’un serveur [3] à destination d’un utilisateur. Ces pages contiennent du Javascript implémentant un client d’un service web de données [2] ainsi qu’un client d’un serveur de fragments de pages web [3] ;
  • en [2], le serveur web est un serveur de données. Il peut être écrit dans n’importe quel langage. Il ne produit pas de pages web au sens classique (HTML, CSS, Javascript) sauf peut-être la 1ère fois. Mais cette 1ère page peut être obtenue d’un serveur web classique [3] (pas un serveur de données). Le Javascript de la page initiale va alors générer les différentes pages web de l’application en obtenant les données [4] à afficher, auprès du serveur web qui agit comme un serveur de données [2]. Il peut également obtenir des fragments de page web [5] pour habiller ces données auprès du serveur de pages web [3] ;
  • en [4], l’utilisateur initie une action ;
  • en [6,7] : il reçoit des données habillées par un fragment de page web ;

Le framework [nuxt.js] |https://fr.nuxtjs.org/| va nous permettre d’implémenter le fonctionnement suivant :

  • la 1ère page de l’application est délivrée par le serveur [node.js] [3]. Par ailleurs les autres pages de l’application sont également présentes sur ce même serveur. Elles sont délivrées lorsque l’utilisateur tape leur URL à la main dans le navigateur. Ces pages embarquent une application [vue.js] (approximativement) ;
  • une fois la 1ère page chargée dans le navigateur, l’application se comporte comme une application [vue.js] classique. Dans notre schéma ci-dessus, elle va alors dialoguer avec le serveur de données [2] ;

Au final, l’application se comporte comme une application [vue.js] sauf pour la 1ère page et lorsque l’utilisateur tape des URL à la main. Dans ces cas, la page est cherchée sur le serveur [3]. Lorsque qu’un moteur de recherche demande les différentes pages de l’aplication, il reçoit les pages du serveur [3]. Celles-ci ont pu être optimisées pour le SEO (Search Engine Optimization). Dans une application [vue.js], le moteur de recherche reçoit une page avec peu de signification SEO. Par exemple, dans l’application du client du serveur de calcul de l’impôt du document [3], la page reçue par le navigateur lors du démarrage de l’application était la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="/client-vuejs-impot/favicon.ico">
    <title>vuejs</title>
  <link href="/client-vuejs-impot/app.js" rel="preload" as="script"></head>
  <body>
    <noscript>
      <strong>We're sorry but vuejs doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  <script type="text/javascript" src="/client-vuejs-impot/app.js"></script></body>
</html>

C’est l’unique page chargée par le navigateur. Toutes les autres pages de l’application sont générées dynamiquement par le Javascript sans l’aide du navigateur. Certains moteurs de recherche se contentent de cette page. D’autres vont plus loin en exécutant le Javascript contenu dans la page (ligne 9 ci-dessus). Une autre page est alors obtenue. Celle-ci peut contenir une opération asynchrone pour aller chercher les données que la page va afficher. Dans ce cas les moteurs de recherche n’attendent pas. On se retrouve alors avec une page incomplète. On se rappelle peut-être que c’est le cas de notre client [vue.js] du serveur de calcul de l’impôt : de façon asynchrone, il initialise au cours du chargement de la 1ère page, une session jSON avec le serveur de calcul de l’impôt. Dans ce cas précis, cela n’influe pas sur la page récupérée par le moteur de recherche. Pour d’autres applications, cela pourrait être pénalisant en termes SEO.

Avec [nuxt.js] on peut servir au moteur de recherche une page plus signifiante pour chacune des pages de l’application.

Les scripts de ce document sont commentés et leur exécution console reproduite. Des explications supplémentaires sont parfois fournies. Le document nécessite une lecture active : pour comprendre un script, il faut à la fois lire son code, ses commentaires et ses résultats d’exécution.

Les exemples du document sont disponibles |ici|.

L’application serveur PHP 7 peut être testée |ici|.

Serge Tahé, décembre 2019

L’environnement de travail

Nous utiliserons le même environnement de travail que celui présenté dans les documents :

Une première application [nuxt.js]

Création de l’application

Pour nos développements [nuxt.js] nous continuons à utiliser VS Code. Nous avons créé un dossier [dvp] vide dans lequel nous allons mettre nos exemples. Puis nous ouvrons ce dossier :

image0

Nous sauvegardons l’espace de travail sous le nom [intro-nuxtjs] [3-5] :

image1

Nous ouvrons un terminal [6-7] :

image2

Jusqu’à maintenant, nous avons utilisé le gestionnaire de packages Javascript [npm]. Pour changer, nous allons ici utiliser le gestionnaire [yarn]. Celui-ci est installé, comme [npm], avec les récentes versions de [node.js]. Pour créer une première application [nuxt], nous utilisons la commande [yarn create nuxt-app <dossier>] [1]. La commande va demander un certain nombre d’informations sur le projet à générer puis, celles-ci obtenues, va le générer [2] :

image3

En [2], toute une arborescence de fichiers a été créée. Le fichier [package.json] donne la liste des biblothèques Javascript téléchargées dans le dossier [node-modules] [4] :

{
  "name": "nuxt-intro",
  "version": "1.0.0",
  "description": "nuxt-intro",
  "author": "serge-tahe",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
  },
  "dependencies": {
    "nuxt": "^2.0.0",
    "bootstrap-vue": "^2.0.0",
    "bootstrap": "^4.1.3",
    "@nuxtjs/axios": "^5.3.6"
  },
  "devDependencies": {
    "@nuxtjs/eslint-config": "^1.0.1",
    "@nuxtjs/eslint-module": "^1.0.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-config-prettier": "^4.1.0",
    "eslint-plugin-prettier": "^3.0.1",
    "prettier": "^1.16.4"
  }
}

Ce fichier reflète les réponses données à la commande [create nuxt-app] pour définir le projet créé (novembre 2019). Le lecteur peut avoir un fichier [package.json] différent :

  • il a pu donner des réponses différentes aux questions ;
  • la commande [create nuxt-app] aura évolué depuis l’écriture de ce document : les dépendances et les versions auront changé ;

La ligne 8 du script est la commande qui lance l’application :

image4

  • en [4], on voit que l’application est disponible à l’URL [localhost:3000] ;
  • en [5-6], on voit que l’application donne naissance à un serveur [6] et à un client (de ce serveur) [5] ;

Demandons l’URL [http://localhost:3000/] dans un navigateur :

image5

Description de l’arborescence d’une application [nuxt]

Reprenons l’arborescence de l’application créée :

image6

Le rôle des dossiers est le suivant :

assets ressources non compilées de l’application (images, …) ;
static les fichiers de ce dossier seront disponibles à la racine de l’application. On met dans ce dossier des fichiers qu’on doit trouver à la racine de l’application comme par exemple le fichier [robots.txt] destiné aux moteurs de recherche ;
components les composants [vue] de l’application utilisés dans les [layouts] et les [pages] ;
layouts les composants [vue] de l’application servant de mise en page des [pages] ;
pages les composants [vue] affichés par les différentes routes de l’application. On pourrait les appeler les vues de l’application. Les pages jouent un rôle particulier dans [nuxt] : les routes sont créées dynamiquement à partir de l’arborescence trouvée dans le dossier [pages] ;
middleware les scripts exécutés à chaque changement de route. Ils permettent de contrôler celles-ci ;
plugins porte un nom prêtant à confusion. Peut contenir des plugins mais également des scripts classiques. Les scripts trouvés dans ce dossier sont exécutés au démarrage de l’application ;
store s’il contient un script [index.js] alors celui-ci définit une instance du store de [Vuex] ;

Si un dossier est vide, on peut le supprimer de l’arborescence. Ci-dessus, les dossiers [assets, static, middleware, plugins, store] peuvent être supprimés [2].

Le fichier de configuration [nuxt.config]

L’exécution de l’application est contrôlée par le fichier [nuxt.config.js] suivant :

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** 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'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}
  • ligne 2 : le type d’application générée :
    • [universal] : application client / serveur. Au chargement initial de l’application ainsi qu’à chaque rafraîchissement de page dans le navigateur, le serveur est sollicité pour délivrer la page ;
    • [sap] : application de type [Single Page Application] : un serveur délivre initialement la totalité de l’application. Ensuite le client opère seul, même en cas de rafraîchissement d’une page dans le navigateur ;
  • lignes 6-18 : définissent l’entête HTML <head> des différentes pages de l’application :
    • ligne 7 : la balise <title> du titre des pages ;
    • lignes 8-16 : les balises <meta> ;
    • ligne 17 : les balises <link>
Dans l’application générée, la balise <head> est la suivante (code source de la page affichée dans le navigateur) :
<title>nuxt-intro</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="nuxt-intro">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="preload" href="/_nuxt/runtime.js" as="script">
<link rel="preload" href="/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/_nuxt/app.js" as="script">

Maintenant, modifions le fichier [nuxt.config] de la façon suivante :

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' }]
},

Lorsque nous réexécutons l’application, la balise <head> est devenue la suivante (code source de la page affichée dans le navigateur) :

  <head >
    <title>Introduction à [nuxt.js]</title>
    <meta data-n-head="ssr" charset="utf-8">
    <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
    <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
    <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
    <link rel="preload" href="/_nuxt/runtime.js" as="script">
    <link rel="preload" href="/_nuxt/commons.app.js" as="script">
    <link rel="preload" href="/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/_nuxt/app.js" as="script">

Revenons au fichier [nuxt.config] :

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    ...
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** 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'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  }
}
  • ligne 12 : entre chaque route du client [nuxt], une barre de chargement (loading) apparaît si le changement de route prend un peu de temps. La propriété [loading] permet de paramétrer cette barre de chargement, ici la couleur de la barre ;
  • ligne 16 : les fichiers [css] globaux. Ils seront automatiquement inclus dans toutes les pages de l’application ;
  • lignes 24-27 : les modules Javascript nécessaires à la compilation (build) de l’application ;
  • lignes 31-36 : les modules Javascript utilisés par l’application ;
  • ligne 41 : paramétrage de la bibliothèque [axios] lorsque celle-ci a été sélectionnée par l’utilisateur pour les dialogues HTTP avec des serveurs tiers ;
  • lignes 45-50 : paramétrage de la compilation (build) du projet ;

On peut ajouter d’autres clés au fichier de configuration. On peut notamment paramétrer le port de service (3000 par défaut) et la racine du projet (par défaut, le dossier racine du projet). C’est ce que nlous faisons maintenant en ajoutant les clés suivantes :

// répertoire du code source
  srcDir: '.',
  router: {
    // URL racine des pages de l’application
    base: '/nuxt-intro/'
  },
  // serveur
  server: {
    // port de service - par défaut 3000
    port: 81,
    // adresses réseau écoutées - par défaut localhost=127.0.0.1
    host: '0.0.0.0'
  }
  • ligne 2 : où trouver le code source du projet. On le trouve ici dans le dossier courant, ç-à-d au même niveau que le fichier [nuxt.config.js]. C’est la valeur par défaut ;
  • lignes 8-13 : configurent le serveur (il ne faut pas oublier qu’une application [nuxt] de type [universal] est installée à la fois sur un serveur et un navigateur client de ce serveur) ;
  • ligne 10 : les pages de l’application seront délivrées sur le port 81 du serveur ;
  • ligne 12 : par défaut [localhost] (adresse réseau 127.0.0.1). Une machine peut avoir plusieurs adresses réseau si elle appartient à plusieurs réseaux. L’adresse 0.0.0.0 indique que le serveur web écoute toutes les adresses réseau de la machine ;
  • lignes 3-6 : configurent le routeur de l’applicatio [nuxt] ;
  • ligne 5 : les pages de l’application seront disponibles à l’URL [http://localhost:81/nuxt-intro/];

Ajoutons ces lignes au fichier [nuxt.config.js] puis exécutons le projet (script npm dev). Le résultat est le suivant :

image7

  • en [1], l’adresse de la machine sur un réseau public ;

  • en [2], le port de service ;

  • en [3], l’URL racine de l’application ;

    1. Le dossier [layouts]

image8

Le dossier [layouts] est destiné aux composants de mise en page. Par défaut, c’est le composant nommé [default.vue] qui est utilisé. Dans ce projet, celui-ci est le suivant :

<template>
  <div>
    <nuxt />
  </div>
</template>

<style>
html {
  font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
    Roboto, 'Helvetica Neue', Arial, sans-serif;
  font-size: 16px;
  word-spacing: 1px;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: border-box;
  margin: 0;
}

.button--green {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #3b8070;
  color: #3b8070;
  text-decoration: none;
  padding: 10px 30px;
}

.button--green:hover {
  color: #fff;
  background-color: #3b8070;
}

.button--grey {
  display: inline-block;
  border-radius: 4px;
  border: 1px solid #35495e;
  color: #35495e;
  text-decoration: none;
  padding: 10px 30px;
  margin-left: 15px;
}

.button--grey:hover {
  color: #fff;
  background-color: #35495e;
}
</style>

Commentaires

  • lignes 1-5 : le [template] du composant ;
  • ligne 3 : la balise <nuxt /> désigne la page courante du routage ;
  • lignes 7-55 : le style embarqué par le composant de mise en page. Comme celui-ci contient la page courante du routage, ce style va s’appliquer à toutes les pages routées de l’application ;

On voit que le but premier de la page [default.vue] est ici d’appliquer un style aux pages routées.

Le dossier [pages]

image9

Le dossier [pages] contient les vues routées, celles que voit l’utilisateur. La page [index.vue] est la page d’accueil de l’application. Avec [nuxt.js], il n’y a pas de fichier de routage. Les routes sont déterminées à partir de la structure du dossier [pages]. Ici la présence d’un fichier [index.vue] va automatiquement crééer une route appelée [index] et de chemin [/index] ramené à [/] puisqu’il s’agit de la page d’accueil. Ainsi la route suivante est créée :

{ name : ‘index’, path : ‘/’}

Le fichier [index.vue] est ici le suivant :

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        nuxt-intro
      </h1>
      <h2 class="subtitle">
        nuxt-intro
      </h2>
      <div class="links">
        <a href="https://nuxtjs.org/" target="_blank" class="button--green">
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

Le [template] des lignes 1-25 affichent la vue suivante :

image10

L’image [1] est générée par la ligne 4 du [template]. On voit donc que la page utilise un composant appelé [logo]. Celui-ci est défini aux lignes 27-35 du script de la page. Ligne 28, la notation [~] désigne la racine du projet.

Vue DevTools

[Vue DevTools] est l’extension de navigateur qui permet d’inspecter les objets [nuxt.js] et [vue.js] dans le navigateur. Nous l’avons déjà utilisée dans le chapitre sur [vue.js]. Examinons ce que cet outil trouve lorsque la page d’accueil de notre application est affichée :

image12

  • en [1], le composant [PagesIndex] désigne la page [pages/index.vue] ;
  • on voit en [2] que ce composant a une propriété [$route] qui est la route qui a amené à la page [index] ;

Comme simple exercice, affichons cette route dans la console.

Modification de la page d’accueil

Nous allons modifier le fichier [index.vue]. Dans notre installation du projet, nous avons installé deux dépendances :

  • [eslint] : qui vérifie la syntaxe des fichiers Javascript et des composants Vue. Si l’extension [ESLint] de VSCode a été installée, cette syntaxe est vérifiée lors de la frappe des textes et les erreurs sont immédiatement signalées ;
  • [prettier] : qui formate les codes Javascript d’une façon standard ;

Ces dépendances sont inscrites dans le fichier [package.json] :

"devDependencies": {
    "@nuxtjs/eslint-config": "^1.0.1",
    "@nuxtjs/eslint-module": "^1.0.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-config-prettier": "^4.1.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-plugin-prettier": "^3.0.1",
    "prettier": "^1.16.4"
}

J’ai pu remarquer (nov 2019) qu’avec l’installation faite par la commande [yarn create nuxt-app], les outils [eslint, prettier] ne fonctionnent pas lors de la frappe des textes. Les erreurs ne sont signalées qu’à la compilation. Après quelques recherches, j’ai trouvé une configuration qui marche :

image13

On installe à la racine du projet, un dossier [.vscode] avec dedans le fichier [settings.json] suivant :

{
  "eslint.validate": [
    {
      "language": "vue",
      "autoFix": true
    },
    {
      "language": "javascript",
      "autoFix": true
    }
  ],
  "eslint.autoFixOnSave": true,
  "editor.formatOnSave": false
}
  • lignes 2-11 : indiquent que lorsque [eslint] valide les fichiers .vue et .js il doit corriger les erreurs qu’il peut corriger ;
  • ligne 12 : lorsqu’un fichier est sauvegardé, [eslint] doit corriger les erreurs qu’il peut corriger ;
  • ligne 13 : inhibe le formatage fait par défaut dans VSCode lors d’une sauvegarde. C’est [prettier] qui le fera ;

Avec cette configuration :

  • les erreurs de syntaxe ou de formatage sont signalées dès la frappe des textes ;
  • les erreurs de formatage sont automatiquement corrigées lors de la sauvegarde du fichier ;

La bibliothèque [prettier] est configurée par le fichier [.prettierrc] :

image14

Ce fichier est par défaut le suivant :

{
  "semi": false,
  "arrowParens": "always",
  "singleQuote": true
}
  • ligne 1 : pas de ; à la fin des instructions ;
  • ligne 2 : si une fonction ‘flèche’ (arrow) a un unique paramètre, celui-ci est entouré de parenthèses ;
  • ligne 3 : les chaînes de caractères sont entourées d’apostrophes (pas de guillemets) ;

Nous ajoutons les deux règles suivantes :

{
  "semi": false,
  "arrowParens": "always",
  "singleQuote": true,
  "printWidth": 120,
  "endOfLine": "auto"
}
  • ligne 5 : la ligne de code peut faire jusqu’à 120 caractères ;
  • ligne 6 : la marque de fin de ligne peut être indifféremment CRLF (windows) ou LF (unix) ;

Enfin, le fichier [package.json] est modifié de la façon suivante :

"scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "lintfix": "eslint --fix --ext .js,.vue --ignore-path .gitignore ."
},
  • ligne 7 : nous ajoutons la commande [lintfix] qui est identique à la commande [lint] de la ligne 6 si ce n’est qu’elle a en plus le paramètre [–fix]. La commande [lint] vérifie la syntaxe et le format de tous les fichiers du projet et signale toute erreur. [lintfix] fera la même chose si ce n’est que les problèmes de formatage qui peuvent être corrigés le seront automatiquement. [lintfix] sera la commande à utiliser si la compilation échoue à cause de problèmes de formatage de fichiers ;

Ceci fait, nous modifions le fichier [index.vue] de la façon suivante :

image15

<script>
/* eslint-disable no-console */
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  },
  // cycle de vie
  created() {
    console.log('created, route=', this.$route)
  }
}
</script>
  • lignes 10-12 : on ajoute la fonction [created] qui est automatiquement exécutée lorsque le composant a été créé ;
  • ligne 11 : on affiche la route courante ;
  • ligne 2 : un commentaire destiné à [eslint]. Sans ce commentaire, [eslint] signale une erreur ligne 11 : il ne veut pas d’instructions [console] dans les fonctions du cycle de vie. [eslint] est configurable. Nous allons garder sa configuration par défaut et nous utiliserons des commentaires tels que celui de la ligne 2 pour désactiver une règle précise de [eslint]. Nous utiliserons deux types de commentaires :
    • /* désactivation règle [eslint] */ : désactivation d’une règle pour tout le fichier ;
    • // désactivation règle [eslint] : désactivation d’une règle pour la ligne qui suit ;

Lors de la frappe, les erreurs sont signalées et une fonction [Quick Fix] disponible :

image16

On exécute le projet :

image17

  • en [1], l’onglet [Vue] des outils de développement du navigateur (F12) ;
  • en [2] et [3], l’affichage de la route ;

Pourquoi deux affichages et non un seul ?

Une application [nuxt] se décompose de deux éléments, un serveur et un client :

  1. le serveur fournit les pages de l’application au démarrage de celle-ci et puis à chaque fois qu’une page est rafraîchie dans le navigateur (F5) ou bien que l’utilisateur tape une URL de l’application à la main ;
  2. chaque page fournie par le navigateur contient la page demandée ainsi que le code Javascript de toute l’application qui est ensuite exécutée sur le navigateur. C’est le client. Tant qu’il n’y a pas rafraîchissement de page sur le navigateur, l’application fonctionne comme une application Vue classique en mode [sap] (Single Page Application). Dès que l’utilisateur provoque manuellement un rafraîchissement de page, celle-ci est demandée au serveur et on retourne à la phase 1 précédente.

Ce qu’il faut comprendre, c’est que ce sont les mêmes pages du dossier [pages] qui sont fournies par le serveur ou le client. Pour cette raison, les concepteurs de [nuxt] appellent ce type de pages, des pages isomorphiques. Les mêmes pages [.vue] peuvent être interprétées à la fois par le client et le serveur. Prenons l’exemple de la page [index] :

<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        nuxt-intro
      </h1>
      <h2 class="subtitle">
        nuxt-intro
      </h2>
      <div class="links">
        <a href="https://nuxtjs.org/" target="_blank" class="button--green">
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
/* eslint-disable no-console */
import Logo from '~/components/Logo.vue'

export default {
  components: {
    Logo
  },
  // cycle de vie
  created() {
    console.log('created, route=', this.$route)
  }
}
</script>

Comme c’est la page d’accueil, au démarrage de l’application elle est servie par le serveur. La page sur le serveur a également un cycle de vie, le même que celle d’une page [Vue] classique sauf pour les fonctions [beforeMount, monted] qui n’existent pas côté serveur. La fonction [created] est elle exécutée ce qui explique le 1er log. Cela signifie au passage que le serveur est capable d’exécuter des scripts Javascript. Ici et en général, ce serveur est un serveur [node.js]. Une fois la page créée sur le serveur, elle arrive sur le navigateur où elle subit de nouveau le cycle de vie. La fonction [created] est exécutée une seconde fois, ce qui donne le 2ième log.

L’architecture d’une application [nuxt] pourrait être la suivante :

image18

  • [1] : le navigateur qui héberge l’application [nuxt] lorsque celle-ci a été chargée sur le navigateur. C’est ce qu’on a appelé le client [nuxt] ;
  • [3] : le serveur qui héberge initialement l’application [nuxt]. Celle-ci est chargée sur le navigateur [1] au démarrage de l’application et à chaque fois que l’utilisateur rafraîchit la page courante du navigateur ou tape à la main une URL de l’application. C’est là que se situe la différence de fonctionnement avec une application Vue classique. Avec celle-ci, une fois chargée sur le navigateur, le serveur n’était plus jamais sollicité par la suite. Une autre différence importante qu’on n’a pas pu voir pour l’instant est que le serveur d’une application Vue est un serveur statique, incapable d’interpréter les pages [.vue], alors que celui d’une application Nuxt de type [universal] est un serveur Javascript. Avant d’envoyer une page au navigateur, le serveur peut exécuter des scripts et aller par exemple chercher des données sur le serveur [2] ;
  • [2] : est le serveur qui fournit des données soit au client [nuxt] [1], soit au serveur [nuxt] [3] ;

On peut dans le schéma ci-dessus distinguer trois sous-systèmes client / serveur :

  • [1, 3] : héberge l’application [nuxt]. [3] la fournit au démarrage de l’application avec la page d’accueil et à chaque fois que l’utilisateur demande une page manuellement. [1] héberge l’application [nuxt] reçue de [3] qui fonctionne alos en mode [SAP] tant que les pages ne sont pas demandées manuellement à [3] ;
  • [1, 2] : en mode [SAP], le client [nuxt] récupère des données externes auprès d’un ou plusieurs serveurs ;
  • [3, 2] : lors de la génération de la page demandée par l’utilisateur, le serveur [3] peut lui aussi récupérer des données externes auprès d’un ou plusieurs serveurs ;

C’est donc le serveur [3] qui distingue une application [nuxt] d’une application [vue]. Ce serveur est sollicité à chaque fois que l’utilisateur demande une page manuellement. Il traite les mêmes pages [.vue] que le client [vue] [1]. C’est un serveur Javascript capable d’exécuter les scripts présents dans la page. Cela peut modifier par exemple la façon de générer la page d’accueil avec des données externes : là où une application [vue] obtient celles-ci forcément à partir du client [1], ici elles peuvent être obtenues par le serveur [3] avant que la page ne soit envoyée au client. La page d’accueil devient ainsi signifiante et peut contribuer à améliorer le SEO de l’application.

Note : en mode développement les trois entités [1, 2, 3] sont souvent sur la même machine. Ce sera le cas ici pour tous nos exemples.

Déplacement du code source de l’application dans un dossier séparé

Par la suite, nous allons créer diverses applications [nuxt] dans le même dossier [dvp]. En effet, le dossier des dépendances [node_modules] généré pour chaque projet [nuxt] peut faire plusieurs centaines de méga-octets. On va créer divers dossiers [nuxt-00, nuxt-01, …] dans le dossier [dvp] pour contenir le code source des exemples à tester. Puis nous utiliserons le fichier de configuration [nuxt-config.js] pour indiquer où se trouve le code source du projet [dvp] qui restera l’unique projet [nuxt] de ce tutoriel.

Nous déplaçons le code source de l’application générée initialement par la commande [yarn create nuxt-app] dans un dossier [nuxt-00] :

image19

  • en [2], on a déplacé les dossiers [components, layouts, pages] dans un dossier [nuxt-00] ;
  • en [3], il nous faut modifier le fichier [nuxt.config.js] ;

Nous modifions le le fichier [nuxt.config.js] de la façon suivante :

export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  ...
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // répertoire du code source
  srcDir: 'nuxt-00',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-00/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: '0.0.0.0'
  }
}

Le fichier est modifié en deux points :

  • ligne 17 : on indique que le code source du projet [dvp] est à trouver dans le dossier [nuxt-00] ;
  • ligne 21 : on indique que l’URL racine de l’application est désormais [/nuxt-00/]. Ce changement n’était pas obligatoire. On pourrait ne pas mettre cette propriété et la racine des URL serait alors [/]. Ici, cela nous permettra de nous souvenir que le code source exécuté est celui du dossier [nuxt-00] ;

Ceci fait, le projet [dvp] est exécuté comme précédemment :

image20

Déploiement de l’application [nuxt-00]

Nous allons exécuter l’application [nuxt-00] dans un environnement autre que l’environnement intégré de VSCode.

Tout d’abord nous compilons l’application :

image21

  • en [3], le résultat de la compilation du client. Sera exécuté par le navigateur ;
  • en [4], le résultat de la compilation du serveur. Sera exécuté par le serveur [node.js] ;

Le résultat de la compilation est placé dans le dossier [.nuxt] :

image22

Nous copions les dossiers [.nuxt, node_modules] et les fichiers [package.json, nuxt.config.js] dans un dossier séparé :

image23

Le fichier [package.json] est simplifié de la façon suivante :

{
  "scripts": {
    "start": "nuxt start"
  }
}
  • on ne garde que le script [start] qui permet d’exécuter la version compilée du projet ;

Le fichier [nuxt.config.js] est simplifié de la façon suivante :

export default {
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-00/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: '0.0.0.0'
  }
}
  • ligne 5 : on fixe l’URL de base de l’application compilée ;
  • lignes 8-14 : on définit le port de service et les adresses réseau écoutées ;

Ceci fait, on ouvre un terminal Laragon et on se positionne dans le dossier contenant la version compilée du projet. On peut ouvrir tout type de terminal mais il faut que l’exécutable [npm] soit dans le PATH du terminal. C’est le cas pour le terminal Laragon.

Ceci fait, on tape la commande [npm run start] :

image24

En [3], on voit qu’un serveur a été lancé et qu’il écoute à l’URL [http://192.168.1.128:81/nuxt-00/]. Maintenant demandons cette URL avec un navigateur [4]. On a bien la même chose qu’auparavant. Côté terminal, des logs ont été écrits [5]. C’est le log placé dans la méthode [created] de la page [index.vue] qui a été exécutée par le serveur [node.js].

image25

Côté navigateur [6], on retrouve également le log de la méthode [created] de la page [index.vue] mais exécutée cette fois par le client.

Mise en place d’un serveur sécurisé

Ci-dessus, l’URL de l’application est [http://192.168.1.128/nuxt-00/]. On voudrait qu’elle soit [https://192.168.1.128/nuxt-00/]. Il nous faut donc construire un serveur sécurisé. Nous montrons comment procéder.

Note : la méthode a été tirée de l’article [https://stackoverflow.com/questions/56966137/how-to-run-nuxt-npm-run-dev-with-https-in-localhost].

Tout d’abord nous créons une clé privée et une clé publique avec [openssl]. [openssl] est normalement installé en même temps que le serveur Laragon. Du coup, cette commande est disponible dans tout terminal Laragon. Ouvrons donc un terminal Laragon et positionnons-nous sur le dossier de l’application déployée :

image26

image27

  • en [2], on tape la commande [openssl genrsa 2048 > server.key] ;
  • en [3], un fichier [server.key] est créé ;
  • en [4], on tape la commande [openssl req -new -x509 -nodes -sha256 -days 365 -key server.key -out server.crt] ;
  • en [5], un fichier [server.crt] est créé ;

Ces deux fichiers constituent un certificat autosigné. La plupart des navigateurs ne les acceptent qu’après approbation de l’utilisateur ayant demandé la page.

Les fichiers [server.key, server.crt] doivent être maintenant utilisés par l’application web. Pour cela le fichier [nuxt.config.js] doit être modifié de la façon suivante :

import path from 'path'
import fs from 'fs'

export default {
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-00/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: '0.0.0.0',
    // certificat autosigné
    https: {
      key: fs.readFileSync(path.resolve(__dirname, 'server.key')),
      cert: fs.readFileSync(path.resolve(__dirname, 'server.crt'))
    }
  }
}

Ce sont les lignes 18-21 qui mettent en place le protocole [https].

Maintenant réexécutons l’application :

image28

Fin du premier exemple

Le premier exemple est désormais terminé. Il nous a appris beaucoup de concepts de [nuxt]. Nous allons maintenant développer d’autres exemples que nous placerons dans des dossiers [nuxt-01, nuxt-02, …]. Comme ces exemples utiliseront un fichier [nuxt.config.js] différent, nous sauvegarderons dans chacun de ces dossiers, le fichier [nuxt.config.js] qui a servi à les exécuter :

image29

Exemple [nuxt-01] : routage et navigation

Nous allons construire une série d’exemples simples pour découvrir progressivement le fonctionnement d’une application [nuxt]. Nous allons commencer par porter l’application [vuejs-11] du document |Introduction au framework VUE.JS par l’exemple|, pour découvrir tout d’abord ce qui différencie l’organisation du code d’une application [nuxt] de celle du code d’une application [vue].

Arborescence du projet

Le projet [vuejs-11] était un projet de navigation entre vues :

image0

L’arborescence du code source du projet [vuejs-11] était le suivant :

image1

  • [main.js] était le script exécuté lors du démarrage de l’application [vue] ;
  • [router.js] fixait les règles de routage ;
  • [App.vue] était la vue structurante de l’application. Elle organisait la mise en page des différentes vues ;
  • [Component1, Component2, Component3, Layout, Navigation] étaient les composants utilisés dans les différentes vues de l’application ;

Dans le portage de l’application [vue] [1] vers une application [nuxt] [2] :

  • les scripts exécutés au démarrage de l’application doivent être déclarés dans la clé [plugins] du fichier [nuxt.config.js]. Par ailleurs, il est possible de séparer les scripts destinés au serveur [nuxt] de ceux destinés au client [nuxt] ;
  • la vue [App.vue] doit être installée dans le dossier [layouts] et être renommée [default.vue] ;
  • les composants [Component1, Component2, Component3] qui sont les cibles du routage doivent migrer dans le dossier [pages]. L’un d’eux, celui qui sert de page d’accueil, doit être renommé [index.vue]. Nous avons ici renommé les fichiers :
    • [Component1] –> [index] : affiche le texte [Home] ;
    • [Component2] –> [page1] : affiche le texte [Page 1] ;
    • [Component3] –> [page2] : affiche le texte [Page 2] ;
[nuxt] utilise le contenu du dossier [pages] pour générer dynamiquement les routes suivantes :
1
2
3
{ name :’index’, ‘path’ :’/’}
{ name :’page1’, ‘path’ :’/page1’}
{ name :’page2’, ‘path’ :’/page2’}
Du coup, le fichier [router.js] utilisé dans le projet [vue] devient inutile dans le projet [nuxt].

Le fichier de configuration [nuxt.config.js] sera le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** 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'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // répertoire du code source
  srcDir: 'nuxt-01',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-01/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: '0.0.0.0'
  }
}
  • ligne 62 : on indique le dossier qui contient le code source du projet [dvp] ;

  • ligne 66 : : on indique l’URL racine de l’application [dvp] (on peut mettre ce qu’on veut) ;

  • ligne 43 : notons que la bibliothèque [bootstrap-vue] est référencée dans la configuration ;

    1. Portage du fichier [main.js]

Le fichier [main.js] du projet [vuejs-11] était le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// imports
import Vue from 'vue'
import App from './App.vue'

// plugins
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);

// bootstrap
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

// routeur
import monRouteur from './router'

// configuration
Vue.config.productionTip = false

// instanciation projet [App]
new Vue({
  name: "app",
  // vue principale
  render: h => h(App),
  // routeur
  router: monRouteur,
}).$mount('#app')

En-dehors des [imports], le code fait les choses suivantes :

  • lignes 5-11 : utilisation de la bibliothèque [bootstrap-vue]. Ce travail est désormais fait par le module [bootstrap-vue/nuxt] de la ligne 43 du fichier de configuration [nuxt.config.js] ;
  • lignes 14 et 25 : utilisation du fichier de routage [router.js]. Ce travail est désormais fait automatiquement par l’application [nuxt] à partir de l’arborescence du dossier [pages] ;
  • lignes 20-26 : instanciation de la vue principale de l’application. Dans une application [nuxt], c’est la vue [layouts/default.vue] qui sert de vue principale ;

Le fichier [main.js] n’a désormais plus de raison d’être. S’il en avait eu une, on l’aurait déclaré dans la clé [plugins] de la ligne 30 du fichier de configuration [nuxt.config.js] ;

La vue principale [default.vue]

image2

La vue principale [layouts / default.vue] est la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<template>
  <div class="container">
    <b-card>
      <!-- un message -->
      <b-alert show variant="success" align="center">
        <h4>[nuxt-01] : routage et navigation</h4>
      </b-alert>
      <!-- la vue courante du routage -->
      <nuxt />
    </b-card>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>
  • ligne 9, dans le projet [vuejs-11], on avait la balise <router-view /> au lieu de la balise <nuxt /> utilisée ici. Les deux semblent utilisables. Je les ai essayées toutes les deux sans voir de changement. J’ai gardé la balise <nuxt /> qui est celle conseillée. Elle affiche la vue courante, ç-à-d la page cible du routage courant ;

    1. Les composants

image3

Par rapport au projet [vuejs-11], les composants [layout, navigation] ne changent pas :

[components / layout.vue]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- disposition des vues -->
<template>
  <!-- ligne -->
  <div>
    <b-row>
      <!-- zone à deux colonnes -->
      <b-col v-if="left" cols="2">
        <slot name="left" />
      </b-col>
      <!-- zone à dix colonnes -->
      <b-col v-if="right" cols="10">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

<script>
export default {
  // paramètres
  props: {
    left: {
      type: Boolean
    },
    right: {
      type: Boolean
    }
  }
}
</script>

Ce composant sert à structurer les pages de l’application en deux colonnes :

  • lignes 7-9 : la colonne de gauche sur 2 colonnes Bootstrap ;
  • lignes 11-13 : la colonne de droite sur 10 colonnes Bootstrap ;

[navigation.vue]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <!-- menu Bootstrap à trois options -->
  <b-nav vertical>
    <b-nav-item to="/" exact exact-active-class="active">
      Home
    </b-nav-item>
    <b-nav-item to="/page1" exact exact-active-class="active">
      Page 1
    </b-nav-item>
    <b-nav-item to="/page2" exact exact-active-class="active">
      Page 2
    </b-nav-item>
  </b-nav>
</template>

Ce composant affiche trois liens de navigation :

image4

Pour savoir quoi mettre comme valeur aux attributs [to] des lignes 4, 7 et 10, il faut regarder le dossier [pages] [2] :

  • la page [index] aura l’URL [/] ;
  • la page [page1] aura l’URL [/page1] ;
  • la page [page2] aura l’URL [/page2] ;

Le composant [navigation] peut être également écrit de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <!-- menu Bootstrap à trois options -->
  <b-nav vertical>
    <nuxt-link to="/" exact exact-active-class="active">
      Home
    </nuxt-link>
    <nuxt-link to="/page1" exact exact-active-class="active">
      Page 1
    </nuxt-link>
    <nuxt-link to="/page2" exact exact-active-class="active">
      Page 2
    </nuxt-link>
  </b-nav>
</template>

La balise <b-nav-item> est remplacée par la balise <nuxt-link> qui désigne un lien de routage. A l’exécution, je n’ai pas vu de grande différence, rien qui pourrait faire pencher la balance vers une balise plutôt que l’autre.

Les pages

image5

La page [index.vue] affiche la vue suivante :

image6

Le code de la page est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">
      Home
    </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // cycle de vie
  beforeCreate(...args) {
    console.log('[home beforeCreate]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  },
  created(...args) {
    console.log('[home created]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  },
  beforeMount(...args) {
    console.log('[home beforeMount]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  },
  mounted(...args) {
    console.log('[home mounted]', 'process.server=', process.server,
 'process.client=', process.client, "nombre d'arguments=", args.length)
  }
}
</script>
  • ligne 5 : le composant de navigation est placé en colonne de gauche ;
  • lignes 7-9 : une alerte est placée dans la colonne de droite ;

Dans la partie <script>, nous mettons du code dans les fonctions du cycle de vie de la page [beforeCreate, created, beforeMount, beforeMounted]. Nous voulons savoir lesquelles sont exécutées par le serveur []nuxt et lesquelles par le client [nuxt]. On rappelle deux choses :

  • lorsqu’une page est demandée soit au démarrage de l’application, cas de la page [index], soit manuellement par l’utilisateur qui rafraîchit la page du navigateur ou tape une URL à la main, elle est délivrée d’abord par le serveur [nuxt]. Celui-ci interprète le code ci-dessus et exécute le Javascript qu’il contient ;

  • lorsque la page envoyée par le serveur [nuxt] arrive sur le navigateur, elle arrive avec le code du client [nuxt]. Celui-ci interprète de nouveau la page ci-dessus ;

  • par des logs, on veut savoir qui fait quoi pour mieux comprendre ce processus ;

  • lignes 30-31 : on utilise dans la fonction un objet global [process] qui existe aussi bien sur le serveur que sur le client :

    • [process.server] est vrai si le code est exécuté par le serveur, faux sinon ;
    • [process.client] est vrai si le code est exécuté par le client, faux sinon ;
    • parce que la variable [process] est non déclarée dans le code, on est obligés de mettre la ligne 14 pour [eslint]. La ligne [16] est nécessaire parce que sinon [eslint] déclare un autre type d’erreur à cause de la variable [process]. La ligne 15 est elle nécessaire pour permettre l’utilisation de [console] dans les fonctions du cycle de vie ;
  • ligne 29 : on veut savoir également si les fonctions du cycle de vie reçoivent des arguments. On va découvrir en effet que [nuxt] transmet des informations à certaines fonctions. On veut savoir si les fonctions du cycle de vie en font partie ;

  • on répète le même code pour les quatre fonctions ;

    1. Le fichier [nuxt.config.js]

C’est lui qui contrôle l’exécution du projet [dvp]. Il a été décrit page .

Exécution du projet

Nous exécutons le projet :

image7

La page affichée est la suivante :

image8

Une fois installée sur le navigateur, l’application [nuxt] devient une application [vue] classique. Nous ne commenterons donc pas le fonctionnement client de l’application [nuxt-01]. Cela a été fait dans le projet [vuejs-11] du document |Introduction au framework VUE.JS par l’exemple|.

L’application [nuxt] ne diffère de l’application [vue] qu’à deux moments :

  • le démarrage initial de l’application qui fournit la page d’accueil ;
  • à chaque fois que l’utilisateur provoque d’une manière ou une autre le rafraîchissement du navigateur ;

Dans ces deux cas :

  • la page demandée est fournie par le serveur ;
  • la page reçue est traitée par le client ;

Regardons les logs du démarrage de l’application (F12 sur le navigateur) :

image9

  • en [1], les logs du serveur (process.server=true). Ils apparaissent précédés de la mention [Nuxt SSR] (SSR= Server Side Rendered) ;
  • en [2], les logs du client sur le navigateur (process.client=true) ;

De ces logs, on peut déduire que :

  • le serveur exécute les fonctions [beforeCreate, created] du cycle de vie ;
  • le client exécute les fonctions [beforeCreate, created, beforeMount, mounted] du cycle de vie ;
  • le serveur a traité la page avant le client ;
  • dans les deux cas, aucune des fonctions exécutées ne reçoit d’arguments ;

Maintenant regardons le code source de la page reçue (option [Code source de la page] dans le navigateur) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-01/">
  ....
  <link rel="preload" href="/nuxt-01/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/app.js" as="script">
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
              <h4>[nuxt-01] : routage et navigation</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-01/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
                    Home
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    window.__NUXT__ = (function (a, b, c, d, e, f, g, h, i, j) {
      return {
        layout: "default", data: [{}], error: null, serverRendered: true,
        logs: [
          { date: new Date(1574069600078), args: [a, b, c, d, e, f, g, "(repeated 1 times)"], type: h, level: i, tag: j },
          { date: new Date(1574070938091), args: [a, b, c, d, e, f, g], type: h, level: i, tag: j }
        ]
      }
    }("[home beforeCreate]", "process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0", "log", 2, ""));
  </script>
  <script src="/nuxt-01/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-01/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/app.js" defer></script>
</body>
</html>

Commentaires

  • la première chose qui peut être remarquée est que le code HTML reçu reflète correctement ce que voit l’utilisateur. Ce n’était pas le cas des applications [vue] pour lesquelles le code source affiché était le code source d’un fichier HTML quasi vide. C’était ce qu’avait reçu le navigateur. Ensuite le client [vue] prenait la main et construisait la page attendue par l’utilisateur. Il fallait alors aller dans l’onglet [inspecteur] des outils de développement du navigateur (F12) pour découvrir le code HTML de la page affichée ;
  • lignes 57-67 : c’est le script qui a affiché les logs tagués [Nuxt SSR]. Ces logs ont été produits côté serveur et les résultats ont été embarqués dans un script inclus dans la page envoyée ;
  • lignes 68-71 : les scripts qui forment le client exécuté côté navigateur ;

Les scripts des lignes 68-71 sont exécutés et transforment la page reçue. Pour connaître la page finalement affichée pour l’utilisateur, il faut aller dans l’onglet [inspecteur] des outils de développement du navigateur (F12) :

image10

Lorsqu’on développe la balise <html> [3], on a le contenu suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-01/">
  ...
  <link rel="preload" href="/nuxt-01/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-01/_nuxt/app.js" as="script">

  <script charset="utf-8" src="/nuxt-01/_nuxt/pages_index.js"></script>
  <script charset="utf-8" src="/nuxt-01/_nuxt/pages_page1.js"></script>
  <script charset="utf-8" src="/nuxt-01/_nuxt/pages_page2.js"></script>
</head>
<body>
  <div id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-success" align="center">
              <h4>[nuxt-01] : routage et navigation</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-01/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div>
                <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
                    Home
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
    window.__NUXT__ = (function (a, b, c, d, e, f, g, h, i) {
      return {
        layout: "default", data: [{}], error: null, serverRendered: true,
        logs: [
          { date: new Date(1574068674481), args: ["[home beforeCreate]", a, b, c, d, e, f], type: g, level: h, tag: i },
          { date: new Date(1574068674482), args: ["[home created]", a, b, c, d, e, f], type: g, level: h, tag: i }
        ]
      }
    }("process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0", "log", 2, ""));
  </script>
  <script src="/nuxt-01/_nuxt/runtime.js" defer=""></script>
  <script src="/nuxt-01/_nuxt/commons.app.js" defer=""></script>
  <script src="/nuxt-01/_nuxt/vendors.app.js" defer=""></script>
  <script src="/nuxt-01/_nuxt/app.js" defer=""></script>

  <iframe id="mc-sidebar-container" ...></iframe>
  <iframe id="mc-topbar-container"...>  </iframe>
  <iframe id="mc-toast-container" ...></iframe>
  <iframe id="mc-download-overlay-container"...></iframe>
</body>

Commentaires

  • à première vue, la page affichée lignes 19-59, semble être la même que la page reçue ;
  • lignes 14-16 : trois nouveaux scripts apparaissent, un pour chacune des pages de l’application ;
  • lignes 76-79 : quatre [iframe] apparaissent ;

Lignes 33, 37 et 42, les liens posent problème. Ils semblent être des liens normaux qui lorsqu’on les clique vont faire une requête vers le serveur. Or à l’exécution, on voit que ce n’est pas vrai : il n’y a pas de requête vers le serveur. Pour comprendre pourquoi, il faut retourner dans l’onglet [inspecteur] du navigateur :

image11

On voit qu’en [1, 2] des événements ont été attachés aux liens. Ce sont les scripts des lignes 71-74 qui ont attaché des gestionnaires d’événements aux liens. Donc :

  • la page affichée par le client est visuellement identique à celle envoyée par le serveur ;
  • un comportement dynamique a été ajouté à la page par le client ;

Maintenant demandons la page [page1] en tapant l’URL à la main [http://192.168.1.128:81/nuxt-01/page1]. Les logs deviennent les suivants :

image12

On obtient les mêmes résultats que pour la page [index] mais pour [page1]. Le code source de la page reçue est lui le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success"><h4>[nuxt-01] : routage et navigation</h4></div> <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-01/" target="_self" class="nav-link">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page1" target="_self" class="nav-link active nuxt-link-active">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-01/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-primary">

                    Page 1
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = { layout: "default", data: [{}], error: null, serverRendered: true, logs: [{ date: new Date(1573917721122), args: ["[page1 beforeCreate]", "process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0"], type: "log", level: 2, tag: "" }] };</script>
  <script src="/nuxt-01/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-01/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-01/_nuxt/app.js" defer></script>
</body>

On obtient le même type de page que la page [index] mais avec l’alerte de la vue [Page 1] (ligne 30). Lignes 41-44, le code du client a été renvoyé avec la page. Au final, demander une URL à la main est identique à redémarrer l’application. Simplement la page affichée n’est pas forcément la page d’accueil, c’est celle qui a été demandée. Une fois la page reçue, c’est le client qui prend la main. Le serveur ne sera plus sollicité à moins que l’utilisateur n’en décide autrement.

Exemple [nuxt-02] : pages serveur et client

Dans ce projet, nous montrons :

  • que la page construite par le client peut être visuellement différente de celle reçue du serveur. On a alors un changement rapide de page, perceptible par l’utilisateur, et qui est donc nuisible à l’ergonomie de l’application. C’est donc une option à éviter ;
  • une solution pour que la page client recrée la même page que celle envoyée par le serveur ;

Le projet [nuxt-02] est obtenue initialement par recopie du projet [nuxt-01].

image0

Un dossier [store] est ajouté au projet ainsi que deux nouvelles pages. Nous y reviendrons.

  1. La page [index]

    1. Le code de la page

Le code de la page [index] devient le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning"> Home - value= {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home created]')
    // serveur seulement
    if (process.server) {
      this.value = 10
    }
    // client et serveur
    console.log('value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  }
}
</script>

Commentaires

  • ligne 7 : la page [index] va afficher la valeur de sa propriété [value] (ligne 28) ;

  • lignes 36-45 : il faut se rappeler ici que la fonction [created] est exécutée à la fois côté serveur et côté client. Lignes 40-42, le serveur fera passer à 10 la valeur de la propriété [value]. Le client, lui, ne touche pas à cette valeur. On veut simplement savoir si cette valeur est conservée par le client. On va découvrir que non ;

    1. Exécution

Nous modifions le fichier [/nuxt.config.js] pour exécuter le projet [nuxt-02] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
...
  // répertoire du code source
  srcDir: 'nuxt-02',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-02/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: 'localhost'
  }
...

Nous exécutons le projet [1] :

image1

La page [index] est alors affichée [2-3]. Elle affiche la valeur [10] pendant quelques instants puis affiche la valeur [0]. Que s’est-il passé ?

étape 1

C’est le serveur qui s’exécute le premier. Il exécute le code de la page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home created]')
    // serveur seulement
    if (process.server) {
      this.value = 10
    }
    // client et serveur
    console.log('value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  }
}
  • à cause de la ligne 23, la propriété [value] de la ligne 10 prend la valeur 10 ;

On peut le vérifier en regardant le code source de la page reçue par le navigateur (option [code source] dans le navigateur) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-02/">
  <link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
  ....
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
              <h4>[nuxt-02] : page serveur, page client</h4>
            </div> <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-02/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
                    Home - value= 10
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = ....;</script>
  <script src="/nuxt-02/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
  • ligne 46 : dans la page reçue, [value] avait la valeur 10 ;

étape 2

On sait qu’après réception de la page, les scripts des lignes 57-60 prennent la main et transforment le comportement de la page reçue, voire les informations affichées comme ici. Ces scripts forment le client qui lui aussi exécute le code de la page [index], le même code que le serveur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home created]')
    // serveur seulement
    if (process.server) {
      this.value = 10
    }
    // client et serveur
    console.log('value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  }
}
  • pour comprendre ce qui se passe, il faut comprendre que le client [nuxt] n’exécutera pas les lignes 22-24 (process.server=false) ;
  • dans une application [vue] classique la propriété [value] de la ligne 10 reste à 0. C’est pourquoi, une fois que le client est passé sur la page reçue, la valeur affichée devient [0] ;

La valeur générée par le serveur [nuxt] pour la propriété [value] n’a servi à rien.

  1. La page [page1]

    1. Le store [Vuex]

Nous avons ajouté un dossier [store] au projet [nuxt-02] :

image2

La présence de ce dossier fait qu’automatiquement [nuxt] va implémenter un store [Vuex]. C’est le fichier [index.js] qui implémente ce store. Ici, le fichier [index.js] est le suivant :

1
2
3
4
5
6
7
8
9
export const state = () => ({
  counter: 0
})

export const mutations = {
  increment(state, inc) {
    state.counter += inc
  }
}

[nuxt] implémente un store [Vuex] à partir du contenu de [index.js] :

  • lignes 1-3 : définition de l’état [state] du store. Cet état est retourné par une fonction. Ici, l’état n’a qu’une propriété, le compteur de la ligne 2. La fonction exportée doit s’appeler [state] ;
  • lignes 5-9 : les opération possibles sur l’état du store. On les appelle des [mutations]. Ici, la mutation [increment] permet d’incrémenter la propriété [counter] d’une quantité [inc]. L’objet exporté doit s’appeler [mutations] ;

Le [store] Vuex implémenté par [nuxt] est disponible à différents endroits. Dans les vues, il est disponible dans la propriété [this.$store].

Le code de la page

Comme la page [index], la page [page1] va afficher une valeur, celle du compteur du store Vuex :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!-- page 1 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="primary"> Page 1 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home created]')
    // serveur seulement
    if (process.server) {
      this.$store.commit('increment', 25)
    }
    // client et serveur
    this.value = this.$store.state.counter
    console.log('value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  }
}
</script>

Commentaires

  • lignes 38-40 : le serveur va incrémenter le compteur de 25 ;
  • ligne 42 : aussi bien le serveur que le client vont afficher la valeur du compteur ;
  • ligne 7 : la valeur du compteur est affichée ;

En lisant ce code, il faut comprendre deux choses :

  • le code exécuté est le même pour le serveur que le client ;
  • l’objet [this] n’est lui pas le même : il y a une version [this] côté serveur et une autre côté [client] ;

Nous cherchons à savoir si le [this.$store] du serveur est le même que le [this.$store] du client. Comme c’est le serveur qui s’exécute en premier (au démarrage de l’application), cela revient à se poser la question : est-ce que le [store] initialisé par le serveur est transmis au client ?

Exécution

On exécute le projet [nuxt-02] et on tape [localhost:81/nuxt-02/page1] à la main pour que le serveur soit sollicité. Comme au démarrage pour la page [index] :

  • le serveur exécute la page [page1.vue] ;
  • envoie la page générée au navigateur. Celle-ci est affichée ;
  • les scripts client embarqués dans la page envoyée prennent la main et exécutent à nouveau la page [page1.vue] ;
  • la page affichée est alors modifiée ;

Le résultat final est le suivant :

image3

Cette fois-ci, la valeur affichée est bien celle fixée par le serveur et visuellement, on ne voit pas la page ‘tressauter’ à cause d’un changement par le client de la valeur affichée par le serveur. Cette fois-ci que s’est-il passé ?

Le serveur a exécuté la page [page1] suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
...

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page1 beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[page1 created]')
    // serveur seulement
    if (process.server) {
      this.$store.commit('increment', 25)
    }
    // client et serveur
    this.value = this.$store.state.counter
    console.log('value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page1 mounted]')
  }
}
</script>
  • les lignes 30-32 ont été exécutées sans erreur. Ce qui signifie que côté serveur également, [this.$store] désigne le store [Vuex]. La ligne 31 a fait passer le compteur du store à 25 ;
  • ceci fait, la page a été envoyée au client ;

Si on regarde la page reçue par le client, on trouve les éléments suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-02/">
  <link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
  ...
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
              <h4>[nuxt-02] : page serveur, page client</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-02/" target="_self" class="nav-link">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page1" target="_self" class="nav-link active nuxt-link-active">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-primary">
                    Page 1 - value = 25
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
  window.__NUXT__ = (function (a, b, c) {
      return {
        layout: "default", data: [{}], error: null, state: { counter: 25 }, serverRendered: true,
        logs: [
          { date: new Date(1574085336802), args: ["[home beforeCreate]"], type: a, level: b, tag: c },
          { date: new Date(1574085336839), args: ["[home created]"], type: a, level: b, tag: c },
          { date: new Date(1574085336869), args: ["value=", "25"], type: a, level: b, tag: c }
        ]
      }
    }("log", 2, ""));</script>
  <script src="/nuxt-02/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
  • ligne 47 : la valeur envoyée par le serveur ;
  • ligne 60 : on constate que l’état du store [Vuex] a été embarqué dans la page. Cela va permettre au client qui va s’exécuter après réception de la page, de reconstituer un nouveau store [Vuex] avec 25 comme valeur initiale du compteur ;

Après réception et affichage de la page reçue du serveur, le client prend la main et exécute à son tour la page [page1] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
...

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page1 beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[page1 created]')
    // serveur seulement
    if (process.server) {
      this.$store.commit('increment', 25)
    }
    // client et serveur
    this.value = this.$store.state.counter
    console.log('value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page1 mounted]')
  }
}
</script>
  • ligne 34 : la propriété [value] de la ligne 18 reçoit à son tour la valeur 25 du compteur ;

Le store de [nuxt] permet donc au serveur de transmettre des informations au client lors du chargement initial de la page, lorsque celle-ci est cherchée sur le serveur. On rappelle qu’une fois cette page obtenue, le serveur n’est plus sollicité et l’application fonctionne comme une application [vue] classique, en mode SAP.

La page [page2]

Dans la page [page2], nous montrons une autre façon pour que :

  • le serveur inclut des informations calculées dans la page ;

  • le client ne modifie pas celles-ci ;

    1. Le code de la page

Le code de la page [page2] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!-- page2 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page2',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  asyncData(context) {
    // qui exécute ce code ?
    console.log('asyncData, client=', process.client, 'serveur=', process.server)
    // seulement pour le serveur
    if (process.server) {
      // on retourne une promesse
      return new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente d'une seconde
        setTimeout(() => {
          // ce résultat sera inclus dans les propriétés de [data]
          resolve({ value: 87 })
          // log
          console.log('asynData terminée')
        }, 1000)
      })
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page2 beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[page2 created]')
  },
  beforeMount() {
    // client seulement
    console.log('[page2 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page2 mounted]')
  }
}
</script>
  • ligne 7 : la page affiche la valeur d’une propriété nommée [value] ;
  • la propriété [value] n’existe pas comme élément d’un objet rendu par la fonction [data]. Ici cette fonction n’existe pas. La propriété [value] est créée dynamiquement par la ligne 36 ;
  • ligne 25 : la fonction [asyncData] est une fonction [nuxt]. Comme son nom l’indique, c’est normalement une fonction asynchrone. Son rôle habituel est d’aller chercher des données externes. [nuxt] assure que la page n’est pas envoyée au navigateur client avant que la fonction [asyncData] n’ait rendu ses données asynchrones ;
  • la fonction [asyncData] reçoit comme paramètre le contexte [nuxt]. Cet objet est très dense et donne accès à beaucoup d’informations sur l’application [nuxt. Nous le découvrirons dans les sections à venir ;
  • ligne 31 : on implémente la fonction [asyncData] avec une [Promise] (cf document |Introduction au langage ECMASCRIPT 6 par l’exemple|). Le constructeur de cette classe accepte comme paramètre une fonction asynchrone qui :
    • signale un succès en rendant des données avec la fonction [resolve]. L’objet rendu par cette fonction est automatiquement inclus dans les propriétés [data] de la page ;
    • signale un échec en rendant une erreur avec la fonction [reject] ;
  • ligne 34 : on simule une fonction asynchrone avec la fonction [setTimeout]. Cette fonction rend l’objet [{ value: 87 }] (ligne 36) au bout d’une seconde (ligne 31) grâce à la fonction [resolve] qui signale un succès de la [Promise]. L’objet rendu par la fonction asynchrone est inclus automatiquement dans les propriétés [data] de la page. Et c’est donc cette propriété que la ligne 7 affiche ;
  • ligne 27 : nous allons découvrir que la fonction [asyncData] est exécutée par le serveur mais pas par le client ;
  • ligne 29 : l’initialisation de la propriété [value] est faite par le serveur ;

Note : l’objet [this] n’est pas connu dans la fonction [asyncData] car l’objet encapsulant le composant [vue] n’a pas encore été créé ;

Exécution

On exécute le projet [nuxt-02] et on tape [localhost:81/nuxt-02/page2] à la main pour que le serveur soit sollicité. Comme au démarrage pour la page [index] :

  • le serveur exécute la page [page2.vue] ;
  • envoie la page générée au navigateur. Celle-ci est affichée ;
  • les scripts client embarqués dans la page envoyée prennent la main et exécutent à nouveau la page [page2.vue] ;
  • la page affichée est alors modifiée ;

Le résultat final est le suivant :

image4

Cette fois-ci, la valeur affichée est bien celle fixée par le serveur et visuellement, on ne voit pas la page ‘tressauter’ à cause d’un changement par le client de la valeur affichée par le serveur. Cette fois-ci que s’est-il passé ?

Le serveur a exécuté la page [page2] suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!-- page2 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page2',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  asyncData(context) {
    // qui exécute ce code ?
    console.log('asyncData, client=', process.client, 'serveur=', process.server)
    // seulement pour le serveur
    if (process.server) {
      // on retourne une promesse
      return new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente d'une seconde
        setTimeout(() => {
          // ce résultat sera inclus dans les propriétés de [data]
          resolve({ value: 87 })
          // log
          console.log('asynData terminée')
        }, 1000)
      })
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page2 beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[page2 created]')
  },
  beforeMount() {
    // client seulement
    console.log('[page2 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page2 mounted]')
  }
}
</script>

C’est la ligne 36 qui a fixé la valeur affichée par la ligne 7. C’est donc ce qu’a reçu le navigateur client. Très exactement il reçoit la page suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-02/">
  <link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
  ...
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
              <h4>[nuxt-02] : page serveur, page client</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-02/" target="_self" class="nav-link">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page2" target="_self" class="nav-link active nuxt-link-active">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div>
                <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-secondary">
                    Page 2 - value = 87
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
  window.__NUXT__ = (function (a, b, c) {
      return {
        layout: "default", data: [{ value: 87 }], error: null, state: { counter: 0 }, serverRendered: true,
        logs: [
          { date: new Date(1574096608555), args: ["asyncData, client=", "false", "serveur=", "true"], type: a, level: b, tag: c },
          { date: new Date(1574096608575), args: ["[page2 beforeCreate]"], type: a, level: b, tag: c },
          { date: new Date(1574096608599), args: ["[page2 created]"], type: a, level: b, tag: c }
        ]
      }
    }("log", 2, ""));</script>
  <script src="/nuxt-02/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
  • ligne 48 : on voit que la valeur dans la page reçue est 87 ;
  • ligne 61 : dans la réponse du serveur, on voit deux objets : [data] et [state] :
    • [state] est l’état du store [Vuex]. Celui-ci a été instancié à partir du contenu du dossier [store] de l’application [nuxt-02] ;
    • [data] contient les propriétés créées par le serveur grâce à la fonction [asyncData]. On retrouve la propriété [value : 87] créée par le serveur. Les scripts du client vont intégrer cette propriété dans celles de la page [page2] ;

Revenons au code de la page [page2] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!-- page2 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page2',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  asyncData(context) {
    // qui exécute ce code ?
    console.log('asyncData, client=', process.client, 'serveur=', process.server)
    // seulement pour le serveur
    if (process.server) {
      // on retourne une promesse
      return new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente d'une seconde
        setTimeout(() => {
          // ce résultat sera inclus dans les propriétés de [data]
          resolve({ value: 87 })
          // log
          console.log('asynData terminée')
        }, 1000)
      })
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page2 beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[page2 created]')
  },
  beforeMount() {
    // client seulement
    console.log('[page2 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page2 mounted]')
  }
}
</script>
  • la ligne 7 utilise la propriété [value]. Or la page ne définit aucune propriété nommée [value]. Cependant les scripts du client ont créé automatiquement cette propriété grâce à l’objet [data: [{ value: 87 }]] reçue du serveur ;

Les logs montrent par ailleurs que la fonction [asyncData] n’a pas été exécutée par le client :

image5

La fonction [asyncData] a été exécutée par le serveur [1] mais pas par le client [2]. Par ailleurs, on peut noter que les fonctions du cycle de vie ne sont pas exécutées par le serveur avant la fin de la fonction [asyncData]. On peut augmenter la durée de l’attente au sein de la fonction [asyncData] pour le vérifier.

La page [page3]

Nous ajoutons une nouvelle page [page3] à notre application :

image6

Le composant [navigation]

Le composant [vavigation] est modifié pour permettre la navigation vers la nouvelle page :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
  <!-- menu Bootstrap à trois options -->
  <b-nav vertical>
    <b-nav-item to="/" exact exact-active-class="active">
      Home
    </b-nav-item>
    <b-nav-item to="/page1" exact exact-active-class="active">
      Page 1
    </b-nav-item>
    <b-nav-item to="/page2" exact exact-active-class="active">
      Page 2
    </b-nav-item>
    <b-nav-item to="/page3" exact exact-active-class="active">
      Page 3
    </b-nav-item>
  </b-nav>
</template>

Le code de [page3]

Le code de la page [page3] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!-- page3 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page3',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  fetch(context) {
    // qui exécute ce code ?
    console.log('fetch, client=', process.client, 'serveur=', process.server)
    // seulement pour le serveur
    if (process.server) {
      // on retourne une promesse
      return new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente d'une seconde
        setTimeout(() => {
          // succès
          resolve()
        }, 1000)
      }).then(() => {
        // on modifie le store
        context.store.commit('increment', 28)
      })
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page3 beforeCreate]')
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[page3 created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page3 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page3 mounted]')
  }
}
</script>
  • ligne 30 : la fonction [fetch] a un comportement analogue à celui de la fonction [asyncData] :

    • elle est exécutée avant les fonctions du cycle de vie ;
    • l’objet [this] n’est pas connu dans cette fonction ;
    • son fonctionnement est asynchrone ;
    • le cycle de vie ne commence pas tant que la fonction asynchrone n’a pas rendu son résultat ;
    • le résultat est ici rendu par la méthode [then] de la [Promise], ligne 43 ;
    • fonction [fetch] reçoit le paramètre [context]. Celui-ci représente le contexte [nuxt] du moment ;
  • ligne 30 : parmi ses nombreuses propriétés, l’objet [context] a une propriété [store] qui représente le store [Vuex] de l’application ;

  • ligne 41 : artificiellement, on signale le succès de la [Promise] au bout d’une seconde (cf. document |Introduction au langage ECMASCRIPT 6 par l’exemple|) ;

  • ligne 45 : la méthode [then] est alors exécutée. On y incrémente le compteur du [store] ;

    1. Exécution

On exécute le projet [nuxt-02] et on tape [localhost:81/nuxt-02/page3] à la main pour que le serveur soit sollicité. Comme au démarrage pour la page [index] :

  • le serveur exécute la page [page3.vue] ;
  • envoie la page générée au navigateur. Celle-ci est affichée ;
  • les scripts client embarqués dans la page envoyée prennent la main et exécutent à nouveau la page [page3.vue] ;
  • la page affichée est alors modifiée ;

Le résultat final est le suivant :

image7

La valeur affichée est bien celle fixée par le serveur et visuellement, on ne voit pas la page ‘tressauter’ à cause d’un changement par le client de la valeur affichée par le serveur. Cette fois-ci que s’est-il passé ?

Le serveur a exécuté la page [page3] suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!-- page3 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page3',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  fetch(context) {
    // qui exécute ce code ?
    console.log('fetch, client=', process.client, 'serveur=', process.server)
    // seulement pour le serveur
    if (process.server) {
      // on retourne une promesse
      return new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente d'une seconde
        setTimeout(() => {
          // succès
          resolve()
        }, 1000)
      }).then(() => {
        // on modifie le store
        context.store.commit('increment', 28)
        // log
        console.log('fetch commit terminé')
      })
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page3 beforeCreate]')
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[page3 created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page3 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page3 mounted]')
  }
}
</script>
  • ligne 45 : la fonction asynchrone [fetch] est la première des fonctions ci-dessus à s’exécuter. Elle reçoit en paramètre, un objet appelé [context] qui est le contexte [nuxt] du moment. Parmi les très nombreuses propriétés de cet objet, la propriété [context.store] représente le store [Vuex] ;
  • ligne 45 : dans la fonction asynchrone [fetch], le serveur fixe le compteur du store à 28 ;
  • ligne 56 : lorsque la fonction [created] s’exécute, [nuxt] garantit que la fonction asynchrone [fetch] a terminé son travail ;
  • ligne 58 : la valeur du compteur du store est affectée à la propriété [value] de la ligne 27 ;
  • ligne 7 : affichage de la valeur de [value], donc du compteur du store ;

Le navigateur client reçoit la page suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-02/">
  <link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-02/_nuxt/app.js" as="script">
  ...
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
              <h4>[nuxt-02] : page serveur, page client</h4>
            </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-02/" target="_self" class="nav-link">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-02/page3" target="_self" class="nav-link active nuxt-link-active">
                        Page 3
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10">
                  <div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-secondary">
                    Page 3 - value = 28
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>
  window.__NUXT__ = (function (a, b, c) {
      return {
        layout: "default", data: [{}], error: null, state: { counter: 28 }, serverRendered: true,
        logs: [
          { date: new Date(1574169916025), args: ["fetch, client=", "false", "serveur=", "true"], type: a, level: b, tag: c },
          { date: new Date(1574169917038), args: ["fetch commit terminé"], type: a, level: b, tag: c },
          { date: new Date(1574169917137), args: ["[page3 beforeCreate]"], type: a, level: b, tag: c },
          { date: new Date(1574169917167), args: ["[page3 created], value=", "28"], type: a, level: b, tag: c }
        ]
      }
    }("log", 2, ""));</script>
  <script src="/nuxt-02/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
  • ligne 52 : on voit que la valeur dans la page reçue est 28 ;
  • ligne 65 : dans la réponse du serveur, on voit que le serveur a envoyé au client l’état [state] du store [Vuex]. Grâce à cette information, les scripts client vont pouvoir reconstituer un store [Vuex] ;

Les scripts client vont à leur tour exécuter le code de la page [page3] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!-- page3 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page3',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  fetch(context) {
    // qui exécute ce code ?
    console.log('fetch, client=', process.client, 'serveur=', process.server)
    // seulement pour le serveur
    if (process.server) {
      // on retourne une promesse
      return new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente d'une seconde
        setTimeout(() => {
          // succès
          resolve()
        }, 1000)
      }).then(() => {
        // on modifie le store
        context.store.commit('increment', 28)
        // log
        console.log('fetch commit terminé')
      })
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page3 beforeCreate]')
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[page3 created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page3 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page3 mounted]')
  }
}
</script>
  • line 58 : la fonction [created] exécutée par le client affecte la valeur du compteur à la propriété [value] de la ligne 27 ;
  • la ligne 7 affiche cette valeur. Puisque c’est la même que celle envoyée par le serveur, on ne voit pas la page ‘tressauter’ à cause d’une modification ;

Les logs montrent par ailleurs que la fonction [fetch] n’a pas été exécutée par le client :

image8

La fonction [fetch] a été exécutée par le serveur [1] mais pas par le client [2]. Par ailleurs, on peut noter que les fonctions du cycle de vie ne sont pas exécutées par le serveur avant la fin de la fonction [fetch] [3]. On peut augmenter la durée de l’attente au sein de la fonction [fetch] pour le vérifier.

Les pages [page1] et [page3] ont montré deux méthodes utilisant le store [Vuex] pour transmettre une information du serveur au client. On peut se demander si elles sont équivalentes. Nous construisons une page [page4] pour le vérifier.

La page [page4]

Nous ajoutons une nouvelle page [page4] à notre application :

image9

Le composant [navigation]

Le composant [navigation] est modifié pour permettre la navigation vers la nouvelle page :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <!-- menu Bootstrap à cinq options -->
  <b-nav vertical>
    <b-nav-item to="/" exact exact-active-class="active">
      Home
    </b-nav-item>
    <b-nav-item to="/page1" exact exact-active-class="active">
      Page 1
    </b-nav-item>
    <b-nav-item to="/page2" exact exact-active-class="active">
      Page 2
    </b-nav-item>
    <b-nav-item to="/page3" exact exact-active-class="active">
      Page 3
    </b-nav-item>
    <b-nav-item to="/page4" exact exact-active-class="active">
      Page 4
    </b-nav-item>
  </b-nav>
</template>

Le code de [page4]

Le code de la page [page4] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!-- page4 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Page 4 - value = {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page4',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  async beforeCreate() {
    // client et serveur
    console.log('[page4 beforeCreate]')
    // seulement pour le serveur
    if (process.server) {
      // on exécute la fonction asynchrone
      const valeur = await new Promise(function(resolve, reject) {
        // on a normalement ici une fonction asynchrone
        // on la simule avec une attente de 10 secondes
        setTimeout(() => {
          // succès - on rend la valeur du compteur
          resolve(52)
        }, 10000)
      })
      // on modifie le store
      this.$store.commit('increment', valeur)
      // log
      console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
    }
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[page4 created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page4 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page4 mounted]')
  }
}
</script>
  • ligne 30 : ce qui était fait auparavant dans la fonction [fetch] est désormais fait dans la méthode [beforeCreate]. On utilise le couple async (ligne 30) / await (ligne 36) pour attendre la fin de la fonction asynchrone ;

  • ligne 36 : on récupère le résultat de la fonction asynchrone rendu ligne 41 après 10 secondes (ligne 42) ;

  • lignes 50-54 : dans la méthode [created] exécutée aussi bien côté serveur que côté client, le compteur est affecté à la propriété [value] de la page ;

    1. Exécution

On exécute le projet [nuxt-02] et on tape [localhost:81/nuxt-02/page4] à la main pour que le serveur soit sollicité. Comme au démarrage pour la page [index] :

  • le serveur exécute la page [page4.vue] ;
  • envoie la page générée au navigateur. Celle-ci est affichée ;
  • les scripts client embarqués dans la page envoyée prennent la main et exécutent à nouveau la page [page4.vue] ;
  • la page affichée est alors modifiée ;

Le résultat final est le suivant :

image10

Contrairement à ce qui était attendu, la valeur affichée en [2] n’est pas 52. Que s’est-il passé ?

Les logs sont les suivants :

image11

On peut remarquer qu’en [1] le log de fin de l’action asynchrone n’a pas été affiché. La fonction [created] qui affiche la valeur du compteur, affiche 0. Tout cela laisse penser que [nuxt] n’a pas attendu la fin de l’action asynchrone.

Si on retourne dans le terminal de VSCode qui a servi au lancement de l’application, on trouve les logs [3-4]. On voit que la fonction asynchrone a bien été exécutée côté serveur.

Au final, la fonction [beforeCreate] a bien été exécutée totalement côté serveur, mais [nuxt] n’a pas attendu la fin de son exécution pour envoyer la page au navigateur client alors qu’il attend bien la fin de la fonction [fetch]. C’est donc cette méthode qu’il faut utiliser si on veut que le serveur initialise un store [Vuex].

Exemple [nuxt-03] : nuxtServerInit

Le projet [nuxt-03] vise à présenter une fonction du store [Vuex] appelée [nuxtServerInit]. Elle permet au serveur d’initialiser le store [Vuex] comme le fait la fonction [fetch]. Mais contrairement à la fonction [fetch], la fonction [nuxtServerInit] n’est jamais exécutée par le client.

image0

Le projet [nuxt-03] est initialement obtenu par recopie du projet [nuxt-01] duquel on supprime la page [page2] du dossier [pages] et du composant [navigation]. Le dossier [store] est obtenu par recopie du dossier [nuxt-02/store].

Le store [Vuex]

Le store [Vuex] sera implémenté par le fichier [store/index.js] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* eslint-disable no-console */
export const state = () => ({
  // compteur
  counter: 0
})

export const mutations = {
  // incrémentation du compteur d'une valeur [inc]
  increment(state, inc) {
    state.counter += inc
  }
}

export const actions = {
  async nuxtServerInit(store, context) {
    // qui exécute ce code ?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server)
    // on attend la fin d'une promesse
    await new Promise(function(resolve, reject) {
      // on a normalement ici une fonction asynchrone
      // on la simule avec une attente d'une seconde
      setTimeout(() => {
        // succès
        resolve()
      }, 1000)
    })
    // on modifie le store
    store.commit('increment', 34)
    // log
    console.log('nuxtServerInit commit terminé')
  }
}
  • lignes 1-12 : sont analogues à ce qu’elles étaient dans le projet [nuxt-02] ;

  • lignes 14-32 : on exporte un objet [actions]. C’est un terme réservé du store de [Vuex] ;

  • ligne 15 : on définit la fonction [nuxtServerInit]. Celle-ci sera exécutée au démarrage de l’application par le serveur. Son rôle usuel est d’initialiser un store [Vuex] à l’aide de données externes obtenues avec une fonction asynchrone. [nuxt] attend que celle-ci rende ses résultats avant d’entamer le cycle de vie de la page demandée. La fonction reçoit deux paramètres :

    • le store [Vuex] à initialiser ;
    • le contexte [nuxt] du moment ;
  • lignes 19-26 : on attend la fin de l’action asynchrone, ici une attente artificielle d’une seconde (ligne 15) ;

  • ligne 28 : on donne au compteur la valeur 34 ;

  • lignes 17 et 30 : des logs pour suivre le déroulement de l’exécution de la fonction [nuxtServerInit] ;

    1. La page [index]

La page [index] sera la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!-- page [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning"> Home - value= {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[home created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  }
}
</script>
  • ligne 37 : la valeur du compteur initialisé par la fonction [nuxtServerInit] est affectée à la propriété [value] de la ligne 27. Cette valeur est affichée par la ligne 7 ;

  • la ligne 37 sera exécutée aussi bien par le serveur que par le client. Dans les deux cas, la propriété [value] recevra la même valeur ce qui assure l’identité de la page générée par le serveur avec celle générée par le client ;

    1. La page [page1]

La page [page1] est obtenue par recopie de la page [index]. On modifie ensuite son texte pour remplacer [home] par [page1] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- page [page1]] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning"> Page1 - value= {{ value }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'

export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page1 beforeCreate]')
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[page1 created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page1 mounted]')
  }
}
</script>

Cette page n’est là que pour rendre possible la navigation entre deux pages.

Exécution

Le fichier [nuxt.config.js] est modifié de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// répertoire du code source
  srcDir: 'nuxt-03',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-03/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: 'localhost'
}

La page affichée à l’exécution est alors la suivante :

image1

  • en [5], on voit que la fonction [nuxtServerInit] a été exécutée par le serveur avant le cycle de vie de la page [index]. [nuxt] a attendu que la fonction asynchrone ait terminé son travail avant de passer au cycle de vie ;
  • en [4], on voit que le client n’a pas exécuté la fonction [nuxtServerInit] ;

Maintenant naviguons deux fois : index –> page1 –> index. Les logs sont alors les suivants :

image2

  • en [1-2], on voit que la fonction [nuxtServerInit] n’est pas exécutée par le client ;

Maintenant tapons l’URL de la page [page1] à la main pour forcer un appel au serveur :

image3

en [3-4], on retrouve le même mécanisme que celui qui avait précédé le chargement de la page [index] au démarrage. On rappelle ici ce qui a déjà été dit : lorsqu’on force l’appel d’une page au serveur, tout se passe comme si l’application redémarrait avec une page d’accueil qui serait la page demandée ;

Exemple [nuxt-04] : maintien d’une session client / serveur

Le projet [nuxt-04] aborde le problème du maintien d’une session client / serveur. On reprend le projet [nuxt-03] avec les modifications suivantes :

  • la page [index] aura un bouton qui permettra d’incrémenter le compteur du store [Vuex] ;
  • la page [page1] reste inchangée ;
  • on voudrait que lorsqu’une page est demandée à la main au serveur, celui-ci renvoie la page demandée avec un store [Vuex] qui aurait pour valeur de compteur, la dernière valeur que celui-ci avait côté client ;

Le projet [nuxt-04] est initialement créé par recopie du projet [nuxt-03] :

image0

Seule la page [index] change :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!-- page [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <template slot="right">
      <b-alert show variant="warning"> Home - value= {{ value }} </b-alert>
      <!-- bouton -->
      <b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
    </template>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      value: 0
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    this.value = this.$store.state.counter
    console.log('[home created], value=', this.value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  },
  // gestion des évts
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incrément du compteur de 1
      this.$store.commit('increment', 1)
      // chgt de la valeur affichée
      this.value = this.$store.state.counter
    }
  }
}
</script>
  • ligne 10 : on a ajouté un bouton pour incrémenter le compteur du store [Vuex] ;
  • ligne 54 : la méthode qui gère le [clic] sur le bouton [Incrémenter] ;
  • ligne 57 : le compteur du store est incrémenté de 1 ;
  • ligne 59 : la valeur du compteur est affectée à la propriété [value] afin qu’elle soit affichée par la ligne 8 ;

On exécute le projet [nuxt-04] avec le fichier [nuxt.config.js] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// répertoire du code source
  srcDir: 'nuxt-04',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-04/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: 'localhost'
}

A l’exécution la première page affichée est la suivante :

image1

En utilisant plusieurs fois le bouton [3], on a la nouvelle page suivante :

image2

Si on utilise le lien [3], on a la page suivante :

image3

  • en [2], la page [page1] [1] affiche bien la valeur du compteur ;

Maintenant, rafraîchissons la page avec [3]. La nouvelle page est la suivante :

image4

  • en [2], on a perdu la valeur courante du compteur. On est revenu à sa valeur initiale ;

Ce résultat est tout a fait compréhensible si on regarde les logs :

  • en [1], le serveur a réexécuté la fonction [nuxtServerInit]. Or celle-ci est la suivante :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* eslint-disable no-console */
export const state = () => ({
  // compteut
  counter: 0
})

export const mutations = {
  // incrémentation du compteur d'une valeur [inc]
  increment(state, inc) {
    state.counter += inc
  }
}

export const actions = {
  async nuxtServerInit(store, context) {
    // qui exécute ce code ?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server)
    // on attend la fin d'une promesse
    await new Promise(function(resolve, reject) {
      // on a normalement ici une fonction asynchrone
      // on la simule avec une attente d'une seconde
      setTimeout(() => {
        // succès
        resolve()
      }, 1000)
    })
    // on modifie le store
    store.commit('increment', 53)
    // log
    console.log('nuxtServerInit commit terminé')
  }
}

La ligne 28 affecte la valeur 53 au compteur.

Examinons la requête HTTP faite par le navigateur lorsqu’on a rafraîchi la page [page1] :

image5

On voit qu’outre la page [page1] [1], le client demande un certain nombre de scripts au serveur. On notera les scripts [pages_index, pages_page1] qui sont les scripts associés aux pages [index, page]. Ces scripts sont fournis à chaque requête au serveur quelque soit la page demandée ;

En [1], la page [page1] est demandée au serveur avec la requête HTTP suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
GET http://localhost:81/nuxt-04/page1
Host: localhost:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

On voit que cette requête ne transmet aucune information au serveur. Celui-ci n’a donc aucun moyen de connaître l’état du store [Vuex] côté client. Il aurait fallu pour cela que le client lui envoie cette information.

Dans le projet suivant [nuxt-05] nous utilisons un cookie pour que le client puisse envoyer de l’information au serveur lorsque celui-ci est sollicité.

Exemple [nuxt-06] : injection dans le contexte d’un gestionnaire de session

Présentation

L’exemple [nuxt-05] a montré qu’on pouvait persister le store même lorsque l’utilisateur force des appels au serveur. Les éléments du store sont réactifs pour que s’ils sont intégrés à des vues celles-ci soient réactives aux changements du store. On peut vouloir aussi persister des éléments au fil des échanges client / serveur sans pour autant vouloir qu’ils soient réactifs, tout simplement parce qu’ils ne sont pas affichés par des vues. On peut alors stocker ceux-ci dans la session sans qu’ils soient pour autant dans le store.

Le store est accessible facilement au travers de propriétés telles que [context.app.$store] en-dehors des vues ou [this.$store] dans les vues. On voudrait quelque chose d’analogue pour la session, quelque chose comme [context.app.$session] ou [this.$session]. On va voir que c’est possible grâce au concept d’injection. Seulement on ne peut pas injecter des objets dans le contexte, seulement des fonctions. Celle-ci sera alors disponible au travers des expressions [context.app.$session()] ou [this.$session()].

Enfin, nous allons présenter le concept [nuxt] de [plugin].

L’exemple [nuxt-06] est obtenu initialement par recopie du projet [nuxt-05] :

image0

  • en [1], nous ajouterons un dossier [plugins] ;

La notion de plugin [nuxt]

[nuxt] nomme [plugin] tout code exécuté au démarrage de l’application, avant même l’exécution de la fonction [nuxtServerInit] par le serveur qui était jusqu’à maintenant la première fonction utilisateur à être exécutée. Les plugins de l’application doivent être déclarés dans la clé [plugins] du fichier de configuration [nuxt.config.js] :

1
2
3
4
5
6
7
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [
    { src: '~/plugins/client/session', mode: 'client' },
    { src: '~/plugins/server/session', mode: 'server' }
],
  • lignes 5-6 : un plugin est désigné par son chemin [src] et son mode d’exécution [mode]. [mode] peut avoir trois valeurs :
    • [client] : le plugin doit être exécuté côté client uniquement ;
    • [server] : le plugin doit être exécuté côté serveur uniquement ;
    • absence de la clé [mode] : dans ce cas, le plugin doit être exécuté à la fois côté client et serveur ;
  • lignes 5-6 : nous avons placé nos deux plugins dans un dossier [plugins]. Il n’y a aucune obligation à cela. Les plugins peuvent être placés n’importe où dans l’arborescence du projet. De même les noms des sous-dossiers [client, server] sont ici arbitraires ;

image1

Le plugin [session] du serveur

Le plugin [server / session.js] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* eslint-disable no-console */
export default (context, inject) => {
  // gestion de la session serveur

  // y-a-t-il une session existante ?
  let value = context.app.$cookies.get('session')
  if (!value) {
    // nouvelle session
    console.log("[plugin session server], démarrage d'une nouvelle session")
    value = initValue
  } else {
    // session existante
    console.log("[plugin session server], reprise d'une session existante")
  }
  // définition de la session
  const session = {
    // contenu de la session
    value,
    // sauvegarde de la session dans un cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }
  // on injecte une fonction dans [context, Vue] qui rendra la session courante
  inject('session', () => session)
}

// valeur initiale de la session
const initValue = {
  initSessionDone: false
}
  • ligne 2 : les plugins sont exécutés à chaque fois qu’il y a un appel au serveur : au démarrage et à chaque fois que l’utilisateur force un appel au serveur en tapant une URL à la main :
    • c’est d’abord le (ou les) plugin(s) du serveur qui est (sont) exécuté(s) en premier ;
    • lorsque le navigateur client a reçu la réponse du serveur, c’est au tour du (ou des) plugin(s) du client de s’exécuter ;
  • ligne 2 : tout plugin, client ou serveur, reçoit deux paramètres :
    • [context] : le contexte du serveur ou du client selon qui exécute le plugin ;
    • [inject] : une fonction qui permet d’injecter une fonction dans le contexte du serveur ou du client ;
  • le but du plugin [server / session] est double :
    • définir une session (lignes 16-23) ;
    • définir au sein du contexte, une fonction [$session] qui rendra comme résultat, la session de la ligne 16. C’est la ligne 25 qui fait cela ;
  • lignes 16-23 : la session encapsulera ses données dans l’objet [value] de la ligne 18 ;
  • lignes 20-22 : elle dispose d’une fonction [save] qui reçoit en paramètre un objet [context]. C’est le code appelant qui lui fournit ce contexte. Avec celui-ci, la fonction [save] sauvegarde la valeur de la session, l’objet [value], dans le cookie de session ;
  • ligne 6 : lorsque le plugin [server / session] s’exécute, il commence par regarder si le serveur a reçu un cookie de session ;
    • si oui, l’objet [value] de la ligne 6 représente la valeur de la session, l’ensemble des données encapsulées dans celle-ci ;
    • si non, lignes 7-11, on fixe la valeur initiale de la session. Celle-ci sera l’objet [initValue] des lignes 29-31. Les éléments de la session seront définis dans la fonction [nuxtServerInit] qui est exécutée après le plugin serveur ;
  • ligne 18 : la notation [value] est un raccourci pour la notation [value:value]. Le [value] de gauche est le nom d’une clé d’objet, le [value] de droite est l’objet [value] déclaré ligne 6 ;
  • ligne 25 : lorsqu’on arrive à cette ligne, la session a été soit créée parce qu’elle n’existait pas, soit récupérée dans la requête HTTP du navigateur client ;
  • ligne 25 : on injecte dans le contexte serveur une nouvelle fonction :
    • le 1er paramètre de [inject] est le nom de la fonction qu’on crée, ici ‘session’. [nuxt] lui donnera en fait le nom ‘$session’ ;
    • le 2ième paramètre est la définition de la fonction. Ici la fonction [$session]
      • n’admettra aucun paramètre ;
      • rendra l’objet [session] de la ligne 16 ;
  • une fois le plugin exécuté :
    • la fonction [$session] est disponible dans [context.app.$session] là où l’objet [context] est disponible, ou [this.$session] dans une vue ou dans le store [vuex] ;
    • la fonction [$session] rend un objet [session] avec une unique clé [value] ;
    • à la création initiale de la session, l’objet [value] n’a qu’une clé [initStoreDone] (lignes 29-31). La clé [initStoreDone:false] sert à indiquer que le store n’a pas encore été placé dans la session. Cela va être fait par la fonction [nuxtServerInit] ;

Initialisation de la session

Une fois le plugin [session / server] exécuté par le serveur, celui-ci va exécuter le script [store / index.js] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/* eslint-disable no-console */
export const state = () => ({
  // compteur
  counter: 0
})

export const mutations = {
  // incrémentation du compteur d'une valeur [inc]
  increment(state, inc) {
    state.counter += inc
  },
  // remplacement du state
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  }
}

export const actions = {
  async nuxtServerInit(store, context) {
    // qui exécute ce code ?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // on attend la fin d'une promesse
    await new Promise(function(resolve, reject) {
      // on a normalement ici une fonction asynchrone
      // on la simule avec une attente d'une seconde
      setTimeout(() => {
        // init session
        initSession(store, context)
        // succès
        resolve()
      }, 1000)
    })
  }
}

function initSession(store, context) {
  // store est le store à initialiser

  // on récupère la session
  const session = context.app.$session()
  // la session a-t-elle été déjà initialisée ?
  if (!session.value.initSessionDone) {
    // on démarre un nouveau store
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // on initialise le store
    store.commit('increment', 77)
    // on met le store dans la session
    session.value.store = store.state
    // on initialise une nouvelle session
    session.value.somethingImportant = { x: 2, y: 4 }
    // la session est désormais initialisée
    session.value.initSessionDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // on met à jour le store avec le store de la session
    store.commit('replace', session.value.store)
  }
  // on sauvegarde la session
  session.save(context)
  // log
  console.log('initSession terminé, store=', store.state, 'session=', session.value)
}

Par rapport au store du projet [nuxt-05], seule la fonction [initSession] (anciennement initStore) des lignes 38-60 change :

  • ligne 42 : on récupère la session grâce à la fonction [$session] qui a été injectée dans le contexte du serveur ;
  • ligne 44 : on regarde si la session a déjà été initialisée ;
  • lignes 45-54 : si ce n’est pas le cas :
    • ligne 48 : le store est initialisé ;
    • ligne 50 : l’état du store est mis dans la session ;
    • ligne 52 : on ajoute un autre objet [somethingImportant] dans la session. Celui-ci ne fera pas partie du store ;
    • ligne 54 : on note le fait que la session est désormais initialisée ;
  • lignes 55-59 : si la session était déjà initialisée :
    • ligne 58 : le nouveau store est initialisé avec le contenu de la session ;
  • ligne 61 : la session est sauvegardée dans le cookie de session. On rappelle que cela consiste à placer le cookie dans la réponse HTTP que le serveur va faire au navigateur client ;

Le plugin [client / session] du client

Une fois que le serveur a exécuté les scripts [plugins / server / session] et [store / index], il va envoyer l’une des pages [index, page1] au navigateur client. Dans la réponse HTTP du serveur, il y aura le cookie de session. Une fois la page reçue par le navigateur client, les scripts client embarqués dans la page vont s’exécuter. Le plugin [client / session] va alors s’exécuter :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* eslint-disable no-console */
export default (context, inject) => {
  // gestion de la session client

  // la session existe forcément, initialisée par le serveur
  console.log('[plugin session client], reprise de la session du serveur')

  // définition de la session
  const session = {
    // contenu de la session
    value: context.app.$cookies.get('session'),
    // sauvegarde de la session dans un cookie
    save(context) {
      context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
    }
  }

  // on injecte une fonction dans [context, Vue] qui rendra la session courante
  inject('session', () => session)
}
  • lorsque le plugin client s’exécute, le cookie de session a déjà été reçu par le navigateur client ;
  • l’objectif du plugin [client] est d’injecter lui-aussi une fonction [$session] dans le contexte du client. Cette fonction rendrait la session envoyée par le serveur ;
  • ligne 19 : la fonction injectée [$session] rendra la session des lignes 9-16 ;
  • lignes 9-16 : l’objet [session] géré par le client. Ce sera une copie de la session envoyée par le serveur ;
  • ligne 11 : la valeur de la session client est prise dans le cookie de session envoyé par le serveur [nuxt] ;
  • lignes 13-15 : comme pour la session du serveur, la session du client a une fonction [save] qui permet de sauvegarder la valeur de la session, [this.value] ligne 14, dans le cookie de session stocké sur le navigateur ;

La page [index]

La page [index] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!-- page [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <template slot="right">
      <b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
      <!-- bouton -->
      <b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
    </template>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  computed: {
    jsonSession() {
      return JSON.stringify(this.$session().value)
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home created], session=', this.$session().value)
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  },
  // gestion des évts
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incrément du compteur de 1
      this.$store.commit('increment', 1)
      // modification session
      const session = this.$session()
      session.value.store = this.$store.state
      session.value.somethingImportant.x++
      session.value.somethingImportant.y++
      // sauvegarde de la session dans le cookie de session
      session.save(this.$nuxt.context)
    }
  }
}
</script>

Il faut se rappeler que cette page est exécutée aussi bien côté serveur que côté client.

  • ligne 8 : on affiche maintenant et la session et le store ;
  • ligne 30 : [jsonSession] est une propriété calculée qui rend la chaîne jSON de la valeur de la session ;
  • ligne 41 : on affiche la valeur de la session à l’aide de la fonction injectée [this.$session]. Celle-ci existe aussi bien dans le contexte du serveur que dans celui du client ;
  • ligne 53 : la méthode [incrementCounter] n’est elle exécutée que côté client ;
  • ligne 56 : le compteur du store est incrémenté et affiché comme auparavant ;
  • ligne 58 : on récupère la session grâce à la fonction injectée [this.$session] ;
  • ligne 59 : le store de la session est mis à jour ;
  • lignes 60-61 : on incrémente les attributs [somethingImportant.x, somethingImportant.y] de la session. Cela juste pour montrer qu’une session peut servir à transporter autre chose que le store ;
  • ligne 63 : la session est sauvegardée dans le cookie de session stocké sur le navigateur. Dans une vue client, le contexte de celui-ci est disponible dans [this.$nuxt.context] ;

Le but de la page [index] est de montrer que la session n’est pas réactive alors que le store l’est. Lorsqu’on va incrémenter les éléments de la session, on découvrira que la vue n’est pas mise à jour. La vue [page1] présente une solution à ce problème.

La page [page1]

La page [page1] est obtenue par recopie de la page [index] puis quelque peu modifiée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!-- page [index] -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <template slot="right">
      <b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
      <!-- bouton -->
      <b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
    </template>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      session: {}
    }
  },
  computed: {
    jsonSession() {
      return JSON.stringify(this.session.value)
    }
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[page1 beforeCreate]')
  },
  created() {
    // client et serveur
    // on met la session dans les propriétés réactives de la page
    this.session = this.$session()
    // log
    console.log('[page1 created], session=', this.session.value)
  },
  beforeMount() {
    // client seulement
    console.log('[page1 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[page1 mounted]')
  },
  // gestion des évts
  methods: {
    incrementCounter() {
      console.log('incrementCounter')
      // incrément du compteur de 1
      this.$store.commit('increment', 1)
      // modification session
      this.session.value.store = this.$store.state
      this.session.value.somethingImportant.x++
      this.session.value.somethingImportant.y++
      // sauvegarde de la session dans le cookie de session
      this.session.save(this.$nuxt.context)
    }
  }
}
</script>
  • ligne 47 : la principale différence vient du fait qu’on met la session courante dans les propriétés de la page (lignes 29-33). Cela va avoir pour effet que désormais la session va devenir réactive. Lorsque la fonction [incrementCounter] va incrémenter les éléments de la session, la vue [page1] sera mise à jour ;

Exécution du projet

Avant d’exécuter le projet, vérifiez le cookie de session de votre navigateur et s’il existe supprimez-le pour que le serveur crée une session neuve :

image2

Maintenant demandons l’URL [http://localhost:81/nuxt-06/] :

image3

Les logs dans le navigateur sont alors les suivants :

image4

  • en [2], le serveur démarre une nouvelle session dans le plugin [session] du serveur ;
  • en [3], cette nouvelle session est initialisée dans [nuxtServerInit] ;
  • en [4], la nouvelle session telle qu’elle est connue sur le serveur ;
  • en [5], le client a correctement récupéré cette session ;

Maintenant incrémentons le compteur trois fois :

image5

  • en [3], le compteur a bien été incrémenté mais pas la session en [2]. Alors que [3] affiche le store qui est réactif, [2] affiche la session qui elle n’est pas réactive :

Maintenant rechargeons la page (F5). Les logs sont les suivants suite à ce rechargement :

image6

  • en [2], on voit que le serveur a reçu un cookie de session envoyé par le navigateur client ;
  • en [4], on voit que le store n’est pas réinitialisé mais repris dans la session reçue ;
  • en [4-5] : on voit que les attributs de la session ont bien tous été incrémentés trois fois ;

La page envoyée par le serveur est alors la suivante ;

image7

La conclusion issue de cette page est que la session peut transporter d’autres éléments que le store mais ceux-ci ne sont pas réactifs.

Maintenant cliquons sur le lien [Page 1] [4]. La nouvelle page affichée est alors la suivante :

image8

Puis utilisons le bouton [Incrémenter] trois fois. La page devient la suivante :

image9

Cette fois-ci la session s’affiche correctement en [2]. Elle est ici réactive. Cela se voit dans les logs :

image10

  • en [1-3], les valeurs de la session ;
  • en [4-6], les getters et setters réactifs des éléments de la session ;

Maintenant cliquons sur le lien [Home] [4]. Nous obtenons la page suivante :

image11

Puis cliquons deux fois sur le bouton [Incrémenter] [4]. La page devient la suivante :

image12

On constate que là également la session est devenue réactive [2].

Demandons la valeur rendue par la fonction [this.$session()] :

image13

  • dans l’onglet [Vue], on sélectionne la page courante [Home] pour en obtenir la référence [$vm0] [3] ;

Puis dans l’onglet [Console] [4], demandons la valeur de la fonction [$vm0.$session()] :

image14

  • en [5], on voit que la session est devenue réactive alors qu’elle ne l’était pas initialement ;
  • en [6], on demande à voir la valeur de la session ;
  • en [7-8], on découvre que cette valeur est elle également devenue réactive ;

On a donc là un résultat inattendu : si un élément devient réactif dans une page parce qu’il a été placé dans les propriétés de la page, alors il devient également réactif dans les pages où il ne fait pas partie des propriétés.

Conclusion

L’exemple [nuxt-05] a montré qu’on pouvait persister le store au fil des requêtes faites au serveur. L’exemple [nuxt-06] fait la même chose avec un objet qu’on a appelé [session] par analogie avec la session web. On a vu que cette session pouvait avoir les mêmes propriétés que le store [Vuex] et devenir réactive elle aussi alors que nativement elle ne l’était pas.

Alors quel est l’intérêt du store [Vuex] ? Je dois avouer que pour l’instant il ne m’est pas apparu. Il est probable que quelque chose m’a échappé. Donc dans le doute, je conseillerai d’utiliser :

  • un store [Vuex] pour y mettre tout ce qui doit être partagé entre les pages du client, et ce qui éventuellement doit être partagé entre le client et le serveur ;
  • un cookie de session si le store doit être persisté lors d’un appel du client vers le serveur, la session ne contenant alors que le store ;

Les exemples [nuxt-05] et [nuxt-06] avaient pour but de montrer comment on pouvait assurer la continuité de l’application lorsque l’utilisateur force l’appel au serveur en tapant manuellement des URL. On rappelle que le comportement par défaut dans ce cas est un redémarrage de l’application, son état du moment étant alors perdu.

Exemple [nuxt-07] : les contextes client et serveur

Présentation

L’exemple [nuxt-07] vise à explorer l’objet [context] du côté serveur et du côté client. Il ne faut pas oublier que ces deux acteurs des applications [nuxt] sont séparés : ils ne partagent rien sauf :

  • ce que le serveur veut bien envoyer au client (dans la réponse HTTP et la page envoyée) ;
  • ce que le client veut bien envoyer au serveur (dans sa requête HTTP) ;

Si donc, comme on va le voir, les objets manipulés par le serveur et ceux manipulés par le client portent le même nom, ils ne sont pas identiques : ils peuvent parfois être des copies l’un de l’autre mais n’ont jamais la même référence. Modifier un objet client n’a aucun effet sur l’objet de même nom, côté serveur, et vice-versa.

L’exemple [nuxt-07] est obtenu initialement par recopie de l’exemple [nuxt-01] :

image0

  • en [2], nous allons ajouter un plugin commun au client et au serveur ;
  • en [3], nous allons modifier quelque peu la page [index] ;

Le plugin [common / main.js]

Le plugin [common / main.js] est exécuté à la fois par le client et le serveur : cela est dû à la configuration [nuxt.config.js] suivante :

1
2
3
4
/*
   ** Plugins to load before mounting the App
   */
plugins: [{ src: '~/plugins/common/main.js' }],
  • ligne 4 : l’absence de la propriété [mode] fait que le plugin [~/plugins/common/main.js] sera exécuté à la fois par le client et le serveur : le serveur d’abord, le client ensuite ;

Ce plugin sera le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* eslint-disable no-undef */
/* eslint-disable no-console */
export default function(...args) {
  // qui exécute ce code ?
  console.log('[main server], process.server=', process.server, 'process.client=', process.client)
  const who = process.server ? 'server' : 'client'
  const main = '[main ' + who + ']'
  // nbre d'arguments
  console.log(main + ', il y a', args.length, 'arguments')

  // 1er argument
  const context = args[0]
  // clés du contexte
  dumpkeys(main + ', context', context)
  // l'application
  dumpkeys(main + ', context.app', context.app)
  // la route
  dumpkeys(main + ', context.route', context.route)
  console.log(main + ', context.route=', context.route)
  // le router
  dumpkeys(main + ', context.app.router', context.app.router)
  // le router.options.routes
  dumpkeys(main + ', context.app.router.options.routes', context.app.router.options.routes)
  console.log(main + ', context.app.router.options.routes=', context.app.router.options.routes)

  // 2ième argument
  const inject = args[1]
  console.log('inject=', typeof inject)
}

function dumpkeys(message, object) {
  // liste des clés de [object]
  const ligne = 'Liste des clés [' + message + ']'
  console.log(ligne)
  // liste des clés
  if (object) {
    console.log(Object.keys(object))
  }
}
  • lignes 31-39 : la fonction [dumpkeys] liste les propriétés de l’objet passé en 2ième paramètre. Cette liste est précédée du message passé en 1er paramètre ;
  • ligne 3 : on veut savoir combien d’arguments reçoit la fonction. Pour cela, on utilise la notation […args] qui va avoir pour effet de mettre les paramètres effectifs de la fonction dans le tableau [args]. On va découvrir qu’il y a deux arguments ;
  • ligne 5 : on affiche qui exécute le code, du serveur ou du client ;
  • ligne 6 : l’exécuteur du code, client ou serveur ;
  • ligne 7 : une constante chaîne de caractères utilisée dans les logs ;
  • ligne 12 : on va découvrir que le 1er argument reçu par la plugin est le contexte de l’exécuteur ;
  • ligne 14 : liste des clés de l’objet [context] ;
  • ligne 16 : on va découvrir que l’objet [context] a une propriété [app] qui représente l’application [nuxt] ;
  • ligne 18 : on va découvrir que l’objet [context] a une propriété [route] qui représente la route courante du routeur ;
  • ligne 21 : on va découvrir que l’objet [app] a une propriété [router] qui représente le routeur ;
  • ligne 23 : l’objet [router.options.routes] représente les différentes routes de l’application ;
  • lignes 27-28 : le second argument du plugin est la fonction [inject] que nous avons utilisée dans l’exemple [nuxt-06] ;

Le plugin exécuté par le serveur

A l’exécution, le serveur affiche la chose suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[main server], process.server= true process.client= false
[main server], il y a 2 arguments
Liste des clés [[main server], context]
[
  'isStatic',
  'isDev',
  'isHMR',
  'app',
  'payload',
  'error',
  'base',
  'env',
  'req',
  'res',
  'ssrContext',
  'redirect',
  'beforeNuxtRender',
  'route',
  'next',
  '_redirected',
  '_errored',
  'params',
  'query',
  '$axios'
]
  • lignes 11-12 : nous avons déjà eu l’occasion d’utiliser les propriétés [base] et [env] dont les valeurs proviennent du fichier [nuxt.config.js] ;
  • ligne 8 : la propriété [app] désigne l’application [nuxt] ;
  • ligne 18 : la propriété [route] désigne la route courante du routeur, ç-à-d la page que va envoyer le serveur ;
  • ligne 13 : la requête HTTP du navigateur client ;
  • ligne 14 : la réponse HTTP du serveur ;

La liste des propriétés de [context.app] est la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Liste des clés [[main server], context.app]
[
  'router',
  'nuxt',
  'head',
  'render',
  'data',
  'beforeCreate',
  'created',
  'mounted',
  'watch',
  'computed',
  'methods',
  'components',
  'context',
  '$axios'
]
  • ligne 3 : la propriété [router] nous donne accès au router de l’application. C’est important sous [nuxt] puisque le routeur est défini par [nuxt] lui-même et non par le développeur. Cette propriété est un accès donné au développeur pour modifier le routeur ;

La liste des propriétés de [context.route] sont les suivantes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Liste des clés [[main server], context.route]
[
  'name',
  'meta',
  'path',
  'hash',
  'query',
  'params',
  'fullPath',
  'matched'
]

La route [context.route] au démarrage du serveur est la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[main server], context.route= {
  name: 'index',
  meta: [
    {}
  ],
  path: '/',
  hash: '',
  query: {},
  params: {},
  fullPath: '/',
  matched: [
    {
      path: '',
      regex: /^(?:\/(?=$))?$/i,
      components: [Object],
      instances: {},
      name: 'index',
      parent: undefined,
      matchAs: undefined,
      redirect: undefined,
      beforeEnter: undefined,
      meta: {},
      props: {}
    }
  ]
}
  • ligne 2 : on voit que la prochaine page du serveur est [index] et que son chemin est [/] (ligne 10) ;
  • ligne 22 : la propriété [meta] permet d’ajouter des propriétés aux routes ;

Les propriétés du routeur [context.app.router] du serveur sont les suivantes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Liste des clés [[main server], context.app.router]
[
  'app',
  'apps',
  'options',
  'beforeHooks',
  'resolveHooks',
  'afterHooks',
  'matcher',
  'fallback',
  'mode',
  'history'
]

C’est dans la propriété [context.app.router.options.routes] qu’on trouve les différentes routes de l’application :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Liste des clés [[main server], context.app.router.options.routes]
[
  '0',
  '1',
  '2'
]
[main server], context.app.router.options.routes= [
  {
    path: '/page1',
    component: [Function: _d7b6c762],
    name: 'page1'
  },
  {
    path: '/page2',
    component: [Function: _d79a9860],
    name: 'page2'
  },
  {
    path: '/',
    component: [Function: _31eaad9f],
    name: 'index'
  }
]

Enfin le 2ième argument :

1
inject= function

La page [index] du serveur

La page [index] est la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">
      Home
    </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  data() {
    return {
      who: process.server ? 'server' : 'client'
    }
  },

  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home ' + this.who + ' created]')
    this.dumpkeys('[home ' + this.who + ' created], this.$nuxt', this.$nuxt)
    this.dumpkeys('[home ' + this.who + ' created], this.$nuxt.context', this.$nuxt.context)
  },
  beforeMount() {
    // client seulement
    console.log('[home ' + this.who + ' beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home ' + this.who + ' mounted]')
  },
  methods: {
    dumpkeys(message, object) {
      // liste des clés de [object]
      const ligne = 'Liste des clés [' + message + ']'
      console.log(ligne)
      if (object) {
        console.log(Object.keys(object))
      }
    }
  }
}
</script>
  • ligne 43 : on a déjà vu que le contexte d’une page pouvait être trouvé dans [this.$nuxt.context] ;
  • ligne 42 : on affiche également les propriétés de l’objet [this.$nuxt] ;

Exécutée par le serveur, cette page donne naissance aux logs suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[home beforeCreate]
[home server created]
Liste des clés [[home server created], this.$nuxt]
[
  '_uid',
  '_isVue',
  '$options',
  '_renderProxy',
  '_self',
  '$parent',
  '$root',
  '$children',
  '$refs',
  '_watcher',
  '_inactive',
  '_directInactive',
  '_isMounted',
  '_isDestroyed',
  '_isBeingDestroyed',
  '_events',
  '_hasHookEvent',
  '_vnode',
  '_staticTrees',
  '$vnode',
  '$slots',
  '$scopedSlots',
  '_c',
  '$createElement',
  '$attrs',
  '$listeners',
  '_routerRoot',
  '_router',
  '_route',
  '_bv__modal',
  '_bv__toast',
  '_vueMeta',
  'nuxt',
  '_watchers',
  'refreshOnlineStatus',
  'refresh',
  'errorChanged',
  'setLayout',
  'loadLayout',
  '_data',
  'layoutName',
  'layout',
  'isOnline',
  '_computedWatchers',
  'isOffline',
  'error',
  'context'
]

Les propriétés du contexte serveur dans la page [index] sont les suivantes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Liste des clés [[home server created], this.$nuxt.context]
[
  'isStatic',
  'isDev',
  'isHMR',
  'app',
  'payload',
  'error',
  'base',
  'env',
  'req',
  'res',
  'ssrContext',
  'redirect',
  'beforeNuxtRender',
  'route',
  'next',
  '_redirected',
  '_errored',
  'params',
  'query',
  '$axios'
]

Ce sont les mêmes propriétés que dans l’objet [context] du plugin.

Le plugin exécuté par le client

Une fois que le serveur a envoyé la page [index] au navigateur client, les scripts client prennent la main. Le plugin [main.js] va alors être exécuté. Ces logs sont les suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[main server], process.server= false process.client= true
[main client], il y a 2 arguments
Liste des clés [[main client], context]
Array(17)0: "isStatic"1: "isDev"2: "isHMR"3: "app"4: "payload"5: "error"6: "base"7: "env"8: "redirect"9: "nuxtState"10: "route"11: "next"12: "_redirected"13: "_errored"14: "params"15: "query"16: "$axios"length: 17__proto__: Array(0)
Liste des clés [[main client], context.app]
Array(14)0: "router"1: "nuxt"2: "head"3: "render"4: "data"5: "beforeCreate"6: "created"7: "mounted"8: "watch"9: "computed"10: "methods"11: "components"12: "context"13: "$axios"length: 14__proto__: Array(0)
Liste des clés [[main client], context.route]
Array(8)0: "name"1: "meta"2: "path"3: "hash"4: "query"5: "params"6: "fullPath"7: "matched"length: 8__proto__: Array(0)
[main client], context.route= ObjectfullPath: "/"hash: ""matched: [{…}]meta: [{…}]name: "index"params: {}path: "/"query: {}__proto__: Object
Liste des clés [[main client], context.app.router]
Array(10)0: "app"1: "apps"2: "options"3: "beforeHooks"4: "resolveHooks"5: "afterHooks"6: "matcher"7: "fallback"8: "mode"9: "history"length: 10__proto__: Array(0)
Liste des clés [[main client], context.app.router.options.routes]
Array(3)0: "0"1: "1"2: "2"length: 3__proto__: Array(0)
[main client], context.app.router.options.routes= Array(3)0: {path: "/page1", name: "page1", component: ƒ}1: {path: "/page2", name: "page2", component: ƒ}2: {path: "/", name: "index", component: ƒ}length: 3__proto__: Array(0)
inject= function

On retrouve des propriétés analogues à celles trouvées côté serveur avec certaines propriétés qui ont disparu et d’autres qui sont apparues. Ainsi ligne 4, on ne retrouve pas les propriétés [req, res] qui étaient les requêtes HTTP du navigateur client et la réponse HTTP du serveur.

La page [index] du client

La page [index] du client produit les logs suivants :

1
2
3
4
5
6
7
8
[home beforeCreate]
[home client created]
Liste des clés [[home client created], this.$nuxt]
(51) ["_uid", "_isVue", "$options", "_renderProxy", "_self", "$parent", "$root", "$children", "$refs", "_watcher", "_inactive", "_directInactive", "_isMounted", "_isDestroyed", "_isBeingDestroyed", "_events", "_hasHookEvent", "_vnode", "_staticTrees", "$vnode", "$slots", "$scopedSlots", "_c", "$createElement", "$attrs", "$listeners", "_routerRoot", "_router", "_route", "_bv__modal", "_bv__toast", "_vueMeta", "nuxt", "_watchers", "refreshOnlineStatus", "refresh", "errorChanged", "setLayout", "loadLayout", "_data", "layoutName", "layout", "isOnline", "_computedWatchers", "isOffline", "error", "context", "_name", "setTransitions", "$loading", "$el"]
Liste des clés [[home client created], this.$nuxt.context]
(17) ["isStatic", "isDev", "isHMR", "app", "payload", "error", "base", "env", "redirect", "nuxtState", "route", "next", "_redirected", "_errored", "params", "query", "$axios"]
[home client beforeMount]
[home client mounted]
  • ligne 4 : les propriétés de l’objet [this.$nuxt]. C’est un objet riche avec 51 propriétés ;
  • ligne 6 : les propriétés de l’objet [this.$nuxt.context]. On retrouve les mêmes propriétés que dans l’objet [context] du plugin client ;

Exemple [nuxt-08] : middlewares de routage

Dans cet exemple, nous introduisons la notion de middlewares de routage, des scripts exécutés à chaque changement de route.

L’exemple [nuxt-08] est obtenu initialement par recopie du projet [nuxt-01] :

image0

Les middlewares de routage doivent être dans un dossier appelé [middleware] [2]. Il peut y avoir un routage à deux niveaux :

  • un routage appliqué à chaque navigation. Celui-ci fait alors l’objet d’une déclaration dans le fichier [nuxt.config.js] ;

  • un routage appliqué à une page particulière, lorsque celle-ci est la cible du routage. Ce routage est alors déclaré dans cette page cible ;

    1. Routage général

Le fichier [middleware / routing.js] assurera le routage général. Il fait l’objet de la déclaration suivante dans le fichier [nuxt.config.js] :

1
2
3
4
  router: {
    base: '/nuxt-08/',
    middleware: ['routing']
},

Les middlewares de routage général sont une propriété du routeur (ligne 1). Il peut y avoir plusieurs middlewares de routage. C’est pourquoi ligne 3, la valeur de la propriété [middleware] est un tableau. On voit qu’on n’utilise pas de chemin pour désigner le middleware. Il sera automatiquement cherché dans le dossier [middleware] du projet ;

Le middleware [routing] ne fait ici que des logs :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* eslint-disable no-undef */
/* eslint-disable no-console */
export default function(...args) {
  // qui exécute ce code ?
  console.log('[routing], process.server=', process.server, 'process.client=', process.client)
  const who = process.server ? 'server' : 'client'
  const routing = '[routing ' + who + ']'
  // nbre d'arguments
  console.log(routing + ', il y a', args.length, 'argument(s)')

  // 1er argument
  const context = args[0]
  // clés du contexte
  dumpkeys(routing + ', context', context)
  // l'application
  dumpkeys(routing + ', context.app', context.app)
  // la route
  dumpkeys(routing + ', context.route', context.route)
  console.log(routing + ', context.route=', context.route)
  // le router
  dumpkeys(routing + ', context.app.router', context.app.router)
  // le router.options.routes
  dumpkeys(routing + ', context.app.router.options.routes', context.app.router.options.routes)
  console.log(routing + ', context.app.router.options.routes=', context.app.router.options.routes)
}

function dumpkeys(message, object) {
  // liste des clés de [object]
  const ligne = 'Liste des clés [' + message + ']'
  console.log(ligne)
  // liste des clés
  if (object) {
    console.log(Object.keys(object))
  }
}
  • ligne 3 : on va découvrir que le middleware reçoit un argument : le contexte de l’exécuteur (serveur ou client) ;

  • lignes 4-25 : on affiche les propriétés de différents objets pour savoir ce qui est utilisable. On va découvrir que le contexte du middleware est quasi identique au contexte du plugin ;

    1. Routage pour une page particulière

On veut contrôler la façon dont on arrive à la page [index]. Pour cela, il nous faut introduire la propriété [middleware] dans cette page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[home beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[home created]')
  },
  beforeMount() {
    // client seulement
    console.log('[home beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[home mounted]')
  },
  // routage
  middleware: ['index-routing']
}
</script>
  • ligne 34 : la propriété [middleware] liste les scripts à exécuter à chaque fois que la prochaine page affichée est la page [index]. Là encore, ces scripts seront cherchés dans le dossier [middleware] du projet ;

Le middleware [index-routing] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* eslint-disable no-undef */
/* eslint-disable no-console */
export default function(...args) {
  // qui exécute ce code ?
  console.log('[index-routing], process.server=', process.server, 'process.client=', process.client)
  const who = process.server ? 'server' : 'client'
  const indexRouting = '[index-routing ' + who + ']'
  // nbre d'arguments
  console.log(indexRouting + ', il y a', args.length, 'argument(s)')

  // 1er argument
  const context = args[0]
  // clés du contexte
  dumpkeys(indexRouting + ', context', context)
  // l'application
  dumpkeys(indexRouting + ', context.app', context.app)
  // la route
  dumpkeys(indexRouting + ', context.route', context.route)
  console.log(indexRouting + ', context.route=', context.route)
  // le router
  dumpkeys(indexRouting + ', context.app.router', context.app.router)
  // le router.options.routes
  dumpkeys(indexRouting + ', context.app.router.options.routes', context.app.router.options.routes)
  console.log(indexRouting + ', context.app.router.options.routes=', context.app.router.options.routes)
  // d'où vient-on ?
  if (context.from) {
    console.log('from=', context.from)
  }
}

function dumpkeys(message, object) {
  // liste des clés de [object]
  const ligne = 'Liste des clés [' + message + ']'
  console.log(ligne)
  // liste des clés
  if (object) {
    console.log(Object.keys(object))
  }
}

Le code de [index-routing] est identique à celui de [routing] et produit les mêmes résultats. Ce qui nous intéresse c’est de voir quand ces deux middlewares sont exécutés.

Exécution du projet

Nous exécutons le projet. Les logs sont alors les suivants :

C’est le script [routing] qui est exécuté en premier par le serveur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
[routing], process.server= true process.client= false
[routing server], il y a 1 argument(s)
Liste des clés [[routing server], context]
[ 'isStatic',
  'isDev',
  'isHMR',
  'app',
  'payload',
  'error',
  'base',
  'env',
  'req',
  'res',
  'ssrContext',
  'redirect',
  'beforeNuxtRender',
  'route',
  'next',
  '_redirected',
  '_errored',
  'params',
  'query',
  '$axios' ]
Liste des clés [[routing server], context.app]
[ 'router',
  'nuxt',
  'head',
  'render',
  'data',
  'beforeCreate',
  'created',
  'mounted',
  'watch',
  'computed',
  'methods',
  'components',
  'context',
  '$axios' ]
Liste des clés [[routing server], context.route]
[ 'name',
  'meta',
  'path',
  'hash',
  'query',
  'params',
  'fullPath',
  'matched' ]
[routing server], context.route= { name: 'index',
  meta: [ {} ],
  path: '/',
  hash: '',
  query: {},
  params: {},
  fullPath: '/',
  matched:
   [ { path: '',
       regex: /^(?:\/(?=$))?$/i,
       components: [Object],
       instances: {},
       name: 'index',
       parent: undefined,
       matchAs: undefined,
       redirect: undefined,
       beforeEnter: undefined,
       meta: {},
       props: {} } ] }
Liste des clés [[routing server], context.app.router]
[ 'app',
  'apps',
  'options',
  'beforeHooks',
  'resolveHooks',
  'afterHooks',
  'matcher',
  'fallback',
  'mode',
  'history' ]
Liste des clés [[routing server], context.app.router.options.routes]
[ '0', '1', '2' ]
[routing server], context.app.router.options.routes= [ { path: '/page1',
    component: [Function: _61cefe10],
    name: 'page1' },
  { path: '/page2',
    component: [Function: _61dd1591],
    name: 'page2' },
  { path: '/', component: [Function: _00d5e140], name: 'index' } ]

On retrouve là ce qu’on avait obtenu avec les plugins.

  • ligne 15 : la propriété [redirect] est souvent utilisée dans les middlewares : elle permet de changer la cible du routage en cours ;

Puis, parce que la page qui va être affichée est la page [index], le serveur exécute le script [index-routing] et affiche les logs suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
[index-routing], process.server= true process.client= false
[index-routing server], il y a 1 argument(s)
Liste des clés [[index-routing server], context]
[ 'isStatic',
  'isDev',
  'isHMR',
  'app',
  'payload',
  'error',
  'base',
  'env',
  'req',
  'res',
  'ssrContext',
  'redirect',
  'beforeNuxtRender',
  'route',
  'next',
  '_redirected',
  '_errored',
  'params',
  'query',
  '$axios' ]
Liste des clés [[index-routing server], context.app]
[ 'router',
  'nuxt',
  'head',
  'render',
  'data',
  'beforeCreate',
  'created',
  'mounted',
  'watch',
  'computed',
  'methods',
  'components',
  'context',
  '$axios' ]
Liste des clés [[index-routing server], context.route]
[ 'name',
  'meta',
  'path',
  'hash',
  'query',
  'params',
  'fullPath',
  'matched' ]
[index-routing server], context.route= { name: 'index',
  meta: [ {} ],
  path: '/',
  hash: '',
  query: {},
  params: {},
  fullPath: '/',
  matched:
   [ { path: '',
       regex: /^(?:\/(?=$))?$/i,
       components: [Object],
       instances: {},
       name: 'index',
       parent: undefined,
       matchAs: undefined,
       redirect: undefined,
       beforeEnter: undefined,
       meta: {},
       props: {} } ] }
Liste des clés [[index-routing server], context.app.router]
[ 'app',
  'apps',
  'options',
  'beforeHooks',
  'resolveHooks',
  'afterHooks',
  'matcher',
  'fallback',
  'mode',
  'history' ]
Liste des clés [[index-routing server], context.app.router.options.routes]
[ '0', '1', '2' ]
[index-routing server], context.app.router.options.routes= [ { path: '/page1',
    component: [Function: _61cefe10],
    name: 'page1' },
  { path: '/page2',
    component: [Function: _61dd1591],
    name: 'page2' },
  { path: '/', component: [Function: _00d5e140], name: 'index' } ]

Les résultats obtenus avec le script [index-routing] sont analogues à ceux obtenus avec le script [routing].

Une fois la page [index] reçue par le navigateur client, les scripts client prennent la main. Les logs deviennent les suivants :

1
2
3
4
[home beforeCreate]
[home created]
[home beforeMount]
[home mounted]

On voit donc qu’au démarrage de l’application le client n’exécute aucun middleware. Cela veut dire que cela se produira à chaque fois que l’utilisateur forcera un appel au serveur. Les middlewares ne sont exécutés par le client que lors d’une navigation au sein du client. Naviguons par exemple vers la page [page1] (nous sommes sur la page [index]) avec le lien [Page 1]. Les logs sont alors les suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[routing], process.server= false process.client= true
[routing client], il y a 1 argument(s)
Liste des clés [[routing client], context]
(18) ["isStatic", "isDev", "isHMR", "app", "payload", "error", "base", "env", "redirect", "nuxtState", "route", "next", "_redirected", "_errored", "params", "query", "$axios", "from"]
Liste des clés [[routing client], context.app]
(14) ["router", "nuxt", "head", "render", "data", "beforeCreate", "created", "mounted", "watch", "computed", "methods", "components", "context", "$axios"]
Liste des clés [[routing client], context.route]
(8) ["name", "meta", "path", "hash", "query", "params", "fullPath", "matched"]
[routing client], context.route= {name: "page1", meta: Array(1), path: "/page1", hash: "", query: {…}, …}
Liste des clés [[routing client], context.app.router]
(10) ["app", "apps", "options", "beforeHooks", "resolveHooks", "afterHooks", "matcher", "fallback", "mode", "history"]
Liste des clés [[routing client], context.app.router.options.routes]
(3) ["0", "1", "2"]
[routing client], context.app.router.options.routes= (3) [{…}, {…}, {…}]0: {path: "/page1", name: "page1", component: ƒ}1: {path: "/page2", name: "page2", component: ƒ}2: {path: "/", name: "index", component: ƒ}length: 3__proto__: Array(0)
[page1 beforeCreate]
[page1 created]
[page1 beforeMount]
[page1 mounted]
  • ligne 2 : le middleware [routing] est exécuté par le client ;
  • ligne 4 : notez la propriété [from] : c’est la route d’où l’on vient ;
  • ligne 9 : [context.route] est la route où l’on va ;
  • lignes 15-18 : affichage de la page [page1] ;

Maintenant revenons à la page [index] avec le lien [Home]. Les logs sont alors les suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[routing], process.server= false process.client= true
[routing client], il y a 1 argument(s)
Liste des clés [[routing client], context]
(18) ["isStatic", "isDev", "isHMR", "app", "payload", "error", "base", "env", "redirect", "nuxtState", "route", "next", "_redirected", "_errored", "params", "query", "$axios", "from"]
Liste des clés [[routing client], context.app]
(14) ["router", "nuxt", "head", "render", "data", "beforeCreate", "created", "mounted", "watch", "computed", "methods", "components", "context", "$axios"]
Liste des clés [[routing client], context.route]
(8) ["name", "meta", "path", "hash", "query", "params", "fullPath", "matched"]
[routing client], context.route= {name: "index", meta: Array(1), path: "/", hash: "", query: {…}, …}
Liste des clés [[routing client], context.app.router]
(10) ["app", "apps", "options", "beforeHooks", "resolveHooks", "afterHooks", "matcher", "fallback", "mode", "history"]
Liste des clés [[routing client], context.app.router.options.routes]
(3) ["0", "1", "2"]
[routing client], context.app.router.options.routes= (3) [{…}, {…}, {…}]0: {path: "/page1", name: "page1", component: ƒ}1: {path: "/page2", name: "page2", component: ƒ}2: {path: "/", name: "index", component: ƒ}length: 3__proto__: Array(0)
[index-routing], process.server= false process.client= true
[index-routing client], il y a 1 argument(s)
Liste des clés [[index-routing client], context]
(18) ["isStatic", "isDev", "isHMR", "app", "payload", "error", "base", "env", "redirect", "nuxtState", "route", "next", "_redirected", "_errored", "params", "query", "$axios", "from"]
Liste des clés [[index-routing client], context.app]
(14) ["router", "nuxt", "head", "render", "data", "beforeCreate", "created", "mounted", "watch", "computed", "methods", "components", "context", "$axios"]
Liste des clés [[index-routing client], context.route]
(8) ["name", "meta", "path", "hash", "query", "params", "fullPath", "matched"]
[index-routing client], context.route= {name: "index", meta: Array(1), path: "/", hash: "", query: {…}, …}
Liste des clés [[index-routing client], context.app.router]
(10) ["app", "apps", "options", "beforeHooks", "resolveHooks", "afterHooks", "matcher", "fallback", "mode", "history"]
Liste des clés [[index-routing client], context.app.router.options.routes]
(3) ["0", "1", "2"]
[index-routing client], context.app.router.options.routes= (3) [{…}, {…}, {…}]0: {path: "/page1", name: "page1", component: ƒ}1: {path: "/page2", name: "page2", component: ƒ}2: {path: "/", name: "index", component: ƒ}length: 3__proto__: Array(0)
from= {name: "page1", meta: Array(1), path: "/page1", hash: "", query: {…}, …}
[home beforeCreate]
[home created]
[home beforeMount]
[home mounted]
  • lignes 1-15 : le client exécute le middleware [routing]. C’est normal. Il est exécuté à chaque changement de route;
  • lignes 16-29 : le client exécute le middleware [index-routing], ceci parce que :
    • [index] est la cible de la route courante (cf ligne 23) ;
    • la page [index] a défini un middleware, nommément [index-routing] ;

On voit donc que les middlewares de routage général sont exécutés par le client avant les middlewares attachés aux pages.

Exemple [nuxt-09] : contrôle de la navigation

L’exemple [nuxt-09] utilise un middleware pour contrôler la navigation du client. Par ailleurs, on ajoute une nouvelle vue pour les cas où l’utilisateur demande au serveur une URL qui n’existe pas dans l’application.

L’exemple [nuxt-09] est initialement obtenu par recopie de l’exemple [nuxt-01] :

image0

La page [_.vue]

Nous avons dit que les routes de l’application étaient construites à partir du contenu du dossier [pages]. Ici, nous avons ajouté dans ce dossier la page [_.vue]. Cette page particulière est affichée à chaque fois que l’application est routée vers une page qui n’existe pas. Dans notre exemple ici, cela ne peut pas arriver pour le client. Mais cela peut arriver pour le serveur si par exemple on lui demande l’URL [/nuxt-09/abcd]. La page [abcd] n’existant pas, c’est la page [_.vue] qui va être affichée. Ici, ce sera la suivante :

image1

Le code de la page [_.vue] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- définition HTML de la vue -->
<template>
  <!-- mise en page -->
  <Layout :left="true" :right="true">
    <!-- alerte dans la colonne de droite -->
    <template slot="right">
      <!-- message sur fond jaune -->
      <b-alert show variant="warning" align="center">
        <h4>La page demandée n'existe pas</h4>
      </b-alert>
    </template>
    <!-- menu de navigation dans la colonne de gauche -->
    <Navigation slot="left" />
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'

export default {
  name: '404',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[404 beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[404 created]')
  },
  beforeMount() {
    // client seulement
    console.log('[404 beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[404 mounted]')
  }
}
</script>

Le middleware du client

Le code du middleware du client [client-routing] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* eslint-disable no-undef */
/* eslint-disable no-console */
export default function({ route, from, redirect }) {
  // seulement le client
  if (process.client) {
    console.log('[client-routing]')
    // ordre de navigation souhaité
    const routes = ['index', 'page1', 'page2', 'index']
    // route courante
    const current = route.name
    // route précédente
    const previous = from.name
    // on veut une navigation circulaire
    // routes[i] vers routes[i+1]
    for (let i = 0; i < routes.length - 1; i++) {
      if (previous === routes[i] && current !== routes[i + 1]) {
        // on reste sur la même page
        redirect({ name: routes[i] })
        return
      }
    }
  }
}
  • ligne 3 : nous savons que la fonction de routing ne reçoit qu’un paramètre, l’objet [context] de celui qui l’exécute, serveur ou client. La notation [function({ route, from, redirect })]
    • est équivalente à [function({ route:route, from:from, redirect:redirect })] ;
    • ce qui fait que { route:route, from:from, redirect:redirect } <– context ;
    • ce qui crée trois paramètres [route, from, redirect] tels que :
      • route=context.route ;
      • redirect=context.redirect ;
      • from=context.from ;
La documentation de [nuxt] utilise abondamment cette notation. Il faut la connaître ;
  • ligne 8 : un tableau des noms de pages dans l’ordre de navigation souhaité

  • ligne 10 : le nom de la page de destination du routage courant ;

  • lignes 12 : le nom de la page précédente du routage courant ;

  • ligne 14 : comme exercice, on ne va autoriser qu’une navigation circulaire [index –> page1 –> page2 –> index] ;

  • lignes 15-21 : on parcourt le tableau donnant l’ordre de navigation souhaité ;

  • ligne 16 : si on découvre que routes[i] était la dernière page routée alors la suivante doit être routes[i+1] ;

  • lignes 18-19 : si ce n’est pas le cas, on redirige l’application vers routes[i], ç-à-d qu’on ne change pas de page : on refuse la navigation ;

    1. Exécution

On exécute l’exemple avec le fichier [nuxt.config.js] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: process.env.npm_package_name || "Introduction à nuxt.js par l'exemple",
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** 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'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // répertoire du code source
  srcDir: 'nuxt-09',
  router: {
    base: '/nuxt-09/',
    middleware: ['client-routing']
  },
  // serveur
  server: {
    port: 81, // default: 3000
    host: 'localhost' // default: localhost
  }
}

Vérifiez les points suivants :

  • lorsque vous êtes sur la page [Home], vous ne pouvez naviguer que vers la page [Page 1] ;
  • lorsque vous êtes sur la page [Page 1], vous ne pouvez naviguer que vers la page [Page 2] ;
  • lorsque vous êtes sur la page [Page 2], vous ne pouvez naviguer que vers la page [Home] ;
  • lorsque vous demandez une URL incorrecte telle que [http://localhost:81/nuxt-09b/abcd] alors vous obtenez la vue qui indique que la page demandée n’existe pas ;

Exemple [nuxt-10] : asyncData et loading

La fonction [asyncData] dans une page permet de charger de façon asynchrone des données souvent externes. [nuxt] attend la fin de la fonction [asyncData] avant de commencer le cycle de vie de la page. Celle-ci n’est donc rendue qu’une fois les données externes obtenues. Elle est exécutée aussi bien par le serveur que par le client avec les règles suivantes :

  • lorsque la page est demandée directement au serveur, seul le serveur exécute la fonction [asyncData] ;
  • ensuite lors de la navigation client, seul le client exécute la fonction [asyncData] ;

Au final seul l’un des deux, client ou serveur, exécute la fonction. Par ailleurs, quand c’est le client qui exécute la fonction [asyncData], [nuxt] affiche une barre de progression qui peut être paramétrée.

La fonction [asyncData] permet de délivrer aux moteurs de recherche des pages avec leurs données ce qui les rend plus signifiantes.

L’exemple [nuxt-10] est obtenu initialement par recopie du projet [nuxt-01] :

image0

Seule la page [page1] évolue.

La page [page1]

Le code de la page [page1] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!-- vue n° 1 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="primary"> Page 1 -- result={{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function () {
        // on rend le résultat asynchrone - un nombre aléatoire ici
        resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
        // log
        console.log('[page1 asyncData finished]')
      }, 5000)
    })
  },

  // cycle de vie
  beforeCreate() {
    console.log('[page1 beforeCreate]')
  },
  created() {
    console.log('[page1 created]')
  },
  beforeMount() {
    console.log('[page1 beforeMount]')
  },
  mounted() {
    console.log('[page1 mounted]')
  }
}
</script>
  • lignes 26-39 : la fonction [asyncData]. Nous avons déjà étudié cette fonction (cf paragraphe lien). Elle est exécutée avant le cycle de vie de la page. Pour cette raison, on ne peut utiliser le mot clé [this] dans la fonction ;

  • ligne 30 : elle doit rendre une promesse [Promise] ou utiliser la syntaxe async / await ;

  • lignes 32-37 : la fonction asynchrone de la promesse est simulée avec une attente de 5 secondes (ligne 37) ;

  • ligne 34 : le résultat de la fonction asynchrone est rendu sous la forme d’un objet {result :…}. L’objet asynchrone rendu par la fonction [asyncData] est intégré dans l’objet [data] de la page. C’est pourquoi l’objet [result] est-il disponible ligne 7 du template alors même que la page n’avait pas défini d’objet [data] ;

    1. Configuration de la barre de progression de [asyncData]

Lorsque la page [page1] est la cible d’une navigation au sein du client (mode SPA), le client exécute la fonction [asyncData] et [nuxt] affiche alors une barre de progression qu’elle cache lorsque la fonction [asyncData] a rendu son résultat. La propriété [loading] du fichier [nuxt.config.js] permet de configurer cette barre :

1
2
3
4
5
6
loading: {
    color: 'blue',
    height: '5px',
    throttle: 200,
    continuous: true
},

Par défaut, l’image d’attente de [nuxt] est une barre de progression, faisant la largeur de la page. Cette barre a une couleur et une épaisseur. Le trait coloré grandit progressivement de 0 % à 100 % de sa taille, plus ou moins vite donc selon la durée de l’attente.

  • ligne 2 : fixe la couleur de la barre de progression ;

  • ligne 3 : fixe l’épaisseur de la barre en pixels ;

  • ligne 4 : [throttle] est le délai en millisecondes avant que l’animation ne démarre. Cela permet de ne pas avoir d’image d’animation lorsque la fonction [asyncData] rend son résultat rapidement ;

  • ligne 5 : [continuous] fixe le comportement de l’animation de la barre de progression. Par défaut, la barre grandit progressivement de 0 % à 100 % de sa taille, plus ou moins vite selon la durée de l’attente. Avec [continuous:true], le barre colorée grandit à vitesse constante de 0 à 100 % de sa taille, puis recommence tant que la fonction [asyncData] n’a pas rendu son résultat ;

    1. Exécution

Lançons l’application, puis demandons, à la main, au serveur la page [page1] :

image1

Les logs sont les suivants :

image2

  • on voit que seul le serveur [1] a exécuté la fonction [asyncData] et il l’a fait avant le cycle de la page ;

Maintenant examinons la page envoyée par le serveur (code source) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-10/">
  <link rel="preload" href="/nuxt-10/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-10/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-10/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-10/_nuxt/app.js" as="script">
...
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
            <h4>[nuxt-10] : asyncData et loading</h4>
              </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-10/" target="_self" class="nav-link">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-10/page1" target="_self" class="nav-link active nuxt-link-active">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-10/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10"><div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-primary"> Page 1 -- result=3 </div></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = (function (a, b, c) {
  return {
    layout: "default", data: [{ result: 3 }], error: null, serverRendered: true,
    logs: [
      { date: new Date(1574939615256), args: ["[page1 asyncData started]"], type: a, level: b, tag: c },
      { date: new Date(1574939620263), args: ["[page1 asyncData finished]"], type: a, level: b, tag: c },
      { date: new Date(1574939620285), args: ["[page1 beforeCreate]"], type: a, level: b, tag: c },
      { date: new Date(1574939620287), args: ["[page1 created]"], type: a, level: b, tag: c }
    ]
  }
    }("log", 2, ""));</script>
  <script src="/nuxt-10/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-10/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-10/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-10/_nuxt/app.js" defer></script>
</body>
</html>
  • ligne 55 : on voit que le serveur a envoyé au client un tableau [data] qui contient l’objet [result:3] qui a été intégré à l’objet [data] de la page [page1] du serveur. Afin que le client puisse faire de même et donc afficher la même page que le serveur, celui-ci lui transmet l’objet [result]. On rappelle que le client ne va pas exécuter la fonction [asyncData]. Il va simplement utiliser les données calculées par le serveur ;

Maintenant naviguons de la page [Home] à la page [Page 1] en utilisant le menu de navigation :

image3

  • en [1], on voit apparaître la barre de progression ;

Au bout de 5 secondes, on a la page [Page 1] :

image4

Les logs sont les suivants :

image5

On voit que le client a exécuté la fonction [asyncData] avant le cycle de vie de la page.

Exemple [nuxt-11] : personnalisation de l’image d’attente

Par défaut, l’image d’attente de [nuxt] est une barre de progression. L’exemple [nuxt-11] montre qu’on peut la remplacer par sa propre image d’attente :

image0

L’exemple [nuxt-11] montre également comment gérer des erreurs de chargement.

image1

L’exemple [nuxt-11] est obtenu initialement par recopie de l’exemple [nuxt-10] :

image2

Nous allons ajouter en [1], un plugin pour le client dont le rôle sera de gérer des événements entre composants.

Le plugin [event-bus]

Le plugin [event-bus] sera exécuté par le client et le serveur, mais on verra qu’il ne fonctionne pas côté serveur. Son code est le suivant :

1
2
3
4
5
6
7
8
// on crée un bus d'événements entre les vues
import Vue from 'vue'
export default (context, inject) => {
  // le bus d'événements
  const eventBus = new Vue()
  // injection d'une fonction [eventBus] dans le contexte
  inject('eventBus', () => eventBus)
}
  • ligne 5 : le bus d’événements est une instance de la classe [Vue]. En effet, celle-ci a les méthodes pour gérer des événements :
    • [$emit] : pour émettre un événement ;
    • [$on] : pour se mettre à l’écoute d’un événement particulier ;
Ce bus d’événements ne gèrera qu’un événement, [loading], qui sera utilisé par les pages pour démarrer / arrêter l’animation d’attente de la fin d’une fonction asynchrone ;
  • ligne 7 : on crée une fonction [$eventBus] (1er argument) dont le rôle sera de rendre l’objet [eventBus] que l’on vient de créer (2ième argument). Cette fonction est injectée dans le contexte pour qu’elle soit disponible dans les objets [context.app] et l’objet [this] des pages ;

    1. Le layout [default.vue]

Le layout [default.vue] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>
  <div class="container">
    <b-card>
      <!-- un message -->
      <b-alert show variant="success" align="center">
        <h4>[nuxt-11] : personnalisation de l'attente, gestion des erreurs</h4>
      </b-alert>
      <!-- la vue courante du routage -->
      <nuxt />
      <!-- loading -->
      <b-alert v-if="showLoading" show variant="light">
        <strong>Requête au serveur de données en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
      <!-- erreur de chargement -->
      <b-alert v-if="showErrorLoading" show variant="danger">
        <strong>La requête au serveur de données a échoué : {{ errorLoadingMessage }}</strong>
      </b-alert>
    </b-card>
  </div>
</template>

<script>
/* eslint-disable no-console */
export default {
  name: 'App',
  data() {
    return {
      showLoading: false,
      showErrorLoading: false
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[default beforeCreate]')
  },
  created() {
    console.log('[default created]')
    // on écoute l'évt [loading]
    this.$eventBus().$on('loading', this.mShowLoading)
    // ainsi que l'évt [errorLoadingMessage]
    this.$eventBus().$on('errorLoading', this.mShowErrorLoading)
  },
  beforeMount() {
    console.log('[default beforeMount]')
  },
  mounted() {
    console.log('[default mounted]')
  },
  methods: {
    // gestion du chargement
    mShowLoading(value) {
      console.log('[default mShowLoading], showLoading=', value)
      this.showLoading = value
    },
    // erreur de chargement
    mShowErrorLoading(value, errorLoadingMessage) {
      console.log('[default mShowErrorLoading], showErrorLoading=', value, 'errorLoadingMessage=', errorLoadingMessage)
      this.showErrorLoading = value
      this.errorLoadingMessage = errorLoadingMessage
    }
  }
}
</script>
  • lignes 11-14 : l’animation d’attente. Elle n’est affichée que si la propriété [showLoading] est vraie (ligne 29) ;

  • lignes 16-18 : le message d’erreur du chargement. Il n’est affiché que si la propriété [showErrorLoading] (ligne 30) est vraie ;

  • lignes 29-30 : au chargement initial du composant, l’animation d’attente est cachée ainsi que le message d’erreur ;

  • lignes 37-43 : lorsqu’elle est créée, la page écoute l’événement [loading] (1er argument) sur le bus d’événements créé par le plugin. A sa réception, elle fait exécuter la méthode [mShowLoading] des lignes 52-55 (2ième argument) ;

  • lignes 52-55 : la valeur reçue par la méthode [mShowLoading] sera un booléen true / false. Elle sert à montrer / cacher le message d’attente ;

  • lignes 41-42 : lorsqu’elle est créée, la page écoute l’événement [errorLoading] (1er argument) sur le bus d’événements créé par le plugin. A sa réception, elle fait exécuter la méthode [mShowErrorLoading] des lignes 57-61 (2ième argument) ;

  • ligne 57 : la méthode [mShowErrorLoading] reçoit deux arguments :

    • le 1er argument est un booléen true / false pour montrer / cacher le message d’erreur ;
    • le 2ième argument n’est présent que s’il y a eu erreur. Il représente le message d’erreur à afficher ;
  • les logs des lignes 53 et 58 vont nous montrer que les méthodes [showLoading] et [showErrorLoading] ne sont pas exécutées côté serveur ;

    1. La page [page1]

Le code de la page [page1] évolue de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!-- vue n° 1 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="primary"> Page 1 -- result={{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page1',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // début attente
    context.app.$eventBus().$emit('loading', true)
    // pas d'erreur
    context.app.$eventBus().$emit('errorLoading', false)
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function() {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // on rend le résultat asynchrone - un nombre aléatoire ici
        resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
      }, 5000)
    })
  },

  // cycle de vie
  beforeCreate() {
    console.log('[page1 beforeCreate]')
  },
  created() {
    console.log('[page1 created]')
  },
  beforeMount() {
    console.log('[page1 beforeMount]')
  },
  mounted() {
    console.log('[page1 mounted]')
  }
}
</script>
  • les modifications ont lieu dans la fonction [asyncData] des lignes 26-47 ;
  • lignes 29-30 : avant que ne commence la fonction asynchrone on émet l’événement [loading] à destination des autres pages de l’application. On rappelle que dans [asyncData], on n’a pas accès à l’objet [this] pas encore créé. On utilise alors le contexte que reçoit en argument la fonction [asyncData] (ligne 26) ;
  • ligne 30 : on utilise le bus d’événements pour indiquer que le chargement va commencer ;
  • ligne 38 : on utilise le bus d’événements pour indiquer que le chargement est terminé ;

Note : A l’exécution, lorsque la page [page1] est demandée au serveur, on ne voit pas l’image d’attente. Dans les logs on voit que côté serveur la méthode [default.mShowLoading] n’est pas appelée. De toute façon, voir l’image d’attente n’a pas de sens lorsque la page est demandée au serveur. Celui-ci n’envoie la page au navigateur client qu’une fois la fonction [asyncData] terminée. L’image d’attente est alors inutile. Ce sera le cas pour toutes les pages de l’application demandées directement au serveur.

La page [index]

Le code de la page [index] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">
      Home
    </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Home',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // début attente
    context.app.$eventBus().$emit('loading', true)
    // pas d'erreur
    context.app.$eventBus().$emit('errorLoading', false)
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function() {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // on rend une erreur
        reject(new Error("le serveur n'a pas répondu assez vite"))
      }, 5000)
    }).catch((e) => context.error({ statusCode: 500, message: e.message }))
  },
  // cycle de vie
  beforeCreate() {
    console.log('[home beforeCreate]')
  },
  created() {
    console.log('[home created]')
  },
  beforeMount() {
    console.log('[home beforeMount]')
  },
  mounted() {
    console.log('[home mounted]')
    // pas d'erreur
    this.$eventBus().$emit('errorLoading', false)
  }
}
</script>
  • lignes 30-49 : la fonction [asyncData] est identique à celle de la page [page1] à un détail près : ligne 46, on termine la fonction asynchrone sur un échec (utilisation de la méthode [reject]) ;
  • ligne 46 : le paramètre de la fonction [reject] est une instance de la classe [Error]. Le paramètre du constructeur [Error] est le message de l’erreur ;
  • ligne 48 : cette erreur est interceptée par la méthode [catch] de la [Promise] qui reçoit l’erreur en paramètre. On utilise alors la fonction [context.error] pour déclarer l’erreur. Le paramètre de la fonction [context.error] est un objet avec ici deux propriétés :
    • [statusCode] : un code HTTP d’erreur ;
    • [message] : un message d’erreur ;

Que [asyncData] soit exécutée par le client ou le serveur, en cas d’erreur [context.error], [nuxt] affiche la page [layouts / error.vue] :

image3

Bien que ce soit une page, la page [error.vue] est cherchée dans le dossier [layouts] (peut-être pour éviter qu’elle soit incluse dans les routes de l’application ?). Ici, la page [error.vue] est la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!-- définition HTML de la vue -->
<template>
  <!-- mise en page -->
  <Layout :left="true" :right="true">
    <!-- alerte dans la colonne de droite -->
    <template slot="right">
      <!-- message sur fond jaune -->
      <b-alert show variant="danger" align="center">
        <h4>L'erreur suivante s'est produite : {{ JSON.stringify(error) }}</h4>
      </b-alert>
    </template>
    <!-- menu de navigation dans la colonne de gauche -->
    <Navigation slot="left" />
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'

export default {
  name: 'Error',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // propriété [props]
  props: { error: { type: Object, default: () => 'waiting ...' } },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[error beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[error created, error=]', this.error)
  },
  beforeMount() {
    // client seulement
    console.log('[error beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[error mounted]')
  }
}
</script>

Lorsque [nuxt] affiche la page [error.vue], elle lui passe en propriété [props], l’erreur qui s’est produite (ligne 33). Si l’erreur a été provoquée par [context.error(objet1)], la propriété [props] de la page [error.vue] aura la valeur [objet1]. La documentation [nuxt] indique que [objet1] doit avoir au moins les attributs [statusCode, message]. La ligne 9 affiche la chaîne jSON de l’objet [objet1] reçu.

La page [page2]

La page [page2] montre une autre façon de gérer l’erreur :

  • dans [page1], l’erreur est affichée dans une page à part [error.vue] ;
  • dans [page2], l’erreur sera affichée dans la page [page2] qui a provoqué l’erreur ;

Le code de [page2] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<!-- vue n° 2 -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary">
      Page 2
    </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Page2',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  asyncData(context) {
    // log
    console.log('[page2 asyncData started]')
    // début attente
    context.app.$eventBus().$emit('loading', true)
    // pas d'erreur
    context.app.$eventBus().$emit('errorLoading', false)
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function() {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
        // on génére arbitrairement une erreur
        const errorLoadingMessage = "le serveur n'a pas répondu assez vite"
        // fin avec succès
        resolve({ showErrorLoading: true, errorLoadingMessage })
        // log
        console.log('[page2 asyncData finished]')
      }, 5000)
    })
  },
  // cycle de vie
  beforeCreate() {
    console.log('[page2 beforeCreate]')
  },
  created() {
    console.log('[page2 created]')
  },
  beforeMount() {
    console.log('[page2 beforeMount]')
  },
  mounted() {
    console.log('[page2 mounted]')
    // client
    if (this.showErrorLoading) {
      console.log('[page2 mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>

De nouveau, on insère une fonction [asyncData] dans le code de la page et comme [index], [page2] va générer une erreur qu’on va cette fois gérer différemment.

  • ligne 44 : le serveur comme le client terminent la promesse sur un succès en rendant le résultat [{ showErrorLoading: true, errorLoadingMessage }]. On sait que cela va avoir pour effet d’inclure les propriétés [showerrorLoading, errorLoadingMessage] dans les propriétés [data] de la page et que le client va recevoir ces propriétés ;

  • lignes 60-67 : on sait que la fonction [mounted] n’est exécutée que par le client ;

  • ligne 63 : le client teste si la propriété [showErrorLoading] a été positionnée (par le serveur ou le client selon les cas). Si oui, il émet l’événement [‘errorLoading’] (ligne 65) pour que la page [default] affiche le message d’erreur [this.errorLoadingMessage]. Au final, le serveur envoie une page sans message d’erreur affiché. Celui-ci est affiché au dernier moment par le client lorsque la page est ‘montée’ ;

    1. Exécution

      1. [nuxt.config]

Le fichier [nuxt.config.js] d’exécution est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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/event-bus' }],
  /*
   ** 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'
  ],
  /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {},
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, ctx) {}
  },
  // répertoire du code source
  srcDir: 'nuxt-11',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-11/'
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: 'localhost'
  }
}
  • ligne 22 : on met la propiété [loading] à [false] pour que [nuxt] n’utilise pas son image d’attente par défaut ;

  • ligne 31 : le plugin qui définit le bus d’événements ;

    1. La page [index] exécutée par le serveur

Demandons la page [index] au serveur (on tape l’URL [http://localhost:81/nuxt-11/] à la main). La page affichée par le navigateur client est la suivante :

image4

Les logs sont les suivants :

image5

  • en [3], on voit que le serveur envoie la page [error.vue] ;
  • en [4], on voit que le client affiche lui aussi la page [error] avec la même erreur que le serveur ;
  • on peut remarquer que la méthode [mShowLoading] de la page [default] n’a pas été appelée côté serveur alors même que la page [index] avait activé une attente. Cette méthode est appelée à réception d’un événement et visiblement la gestion événementielle n’est pas implémentée côté serveur ;

Examinons le code source de la page reçue par le navigateur client :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!doctype html>
<html data-n-head-ssr>
<head>
  <title>Introduction à [nuxt.js]</title>
  <meta data-n-head="ssr" charset="utf-8">
  <meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
  <meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
  <link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
  <base href="/nuxt-11/">
  <link rel="preload" href="/nuxt-11/_nuxt/runtime.js" as="script">
  <link rel="preload" href="/nuxt-11/_nuxt/commons.app.js" as="script">
  <link rel="preload" href="/nuxt-11/_nuxt/vendors.app.js" as="script">
  <link rel="preload" href="/nuxt-11/_nuxt/app.js" as="script">
  ...
</head>
<body>
  <div data-server-rendered="true" id="__nuxt">
    <div id="__layout">
      <div class="container">
        <div class="card">
          <div class="card-body">
            <div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
            <h4>[nuxt-11] : personnalisation de l'attente, gestion des erreurs</h4>
              </div>
            <div>
              <div class="row">
                <div class="col-2">
                  <ul class="nav flex-column">
                    <li class="nav-item">
                      <a href="/nuxt-11/" target="_self" class="nav-link active nuxt-link-active">
                        Home
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-11/page1" target="_self" class="nav-link">
                        Page 1
                      </a>
                    </li>
                    <li class="nav-item">
                      <a href="/nuxt-11/page2" target="_self" class="nav-link">
                        Page 2
                      </a>
                    </li>
                  </ul>
                </div> <div class="col-10"><div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-danger">
                <h4>L'erreur suivante s'est produite : {&quot;statusCode&quot;:500,&quot;message&quot;:&quot;le serveur n'a pas répondu assez vite&quot;}</h4>
                    </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script>window.__NUXT__ = (function (a, b, c, d) {
  d.statusCode = 500; d.message = "le serveur n'a pas répondu assez vite";
  return {
    layout: "default", data: [d], error: d, serverRendered: true,
    logs: [
      { date: new Date(1575047424168), args: ["[event-bus créé]"], type: a, level: b, tag: c },
      { date: new Date(1575047424175), args: ["[page1 asyncData started]"], type: a, level: b, tag: c },
      { date: new Date(1575047429455), args: ["[page1 asyncData finished]"], type: a, level: b, tag: c },
      { date: new Date(1575047429515), args: ["[default beforeCreate]"], type: a, level: b, tag: c },
      { date: new Date(1575047429675), args: ["[default created]"], type: a, level: b, tag: c },
      { date: new Date(1575047430157), args: ["[error beforeCreate]"], type: a, level: b, tag: c },
      { date: new Date(1575047430246), args: ["[error created, error=]", "{ statusCode: 500,\n  message: 'le serveur n\\'a pas répondu assez vite' }"], type: a, level: b, tag: c }]
  }
    }("log", 2, "", {}));</script>
  <script src="/nuxt-11/_nuxt/runtime.js" defer></script>
  <script src="/nuxt-11/_nuxt/commons.app.js" defer></script>
  <script src="/nuxt-11/_nuxt/vendors.app.js" defer></script>
  <script src="/nuxt-11/_nuxt/app.js" defer></script>
</body>
</html>
  • ligne 57 : on voit que le serveur a envoyé un objet [d] qui représente l’erreur qui s’est produite côté serveur ;

  • ligne 59 : on voit une propriété [error] ayant pour valeur l’objet [d]. On peut imaginer que c’est la présence de la propriété [error] dans la page envoyée par le serveur qui fait que les scripts client vont afficher la page [error.vue] avec l’erreur [error] ;

    1. La page [page1] exécutée par le serveur

On tape à la main l’URL [http://localhost:81/nuxt-11/page1]. Au bout de 5 secondes, le navigateur affiche la page suivante :

image6

Les logs affichés sont les suivants :

image7

  • en [1], les logsdu serveur. On peut remarquer que la méthode [mShowLoading] de la page [default] n’a pas été appelée ;

  • en [2], les logs du client ;

    1. La page [page2] exécutée par le serveur

Nous tapons à la main l’URL [http://localhost:81/nuxt-11/page2]. Au bout de 5 secondes, le navigateur affiche la page suivante :

image8

Examinons les logs affichés dans le navigateur :

image9

  • en [1], les logs du serveur. On rappelle que le serveur a mis les propriétés [showErrorLoading, errorLoadingMessage] dans la page envoyée au navigateur client. On sait qu’alors ces propriétés vont être intégrées dans les [data] de la page affichée par le client

  • en [3], lorsque la page [page2] est montée, elle trouve la propriété [showErrorLoading] à vrai. Elle envoie alors un événement à la page [default], pour qu’elle affiche le message d’erreur envoyé par le serveur [4] ;

    1. La page [index] exécutée par le client

On utilise maintenant les liens de navigation pour afficher les trois pages. Toutes les pages affichées par le client sont identiques à celles affichées par le serveur. La seule différence est que l’image d’attente de la fin des 5 secondes est affichée à chaque fois.

On commence par la page [index]. L’image d’attente est alors affichée :

image10

puis au bout de 5 secondes, on obtient la page suivante :

image11

La page finale est donc identique à celle obtenue côté serveur.

image12

Rappelons la fonction [asyncData] de la page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // début attente
    context.app.$eventBus().$emit('loading', true)
    // pas d'erreur
    context.app.$eventBus().$emit('errorLoading', false)
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function() {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // on rend une erreur
        reject(new Error("le serveur n'a pas répondu assez vite"))
      }, 5000)
    }).catch((e) => context.error({ statusCode: 500, message: e.message }))
}

Les logs du client sont les suivants :

image13

  • en [1], la fonction [asyncData] démarre ;

  • en [2], on met en route l’image d’attente ;

  • en [2-3], on voit que la page [default] a reçu les événements [loading, true] [2] et [errorLoading, false] envoyés par la fonction [asyncData] de la page [index] (lignes 5 et 7);

  • en [4], fin de l’attente. La page [default] a reçu l’événement [loading, false] envoyé par la page [index] (ligne 13);

  • en [5], la fonction [asyncData] a terminé son travail ;

  • parce que la fonction [asyncData] a créé une erreur avec [context.error] (ligne 19), la page [error] est affichée [6];

    1. La page [page1] exécutée par le client

Après l’attente de 5 secondes, le client affiche la page suivante :

image14

Rappelons le code de la fonction [asyncData] de [page1] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
asyncData(context) {
    // log
    console.log('[page1 asyncData started]')
    // début attente
    context.app.$eventBus().$emit('loading', true)
    // pas d'erreur
    context.app.$eventBus().$emit('errorLoading', false)
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function() {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
        // log
        console.log('[page1 asyncData finished]')
        // on rend le résultat asynchrone - un nombre aléatoire ici
        resolve({ result: Math.floor(Math.random() * Math.floor(100)) })
      }, 5000)
    })
},

Les logs sont les suivants :

image15

La page [page2] exécutée par le client

Après l’attente de 5 secondes, le client affiche la page suivante :

image16

Rappelons le code des fonctions [asyncData] et [mounted] de [page2] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
asyncData(context) {
    // log
    console.log('[page2 asyncData started]')
    // début attente
    context.app.$eventBus().$emit('loading', true)
    // pas d'erreur
    context.app.$eventBus().$emit('errorLoading', false)
    // on rend une promesse
    return new Promise(function(resolve, reject) {
      // on simule une fonction asynchrone
      setTimeout(function() {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
        // on génére arbitrairement une erreur
        const errorLoadingMessage = "le serveur n'a pas répondu assez vite"
        // fin avec succès
        resolve({ showErrorLoading: true, errorLoadingMessage })
        // log
        console.log('[page2 asyncData finished]')
      }, 5000)
    })
  }

mounted() {
    console.log('[page2 mounted]')
    // client
    if (this.showErrorLoading) {
      console.log('[page2 mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}

Les logs sont les suivants :

image17

  • en [1], la page [default] a reçu l’événement [showErrorLoading, true] envoyé par [page2] (ligne 29) qui lui demande d’afficher le message d’erreur ;

Exemple [nuxt-12] : requêtes HTTP avec axios

Présentation

Dans ce nouvel exemple, nous allons découvrir comment dans les fonctions [asyncData] on peut faire des requêtes HTTP avec la bibliothèque [axios]. Par ailleurs, nous allons utiliser des notions déjà acquises :

  • l’utilisation de plugins de l’exemple [nuxt-06] :
  • la persistance du store dans un cookie de session de l’exemple [nuxt-06] ;
  • le contrôle de la navigation avec des middlewares de l’exemple [nuxt-09] ;
  • la gestion des erreurs  de l’exemple [nuxt-11] ;

L’architecture de l’exemple sera le suivant :

image0

  • l’application [nuxt] sera stockée sur le serveur [node.js] [3], téléchargée par le navigateur [1] qui l’exécutera ensuite ;
  • le client [nuxt] [1] aussi bien que le serveur [nuxt] [3] feront des requêtes HTTP au serveur de données [2]. Ce serveur sera le serveur de calcul de l’impôt développé dans la partie PHP 7. Nous utiliserons sa dernière version, la version 14, avec les requêtes CORS autorisées ;

L’architecture de l’exemple peut être simplifiée de la façon suivante :

image1

  • en [1], le serveur [node.js] délivre les pages [nuxt] au navigateur [2]. C’est la couche [web] [8] du serveur qui délivre ces pages. Pour délivrer la page, le serveur a pu demander des données externes au serveur de données [3]. C’est la couche [DAO] [9] qui fait les requêtes HTTP nécessaires ;
  • à chaque appel de page au serveur [node.js][1], le navigateur [2] reçoit la totalité de l’application [nuxt] qui va alors s’exécuter en mode SPA. Le bloc [UI] (User Interface) [4] présente des pages [vue.js] à l’utilisateur. Les actions de celui-ci ou le cycle de vie naturel des pages peuvent provoquer des appels de données externes au serveur de données [3]. C’est la couche [DAO] [5] qui fait alors les requêtes HTTP nécessaires ;

Arborescence du projet

image2

Le fichier de configuration [nuxt.config.js]

Le projet sera contrôlé par le fichier [nuxt.config.js] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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) { }
  },
  // répertoire du code source
  srcDir: 'nuxt-12',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-12/',
    // middleware de routage
    middleware: ['routing']
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: 'localhost'
  },
  // environnement
  env: {
    // configuration axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // configuration du cookie de session [nuxt]
    maxAge: 60 * 5
  }
}
  • ligne 22 : nous gérons nous-mêmes l’alerte d’attente de fin d’une action asynchrone ;
  • ligne 31 : nous allons utiliser divers plugins qui seront spécialisés soit pour le client soit pour le serveur mais pas pour les deux à la fois ;
  • ligne 52 : le module [axios] est intégré à [nuxt]. Cela va avoir pour conséquence que l’objet [axios] qui fera les requêtes HTTP de l’application [nuxt] vers le serveur PHP de calcul de l’impôt sera disponible dans [context.$axios] ;
  • ligne 54 : le module [cookie-universal-nuxt] va nous permettre de sauvegarder la session [nuxt] dans un cookie ;
  • ligne 60 : la propriété [axios] nous permet de configurer le module [@nuxtjs/axios] de la ligne 52. Nous n’utiliserons pas cette possibilité à laquelle nous préfèrerons la propriété [env] de la ligne 88 ;
  • ligne 90 : durée d’attente maximale de la réponse du serveur de calcul de l’impôt ;
  • ligne 91 : nécessaire au client [nuxt] - autorise l’utilisation de cookies dans les échanges avec le serveur de calcul de l’impôt ;
  • ligne 92 : l’URL de base du serveur de calcul de l’impôt ;
  • ligne 94 : durée de vie de la session nuxt (5 mn) ;
  • ligne 77 : la navigation des client et serveur [nuxt] sera contrôlée par un middleware de routage ;

La couche [UI] de l’application

image3

Nous allons donner à l’application [nuxt] l’accès à l’API du serveur de calcul de l’impôt via la vue suivante :

image4

  • en [2], le menu qui donne accès à l’API du serveur de calcul de l’impôt :
    • [Authentification] : correspond à la page [authentification]. Cette page fait une requête d’authentification auprès du serveur de calcul de l’impôt avec les identifiants [admin, admin] qui sont pour l’instant les seuls autorisés. Le résultat affiché est analogue à [3] ;
    • [Requête AdminData] : correspond à la page [get-admindata]. Cette page demande au serveur de calcul de l’impôt, les données, appelées ici [adminData], qui permettent le calcul de l’impôt. Le résultat affiché est analogue à [3] ;
    • [Fin session impôt] : correspond à la page [fin-session]. Cette page fait une requête de fin de session PHP auprès du serveur de calcul de l’impôt. Le serveur annule alors la session PHP courante et en initialise une nouvelle vierge ;

Les couches [dao] de l’application [nuxt]

Comme indiqué plus haut, l’architecture de l’application [nuxt] sera la suivante :

image5

  • en [1], le serveur [node.js] délivre les pages [nuxt] au navigateur [2]. C’est la couche [web] [8] du serveur qui délivre ces pages. Pour délivrer la page, le serveur a pu demander des données externes au serveur de données [3]. C’est la couche [DAO] [9] qui fait les requêtes HTTP nécessaires ;
  • à chaque appel de page au serveur [node.js][1], le navigateur [2] reçoit la totalité de l’application [nuxt] qui va alors s’exécuter en mode SPA. Le bloc [UI] (User Interface) [4] présente des pages [vue.js] à l’utilisateur. Les actions de celui-ci ou le cycle de vie des pages peuvent provoquer des appels de données externes au serveur de données [3]. C’est la couche [DAO] [5] qui fait alors les requêtes HTTP nécessaires ;

Nous utiliserons la version 14 du serveur de calcul de l’impôt développé dans le document |Introduction au langage PHP7 par l’exemple|. Nous utiliserons une partie seulement de son API (Application Programming Interface) jSON :

Requête Réponse
[initial

isation d’une session jSON avec le serveur de calcul de l’impôt]

[une session PHP est créée avec le serveur de calcul de l’impôt]

GET main.p

hp?action=init-session&type=json

{
    "action": "init-session",
    "état": 700,
    "réponse": "se
ssion démarrée avec type [json] »
}
[au
thentification de l’utilisateur]
[l’authentification

est stockée dans la session PHP]

POST main.php

?action=authentifier-utilisateur

paramètres postés : user, admin
{
    "acti
on »: « authentifier-utilisateur »,
« état »: 200, « réponse »: « Authenti
fication réussie [admin, admin] »
}
[demande des donn
ées de l’administration fiscale]
[les données reçues so

nt stockées dans la session PHP]

G

ET main.php?action=get-admindata

{

   "action": "get-admindata",
    "état": 1000,
    "réponse": {
        "limites": [
            9964,
            27519,
            73779,
            156244,
            0
        ],
        "coeffR": [
            0,
            0.14,
            0.3,
            0.41,
            0.45
        ],
        "coeffN": [
            0,
            1394.96,
            5798,
            13913.69,
            20163.45
        ],
      « plafondQfDemiPart »: 1551,
« plafondRevenusC
elibatairePourReduction »: 21037,
« plafondRev

enusCouplePourReduction »: 42074,

    « valeurReducDemiPart »: 3797,
« 

plafondDecoteCelibataire »: 1196,

    « plafondDecoteCouple »: 1970,
« plaf
ondImpotCouplePourDecote »: 2627,
« plafondIm
potCelibatairePourDecote »: 1595,
« a

battementDixPourcentMax »: 12502,

 « abattementDixPourcentMin »: 437
    } }
[fin de la session PHP avec
le serveur de calcul de l’impôt]
[supprime la session PHP cou

rante et crée une nouvelle sessi on PHP. Dans celle-ci, la sessio n jSON reste activée, mais l’uti lisateur n’est plus authentifié]

GET main.php?action=fin-session
{
    "action": "fin-session",
    "état": 400,
  « réponse »: « session supprimée »
}
Le couche [dao] du serveur [nuxt]

image6

Le serveur [node.js] [1] va utiliser la couche [dao] décrite dans le document |Introduction au framework VUE.JS par l’exemple|. Nous rappelons ici son code :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
'use strict';

// imports
import qs from 'qs'

class Dao {

  // constructeur
  constructor(axios) {
    this.axios = axios;
    // cookie de session
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
  }

  // init session
  async  initSession() {
    // options de la requête HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // paramètres de l'URL
      params: {
        action: 'init-session',
        type: 'json'
      }
    };
    // exécution de la requête HTTP
    return await this.getRemoteData(options);
  }

  async  authentifierUtilisateur(user, password) {
    // options de la requête HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corps du POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // paramètres de l'URL
      params: {
        action: 'authentifier-utilisateur'
      }
    };
    // exécution de la requête HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // options de la requête HHTP [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // paramètres de l'URL
      params: {
        action: 'get-admindata'
      }
    };
    // exécution de la requête HTTP
    const data = await this.getRemoteData(options);
    // résultat
    return data;
  }

  async  getRemoteData(options) {
    // pour le cookie de session
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // exécution de la requête HTTP
    let response;
    try {
      // requête asynchrone
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
      if (error.response) {
        // la réponse du serveur est dans [error.response]
        response = error.response;
      } else {
        // on relance l'erreur
        throw error;
      }
    }
    // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // on récupère le cookie de session s'il existe
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie est un tableau
      // on cherche le cookie de session dans ce tableau
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // on cherche le cookie de session
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // on mémorise le cookie de session
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // on a trouvé
          trouvé = true;
        } else {
          // élément suivant
          i++;
        }
      }
    }
    // la réponse du serveur est dans [response.data]
    return response.data;
  }
}

// export de la classe
export default Dao;
  • toutes les méthodes de la couche [dao] rendent l’objet envoyé par le serveur de données [{action : ‘xx’, état : nn, réponse : {…}] avec :
    • [action] : le nom de l’action exécutée par le serveur de données ;
    • [état] : indicateur numérique :
      • [initSession] : état=700 pour une réponse sans erreur ;
      • [authentifierUtilisateur] : état=200 pour une réponse sans erreur ;
      • [getAdminData] : état=1000 pour une réponse sans erreur ;
      • [fin-session] : état=400 pour une réponse sans erreur ;
    • [réponse] : réponse associée à l’indicateur numérique [état]. Peut varier selon cet indicateur numérique ;

Examinons le constructeur de la classe [Dao] :

1
2
3
4
5
6
7
// constructeur
  constructor(axios) {
    this.axios = axios;
    // cookie de session
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
}
  • ligne 2 : l’objet [axios] fourni en argument au constructeur est fourni par le code appelant. C’est lui qui va faire les requêtes HTTP ;
  • ligne 5 : le nom du cookie de session envoyé par le serveur de données écrit en PHP ;
  • ligne 6 : le cookie de session qui est échangé entre la couche [dao] et le serveur de données. Celui-ci est initialisé par la fonction [getRemoteData] des lignes 67-113 ;

Pour le cookie de session, il nous faut considérer deux couches [dao] séparées :

  • celle du navigateur ;
  • celle du serveur ;

Nous allons devoir gérer trois cookies de session :

  1. celui échangé entre le client [nuxt] et le serveur PHP 7 ;
  2. celui échangé entre le serveur [nuxt] et le serveur PHP 7 ;
  3. celui échangé entre le client [nuxt] et le serveur [nuxt] ;

Nous ferons en sorte que le cookie de la session avec le serveur PHP soit le même pour le client et le serveur [nuxt]. Nous appellerons ce cookie, cookie de la session PHP. Ce cookie est celui des cas 1 et 2. Nous appellerons cookie de la session [nuxt], le cookie du cas 3. Nous aurons donc deux sessions :

  • une session PHP avec le cookie de session PHP ;
  • une session [nuxt] avec le cookie de session [nuxt] ;

Pourquoi utiliser le même cookie pour les sessions PHP du client et du navigateur [nuxt] ? Nous voulons que l’application puisse dialoguer avec le serveur PHP 7 indifféremment avec le client ou le serveur [nuxt] :

  • si une action A du serveur [nuxt] met le serveur PHP dans un état E, cet état est reflété dans la session PHP entretenue par le serveur PHP ;
  • en utilisant le même cookie de session PHP que le serveur, une action B du client [nuxt] qui suivrait l’action A du serveur [nuxt] retrouverait le serveur PHP dans l’état E laissé par le serveur [nuxt] et pourrait donc s’appuyer sur le travail déjà fait par le serveur [nuxt] ;
  • si après l’action B du client [nuxt], vient une action C du serveur [nuxt], pour la même raison que précédemment, cette action va pouvoir s’appuyer sur le travail fait par l’action B du client [nuxt] ;

Pour que le navigateur du client [nuxt] puisse dialoguer avec le serveur PHP du calcul de l’impôt, nous utiliserons la version 14 de ce serveur qui autorise les appels inter-domaines, ç-à-d ceux d’un navigateur vers le serveur PHP. Les appels du serveur [nuxt] vers le serveur PHP ne sont pas, eux, des appels inter-domaines. Cette notion n’existe que pour les appels faits depuis un navigateur.

Revenons au code du constructeur de la classe [Dao] précédente :

1
2
3
4
5
6
7
// constructeur
  constructor(axios) {
    this.axios = axios;
    // cookie de session
    this.sessionCookieName = "PHPSESSID";
    this.sessionCookie = '';
}
  • les lignes 5 et 6 correspondent au cookie de la session PHP avec le serveur de calcul de l’impôt ;

La gestion du cookie de la session PHP ci-dessus ne convient pas au serveur [nuxt] : sa couche [dao] est instanciée à chaque nouvelle requête faite au serveur [nuxt]. On se rappelle en effet que demander une page au serveur [nuxt] revient à réinitialiser l’application [nuxt]. Ainsi lorsqu’à l’issue de la 1ère requête faite au serveur de données par le serveur [nuxt], le cookie de session PHP de la couche [dao] est initialisé, cette valeur est perdue lors de la requête HTTP suivante du même serveur [nuxt], car entre-temps sa couche [dao] a été recréée, le constructeur réexécuté et le cookie de session PHP réinitialisé avec la chaîne vide (ligne 6) ;

Une solution est d’utiliser un autre constructeur pour la couche [dao] du serveur :

1
2
3
4
5
6
7
8
9
// constructeur
  constructor(axios, phpSessionCookie) {
    // bibliothèque axios
    this.axios = axios
    // valeur du cookie de session
    this.phpSessionCookie = phpSessionCookie
    // nom du cookie de session du serveur PHP
    this.phpSessionCookieName = 'PHPSESSID'
  }
  • ligne 2 : cette fois-ci le cookie de la session PHP sera fourni au constructeur de la couche [dao] du serveur de données ;

Comment le serveur [nuxt] pourra-t-il fournir ce cookie de session PHP au constructeur de sa couche [dao] ? Nous stockerons le cookie de session PHP dans le cookie de session [nuxt] échangé entre le navigateur et le serveur [nuxt]. Le processus est le suivant :

  1. l’application [nuxt] est lancée ;
  2. lorsque le serveur [nuxt] fait sa 1ère requête HTTP avec le serveur PHP, il stocke le cookie de la session PHP qu’il a reçu dans le cookie de session [nuxt] qu’il échange avec le client [nuxt] ;
  3. le navigateur qui loge le client [nuxt] reçoit ce cookie de session [nuxt] et le renvoie donc systématiquement à chaque nouvelle requête au serveur [nuxt] ;
  4. lorsque le serveur [nuxt] devra faire une nouvelle requête au serveur PHP, il retrouvera le cookie de session PHP dans le cookie de session [nuxt] que le navigateur lui aura envoyé. Il l’enverra alors au serveur PHP ;

Il y a bien deux cookies de session et il ne faut pas les confondre :

  • le cookie de session [nuxt] échangé entre le serveur [nuxt] et le navigateur du client [nuxt] ;
  • le cookie de session PHP échangé entre le serveur [nuxt] et le serveur PHP ou entre le client [nuxt] et le serveur PHP ;

Revenons maintenant sur le code de la méthode de la classe [Dao]. Elle n’inclut pas de fonction pour clôre la session PHP avec le serveur de calcul de l’impôt. Nous ajoutons celle-ci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// fin de la session de calcul de l'impôt
  async finSession() {
    // options de la requête HHTP  [get /main.php?action=fin-session]
    const options = {
      method: 'GET',
      // paramètres de l'URL
      params: {
        action: 'fin-session'
      }
    }
    // exécution de la requête HTTP
    const data = await this.getRemoteData(options)
    // résultat
    return data
  }

Aux tests, on découvre que la fonction [getRemoteData] appelée ligne 12 ne convient pas pour la méthode [finSession] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
async  getRemoteData(options) {
    // pour le cookie de session
    if (!options.headers) {
      options.headers = {};
    }
    options.headers.Cookie = this.sessionCookie;
    // exécution de la requête HTTP
    let response;
    try {
      // requête asynchrone
      response = await this.axios.request('main.php', options);
    } catch (error) {
      // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
      if (error.response) {
        // la réponse du serveur est dans [error.response]
        response = error.response;
      } else {
        // on relance l'erreur
        throw error;
      }
    }
    // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // on récupère le cookie de session s'il existe
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      // setCookie est un tableau
      // on cherche le cookie de session dans ce tableau
      let trouvé = false;
      let i = 0;
      while (!trouvé && i < setCookie.length) {
        // on cherche le cookie de session
        const results = RegExp('^(' + this.sessionCookieName + '.+?);').exec(setCookie[i]);
        if (results) {
          // on mémorise le cookie de session
          // eslint-disable-next-line require-atomic-updates
          this.sessionCookie = results[1];
          // on a trouvé
          trouvé = true;
        } else {
          // élément suivant
          i++;
        }
      }
    }
    // la réponse du serveur est dans [response.data]
    return response.data;
  }
  • lignes 30-43 : on recherche le cookie [PHPSESSID=xxx]. Si on le trouve, il est mémorisé dans la classe (ligne 36) ;

Ce code ne convient pas à la nouvelle méthode [finSession] car sur l’action [fin-session], le serveur PHP envoie deux cookies avec le nom [PHPSESSID]. Voici un exemple obtenu avec un client [Postman] :

image7

  • en [1], la demande du client [Postman] ;
  • en [3], la réponse du serveur PHP ;
  • en [4], les entêtes HTTP de la réponse du serveur PHP ;

image8

  • en [5], le serveur PHP indique d’abord qu’il a supprimé la session PHP courante ;
  • en [6], le serveur PHP envoie le cookie de la nouvelle session PHP ;

Avec le code actuel, la fonction [getRemoteData] récupère le cookie [5] alors que c’est le cookie [6] qu’il faut mémoriser.

Il faut donc faire évoluer le code de la fonction [getRemoteData] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
async getRemoteData(options) {
    // y-a-t-il un cookie de session PHP ?
    if (this.phpSessionCookie) {
      // y-a-t-il des entêtes ?
      if (!options.headers) {
        // on crée un objet vide
        options.headers = {}
      }
      // entête du cookie de session PHP
      options.headers.Cookie = this.phpSessionCookie
    }
    // exécution de la requête HTTP
    let response
    try {
      // requête asynchrone
      response = await this.axios.request('main.php', options)
    } catch (error) {
      // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
      if (error.response) {
        // la réponse du serveur est dans [error.response]
        response = error.response
      } else {
        // on relance l'erreur
        throw error
      }
    }
    // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // on cherche le cookie de session PHP dans les cookies reçus
    // tous les cookies reçus
    const cookies = response.headers['set-cookie']
    if (cookies) {
      // cookies est un tableau
      // on cherche le cookie de session PHP dans ce tableau
      let trouvé = false
      let i = 0
      while (!trouvé && i < cookies.length) {
        // on cherche le cookie de session PHP
        const results = RegExp('^(' + this.phpSessionCookieName + '.+?)$').exec(cookies[i])
        if (results) {
          // on mémorise le cookie de session PHP
          const phpSessionCookie = results[1]
          // y-a-t-il dedans le mot [deleted] ?
          const results2 = RegExp(this.phpSessionCookieName + '=deleted').exec(phpSessionCookie)
          if (!results2) {
            // on a le bon cookie de session PHP
            this.phpSessionCookie = phpSessionCookie
            // on a trouvé
            trouvé = true
          } else {
            // élément suivant
            i++
          }
        } else {
          // élément suivant
          i++
        }
      }
    }
    // la réponse du serveur est dans [response.data]
    return response.data
  }
  • ligne 41 : on a trouvé un cookie avec le nom [PHPSESSID]. on le mémorise localement ;
  • ligne 43 : on regarde si dans le cookie sauvegardé, il y a la chaîne [PHPSESSID=deleted] ;
  • ligne 46 : si la réponse est non, alors c’est qu’on a trouvé le bon cookie [PHPSESSID]. On le mémorise dans la classe ;

Après la fonction [getRemoteData], le cookie de session PHP est mémorisé dans la classe, dans [this.phpSessionCookie]. On a dit que la classe était instanciée à chaque nouvelle requête HTTP du serveur [nuxt]. Le cookie de session PHP doit donc être exfiltré de la classe. Pour cela, on ajoute une nouvelle méthode à celle-ci :

1
2
3
4
// accès au cookie de la session PHP
  getPhpSessionCookie() {
    return this.phpSessionCookie
}
  • le serveur [nuxt] demande une action à sa couche [dao] en fournissant le cookie de la session PHP à son constructeur, s’il en a un ;
  • une fois l’action faite, le serveur [nuxt] récupère le cookie de session PHP mémorisé par la couche [dao] à l’aide de la méthode [getPhpSessionCookie] précédente. Ce cookie peut être le même que le précédent ou un autre. Ce dernier cas arrive à deux occasions :
    • lors de l’exécution de la méthode [initSession] (il n’y avait pas de cookie de session PHP avant) ;
    • lors de l’exécution de la méthode [finSession] (le serveur PHP change le cookie de session PHP) ;

Notons une particularité sur le cookie de session PHP. Le serveur [nuxt] ne reçoit pas toujours ce cookie de la part du serveur PHP. En effet, celui-ci ne l’envoie qu’une fois. Ensuite il ne l’envoie plus. Lorsqu’on regarde le code de [getRemoteData] et celui de [getPhpSessionCookie] on verra alors que lorsque le serveur PHP n’envoie pas de cookie de session, la fonction [getPhpSessionCookie] renvoie alors le cookie de session PHP fourni au constucteur. C’est ainsi que le serveur envoie toujours au serveur PHP le dernier cookie de session PHP que celui-ci lui a envoyé.

La couche [dao] du client [nuxt]

image9

Pour le client [nuxt] qui s’exécute dans un navigateur, on reprend le code de la classe [Dao] du document |Introduction au framework VUE.JS par l’exemple| :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"use strict";

// imports
import qs from "qs";

class Dao {
  // constructeur
  constructor(axios) {
    this.axios = axios;
  }

  // init session
  async initSession() {
    // options de la requête HHTP [get /main.php?action=init-session&type=json]
    const options = {
      method: "GET",
      // paramètres de l'URL
      params: {
        action: "init-session",
        type: "json"
      }
    };
    // exécution de la requête HTTP
    return await this.getRemoteData(options);
  }

  async authentifierUtilisateur(user, password) {
    // options de la requête HHTP [post /main.php?action=authentifier-utilisateur]
    const options = {
      method: "POST",
      headers: {
        "Content-type": "application/x-www-form-urlencoded"
      },
      // corps du POST
      data: qs.stringify({
        user: user,
        password: password
      }),
      // paramètres de l'URL
      params: {
        action: "authentifier-utilisateur"
      }
    };
    // exécution de la requête HTTP
    return await this.getRemoteData(options);
  }

  async getAdminData() {
    // options de la requête HHTP  [get /main.php?action=get-admindata]
    const options = {
      method: "GET",
      // paramètres de l'URL
      params: {
        action: "get-admindata"
      }
    };
    // exécution de la requête HTTP
    const data = await this.getRemoteData(options);
    // résultat
    return data;
  }

  async getRemoteData(options) {
    // exécution de la requête HTTP
    let response;
    try {
      // requête asynchrone
      response = await this.axios.request("main.php", options);
    } catch (error) {
      // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
      if (error.response) {
        // la réponse du serveur est dans [error.response]
        response = error.response;
      } else {
        // on relance l'erreur
        throw error;
      }
    }
    // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // la réponse du serveur est dans [response.data]
    return response.data;
  }
}

// export de la classe
export default Dao;

Ce code se distingue de la couche [dao] du serveur [nuxt] par le fait qu’il ne gère pas le cookie de la session PHP avec le serveur de calcul de l’impôt : c’est le navigateur qui le fait.

Nous allons, comme nous l’avons fait pour la couche [dao] du serveur [nuxt], ajouter une méthode [finSession] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// fin de la session de calcul de l'impôt
  async finSession() {
    // options de la requête HHTP  [get /main.php?action=fin-session]
    const options = {
      method: 'GET',
      // paramètres de l'URL
      params: {
        action: 'fin-session'
      }
    }
    // exécution de la requête HTTP
    const data = await this.getRemoteData(options)
    // résultat
    return data
  }

Lorsque le client [nuxt] exécute cette méthode, il reçoit, comme le serveur [nuxt], deux cookies de session PHP. C’est en fait le navigateur qui les reçoit et il gère correctement la situation : il ne garde que le cookie de la nouvelle session PHP qu’a initiée le serveur de calcul de l’impôt. Donc à la prochaine action du client [nuxt] vers le serveur PHP, le cookie de session PHP sera correct car c’est le navigateur qui envoie celui-ci. Il y a cependant un problème : le serveur [nuxt] n’a pas connaissance du fait que le cookie de session PHP a changé. Dans ses échanges avec le serveur PHP, il va alors envoyer un cookie de session PHP qui n’existe plus et on va avoir des problèmes. Il faudrait que le client [nuxt] avertisse le serveur [nuxt] que le cookie de session PHP a changé et lui transmette celui-ci. On sait comment il peut faire cela : via le cookie de session [nuxt], le cookie échangé entre le client et le serveur [nuxt]. Le client [nuxt] a au moins deux façons de récupérer le nouveau cookie de session PHP :

  1. en le demandant au navigateur ;
  2. en utilisant la méthode [getRemoteData] du serveur qui sait comment récupérer le nouveau cookie de session PHP ;

Nous allons utiliser la 2ième solution car elle est déjà toute prête. La méthode [getRemoteData] du client [nuxt] devient alors la suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
async getRemoteData(options) {
    // exécution de la requête HTTP
    let response
    try {
      // requête asynchrone
      response = await this.axios.request('main.php', options)
    } catch (error) {
      // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
      if (error.response) {
        // la réponse du serveur est dans [error.response]
        response = error.response
      } else {
        // on relance l'erreur
        throw error
      }
    }
    // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // on cherche le cookie de session PHP dans les cookies reçus
    // tous les cookies reçus
    const cookies = response.headers['set-cookie']
    if (cookies) {
      // cookies est un tableau
      // on cherche le cookie de session PHP dans ce tableau
      let trouvé = false
      let i = 0
      while (!trouvé && i < cookies.length) {
        // on cherche le cookie de session PHP
        const results = RegExp('^(' + this.phpSessionCookieName + '.+?)$').exec(cookies[i])
        if (results) {
          // on mémorise le cookie de session PHP
          const phpSessionCookie = results[1]
          // y-a-t-il dedans le mot [deleted] ?
          const results2 = RegExp(this.phpSessionCookieName + '=deleted').exec(phpSessionCookie)
          if (!results2) {
            // on a le bon cookie de session PHP
            this.phpSessionCookie = phpSessionCookie
            // on a trouvé
            trouvé = true
          } else {
            // élément suivant
            i++
          }
        } else {
          // élément suivant
          i++
        }
      }
    }
    // la réponse du serveur est dans [response.data]
    return response.data
  }

On a gardé dans [getRemoteData] uniquement le code qui exploite la réponse du serveur PHP à la recherche du cookie de session PHP. On n’a pas gardé le code qui incluait le cookie de session PHP dans la requête au serveur PHP car c’est le navigateur qui abrite le client [nuxt] qui s’en charge.

Une fois le cookie de session PHP obtenu par le client [nuxt], celui-ci doit être mis dans la session [nuxt] pour que le serveur [nuxt] puisse en bébéficier. Ce n’est pas la couche [dao] qui s’occupe de cela mais elle donne accès par une méthode au cookie de session PHP qu’elle a mémorisé :

1
2
3
4
// accès au cookie de la session PHP
  getPhpSessionCookie() {
    return this.phpSessionCookie
}

La fonction [getPhpSessionCookie] ne rend pas toujours un cookie de session valide :

  • il faut se souvenir ici que la couche [dao] du client [nuxt] est persistante. Elle est instanciée une fois et reste ensuite en mémoire ;
  • tant que le serveur PHP n’envoie pas un cookie de session PHP au client [nuxt], la fonction [getPhpSessionCookie] du client [nuxt] renvoie une valeur [undefined] ;
  • lorsque le serveur PHP envoie un cookie de session PHP au client [nuxt], celui-ci est mémorisé dans [this.phpSessionCookie] et le restera tant qu’il ne sera pas changé par un nouveau cookie de session PHP envoyé par le serveur PHP. La fonction [getPhpSessionCookie] du client [nuxt] renvoie alors le dernier cookie de session PHP reçu ;

La couche [dao] du client [nuxt] ne diffère de celle du serveur [nuxt] que par un point : elle n’envoie pas le cookie de session PHP elle-même car c’est le navigateur qui le fait. Néanmoins on a préféré garder deux couches [dao] distinctes car les raisonnements qui mènent à leurs écritures respectives sont différents.

La session [nuxt]

image10

La session [nuxt] (entre client et serveur nuxt) sera encapsulée dans l’objet [session] suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* eslint-disable no-console */
// définition de la session
const session = {
  // contenu de la session
  value: {
    // store non initialisé
    initStoreDone: false,
    // valeur du store Vuex
    store: ''
  },
  // sauvegarde de la session dans un cookie
  save(context) {
    // sauvegarde du store en session
    this.value.store = context.store.state
    console.log('nuxt-session save=', this.value)
    // sauvegarde de la valeur de la session
    context.app.$cookies.set('nuxt-session', this.value, { path: context.base, maxAge: context.env.maxAge })
  },
  // 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)
  }
}
// export de la session
export default session
  • lignes 5-10 : la session n’a qu’une propriété [value] avec deux sous-propriétés :
    • [initStoreDone] qui indique si le store a été initialisé ou pas ;
    • [store] : la valeur [store.state] du store Vuex de l’application ;
  • lignes 12-18 : la méthode [save] sert à sauvegarder la session [nuxt] dans un cookie. On utilise ici la bibliothèque [cookie-universal-nuxt] pour gérer le cookie. On notera le nom du cookie de la session [nuxt] : [nuxt-session] (ligne 17) ;
  • lignes 20-26 : la méthode [reset] réinitialise la session [nuxt] ;
    • ligne 23 : le store Vuex est réinitialisé puis sauvegardé en session, ligne 25 ;

Les plugins de gestion de la session [nuxt]

image11

Le plugin de gestion de la session [nuxt] du serveur [nuxt]

Au démarrage de l’application, c’est le serveur [nuxt] qui opère le premier. C’est donc lui qui va initialiser la session [nuxt]. Le script [server/plgSession] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* eslint-disable no-console */

// import de la session
import session from '@/entities/session'

export default (context, inject) => {
  // gestion de la session serveur
  console.log('[plugin server plgSession]')

  // y-a-t-il une session existante ?
  const value = context.app.$cookies.get('nuxt-session')
  if (!value) {
    // nouvelle session
    console.log("[plugin server plgSession], démarrage d'une nouvelle session")
  } else {
    // session existante
    console.log("[plugin server plgSession], reprise d'une session existante")
    session.value = value
  }

  // on injecte une fonction dans [context, Vue] qui rendra la session courante
  inject('session', () => session)
}
  • ligne 4 : on importe le code de la session [nuxt] ;
  • ligne 11 : on récupère la valeur du cookie de la session [nuxt] ;
  • lignes 12-15 : si le cookie de la session [nuxt] n’existait pas, alors la session [nuxt] importée ligne 4 est suffisante. Il n’y a rien de plus à faire ;
  • lignes 15-19 : si le cookie de la session [nuxt] existait, alors ligne 18 on stocke sa valeur dans la session importée ligne 4 ;
  • ligne 22 : la session a été soit initialisée soit restaurée. On la rend disponible via la fonction [$session] ;

Le plugin de gestion de la session [nuxt] du client [nuxt]

Le script [client/plgSession] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* eslint-disable no-console */

// import de la session
import session from '@/entities/session'

export default (context, inject) => {
  // gestion de la session client
  console.log('[plugin client plgSession], reprise de la session [nuxt] du serveur')
  // on récupère la session existante du serveur nuxt
  session.value = context.app.$cookies.get('nuxt-session')

  // on injecte une fonction dans [context, Vue] qui rendra la session courante
  inject('session', () => session)
}
  • ligne 4 : la session [nuxt] est importée ;
  • ligne 10 : on récupère la session [nuxt] courante dans le cookie [nuxt-session] ;
  • ligne 13 : on rend la session [nuxt] importée ligne 4 au travers de la fonction injectée [$session] ;

Les plugins des couches [dao]

image12

Le plugin de la couche [dao] du client [nuxt]

Le script [client/plgDao] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* eslint-disable no-console */
// on crée un point d'accès à la couche [Dao]
import Dao from '@/api/client/Dao'
export default (context, inject) => {
  // configuration axios
  context.$axios.defaults.timeout = context.env.timeout
  context.$axios.defaults.baseURL = context.env.baseURL
  context.$axios.defaults.withCredentials = context.env.withCredentials
  // instanciation de la couche [dao]
  const dao = new Dao(context.$axios)
  // injection d'une fonction [$dao] dans le contexte
  inject('dao', () => dao)
  // log
  console.log('[fonction client $dao créée]')
}
  • ligne 3 : la couche [dao] du client [nuxt] est importée ;
  • lignes 6-8 : on configure l’objet [context.$axios] qui va faire les requêtes HTTP de la couche [dao] du client [nuxt] avec les informations du fichier [nuxt.config] :
1
2
3
4
5
6
7
8
9
// environnement
  env: {
    // configuration axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // configuration du cookie de session [nuxt]
    maxAge: 60 * 5
  }
  • ligne 10 : la couche [dao] du client [nuxt] est instanciée ;
  • ligne 12 : la fonction [$dao] est injectée dans le contexte et les pages du client. Cette fonction donne accès à la couche [dao] de la ligne 10 ;

On retiendra donc que pour avoir accès à la couche [dao] du client [nuxt] lorsque celui-ci est exécuté, on écrira :

  • [context.app.$dao()] là où le contexte est connu ;
  • [this.$dao()] dans une page [Vue.js] ;

Le plugin de la couche [dao] du serveur [nuxt]

Le script [server/plgDao] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* eslint-disable no-console */
// on crée un point d'accès à la couche [Dao]
import Dao from '@/api/server/Dao'
export default (context, inject) => {
  // configuration axios
  context.$axios.defaults.timeout = context.env.timeout
  context.$axios.defaults.baseURL = context.env.baseURL
  // on récupère le cookie de session
  const store = context.app.$session().value.store
  const phpSessionCookie = store ? store.phpSessionCookie : ''
  console.log('session=', context.app.$session().value, 'phpSessionCookie=', phpSessionCookie)
  // instanciation de la couche [dao]
  const dao = new Dao(context.$axios, phpSessionCookie)
  // injection d'une fonction [$dao] dans le contexte
  inject('dao', () => dao)
  // log
  console.log('[fonction server $dao créée]')
}
  • ligne 3 : la couche [dao] du serveur [nuxt] est importée ;
  • lignes 6-7 : on configure l’objet [context.$axios] qui va faire les requêtes HTTP de la couche [dao] du serveur [nuxt] avec les informations du fichier [nuxt.config] :
1
2
3
4
5
6
7
8
9
// environnement
  env: {
    // configuration axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // configuration du cookie de session [nuxt]
    maxAge: 60 * 5
  }
  • ligne 9 : on récupère le store de l’application [nuxt] ;
  • ligne 10 : si le store existe, on récupère le cookie de la session PHP car on en a besoin pour instancier la couche [dao] du serveur [nuxt] ;
  • ligne 13 : on instancie la couche [dao] du serveur [nuxt] ;
  • ligne 15 : la fonction [$dao] est injectée dans le contexte et les pages du serveur [nuxt]. Cette fonction donne accès à la couche [dao] de la ligne 13 ;

On retiendra donc que pour avoir accès à la couche [dao] du serveur [nuxt] lorsque celui-ci est exécuté, on écrira :

  • [context.app.$dao()] là où le contexte est connu ;
  • [this.$dao()] dans une page [Vue.js] ;

Le store Vuex

image13

Le store [Vuex] va mémoriser toutes les données qui doivent être partagées par les différentes composantes de l’application [pages, client, serveur] sans que pour autant ces données soient réactives.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* eslint-disable no-console */

// état du store
export const state = () => ({
  // session jSON démarrée
  jsonSessionStarted: false,
  // utilisateur authentifié
  userAuthenticated: false,
  // cookie de session PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})

// mutations du store
export const mutations = {
  // remplacement du state
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reset du store
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}

// actions du store
export const actions = {
  nuxtServerInit(store, context) {
    // qui exécute ce code ?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // init session
    initStore(store, context)
  }
}

function initStore(store, context) {
  // store est le store à initialiser
  // on récupère la session
  const session = context.app.$session()
  // la session a-t-elle été déjà initialisée ?
  if (!session.value.initStoreDone) {
    // on démarre un nouveau store
    console.log("nuxtServerInit, initialisation d'une nouvelle session")
    // on met le store dans la session
    session.value.store = store.state
    // le store est désormais initialisé
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // on met à jour le store avec le store de la session
    store.commit('replace', session.value.store)
  }
  // on sauvegarde la session
  session.save(context)
  // log
  console.log('initStore terminé, store=', store.state)
}

Les données mémorisées dans le store sont les suivantes :

  • ligne 6 : [jsonSessionStarted] sera positionnée à vrai dès que l’initialisation d’une session jSON avec le serveur PHP aura été réussie, qu’elle ait été faite par le client ou le serveur [nuxt]. A l’issue de cette initialisation, le cookie de session avec le serveur PHP aura été récupéré et placé dans la propriété [phpSessionCookie], ligne 10 ;
  • ligne 8 : [userAuthenticated] sera positionnée à vrai dès que l’authentification auprès du serveur PHP aura été réussie, qu’elle ait été faite par le client ou le serveur [nuxt] ;
  • ligne 12 : [adminData] sera la valeur [adminData] obtenue auprès du serveur PHP une fois l’authentification réussie ;
  • lignes 18-22 : la mutation [replace] permet d’initialiser les propriétés précédentes avec celles d’un objet passé en paramètre ;
  • lignes 24-26 : la mutation [reset] redonne leurs valeurs initiales aux propriétés du store ;
  • lignes 31-37 : la fonction [nuxtServerInit] délègue son travail à la fonction [initStore] ;
  • lignes 39-60 : la fonction [initStore] a deux rôles :
    • si le store n’a pas été initialisé, il est initialisé et mis en session ;
    • si le store a déjà été initialisé, sa valeur est récupérée dans la session [nuxt] ;
  • ligne 42 : on récupère la session nuxt ;
  • ligne 44 : on regarde si le store a été initialisé :
    • si ce n’est pas le cas, on met le store initial dans la session (ligne 48) ;
    • puis ligne 50, on indique que le store a été initialisé ;
  • lignes 51-55 : si le store était initialisé, on utilise alors celui-ci, ligne 54, pour initialiser le store à la valeur contenue dans la session ;
  • ligne 57 : dans tous les cas, la session est sauvegardée dans le cookie [nuxt-session], avec le store qu’elle contient ;

Le plugin [plgEventBus]

image14

Ce plugin vise à rendre un bus d’événements accessible au client [nuxt] via une fonction [$eventBus] injectée dans le contexte du client [nuxt]. Il est inutile de l’injecter dans le contexte du serveur [nuxt] car celui-ci ne sait pas gérer les événements. Néanmoins nous avons déjà vu que l’injecter côté serveur puis l’utiliser ne provoque pas d’erreur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* eslint-disable no-console */
// on crée un bus d'événements entre les vues
import Vue from 'vue'
export default (context, inject) => {
  // le bus d'événements
  const eventBus = new Vue()
  // injection d'une fonction [$eventBus] dans le contexte
  inject('eventBus', () => eventBus)
  // log
  console.log('[fonction $eventBus créée]')
}

Nous avons déjà rencontré ce plugin au paragraphe lien. La fonction [$eventBus] sera disponible au client via les notations :

  • [context.app.$eventBus()] là où le contexte est disponible ;
  • [this.$eventBus()] dans les pages [Vue.js] du client ;

Les composants de l’application [nuxt]

image15

Le composant [layout] est celui des exemples précédents :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- disposition des vues -->
<template>
  <!-- ligne -->
  <div>
    <b-row>
      <!-- zone à trois colonnes -->
      <b-col v-if="left" cols="3">
        <slot name="left" />
      </b-col>
      <!-- zone à neuf colonnes -->
      <b-col v-if="right" cols="9">
        <slot name="right" />
      </b-col>
    </b-row>
  </div>
</template>

<script>
export default {
  // paramètres
  props: {
    left: {
      type: Boolean
    },
    right: {
      type: Boolean
    }
  }
}
</script>

Le composant [navigation] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <!-- menu Bootstrap à trois options -->
  <b-nav vertical>
    <b-nav-item to="/authentification" exact exact-active-class="active">
      Authentification
    </b-nav-item>
    <b-nav-item to="/get-admindata" exact exact-active-class="active">
      Requête AdminData
    </b-nav-item>
    <b-nav-item to="/fin-session" exact exact-active-class="active">
      Fin session impôt
    </b-nav-item>
  </b-nav>
</template>

Les layouts de l’application [nuxt]

image16

[default]

Le layout [default] est celui utilisé pour l’exemple [nuxt-11] au paragraphe lien :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<template>
  <div class="container">
    <b-card>
      <!-- un message -->
      <b-alert show variant="success" align="center">
        <h4>[nuxt-12] : requêtes HTTP avec axios</h4>
      </b-alert>
      <!-- la vue courante du routage -->
      <nuxt />
      <!-- message d’attente -->
      <b-alert v-if="showLoading" show variant="light">
        <strong>Requête au serveur de données en cours...</strong>
        <div class="spinner-border ml-auto" role="status" aria-hidden="true"></div>
      </b-alert>
      <!-- erreur d’une opération asynchrone -->
      <b-alert v-if="showErrorLoading" show variant="danger">
        <strong>La requête au serveur de données a échoué : {{ errorLoadingMessage }}</strong>
      </b-alert>
    </b-card>
  </div>
</template>

<script>
/* eslint-disable no-console */
export default {
  name: 'App',
  data() {
    return {
      showLoading: false,
      showErrorLoading: false
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[default beforeCreate]')
  },
  created() {
    console.log('[default created]')
    if (process.client) {
      // on écoute l'évt [loading]
      this.$eventBus().$on('loading', this.mShowLoading)
      // ainsi que l'évt [errorLoadingMessage]
      this.$eventBus().$on('errorLoading', this.mShowErrorLoading)
    }
  },
  beforeMount() {
    console.log('[default beforeMount]')
  },
  mounted() {
    console.log('[default mounted]')
  },
  methods: {
    // gestion du message d’attente
    mShowLoading(value) {
      console.log('[default mShowLoading], showLoading=', value)
      this.showLoading = value
    },
    // erreur d’une opération asynchrone
    mShowErrorLoading(value, errorLoadingMessage) {
      console.log('[default mShowErrorLoading], showErrorLoading=', value, 'errorLoadingMessage=', errorLoadingMessage)
      this.showErrorLoading = value
      this.errorLoadingMessage = errorLoadingMessage
    }
  }
}
</script>
  • lignes 10-14 : affichent le message d’attente de la fin d’une opération asynchrone du client [nuxt] ;
  • lignes 15-18 : affichent l’éventuel message d’erreur d’une opération asynchrone ;
  • ligne 37 : la fonction [created] de la page [default] est exécutée avant la fonction [mounted] des pages ;
  • ligne 39 : si l’exécuteur est le client [nuxt], alors la page [default] se met à l’écoute des événements :
    • [loading] qui signale le début ou la fin d’une attente. La fonction [mShowLoading] est alors exécutée ;
    • [errorLoading] qui signale qu’il faut afficher un message d’erreur. La fonction [mShowErrorLoading] est alors exécutée ;
  • les pages [nuxt] :
    • font afficher le message d’attente en émettant l’événement [‘loading’, true] sur le bus d’événements ;
    • cachent le message d’attente en émettant l’événement [‘loading’, false] sur le bus d’événements ;
    • font afficher un message d’erreur en émettant l’événement [‘errorLoading’, true] sur le bus d’événements ;
    • cachent le message d’erreur en émettant l’événement [‘errorLoading’, false] sur le bus d’événements ;

[error]

Le layout [error] affiche un message d’erreur système (non géré par le développeur) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!-- définition HTML de la vue -->
<template>
  <!-- mise en page -->
  <Layout :left="true" :right="true">
    <!-- alerte dans la colonne de droite -->
    <template slot="right">
      <!-- message sur fond rose -->
      <b-alert show variant="danger" align="center">
        <h4>L'erreur suivante s'est produite : {{ JSON.stringify(error) }}</h4>
      </b-alert>
    </template>
    <!-- menu de navigation dans la colonne de gauche -->
    <Navigation slot="left" />
  </Layout>
</template>

<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */

import Layout from '@/components/layout'
import Navigation from '@/components/navigation'

export default {
  name: 'Error',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // propriété [props]
  props: { error: { type: Object, default: () => 'waiting ...' } },
  // cycle de vie
  beforeCreate() {
    // client et serveur
    console.log('[error beforeCreate]')
  },
  created() {
    // client et serveur
    console.log('[error created, error=]', this.error)
  },
  beforeMount() {
    // client seulement
    console.log('[error beforeMount]')
  },
  mounted() {
    // client seulement
    console.log('[error mounted]')
  }
}
</script>

La page [index] exécutée par le serveur [nuxt]

image17

La page [index.vue] a la particularité d’être accessible uniquement via le serveur [nuxt]. Aucun lien n’est présenté à l’utilisateur pour y avoir accès via le client [nuxt]. Son code est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">Initialisation de la session avec le serveur de calcul de l'impôt : {{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'InitSession',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    try {
      // on démarre une session jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // on récupère le cookie de session PHP pour les prochaines requêtes
      const phpSessionCookie = dao.getPhpSessionCookie()
      // on mémorise le cookie de session PHP dans la session [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // y-a-t-il eu erreur ?
      if (response.état !== 700) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // on note le fait que la session jSON a démarré
      context.store.commit('replace', { jsonSessionStarted: true })
      // on rend le résultat
      return { result: '[succès]' }
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e)
      // on note le fait que la session jSON n'a pas démarré
      context.store.commit('replace', { jsonSessionStarted: false })
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // on sauvegarde le store
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    console.log('[index beforeMount]')
  },
  mounted() {
    console.log('[index mounted]')
    // client seulement
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>
  • ligne 7 : la page affiche le résultat [result] d’une requête asynchrone (lignes 46 et 51) ;
  • ligne 31 : l’opération asynchrone est l’ouverture d’une session jSON avec le serveur de calcul de l’impôt ;
  • ligne 25 : on sait que lorsque la page est demandée directement au serveur [nuxt], la fonction [asyncData] n’est exécutée que par le serveur et pas par le client [nuxt] qui s’exécute lorsque le navigateur a reçu la réponse du serveur [nuxt] ;
  • ligne 30 : on récupère la couche [dao] dans le contexte du serveur [nuxt] ;
  • ligne 35 : si le serveur n’avait pas encore fait de requête au serveur de calcul de l’impôt, il reçoit son premier cookie de session PHP, sinon le dernier cookie de session PHP qu’il a reçu (revoir le code de la couche [dao] du serveur [nuxt] au paragraphe lien) ;
  • ligne 37 : on mémorise ce cookie de session PHP dans le store ;
  • lignes 39-42 : on regarde si l’opération a réussi. Si ce n’est pas le cas, une exception est lancée qui sera interceptée par le [catch] de la ligne 47 ;
  • ligne 44 : on note dans le store que la session jSON avec le serveur PHP est démarrée ;
  • ligne 46 : on rend le résultat [result] qui est affiché ligne 7 ;
  • lignes 47-54 : on traite une éventuelle exception. Celle-ci peut-être de deux natures :
    • l’opération HTTP de la ligne 31 a échoué sur une erreur de communication serveur [nuxt] / serveur PHP ;
    • l’opération HTTP de la ligne 31 a réussi mais le résultat reçu a signalé une erreur (lignes 39-42) ;
  • ligne 51 : on note que la session jSON avec le serveur PHP n’a pas démarré ;
  • ligne 53 : on rend le résultat [result] qui est affiché ligne 7. Par ailleurs, on positionne les propriétés [showErrorLoading] et [errorLoadingMessage] que le client [nuxt] va utiliser pour afficher un message d’erreur lorsqu’il recevra la page envoyée par le serveur [nuxt] (lignes 72-79) ;
  • lignes 54-60 : code exécuté dans tous les cas (réussite ou échec) ;
  • ligne 56 : on récupère la session [nuxt] dans le contexte du serveur [nuxt] ;
  • ligne 57 : on la sauvegarde ;
  • lignes 63-68 : une fois la fonction [asyncData] terminée, le serveur [nuxt] exécute les fonctions [beforeCreate] et [create] ;

Note : l’exécution de la page [index] par le serveur [nuxt] peut échouer par exemple si le serveur de calcul de l’impôt n’est pas lancé lorsque l’application [nuxt] est elle lancée :

image18

Dans ce cas, la seule solution est de lancer le serveur de calcul de l’impôt puis l’application [nuxt] elle-même puisque le menu de navigation ne propose pas d’option pour initier une session jSON avec le serveur de calcul de l’impôt ;

La page [index] exécutée par le client [nuxt]

La page [index] n’est exécutée par le client [nuxt] qu’après que le serveur [nuxt] la lui ait envoyée. Celui-ci lui a envoyé les informations [result] et éventuellement [showErrorLoading] et [errorLoadingMessage].

On sait que la fonction [asyncData] ne sera pas exécutée. Restent alors les fonctions du cycle de vie et notamment la fonction [mounted] :

1
2
3
4
5
6
7
8
mounted() {
    console.log('[index mounted]')
    // client seulement
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • le client [nuxt] intègre automatiquement dans les propriétés de la page les éléments [result] et éventuellement [showErrorLoading, errorLoadingMessage] que lui a envoyés le serveur [nuxt] :
  • la propriété [result] est affichée par la ligne 7 ;
  • les propriétés [showErrorLoading, errorLoadingMessage] sont utilisées par la méthode [mounted] : ligne 4, on teste la propriété [showErrorLoading]. Si elle est vraie, on utilise, ligne 6, le bus d’événements du client [nuxt] pour signaler qu’il y a un message d’erreur à afficher ;
  • l’événement [errorLoading] lancé ligne 6, est intercepté par la page [layouts/default] décrite au paragraphe lien ;

La page [authentification] exécutée par le serveur [nuxt]

La page [authentification] est chargée d’identifier un utilisateur auprès du serveur de calcul de l’impôt. Son code est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<!-- page d’authentification -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">Authentification auprès du serveur de calcul de l'impôt : {{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Authentification',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[authentification asyncData started]')
    if (process.client) {
      // début attente du client [nuxt]
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {
      // on s'authentifie auprès du serveur
      const dao = context.app.$dao()
      const response = await dao.authentifierUtilisateur('admin', 'admin')
      // log
      console.log('[authentification asyncData response=]', response)
      // résultat
      const userAuthenticated = response.état === 200
      // on note le fait que l'utilisateur est authentifié ou pas
      context.store.commit('replace', { userAuthenticated })
      // on sauvegarde le store dans la session [nuxt]
      const session = context.app.$session()
      session.save(context)
      // erreur d’authentification ?
      if (!userAuthenticated) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // on rend le résultat
      return { result: '[succès]' }
    } catch (e) {
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // log
      console.log('[authentification asyncData finished]')
      if (process.client) {
        // fin attente du client [nuxt]
        context.app.$eventBus().$emit('loading', false)
      }
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[authentification beforeCreate]')
  },
  created() {
    console.log('[authentification created]')
  },
  beforeMount() {
    console.log('[authentification beforeMount]')
  },
  mounted() {
    console.log('[authentification mounted]')
    // client seulement
    if (this.showErrorLoading) {
      console.log('[authentification mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>
  • ligne 7 : la page affiche le résultat [result] de la requête asynchrone [asyncData] des lignes 25-65 ;
  • lignes 28-33 : le serveur n’exécute pas ces lignes destinées au client [nuxt] ;
  • ligne 36 : on récupère la couche [dao] du serveur [nuxt] ;
  • ligne 37 : on s’authentifie auprès du serveur de calcul de l’impôt avec les identifiants de test [admin, admin] qui sont les seuls acceptés par le serveur de calcul de l’impôt ;
  • ligne 41 : l’opération d’authentification a réussi si seulement la réponse à l’état 200 ;
  • ligne 43 : on met dans le store la propriété [userAuthenticated] ;
  • lignes 44-46 : le store est sauvegardé dans la session [nuxt] ;
  • lignes 48-51 : si l’authentification a échoué, on lance une exception avec le message d’erreur que le serveur de calcul de l’impôt a envoyé ;
  • sinon ligne 53, on retoune un résultat de réussite qui sera affiché ligne 7 ;
  • lignes 54-57 : en cas d’erreur on positionne trois propriétés de la page [result, showErrorLoading, errorLoadingMessage]. La propriété [result] sera affichée ligne 7. Les trois propriétés seront envoyées au client [nuxt] ;
  • lignes 60-63 : ne sont pas exécutées par le serveur [nuxt] ;
  • une fois que [asyncData] a rendu son résultat, celui-ci est affiché ligne 7. Puis les méthodes [beforeCreate] (lignes 67-69) et [created] (lignes 70-72) sont exécutées ;
  • c’est fini ;

Note : l’exécution de la page [authentification] par le serveur [nuxt] peut échouer par exemple si la session jSON avec le serveur de calcul de l’impôt n’a pas été initialisée. Cela est possible de la façon suivante :

  • supprimez le cookie de session PHP de votre navigateur (pour repartir de zéro) :

image19

  • lancez l’application [nuxt] alors que le serveur de calcul n’a pas été lancé : vous obtenez une erreur ;
  • lancez le serveur de calcul de l’impôt ;
  • demandez l’URL [/authentification] directement dans la barre d’adresses du navigateur :

image20

Dans ce cas, la seule solution est de nouveau de recharger la page [index].

La page [authentification] exécutée par le client [nuxt]

Reprenons le code de la page :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<!-- page d’authentification -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">Authentification auprès du serveur de calcul de l'impôt : {{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'Authentification',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[authentification asyncData started]')
    if (process.client) {
      // début attente du client [nuxt]
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {
      // on s'authentifie auprès du serveur
      const dao = context.app.$dao()
      const response = await dao.authentifierUtilisateur('admin', 'admin')
      // log
      console.log('[authentification asyncData response=]', response)
      // résultat
      const userAuthenticated = response.état === 200
      // on note le fait que l'utilisateur est authentifié ou pas
      context.store.commit('replace', { userAuthenticated })
      // on sauvegarde le store dans la session [nuxt]
      const session = context.app.$session()
      session.save(context)
      // erreur d’authentification ?
      if (!userAuthenticated) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // on rend le résultat
      return { result: '[succès]' }
    } catch (e) {
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // log
      console.log('[authentification asyncData finished]')
      if (process.client) {
        // fin attente du client [nuxt]
        context.app.$eventBus().$emit('loading', false)
      }
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[authentification beforeCreate]')
  },
  created() {
    console.log('[authentification created]')
  },
  beforeMount() {
    console.log('[authentification beforeMount]')
  },
  mounted() {
    console.log('[authentification mounted]')
    // client seulement
    if (this.showErrorLoading) {
      console.log('[authentification mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>

Il y a deux cas d’exécution de la page [authentification] par le client [nuxt] :

  1. le client [nuxt] s’exécute après que le serveur [nuxt] ait envoyé au navigateur du client [nuxt] la page [authentification] ;
  2. le client [nuxt] parce que l’utilisateur a cliqué sur le lien [Authentification] du menu de navigation :

image21

Etudions d’abord le 1er cas. Dans ce cas, le client [nuxt] n’exécute pas la fonction [asyncData]. Il intègre dans les propriétés de la page les éléments [result] et éventuellement [showErrorLoading, errorLoadingMessage] que lui a envoyés le serveur [nuxt] :

  • la propriété [result] est affichée par la ligne 7 ;
  • les propriétés [showErrorLoading, errorLoadingMessage] sont utilisées par la méthode [mounted] : ligne 79, on teste la propriété [showErrorLoading]. Si elle est vraie, on utilise, ligne 81, le bus d’événements du client [nuxt] pour signaler qu’il y a un message d’erreur à afficher ;

Le mécanisme de l’affichage du message d’erreur a été expliqué pour la page [index] au paragraphe lien.

Le cas 2 est celui du client [nuxt] exécuté lorsque l’utilisateur clique sur le lien [Authentification]. Dans ce cas, le client [nuxt] s’exécute de façon autonome et pas après le serveur [nuxt]. La fonction [asyncData] est alors exécutée. Nous ne donnons que les détails qui diffèrent des explications données pour la page exécutée par le serveur [nuxt] :

  • lignes 28-33 : le client [nuxt] demande l’affichage du message d’attente et la disparition d’un éventuel message d’erreur qui aurait été précédemment affiché ;
  • ligne 36 : c’est désormais la couche [dao] du client [nuxt] qui est obtenue ici ;
  • lignes 60-63 : le client [nuxt] demande la fin de l’affichage du message d’attente ;
  • une fois [asyncData] terminée, le cycle de vie de la page va avoir lieu. la fonction [mounted] des lignes 76-83 va être exécutée. S’il y a eu erreur, le message d’erreur va alors être affiché ;

Note : pour provoquer une erreur, suivez la procédure expliquée pour le serveur [nuxt] à la fin du paragraphe lien, mais au lieu de demander la page [authentification] en tapant son URL dans la barre d’adresses, utilisez le lien [Authentification] du menu de navigation. C’est alors le client [nuxt] qui s’exécute.

La page [get-admindata]

Le code de la page [get-admindata] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!-- vue get-admindata -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message -->
    <b-alert slot="right" show variant="secondary"> Demande de [adminData] au serveur de calcul de l'impôt : {{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'GetAdmindata',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[get-admindata asyncData started]')
    if (process.client) {
      // début attente
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {
      // on demande la donnée [admindata]
      const response = await context.app.$dao().getAdminData()
      // log
      console.log('[get-admindata asyncData response=]', response)
      // résultat
      const adminData = response.état === 1000 ? response.réponse : ''
      // on met la donnée dans le store
      context.store.commit('replace', { adminData })
      // on sauvegarde le store dans la session [nuxt]
      const session = context.app.$session()
      session.save(context)
      // y-a-t-il eu erreur ?
      if (!adminData) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // on rend la valeur reçue
      return { result: adminData }
    } catch (e) {
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // log
      console.log('[get-admindata asyncData finished]')
      if (process.client) {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
      }
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[get-admindata beforeCreate]')
  },
  created() {
    console.log('[get-admindata created]')
  },
  beforeMount() {
    console.log('[get-admindata beforeMount]')
  },
  mounted() {
    console.log('[get-admindata mounted]')
    // client
    if (this.showErrorLoading) {
      console.log('[get-admindata mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>

Cette page est très semblable à la page [authentification]. Les explications sont analogues aussi bien pour son exécution par le serveur [nuxt] que pour son exécution par le client [nuxt]. Notons cependant que la ligne 7 affiche non pas succès / échec comme précédemment mais la valeur de la donnée reçue du serveur de calcul de l’impôt (ligne 52) :

image22

Le résultat ci-dessus est obtenu aussi bien avec le serveur qu’avec le client [nuxt]. Pour provoquer une erreur, demandez la page [get-admindata], via le serveur ou le client [nuxt], sans être authentifié :

image23

La page [fin-session]

Le code de la page est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!-- page principale -->
<template>
  <Layout :left="true" :right="true">
    <!-- navigation -->
    <Navigation slot="left" />
    <!-- message-->
    <b-alert slot="right" show variant="warning">Fin de la session avec le serveur de calcul de l'impôt : {{ result }} </b-alert>
  </Layout>
</template>

<script>
/* eslint-disable no-console */

import Navigation from '@/components/navigation'
import Layout from '@/components/layout'

export default {
  name: 'FinSession',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[fin-session asyncData started]')
    // cas du client [nuxt]
    if (process.client) {
      // début attente
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {
      // on demande une nouvelle session PHP au serveur de calcul de l'impôt
      const dao = context.app.$dao()
      const response = await dao.finSession()
      // log
      console.log('[fin-session asyncData response=]', response)
      // y-at-il eu erreur ?
      if (response.état !== 400) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // le serveur a envoyé un nouveau cookie de session PHP
      // on le récupère à la fois pour le serveur et le client nuxt
      // si ce code est exécuté par le client [nuxt], le cookie de session PHP doit être mis dans la session nuxt
      // pour que le plugin [plgDao] du serveur [nuxt] puisse le récupérer et initialiser la couche [dao] avec
      // si ce code est exécuté par le serveur [nuxt], le cookie de session PHP doit être mis dans la session nuxt
      // pour que le routing du client [nuxt] le récupère et le passe au navigateur
      const phpSessionCookie = dao.getPhpSessionCookie()
      // on note dans le store le fait que la session jSON est démarrée et on mémorise le cookie de session PHP
      context.store.commit('replace', { jsonSessionStarted: true, phpSessionCookie, userAuthenticated: false, adminData: '' })
      // on sauvegarde le store dans la session [nuxt]
      const session = context.app.$session()
      session.save(context)
      // on rend le résultat
      return { result: "[succès]. La session jSON reste initialisée mais vous n'êtes plus authentifié(e)." }
    } catch (e) {
      // log
      console.log('[fin-session asyncData error=]', e)
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // log
      console.log('[fin-session asyncData finished]')
      if (process.client) {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
      }
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[fin-session beforeCreate]')
  },
  created() {
    console.log('[fin-session created]')
  },
  beforeMount() {
    console.log('[fin-session beforeMount]')
  },
  mounted() {
    console.log('[fin-session mounted]')
    // client seulement
    if (this.showErrorLoading) {
      console.log('[fin-session mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
  }
}
</script>

Le code est très analogue à celui des pages précédentes et les explications sont les mêmes. Il faut simplement s’attarder sur un point : l’opération asynchrone de la ligne 38, fait que le serveur de calcul de l’impôt va envoyer un nouveau cookie de session PHP. Les explications pour la gestion de ce cookie diffèrent selon que c’est le serveur ou le client [nuxt] qui exécute ce code.

Commençons par le serveur [nuxt] :

  • ligne 37 : c’est la couche [dao] du serveur [nuxt] qui est instanciée. Rappelons le code de son constructeur :
1
2
3
4
5
6
7
8
9
// constructeur
  constructor(axios, phpSessionCookie) {
    // bibliothèque axios
    this.axios = axios
    // valeur du cookie de session
    this.phpSessionCookie = phpSessionCookie
    // nom du cookie de session du serveur PHP
    this.phpSessionCookieName = 'PHPSESSID'
}
On voit ligne 1, que le constructeur a besoin du cookie de session PHP du moment, le dernier reçu, que ce soit par le serveur ou le client [nuxt] ;
  • ligne 52 : le serveur [nuxt] récupère le cookie de la nouvelle session PHP ou bien l’ancien cookie si l’opération de fin de session a échoué ;
  • ligne 54 : le cookie de session PHP est mis dans le store puis sauvegardé dans la session [nuxt] aux lignes 56-57 ;
  • après le serveur c’est le client [nuxt] qui exécute la page [fin-session] avec les données envoyées par le serveur. On sait qu’il ne va pas exécuter la fonction [asyncData] ;
  • au final, après que serveur et client [nuxt] ont terminé leur travail, on sait que le cookie PHP nécessaire aux échanges avec le serveur de calcul de l’impôt est dans la session [nuxt] ;

Le fait que le cookie PHP soit dans la session [nuxt] est suffisant pour le serveur, car c’est là que va le prendre sa couche [dao]. Dans le plugin [server/plgDao] qui initialise la couche [dao] du serveur, on a écrit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* eslint-disable no-console */
// on crée un point d'accès à la couche [Dao]
import Dao from '@/api/server/Dao'
export default (context, inject) => {
  // configuration axios
  context.$axios.defaults.timeout = context.env.timeout
  context.$axios.defaults.baseURL = context.env.baseURL
  // on récupère le cookie de session
  const store = context.app.$session().value.store
  const phpSessionCookie = store ? store.phpSessionCookie : ''
  console.log('session=', context.app.$session().value, 'phpSessionCookie=', phpSessionCookie)
  // instanciation de la couche [dao]
  const dao = new Dao(context.$axios, phpSessionCookie)
  // injection d'une fonction [$dao] dans le contexte
  inject('dao', () => dao)
  // log
  console.log('[fonction server $dao créée]')
}
  • ligne 13, la couche [dao] du serveur [nuxt] est instanciée avec le cookie de session PHP pris dans la session [nuxt], lignes 9-10 ;

Pour le client [nuxt], c’est une autre histoire. Ce n’est pas lui en effet qui envoie le cookie mais le navigateur qui l’exécute. Or ce navigateur ne connaît pas le cookie de la nouvelle session PHP reçu par le serveur [nuxt]. Si on utilise les liens du menu de navigation [3] :

image24

Le serveur de calcul de l’impôt va recevoir du navigateur un cookie de session PHP obsolète et il va répondre qu’à ce cookie aucune session jSON n’est associée. Il nous faut trouver le moyen de passer au navigateur le nouveau cookie de session PHP.

On peut utiliser un middleware de routing pour ce faire :

image25

Le script [client/routing] est le middleware de routage déclaré dans le fichier [nuxt.config] :

1
2
3
4
5
6
7
// routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-12/',
    // middleware de routage
    middleware: ['routing']
},

Le script [middleware/routing] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* eslint-disable no-console */

// on importe le middleware du client
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.client) {
    // routage client
    clientRouting(context)
  }
}
  • lignes 9-12 : on ne route que le client avec une fonction importée ligne 4 ;

Le script [middleware/client/routing] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* eslint-disable no-console */
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestion du cookie de la session PHP dans le navigateur
  // le cookie de la session PHP du navigateur doit être identique à celui trouvé en session nuxt
  // l'acion [fin-session] reçoit un nouveau cookie PHP (serveur comme client nuxt)
  // si c'est le serveur qui le reçoit, le client doit le transmettre au navigateur
  // pour ses propres échanges avec le serveur PHP
  // on est ici dans un routing client

  // on récupère le cookie de la session PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // s'il existe, on affecte le cookie de session PHP au navigateur
    document.cookie = phpSessionCookie
  }
}

Revenons à la situation juste après l’exécution de la page [fin-session] par le serveur [nuxt] :

image26

Si on clique sur l’un des liens du menu [3], le client [nuxt] va prendre la main. Comme il va y avoir changement de page, le script de routing du client va s’exécuter :

  • ligne 13 : le cookie de session PHP est trouvé dans le store de l’application [nuxt] ;
  • ligne 14 : s’il n’est pas vide on le transmet au navigateur (ligne 16). A partir de ce moment le navigateur du client [nuxt] a le bon cookie de session PHP ;

Le script [client/routing] est exécuté à chaque changement de page du client [nuxt]. Le code du script est valide quelque soit la page cible : simplement, la plupart du temps, il donne au navigateur un cookie de session PHP qu’il a déjà, sauf dans deux cas :

  • juste après le démarrage de l’application, le serveur [nuxt] exécute la page [index] et reçoit un 1er cookie de session PHP que le navigateur du client [nuxt] n’a pas ;
  • lorsque le serveur [nuxt] exécute la page [fin-session] comme il vient d’être expliqué ;

Maintenant étudions le cas où la page [fin-session] est exécutée par le client [nuxt] uniquement, parce qu’on a cliqué sur son lien dans le menu de navigation. C’est désormais le client [nuxt] qui exécute la fonction [asyncData] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
try {
      // on demande une nouvelle session PHP au serveur de calcul de l'impôt
      const dao = context.app.$dao()
      const response = await dao.finSession()
      // log
      console.log('[fin-session asyncData response=]', response)
      // y-at-il eu erreur ?
      if (response.état !== 400) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // le serveur a envoyé un nouveau cookie de session PHP
      // on le récupère à la fois pour le serveur et le client nuxt
      // si ce code est exécuté par le client [nuxt], le cookie de session PHP doit être mis dans la session nuxt
      // pour que le plugin [plgDao] du serveur [nuxt] puisse le récupérer et initialiser la couche [dao] avec
      // si ce code est exécuté par le serveur [nuxt], le cookie de session PHP doit être mis dans la session nuxt
      // pour que le routing du client [nuxt] le récupère et le passe au navigateur
      const phpSessionCookie = dao.getPhpSessionCookie()
      // on note dans le store le fait que la session jSON est démarrée et on mémorise le cookie de session PHP
      context.store.commit('replace', { jsonSessionStarted: true, phpSessionCookie, userAuthenticated: false, adminData: '' })
      // on sauvegarde le store dans la session [nuxt]
      const session = context.app.$session()
      session.save(context)
      // on rend le résultat
      return { result: "[succès]. La session jSON reste initialisée mais vous n'êtes plus authentifié(e)." }
    } catch (e) {
      // log
      console.log('[fin-session asyncData error=]', e)
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // log
      console.log('[fin-session asyncData finished]')
      if (process.client) {
        // fin attente
        context.app.$eventBus().$emit('loading', false)
      }
    }
  • ligne 3 : c’est la couche [dao] du client [nuxt] qui est obtenu ici ;
  • ligne 18 : le cookie de session PHP récupéré par la couche [dao] du client [nuxt] est mémorisé, mis dans le store (ligne 20) puis sauvegardé en session [nuxt] (lignes 22-23) ;
  • à partir de là tout va bien car on sait que la couche [dao] du serveur [nuxt] va chercher le cookie de session PHP dans la session [nuxt] ;

Exécution

Pour exécuter cet exemple, il faut prendre soin avant l’exécution de supprimer le cookie de session [nuxt] et le cookie PHP du navigateur exécutant le client [nuxt] afin de partir d’une situation nette. Ci-dessous un exemple avec le navigateur Chrome :

image27

Conclusion

Cet exemple a été particulièrement complexe. Il réunissait des connaissances acquises dans les exemples précédents : persistance du store dans une session [nuxt], plugins d’injections de fonctions, middleware de routage, gestion des erreurs des opérations asynchrones. La complexité a été accrue par le fait qu’on voulait que l’utilisateur puisse aussi bien utiliser les liens du menu de navigation que taper des URL à la main sans que ça casse l’application. Pour cela, on a été obligés de regarder comment se comportait chaque page selon qu’elle était exécutée par le client ou le serveur [nuxt].

Cette unité de comportement du client et du serveur [nuxt] n’est pas indispensable. On peut se mettre dans le cas fréquent où :

  • la première page est délivrée par le serveur [nuxt] ;
  • toutes les pages suivantes sont délivrées par le client [nuxt] qui travaille alors en mode [SPA] ;

Néanmoins même dans ce cas, il faut vérifier ce que donne l’exécution de toutes les pages par le serveur [nuxt] car c’est ce qu’obtiendront les moteurs de recherche qui les demanderont.

Exemple [nuxt-13] : contrôle de la navigation de [nuxt-12]

Dans cet exemple, nous nous intéressons à la navigation de [nuxt-12]. On ne l’a pas fait dans [nuxt-12] parce que le contrôle de la navigation aurait complexifié un exemple déjà complexe.

Objectif : nous voulons que l’utilisateur ne puisse faire que des actions autorisées :

  • si la session jSON n’a pas démarré, alors seule l’URL [/] est autorisée ;
  • si la session jSON a démarré mais que l’utilisateur n’est pas authentifié, alors seule l’URL [/authentification] est autorisée ;
  • si la session jSON a démarré et que l’utilisateur est authentifié, alors seules les URL [/get-admindata, /fin-session] sont autorisées ;
  • lorsque la cible du routage du moment n’est pas autorisée, alors on procèdera à une redirection vers une URL autorisée ;

L’exemple [nuxt-13] est obtenu initialement par recopie de l’exemple [nuxt-12] :

image0

C’est dans le dossier du routage [middleware] que vont prendre place les modifications.

Routage de l’application [nuxt]

Le routage de l’application est configuré de la façon suivante dans le fichier [nuxt.config] :

1
2
3
4
5
6
7
// routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-13/',
    // middleware de routage
    middleware: ['routing']
},
  • ligne 6 : le routage de l’application est contrôlé par le fichier [middleware/routing] ;

Le fichier [middleware/routing] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 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)
  }
}
  • lignes 10-16 : on traite différemment les routages du client et du serveur [nuxt]. C’est un point où ils diffèrent grandement ;
  • ligne 4 : le routage du serveur est implémenté par le script [middleware/server/routing] ;
  • ligne 5 : le routage du client est implémenté par le script [middleware/client/routing] ;

Routage du client [nuxt]

Le routage du client [nuxt] reste ce qu’il était dans [nuxt-12] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* eslint-disable no-console */
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestion du cookie de la session PHP dans le navigateur
  // le cookie de la session PHP du navigateur doit être identique à celui trouvé en session nuxt
  // l'acion [fin-session] reçoit un nouveau cookie PHP (serveur comme client nuxt)
  // si c'est le serveur qui le reçoit, le client doit le transmettre au navigateur
  // pour ses propres échanges avec le serveur PHP
  // on est ici dans un routing client

  // on récupère le cookie de la session PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // s'il existe, on affecte le cookie de session PHP au navigateur
    document.cookie = phpSessionCookie
  }

  ...
}

Pour éviter que le client aille dans des routes non autorisées on va simplement lui offrir dans le menu de navigation client les seules routes autorisées. Le composant [components/navigation] devient le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template>
  <!-- menu Bootstrap à trois options -->
  <b-nav vertical>
    <b-nav-item v-if="$store.state.jsonSessionStarted && !$store.state.userAuthenticated" to="/authentification" exact exact-active-class="active">
      Authentification
    </b-nav-item>
    <b-nav-item
      v-if="$store.state.jsonSessionStarted && $store.state.userAuthenticated && !$store.state.adminData"
      to="/get-admindata"
      exact
      exact-active-class="active"
    >
      Requête AdminData
    </b-nav-item>
    <b-nav-item v-if="$store.state.jsonSessionStarted && $store.state.userAuthenticated" to="/fin-session" exact exact-active-class="active">
      Fin session impôt
    </b-nav-item>
  </b-nav>
</template>
  • ligne 4 : l’option [Authentification] n’est offerte que si la session jSON a démarré mais que l’utilisateur n’est pas authentifié. Si la session jSON n’a pas démarré ou que l’utilisateur est déjà authentifié alors l’option n’est pas offerte ;
  • lignes 7-11 : l’option [Requête AdminData] n’est offerte que si la session jSON a démarré, que l’utilisateur est authentifié et qu’on n’a pas encore récupéré la donnée [AdminData]. Si l’une de ces trois conditions n’est pas satisfaite (session jSON pas démarrée, utilisateur pas authentifié ou la donnée [AdminData] déjà récupérée, l’option n’est pas offerte ;
  • ligne 15 : l’option [Fin session impôt] est offerte dès que la session jSON a démarré et que l’utilisateur est authentifié, sinon elle ne l’est pas ;

Routage du serveur [nuxt]

Le routage du serveur est en général plus complexe que celle du client car l’utilisateur peut taper n’importe quelle URL dans la barre d’adresses de son navigateur. On peut laisser faire (après tout l’utilisateur n’est pas censé faire ça) ou essayer de contrôler les choses. C’est ce que nous allons faire ici, pour l’exemple, car dans le cas de l’application [nuxt-12], on peut très bien s’en passer puisque le serveur de calcul de l’impôt est bien protégé contre ces URL à la main et sait envoyer les messages d’erreur adéquats. Nous l’avons vu dans [next-12] où il n’y avait aucun contrôle de routage.

Le routage d’un serveur [nuxt] est très différent d’un client [nuxt] quant à la notion de redirection :

  • lorsque un serveur [nuxt] est redirigé, il envoie un ordre de redirection au navigateur client avec la cible de la redirection. Le navigateur fait alors une nouvelle requête au serveur [nuxt] en lui demandant la cible qui lui a été transmise. Tout se passe comme si l’utilisateur avait tapé à la main l’URL de la cible de la redirection : toute l’application [nuxt] redémarre et donc tout son cycle de vie (plugins serveur, store, routage serveur, pages) ;
  • lorsqu’un client [nuxt] est redirigé, rien de tel n’arrive. Il y a un simple changement de page, le même que celui qui aurait été obtenu si l’utilisateur avait cliqué sur un lien menant à la cible de la redirection. Le cycle de vie est alors différent (routage client, affichage cible de la route) ;

Pour cette raison, il est préférable de séparer le routage client du routage serveur même si les deux codes peuvent paraître analogues.

Le script de routage du serveur [middleware/server/routing] sera le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 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 dans le store [nuxt]
  const store = context.store
  // d'où vient-on ?
  const from = store.state.from || 'nowhere'
  ...
}
  • dans le routage du client, la fonction de routage reçoit le contexte [context] avec la propriété [context.from] qui est la route de la page d’où l’on vient. La route où l’on va est obtenue par [context.route] ;
  • dans le routage du serveur, la fonction de routage reçoit le contexte [context] sans la propriété [context.from]. Le routage du serveur n’intervient que lorsqu’une URL est demandée à la main au serveur [nuxt]. On sait qu’alors toute l’application [nuxt] est réinitialisée. C’est comme si on repartait de zéro et il n’y a donc pas de notion de ‘page précédente’ ;
  • grâce à la session [nuxt] on sait que le serveur peut récupérer cette session et donc ne pas repartir de zéro. C’est donc dans cette session [nuxt] et plus particulièrement dans le store de cette session que nous stockerons le nom de la dernière page affichée par le navigateur client avant qu’une URL soit demandée au serveur [nuxt] ;
  • lignes 7-9 : on récupère le nom de la dernière page affichée par le navigateur client. Au démarrage de l’application, cette information [from] n’existe pas dans le store. On affecte alors le nom [nowhere] à la variable [from] ;

Pour que le serveur [nuxt] puisse récupérer dans le store le nom de la dernière page affichée par le navigateur client, il faut que le client [nuxt] mette également cette information dans le store. Le script de routage du client [nuxt] est donc complété de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* eslint-disable no-console */
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestion du cookie de la session PHP dans le navigateur
  // le cookie de la session PHP du navigateur doit être identique à celui trouvé en session nuxt
  // l'acion [fin-session] reçoit un nouveau cookie PHP (serveur comme client nuxt)
  // si c'est le serveur qui le reçoit, le client doit le transmettre au navigateur
  // pour ses propres échanges avec le serveur PHP
  // on est ici dans un routing client

  // on récupère le cookie de la session PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // s'il existe, on affecte le cookie de session PHP au navigateur
    document.cookie = phpSessionCookie
  }

  // on met dans la session le nom de la page où on va - pas de redirection serveur
  context.store.commit('replace', { serverRedirection: false, from: context.route.name })
  // on sauvegarde le store dans la session [nuxt]
  const session = context.app.$session()
  session.value.store = context.store.state
  session.save(context)
}
  • les lignes 19-24 sont ajoutées ;
  • ligne 20 : on met dans le store le nom de la page [context.route.name] qui va s’afficher et qui sera donc lors du routage suivant, la page d’où l’on vient. Par ailleurs, on va voir que dans le routage du serveur [nuxt], celui-ci a besoin de savoir si le routage en cours est issu d’une précédente redirection du serveur [nuxt]. Ici ce n’est pas le cas, et on met donc la propriété [serverRedirection] à [false] ;
  • lignes 22-24 : l’état du store est mis dans la session [nuxt] (ligne 23) puis la session [nuxt] est sauvegardée dans un cookie (ligne 24) qui lui-même sera sauvegardé dans le navigateur du client [nuxt] ;

Revenons sur le script de routage du serveur [nuxt] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/* 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 dans le store [nuxt]
  const store = context.store
  // d'où vient-on ?
  const from = store.state.from || 'nowhere'
  // où va-t-on ?
  const to = context.route.name
  // é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
  }

  // est-ce un rechargement de page ?
  if (to === from) {
    // rien à faire
    done = true
  }

  // contrôle de la navigation du serveur [nuxt]
  // on s'inspire de la navigation client dans le composant [navigation]

  // cas où la session PHP n'a pas démarré
  if (!done && !store.state.jsonSessionStarted && to !== 'index') {
    // redirection
    redirection = 'index'
    // travail terminé
    done = true
  }

  // cas où l'utilisateur n'est pas authentifié
  if (!done && store.state.jsonSessionStarted && !store.state.userAuthenticated && to !== 'authentification') {
    // redirection
    redirection = from
    // travail terminé
    done = true
  }

  // cas où l'utilisateur a été authentifié
  if (!done && store.state.jsonSessionStarted && store.state.userAuthenticated && to !== 'get-admindata' && to !== 'fin-session') {
    // on reste sur la même page
    redirection = from
    // travail terminé
    done = true
  }

  // cas où [adminData] a été obtenu
  if (!done && store.state.jsonSessionStarted && store.state.userAuthenticated && store.state.adminData && to !== 'fin-session') {
    // on reste sur la même page
    redirection = from
    // travail terminé
    done = true
  }

  // on a 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
  if (redirection) {
    context.redirect({ name: redirection })
  }
}
  • lignes 6-9 : on récupère la valeur de [from] dans le store du serveur [nuxt] ;
  • ligne 11 : on note la cible du routage courant ;
  • ligne 13 : le routage peut amener à une redirection du navigateur client. [redirection] sera la cible de cette redirection ;
  • ligne 15 : [done] à [true] indique que le routage est terminé ;
  • lignes 17-21 : on regarde d’abord si le routage courant est issu d’une demande de redirection envoyée au navigateur client. Cette information est stockée dans la propriété [serverRedirection] du store. Si cette propriété est à vrai, alors c’est que le serveur [nuxt] a envoyé une redirection au navigateur client lors de la précédente requête au serveur [nuxt]. Dans ce cas, il n’y a pas de routage à faire. Lors de la précédente requête, le routeur du serveur [nuxt] a décidé que le navigateur client devait être redirigé. Cette décision n’a pas à être remise en cause par un nouveau routage ;
  • lignes 23-27 : on regarde si le routage en cours est un rechargement de page. Si oui, on laisse faire ;
  • à partir de la ligne 29, on reprend les règles appliquées dans le composant [navigation] du client [nuxt] (cf paragraphe précédent) ;
  • lignes 32-38 : on traite le cas où la session jSON n’a pas démarré et que la cible du routage n’est pas la page [index]. Dans ce cas, on redirige le navigateur client vers la page [index] ;
  • lignes 40-46 : on traite le cas où la session jSON a démarré, l’utilisateur n’est pas authentifié et la cible du routage courant n’est pas la page [authentification]. Dans ce cas, on refuse le routage et on reste là où on était ;
  • lignes 48-54 : on traite le cas où la session jSON a démarré, l’utilisateur est authentifié et la cible du routage courant n’est ni la page [get-admindata], ni la page [fin-session] qui sont alors les seules destinations possibles. Dans ce cas, on refuse le routage demandé et on revient là où on était précédemment ;
  • lignes 56-62 : on traite le cas où [adminData] a été obtenu. Dans ce cas, il n’y a qu’une cible possible pour le routage : la page [fin-session]. Si ce n’était pas elle qui était demandée, on refuse le routage et on revient là où on était précédemment ;
  • lignes 64-72 : s’il y a eu redirection, on le note dans le store du serveur [nuxt] : [serverRedirection: true]. On notera qu’on ne donne pas de valeur à la propriété [from] du store. La raison en est qu’il va y avoir redirection du navigateur client et on a vu que dans ce cas, il n’y avait pas de routage (lignes 17-20) et la propriété [from] du store n’est pas utilisée ;
  • lignes 66-69 : s’il n’y a pas de redirection, alors on le note également dans le store du serveur [nuxt] : [serverRedirection: false]. Par ailleurs, le routage en cours va afficher la page [to] qui pour la requête suivante (client ou serveur [nuxt]) deviendra la page précédente. C’est pourquoi on écrit [from: to] ;
  • lignes 73-76 : on sauvegarde le store dans la session [nuxt] elle-même sauvegardée dans un cookie ;
  • lignes 77-80 : si [redirection] n’est pas vide, alors on demande au navigateur de se rediriger. Sinon (on ne le voit pas ici), le cycle de vie du serveur [nuxt] va se poursuivre : la page [to] va être traitée par le serveur [nuxt] et envoyée au navigateur du client [nuxt] avec le cookie de session [nuxt] ;

Le routage choisi ici pour le serveur [nuxt] est arbitraire. On aurait pu en choisir un autre ou comme il a été dit ne pas en faire du tout. Celui choisi ci-dessus a le mérite de toujours laisser l’application dans un état stable quelque soit l’URL demandée par l’utilisateur.

On peut améliorer un point lorsque la page chargée au final est la page d’origine. Il y a deux cas :

  • l’utilisateur a provoqué un rechargement de la page (to===from) ;
  • il y a redirections vers la page d’origine (redirection===from) ;

Dans les deux cas la page d’origine va être de nouveau exécutée avec son appel asynchrone au serveur de calcul de l’impôt. Prenons un exemple. Si une fois authentifié, l’utilisateur recharge la page (F5). Dans ce cas dans le routage ci-dessus, on a : [to]=[from]=[authentification]. Il n’y a pas redirection. La page [to=authentification] va être exécutée par le serveur [nuxt]. Si on ne fait rien, la fonction [asyncData] va s’exécuter de nouveau. C’est inutile puisque l’authentification a déjà été faite.

On peut améliorer les choses en modifiant légèrement la page [authentification] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// données asynchrones
  async asyncData(context) {
    // log
    console.log('[authentification asyncData started]')
    // on ne fait pas les choses deux fois si la page a déjà été demandée
    if (process.server && context.store.state.userAuthenticated) {
      console.log('[authentification asyncData canceled]')
      return { result: '[succès]' }
    }
    // client [nuxt]
    if (process.client) {
      // début attente
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {
      // on s'authentifie auprès du serveur
...
  • lignes 6-9 : si la page est exécutée par le serveur [nuxt] et qu’on découvre dans le store que l’authentification a déjà été faite, alors on retourne directement le résultat souhaité (ligne 8) ;

On fait la même chose pour toutes les pages :

Page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// données asynchrones
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // on ne fait pas les choses deux fois si la page a déjà été demandée
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
...

Page [get-admindata]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// données asynchrones
  async asyncData(context) {
    // log
    console.log('[get-admindata asyncData started]')
    // on ne fait pas les choses deux fois si la page a déjà été demandée
    if (process.server && context.store.state.adminData) {
      console.log('[get-admindata asyncData canceled]')
      return { result: context.store.state.adminData }
    }
    // client
    if (process.client) {
      // début attente
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {
   ...

Page [fin-session]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// données asynchrones
  async asyncData(context) {
    // log
    console.log('[fin-session asyncData started]')
    // on ne fait pas les choses deux fois si la page a déjà été demandée
    if (process.server && context.store.state.jsonSessionStarted && !context.store.state.userAuthenticated) {
      console.log('[fin-session asyncData canceled]')
      return { result: "[succès]. La session jSON reste initialisée mais vous n'êtes plus authentifié(e)." }
    }
    // cas du client [nuxt]
    if (process.client) {
      // début attente
      context.app.$eventBus().$emit('loading', true)
      // pas d'erreur
      context.app.$eventBus().$emit('errorLoading', false)
    }
    try {

Exécution

Pour exécuter cet exemple, il faut prendre soin avant l’exécution de supprimer le cookie de session [nuxt] et le cookie PHP du navigateur exécutant le client [nuxt] afin de partir d’une situation nette. Ci-dessous un exemple avec le navigateur Chrome :

image1

Conclusion

Le routage du serveur [nuxt] est complexe car il faut prévoir toutes les URL que peut taper à la main l’utilisateur. C’est un cas d’école. Une application [nuxt] n’est pas destinée à être utilisée de cette façon. Une fois la page [index] servie par le routeur du serveur [nuxt], on pourrait rediriger les appels suivants faits au serveur vers une page d’erreur.

Dans le cas précis de notre exemple [nuxt-13], le routage du serveur [nuxt] était inutile. Celui fait par défaut (absence de routage en fait) dans l’exemple [nuxt-12] convenait très bien.

Exemple [nuxt-20] : portage de l’exemple [vuejs-22]

Présentation

Nous nous proposons ici de porter l’exemple [vuejs-22] qui était une application [vue.js] de type SPA, dans un contexte [nuxt] SSR. [vuejs-22] était une application cliente du serveur de calcul de l’impôt qui présentait les vues suivantes :

La 1ère vue est la vue d’authentification :

image0

La seconde vue est celle du calcul de l’impôt :

image1

La 3ième vue est celle qui affiche la liste des simulations faites par l’utilisateur :

image2

L’écran ci-dessus montre qu’on peut supprimer la simulation n° 1. On obtient alors la vue suivante :

image3

Si on supprime maintenant la dernière simulation, on obtient la nouvelle vue suivante :

image4

Nous allons porter l’application [vuejs-22] vers l’application [nuxt-20] de façon progressive. Nous n’expliquerons pas de nouveau les codes de [vuejs-22]. Le lecteur est invité à relire le document |Introduction au framework VUE.JS par l’exemple|. Les différentes étapes devraient montrer les différences entre une application [vuejs] et une application [nuxt].

étape 1

Le projet [nuxt-20] est initialement obtenu par recopie du projet [nuxt-12]. Celui-ci est en effet un bon point de départ :

  • il sait dialoguer avec le serveur de calcul de l’impôt ;
  • il gère correctement les erreurs que celui-ci envoie ;
  • les client et serveur [nuxt] savent communiquer via une session [nuxt] ;

On a donc une bonne infrastructure de départ. Notre principal travail devrait être de modifier :

  • les pages. On prendra celles du projet [vuejs-22] qu’il faudra adapter au nouvel environnement ;
  • la gestion du store. Des informations supplémentaires devraient apparaître (liste des simulations) et d’autres pourraient devenir inutiles ;
  • la gestion du routage du client et du serveur [nuxt] ;

Donc tout d’abord, on crée le projet [nuxt-20] en recopiant le projet [nuxt-12] :

image5

Puis on supprime les pages et composants devenus inutiles [2] :

  • le composant [components/navigation] disparaît ;
  • le layout [layout/default] disparaît ;
  • les pages [index, authentification, get-admindata, fin-session] disparaissent ;

Puis on intègre dans [nuxt-20] des éléments de [vuejs-22] [3] :

  • les trois pages [Authentification, CalculImpot, ListeSimulations] de l’application [vuejs-22] vont dans le dossier [pages] ;
  • les composants [FormCalculImpot, Menu, Layout] de l’application [vuejs-22] vont dans le dossier [components] ;
  • la page [Main] de [vuejs-22] qui servait de [layout] à l’application [vuejs-22] va dans le dossier [layouts] ;

On renomme les éléments intégrés [4] :

image6

  • dans [layouts], [Main] est devenue [default] puisque c’est le nom par défaut du layout d’une application [nuxt] ;
  • dans [pages], la page [Authentification] est devenue [index], car [Authentification] jouait ce rôle dans l’application [vuejs-22] ;

A ce point là, on peut faire une compilation du projet pour voir les premières erreurs. On modifie le fichier [nuxt.config] de l’exemple [nuxt-12] afin d’exécuter désormais [nuxt-20] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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) {}
  },
  // répertoire du code source
  srcDir: 'nuxt-20',
  // routeur
  router: {
    // racine des URL de l'application
    base: '/nuxt-20/',
    // middleware de routage
    middleware: ['routing']
  },
  // serveur
  server: {
    // port de service, 3000 par défaut
    port: 81,
    // adresses réseau écoutées, par défaut localhost : 127.0.0.1
    // 0.0.0.0 = toutes les adresses réseau de la machine
    host: 'localhost'
  },
  // environnement
  env: {
    // configuration axios
    timeout: 2000,
    withCredentials: true,
    baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
    // configuration du cookie de session [nuxt]
    maxAge: 60 * 5
  }
}

On fait ensuite un [build] du projet :

image7

Les erreurs signalées sont les suivantes :

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’erreur de la ligne 1 indique qu’on référence une image inexistante. On la récupèrera dans [vuejs-22] ;
  • l’erreur de la ligne 2 montre que le composant [./FormCalculImpot] n’existe pas. Effectivement, ce composant est désormais dans [@/components/form-calcul-impot] ;
  • les erreurs des lignes [3-5] montrent que le composant [./Layout] n’existe pas. Effectivement, ce composant est désormais dans [@/components/layout] ;
  • les erreurs des lignes [6-7] montrent que le composant [./Menu] n’existe pas. Effectivement, il s’appelle maintenant [@/components/menu] ;

On ajoute l’image [assets/logo.jpg] au projet [nuxt-20] :

image8

Par ailleurs, dans toutes les pages on va corriger le chemin des composants. Prenons l’exemple de la page [calcul-impot] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- définition HTML de la vue -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulaire de calcul de l'impôt à droite -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- menu de navigation à gauche -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- zone d'affichage des résultat du calcul de l'impôt sous le formulaire -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- zone de trois colonnes vide -->
      <b-col sm="3" />
      <!-- zone de neuf colonnes -->
      <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
  },

Les trois [import] des lignes 26-28 deviennent :

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

On vérifie et éventuellement corrige ainsi les [import] de tous les composants, layouts et pages. Une fois ces corrections faites, on peut tenter un nouveau [build]. Normalement il n’y a plus d’erreurs.

On peut alors tenter une exécution :

image9

Des erreurs apparaissent :

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

On voit que la commande [dev] alliée au module [eslint] est plus stricte, au niveau syntaxique, que la commande [build]. Ici, elle réclame que l’opérateur de comparaison [!=] soit écrit [!==] qui est un opérateur plus strict (il vérifie également le type des opérandes). Ces erreurs se produisent dans la page [index.vue].

On corrige les erreurs ci-dessus et on relance l’exécution du projet. On a alors un warning du module [eslint] :

1
2
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

image10

On corrige cette erreur avec le [Quick fix] du module [eslint] [2].

On relance l’exécution du projet. On n’a plus d’erreur de compilation. On demande alors l’URL [http://localhost:81/nuxt-20/] avec un navigateur. On obtient une erreur d’exécution :

image11

L’erreur se trouve dans [index.vue] [2]. L’erreur [1] vient du fait que dans [vuejs-22], la couche [dao] était disponible dans [this.$dao] alors que dans [nuxt-12] dont nous avons adopté l’infrastructure, elle est disponible dans la fonction [this.$dao()].

L’erreur est dans la fonction [created] du cycle de vie de la page [index] :

image12

Pour l’instant, on se contente de renommer [created] en [created2] pour que la fonction du cycle de vie [created] ne soit pas exécutée [3].

On sauve la modification et on recharge la page [index] dans le navigateur. Cette fois-ci c’est bon :

image13

étape 2

Les pages du projet [vuejs-22] utilisaient les éléments injectés suivants :

  • $dao : pour la couche [dao] du client [vue.js] ;
  • $session : pour une session stockée dans le [localStorage] du navigateur ;

Ces éléments n’existent plus dans l’infrastructure du projet [nuxt-12] que nous avons recopiée :

  • il y a désormais deux couches [dao], l’une pour le client [nuxt], l’autre pour le serveur [nuxt]. Toutes deux sont disponibles via une fonction injectée appelée [$dao]. Cela signifie que dans les pages de l’application [this.$dao] doit être remplacé par [this.$dao()] ;
  • la session [nuxt] gérée par l’application [nuxt-20] n’a plus rien à voir avec l’objet [$session] de l’application [vuejs-22] où il n’y avait pas de notion de cookie de session. Néanmoins, elles ont une fonctionnalité analogue : stocker des informations persistantes au fil des actions de l’utilisateur. La session [nuxt] stocke les informations dans le store plutôt que directement dans la session. Dans les pages de l’application [this.$session] doit être remplacé par [this.$store] lorsqu’il s’agit de mémoriser des informations dans la session et par [this.$session()] lorsqu’il s’agit de manipuler la session elle-même ;
  • pour connaître l’état d’une propriété P du store, il faudra écrire [this.$store.state.P] ;
  • pour changer la propriété P du store, il faudra écrire [this.$store.commit(‘replace’, {P:value}] ;

Nous faisons ces modifications dans la page [index] :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<!-- 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 {
  // composants utilisés
  components: {
    Layout
  },
  // état du composant
  data() {
    return {
      // utilisateur
      user: '',
      // son mot de passe
      password: '',
      // contrôle l'affichage d'un msg d'erreur
      showError: false,
      // le message d'erreur
      message: ''
    }
  },

  // propriétés calculées
  computed: {
    // saisies valides
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // cycle de vie : le composant vient d'être créé
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // l'utilisateur peut-il faire des simulations ?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
      // alors l'utilisateur peut faire des simulations
      this.$router.push({ name: 'calculImpot' })
      // retour à la boucle événementielle
      return
    }
    // si la session jSON a déjà été démarrée, on ne la redémarre pas de nouveau
    if (!this.$store.state.started) {
      // début attente
      this.$emit('loading', true)
      // on initialise la session avec le serveur - requête asynchrone
      // on utilise la promesse rendue par les méthodes de la couche [dao]
      this.$dao()
        // on initialise une session jSON
        .initSession()
        // on a obtenu la réponse
        .then((response) => {
          // fin attente
          this.$emit('loading', false)
          // analyse de la réponse
          if (response.état !== 700) {
            // on affiche l'erreur
            this.message = response.réponse
            this.showError = true
            // retour à la boucle événementielle
            return
          }
          // la session a démarré
          this.$store.commit('replace', { started: true })
          console.log('[authentification], session=', this.$session())
        })
        // en cas d'erreur
        .catch((error) => {
          // on remonte l'erreur à la vue [Main]
          this.$emit('error', error)
        })
        // dans tous les cas
        .finally(() => {
          // on sauvegarde la session
          this.$session().save()
        })
    }
  },

  // gestionnaires d'évts
  methods: {
    // ----------- authentification
    async login() {
      try {
        // début attente
        this.$emit('loading', true)
        // on n'est pas encore authentifié
        this.$store.commit('replace', { authenticated: false })
        // authentification bloquante auprès du serveur
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // fin du chargement
        this.$emit('loading', false)
        // analyse de la réponse du serveur
        if (response.état !== 200) {
          // on affiche l'erreur
          this.message = response.réponse
          this.showError = true
          // retour à la boucle événementielle
          return
        }
        // pas d'erreur
        this.showError = false
        // on est authentifié
        this.$store.commit('replace', { authenticated: true })
        // --------- on demande maintenant les données de l'administration fiscale
        // au départ, pas de donnée
        this.$métier.setTaxAdminData(null)
        // début attente
        this.$emit('loading', true)
        // demande bloquante auprès du serveur
        const response2 = await this.$dao().getAdminData()
        // fin du chargement
        this.$emit('loading', false)
        // analyse de la réponse
        if (response2.état !== 1000) {
          // on affiche l'erreur
          this.message = response2.réponse
          this.showError = true
          // retour à la boucle événementielle
          return
        }
        // pas d'erreur
        this.showError = false
        // on mémorise dans la couche [métier] la donnée reçue
        this.$métier.setTaxAdminData(response2.réponse)
        // on peut passer au calcul de l'impôt
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // on remonte l'erreur au composant principal
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier })
        // on sauvegarde la session
        this.$session().save()
      }
    }
  }
}
</script>

Notons les points suivants :

  • ligne 69 : la fonction [created2] a été renommée [mounted], ceci pour que le serveur [nuxt] ne l’exécute pas (il n’exécute ni [beforeMount] ni [mounted]). Seul le client [nuxt] l’exécutera comme c’était le cas avec l’exemple [vuejs-22] ;
  • ligne 73 : on référence [this.$métier] qui pour l’instant n’existe pas ;
  • ligne 75 : nous n’avons jamais utilisé cette méthode dans une application [nuxt]. Il faudra voir si elle fonctionne dans un contexte [nuxt] ;
  • ligne 112, 172 : dans [vuejs-22], la session du projet était sauvegardée de cette façon. Avec le projet [nuxt-20], la méthode [save] doit recevoir le contexte courant. On sait que dans une page [nuxt], l’objet [context] est disponible dans [this.$nuxt.context] ;

Les lignes 112 et 172 sont donc réécrites de la façon suivante :

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

On notera que ce code n’est pas optimisé. Plutôt que d’utiliser plusieurs fois la fonction [this.$session()], il serait préférable d’écrire :

1
const session=this.$session()

puis utiliser ensuite la variable [session]. On peut tenir le même raisonnement pour la fonction [this.$dao()].

Ces corrections faites, nous pouvons recharger l’URL [http://localhost:81/nuxt-20/] avec un navigateur. Nous obtenons toujours la même page que précédemment :

image14

Regardons les logs du navigateur :

image15

Le log [1] est le dernier log fait par le client [nuxt]. En [2], on voit que la propriété [started] est à [vrai], ce qui veut dire que la fonction [mounted] a réussi à démarrer une session jSON avec le serveur de calcul de l’impôt. On voit également que le store a des propriétés qu’il faudra soit abandonner soit renommer. Rappelons que nous utilisons le store de l’exemple [nuxt-12].

Maintenant redemandons l’URL [http://localhost:81/nuxt-20/] alors que le serveur de calcul de l’impôt n’est pas lancé. On prend soin tout d’abord de supprimer le cookie de la session [nuxt] :

image16

La copie d’écran ci-dessus est une copie d’écran Chrome. Ceci fait, l’URL [http://localhost:81/nuxt-20/] donne le résultat suivant :

image17

L’erreur était correctement gérée par le projet [vuejs-22]. Elle reste correctement gérée par le projet [nuxt-20].

étape 3

Maintenant que nous avons la page d’authentification, il faut regarder le code exécuté lorsque l’utilisateur clique sur le bouton [Valider] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// gestionnaires d'évts
  methods: {
    // ----------- authentification
    async login() {
      try {
        // début attente
        this.$emit('loading', true)
        // on n'est pas encore authentifié
        this.$store.commit('replace', { authenticated: false })
        // authentification bloquante auprès du serveur
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // fin du chargement
        this.$emit('loading', false)
        // analyse de la réponse du serveur
        if (response.état !== 200) {
          // on affiche l'erreur
          this.message = response.réponse
          this.showError = true
          // retour à la boucle événementielle
          return
        }
        // pas d'erreur
        this.showError = false
        // on est authentifié
        this.$store.commit('replace', { authenticated: true })
        // --------- on demande maintenant les données de l'administration fiscale
        // au départ, pas de donnée
        this.$métier.setTaxAdminData(null)
        // début attente
        this.$emit('loading', true)
        // demande bloquante auprès du serveur
        const response2 = await this.$dao().getAdminData()
        // fin du chargement
        this.$emit('loading', false)
        // analyse de la réponse
        if (response2.état !== 1000) {
          // on affiche l'erreur
          this.message = response2.réponse
          this.showError = true
          // retour à la boucle événementielle
          return
        }
        // pas d'erreur
        this.showError = false
        // on mémorise dans la couche [métier] la donnée reçue
        this.$métier.setTaxAdminData(response2.réponse)
        // on peut passer au calcul de l'impôt
        this.$router.push({ name: 'calculImpot' })
      } catch (error) {
        // on remonte l'erreur au composant principal
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier })
        // on sauvegarde la session
        this.$session().save(this.$nuxt.context)
      }
    }
  }

Le principal problème ici semble être l’absence de la donnée [this.$métier]. Pour y remédier nous allons :

  • inclure la classe [Métier] de l’exemple [vuejs-22]. Nous la mettrons dans le dossier [api] ;
  • injecter une fonction [$métier] dans le contexte du client [nuxt] qui donnera accès à cette classe ;

Tout d’abord la copie de la classe [Métier] dans le dossier [api] :

image18

Une fois la classe [Métier] présente dans le projet, on crée un nouveau plugin pour le client [nuxt]. Ce plugin appelé [pluginMétier] va injecter une fonction [$métier] qui donnera accès à la classe [Métier] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 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]')
}

Ceci fait, nous pouvons corriger la page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// cycle de vie : le composant vient d'être créé
  mounted() {
    // eslint-disable-next-line
    console.log("Authentification mounted");
    // l'utilisateur peut-il faire des simulations ?
    if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
      // alors l'utilisateur peut faire des simulations
      this.$router.push({ name: 'calcul-impot' })
      // retour à la boucle événementielle
      return
    }
    // si la session jSON a déjà été démarrée, on ne la redémarre pas de nouveau
    ...
  },

  // gestionnaires d'évts
  methods: {
    // ----------- authentification
    async login() {
      try {
        // début attente
        this.$emit('loading', true)
        // on n'est pas encore authentifié
        this.$store.commit('replace', { authenticated: false })
        // authentification bloquante auprès du serveur
        const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
        // fin du chargement
        this.$emit('loading', false)
        // analyse de la réponse du serveur
        if (response.état !== 200) {
          // on affiche l'erreur
          this.message = response.réponse
          this.showError = true
          // retour à la boucle événementielle
          return
        }
        // pas d'erreur
        this.showError = false
        // on est authentifié
        this.$store.commit('replace', { authenticated: true })
        // --------- on demande maintenant les données de l'administration fiscale
        // au départ, pas de donnée
        this.$métier().setTaxAdminData(null)
        // début attente
        this.$emit('loading', true)
        // demande bloquante auprès du serveur
        const response2 = await this.$dao().getAdminData()
        // fin du chargement
        this.$emit('loading', false)
        // analyse de la réponse
        if (response2.état !== 1000) {
          // on affiche l'erreur
          this.message = response2.réponse
          this.showError = true
          // retour à la boucle événementielle
          return
        }
        // pas d'erreur
        this.showError = false
        // on mémorise dans la couche [métier] la donnée reçue
        this.$métier().setTaxAdminData(response2.réponse)
        // on peut passer au calcul de l'impôt
        this.$router.push({ name: 'calcul-impot' })
      } catch (error) {
        // on remonte l'erreur au composant principal
        this.$emit('error', error)
      } finally {
        // maj store
        this.$store.commit('replace', { métier: this.$métier() })
        // on sauvegarde la session
        this.$session().save(this.$nuxt.context)
      }
    }
  }
  • lignes 43, 61, 69, [this.$métier] a été remplacé par [this.$métier()] ;
  • lignes 8, 63 : le nom de la page [CalculImpot] du projet [vuejs-22] est devenue la page [calcul-impot] dans le projet [nuxt-20] ;

Ces corrections faites, on peut tenter de valider la page d’authentification :

image19

La page obtenue est la suivante :

image20

On a bien obtenu la page de calcul de l’impôt. Maintenant regardons les logs :

image21

En [2], on voit que le fait d’être authentifié a été correctement mémorisé. En [3-4], on voit qu’on a récupéré la donnée [taxAdminData] qui permet le calcul de l’impôt par la classe [Métier].

étape 4

Examinons la page [calcul-impot] que nous avons obtenue :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!-- définition HTML de la vue -->
<template>
  <div>
    <Layout :left="true" :right="true">
      <!-- formulaire de calcul de l'impôt à droite -->
      <FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
      <!-- menu de navigation à gauche -->
      <Menu slot="left" :options="options" />
    </Layout>
    <!-- zone d'affichage des résultat du calcul de l'impôt sous le formulaire -->
    <b-row v-if="résultatObtenu" class="mt-3">
      <!-- zone de trois colonnes vide -->
      <b-col sm="3" />
      <!-- zone de neuf colonnes -->
      <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>
  • lignes 44 et 48 : les liens du menu de navigation sont corrects. La page [/fin-session] n’existe pas. Le projet [vuejs-22] réglait ce problème avec du routage. Nous ferons de même avec le projet [nuxt-20] ;
  • ligne 76 : on référence une mutation [addSimulation] qui n’existe pas pour l’instant. Nous allons la créer ;
  • ligne 78 : comme dans la page [index], il faut écrire [this.$session().save(this.$nuxt.context)] ;

Modifions le store [store/index]. Hérité du projet [nuxt-12], il est pour l’instant le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* eslint-disable no-console */

// état du store
export const state = () => ({
  // session jSON démarrée
  jsonSessionStarted: false,
  // utilisateur authentifié
  userAuthenticated: false,
  // cookie de session PHP
  phpSessionCookie: '',
  // adminData
  adminData: ''
})

// mutations du store
export const mutations = {
  // remplacement du state
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reset du store
  reset() {
    this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
  }
}

// actions du store
export const actions = {
  nuxtServerInit(store, context) {
    // qui exécute ce code ?
    console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
    // init session
    initStore(store, context)
  }
}

function initStore(store, context) {
  // store est le store à initialiser
  // on récupère la session
  const session = context.app.$session()
  // la session a-t-elle été déjà initialisée ?
  if (!session.value.initStoreDone) {
    // on démarre un nouveau store
    console.log("nuxtServerInit, initialisation d'un nouveau store")
    // on met le store dans la session
    session.value.store = store.state
    // le store est désormais initialisé
    session.value.initStoreDone = true
  } else {
    console.log("nuxtServerInit, reprise d'un store existant")
    // on met à jour le store avec le store de la session
    store.commit('replace', session.value.store)
  }
  // on sauvegarde la session
  session.save(context)
  // log
  console.log('initStore terminé, store=', store.state)
}
  • lignes 3-27 : nous allons reprendre le state et les mutations de l’application [vuejs-22] (cf document [3]) :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// état du store
export const state = () => ({
  // session jSON démarrée
  started: false,
  // utilisateur authentifié
  authenticated: false,
  // cookie de session PHP
  phpSessionCookie: '',
  // liste des simulations
  simulations: [],
  // le n° de la dernière simulation
  idSimulation: 0,
  // couche [métier]
  métier: null
})

// mutations du store
export const mutations = {
  // remplacement du state
  replace(state, newState) {
    for (const attr in newState) {
      state[attr] = newState[attr]
    }
  },
  // reset du store
  reset() {
        this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
  },
  // suppression ligne n° index
  deleteSimulation(state, index) {
    // eslint-disable-next-line no-console
    console.log('mutation deleteSimulation')
    // on supprime la ligne n° [index]
    state.simulations.splice(index, 1)
    console.log('store simulations', state.simulations)
  },
  // ajout d'une simulation
  addSimulation(state, simulation) {
    // eslint-disable-next-line no-console
    console.log('mutation addSimulation')
    // n° de la simulation
    state.idSimulation++
    simulation.id = state.idSimulation
    // on ajoute la simulation au tableau des simulations
    state.simulations.push(simulation)
  }
}
  • lignes 4 et 6 : nous introduisons les propriétés déjà utilisées ;
  • ligne 8 : nous gardons le cookie de session PHP. Il est fondamental pour que le client et le serveur [nuxt] aient la même session PHP avec le serveur de calcul de l’impôt ;
  • ligne 10 : la liste des simulations faites par l’utilisateur ;
  • ligne 12 : le n° de la dernière simulation faite par l’utiliasteur ;
  • ligne 14 : la couche [métier] ;
  • lignes 30-47 : les mutations présentes dans le store du projet [vuejs-22] et référencées par les pages de l’application. Le projet [vuejs-22] avait une mutation appelée [clear] qui vidait la liste des simulations. On ne la met pas car la mutation [reset] déjà présente devrait faire l’affaire ;
  • lignes 26-28 : la mutation [reset] est modifiée pour prendre en compte le nouveau contenu du state ;

La page [calcul-impot] utilise le composant [form-calcul-impot] suivant :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
<!-- 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 {
  // état interne
  data() {
    return {
      // marié ou pas
      marié: 'non',
      // nombre d'enfants
      enfants: '',
      // salaire annuel
      salaire: ''
    }
  },
  // état interne calculé
  computed: {
    // validation du formulaire
    formInvalide() {
      return (
        // salaire invalide
        !this.salaire.match(/^\s*\d+\s*$/) ||
        // ou enfants invalide
        !this.enfants.match(/^\s*\d+\s*$/) ||
        // ou données fiscales pas obtenues
        !this.$métier.taxAdminData
      )
    },
    // validation du salaire
    salaireValide() {
      // doit être numérique >=0
      return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
    },
    // validation des enfants
    enfantsValide() {
      // doit être numérique >=0
      return Boolean(this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/))
    }
  },
  // cycle de vie
  created() {
    // log
    // eslint-disable-next-line
    console.log("FormCalculImpot created");
  },
  // gestionnaire d'évts
  methods: {
    calculerImpot() {
      // on calcule l'impôt à l'aide de la couche [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);
      // on complète le résultat
      résultat.marié = this.marié
      résultat.enfants = this.enfants
      résultat.salaire = this.salaire
      // on émet l'évt [resultatObtenu]
      this.$emit('resultatObtenu', résultat)
    }
  }
}
</script>
  • lignes 65, 89 : la référence [this.$métier] doit être changée en [this.$métier()] ;

Ces corrections faites, on peut tenter une simulation :

image22

On obtient la réponse suivante :

image23

Si on regarde les logs :

image24

  • en [9-10], on voit que la 1ère simulation se trouve bien dans le [store] ;
  • en [5], le n° de la dernière simulation a bien été incrémenté ;

étape 5

Maintenant que nous avons fait une simulation, cliquons sur le lien [Liste des simulations]. Nous obtenons la page suivante :

image25

Le routage du client [nuxt] s’est fait correctement. Regardons le code de la page [liste-des-simulations] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!-- 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>
  • lignes 47-56 : les cibles du menu de navigation sont correctes ;
  • ligne 75 : le store est correctement référencé ;
  • ligne 89 : on utilise une mutation [deleteSimulation] que nous avons intégrée lors de l’étape précédente ;
  • ligne 91 : cette ligne doit être réécrite comme [this.$session().save(this.$nuxt.context)] ;

Nous faisons les modifications nécessaires, puis nous tentons de supprimer la simulation affichée :

image26

On obtient alors la page suivante :

image27

Regardons les logs :

image28

  • en [6], on voit que le tableau des simulations est vide ;

Maintenant revenons au formulaire de calcul de l’impôt :

image29

On obtient la page suivante :

image30

Donc le routage a fonctionné.

étape 6

Il nous reste à gérer l’option de navigation [Fin de session] du menu de navigation :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// options du menu
      options: [
        {
          text: 'Liste des simulations',
          path: '/liste-des-simulations'
        },
        {
          text: 'Fin de session',
          path: '/fin-session'
        }
]
  • ligne 9, la page [/fin-session] n’existe pas. Le projet [vuejs-22] gérait ce cas avec des règles de routage dans un fichier [router.js]:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 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
  • les lignes 64-76 géraient le cas particulier de la route vers le chemin [/fin-session] ;
  • ligne 66 : on vide la session courante ;
  • lignes 68-70 : on affiche la vue [authentification] ;

Nous allons essayer de faire quelque chose d’analogue dans le fichier de routage du client [nuxt] :

image31

Le script [client/routing.js] devient le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* eslint-disable no-console */
export default function(context) {
  // qui exécute ce code ?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestion du cookie de la session PHP dans le navigateur
  // le cookie de la session PHP du navigateur doit être identique à celui trouvé en session nuxt
  // l'action [fin-session] reçoit un nouveau cookie PHP (serveur comme client nuxt)
  // si c'est le serveur qui le reçoit, le client doit le transmettre au navigateur
  // pour ses propres échanges avec le serveur PHP
  // on est ici dans un routing client

  // on récupère le cookie de la session PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // s'il existe, on affecte le cookie de session PHP au navigateur
    document.cookie = phpSessionCookie
  }

  // où va-t-on ?
  const to = context.route.path
  if (to === '/fin-session') {
    // on nettoie la session
    const session = context.app.$session()
    session.reset(context)
    // on redirige vers la page index
    context.redirect({ name: 'index' })
  }
}
  • on a rajouté les lignes [19-27] au code existant ;
  • ligne 20 : on récupère le [path] de la cible de la route courante ;
  • ligne 21 : on regarde si c’est [/fin-session]. Si oui :
    • lignes 23-24 : la session est réinitialisée ;
    • ligne 26 : on redirige le client [nuxt] vers la page d’accueil ;

La méthode [session.reset(context)] (ligne 24) de la session est la suivante :

1
2
3
4
5
6
7
8
// 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)
}

La méthode [context.store.commit(“reset”)] (ligne 5) est la suivante :

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

Lorsqu’on utilise maintenant le lien [Fin de session], la page d’accueil est affichée avec les logs suivants :

image32

  • en [3], on voit qu’on n’est plus authentifié ;
  • en [4], on voit que la session jSON est démarrée ;
  • en [6], la couche [métier] n’est plus présente dans le store (elle est toujours présente dans les pages avec [this.$métier()]) ;
  • en [5, 7], il n’y a plus de simulations ;

Il faut bien comprendre ce qui se passe dans une fin de session :

  • la session [nuxt] est réinitialisée : la propriété [started] du store passe à [false] ;
  • il y a redirection vers la page [index] ;
  • la méthode [mounted] de la page [index] est exécutée. Celle-ci démarre une nouvelle session jSON avec le serveur de calcul de l’impôt. Si l’opération réussit, la propriété [started] du store passe à [true] ;

étape 7

A ce stade, l’application [nuxt-20] a toutes les fonctionnalités de l’application [vuejs-22]. Le portage semble terminé.

Nous allons aller un peu plus loin dans un esprit [nuxt]. La méthode [mounted] de la page [index] pose problème. Elle lance une opération asynchrone dont un moteur de recherche n’attendra pas la fin. On sait que dans ce cas là, il faut mettre l’opération asynchrone dans une fonction [asyncData] car alors, le serveur [nuxt] qui l’exécute attend qu’elle soit terminée avant de délivrer la page au moteur de recherche.

Nous nous aidons ici de la fonction [asyncData] écrite dans l’application [nuxt-12] pour la page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
export default {
  name: 'InitSession',
  // composants utilisés
  components: {
    Layout,
    Navigation
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // on ne fait pas les choses deux fois si la page a déjà été demandée
    if (process.server && context.store.state.jsonSessionStarted) {
      console.log('[index asyncData canceled]')
      return { result: '[succès]' }
    }
    try {
      // on démarre une session jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // on récupère le cookie de session PHP pour les prochaines requêtes
      const phpSessionCookie = dao.getPhpSessionCookie()
      // on mémorise le cookie de session PHP dans la session [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // y-a-t-il eu erreur ?
      if (response.état !== 700) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // on note le fait que la session jSON a démarré
      context.store.commit('replace', { jsonSessionStarted: true })
      // on rend le résultat
      return { result: '[succès]' }
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e)
      // on note le fait que la session jSON n'a pas démarré
      context.store.commit('replace', { jsonSessionStarted: false })
      // on signale l'erreur
      return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // on sauvegarde le store
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    console.log('[index beforeMount]')
  },
  mounted() {
    console.log('[index mounted]')
    // client seulement
    if (this.showErrorLoading) {
      console.log('[index mounted, showErrorLoading=true]')
      this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
    }
}
  • lignes 13, 33, 40 : il faut changer la propriété [jsonSessionStarted] en [started] ;
  • ligne 13 : dans l’application [nuxt-12], seul le serveur [nuxt] exécutait la page [index] et sa fonction [asyncData]. Le client [nuxt] n’exécutait la page [index] qu’après l’avoir reçue du serveur [nuxt] et alors il n’exécutait pas la fonction [asyncData]. Dans [nuxt-20], c’est différent : le lien [Fin de session] va afficher la page [index] dans l’environnement du client [nuxt]. La fonction [asyncData] va alors être exécutée. Cependant, lorsqu’on arrive à la page [index] de cette façon, la session [nuxt] a été réinitialisée entre-temps et la propriété [started] du store vaut [false] et la condition de la ligne 13 sera forcément fausse. On peut donc laisser [process.server] et ainsi le client [nuxt] ne fera pas ce test ;
  • lignes 15, 35, 42 : une propriété [result] est mise dans les propriétés [data] de la page [index]. Dans [nuxt-20], cette propriété ne sera pas utilisée et nous l’enlèverons du résultat rendu par la fonction ;
  • lignes 61-67 : cette méthode [mounted] doit être conservée car c’est elle qui permet au client [nuxt] d’afficher le message d’erreur. Néanmoins la façon de gérer l’erreur sera modfiée ;

Dans la page [index] actuelle, nous intégrons la fonction [asyncData] ci-dessus en lieu et place de l’ancienne fonction [mounted] et nous ajoutons une nouvelle fonction [mounted]. Le code de la page [index] de l’exemple [nuxt-20] devient alors le suivant :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
...

<!-- dynamique de la vue -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
  // composants utilisés
  components: {
    Layout
  },
  // état du composant
  data() {
    return {
      // utilisateur
      user: '',
      // son mot de passe
      password: '',
      // affichage erreur
      showError: false
    }
  },

  // propriétés calculées
  computed: {
    // saisies valides
    valid() {
      return this.user && this.password && this.$store.state.started
    }
  },
  // données asynchrones
  async asyncData(context) {
    // log
    console.log('[index asyncData started]')
    // on ne fait pas les choses deux fois si la page a déjà été demandée
    if (process.server && context.store.state.started) {
      console.log('[index asyncData canceled]')
      return
    }
    try {
      // on démarre une session jSON
      const dao = context.app.$dao()
      const response = await dao.initSession()
      // log
      console.log('[index asyncData response=]', response)
      // on récupère le cookie de session PHP pour les prochaines requêtes
      const phpSessionCookie = dao.getPhpSessionCookie()
      // on mémorise le cookie de session PHP dans la session [nuxt]
      context.store.commit('replace', { phpSessionCookie })
      // y-a-t-il eu erreur ?
      if (response.état !== 700) {
        // l'erreur se trouve dans response.réponse
        throw new Error(response.réponse)
      }
      // on note le fait que la session jSON a démarré
      context.store.commit('replace', { started: true })
      // pas de résultat
      return
    } catch (e) {
      // log
      console.log('[index asyncData error=]', e.message)
      // on note le fait que la session jSON n'a pas démarré
      context.store.commit('replace', { started: false })
      // on signale l'erreur
      return { showErrorLoading: true, errorLoadingMessage: e.message }
    } finally {
      // on sauvegarde le store
      const session = context.app.$session()
      session.save(context)
      // log
      console.log('[index asyncData finished]')
    }
  },
  // cycle de vie
  beforeCreate() {
    console.log('[index beforeCreate]')
  },
  created() {
    console.log('[index created]')
  },
  beforeMount() {
    // client seulement
    console.log('[index beforeMount]')
    // gestion de l'erreur éventuelle
    if (this.showErrorLoading) {
      // log
      console.log('[index beforeMount, showErrorLoading=true]')
      // on remonte l'erreur au composant principal [default]
      this.$emit('error', new Error(this.errorLoadingMessage))
    }
  },
  mounted() {
    console.log('[index mounted]')
  },

  // gestionnaires d'évts
  methods: {
    // ----------- authentification
    async login() {
      ...
}
</script>
  • lignes 58, 65 : la fonction [asyncData] ne rend plus la propriété [result] inutilisée ici ;
  • ligne 81 : la méthode [beforeMount] du client [nuxt]. Elle a été préférée à la méthode [mounted] pour gérer l’erreur éventuelle de [asyncData] ;
  • ligne 85 : on teste si la propriété [errorLoading] a été positionnée. Elle ne peut l’être que par la fonction [asyncData] ;
  • lignes 85-90 : si fonction [asyncData] a signalé une erreur, on la passe à la page [default] via l’événement [error]. C’était comme cela que l’ancienne fonction [created] que nous venons de remplacer, gérait l’éventuelle erreur ;

Faisons quelques tests.

Nous supprimons d’abord et le cookie de la session [nuxt] et le cookie de session PHP s’ils existent. Nous demandons ensuite la page [http://localhost:81/nuxt-20/] alors que le serveur de calcul de l’impôt n’est pas lancé. Nous obtenons la page suivante :

image33

Nous rechargeons la même page après avoir lancé le serveur de calcul de l’impôt :

image34

Regardons les logs :

image35

  • en [2-3], on voit que la session jSON a été démarrée ;
  • en [4], on voit le cookie de session PHP que le serveur [nuxt] a récupéré lors de son échange avec le serveur de calcul de l’impôt. Le client [nuxt] va désormais l’utiliser ;

Maintenant identifions-nous :

image36

Nous obtenons la page suivante :

image37

En [1], nous avons obtenu un message d’erreur. Cela veut dire que le navigateur n’a pas envoyé le bon cookie de la session PHP démarrée par le serveur [nuxt] à l’étape précédente. Dans [nuxt-12], le passage du cookie de session PHP du serveur [nuxt] au client [nuxt] se faisait dans le routage du client [nuxt] du script [middleware/client/routing] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* eslint-disable no-console */
export default function(context) {  // qui exécute ce code ?
  console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
  // gestion du cookie de la session PHP dans le navigateur
  // le cookie de la session PHP du navigateur doit être identique à celui trouvé en session nuxt
  // l'acion [fin-session] reçoit un nouveau cookie PHP (serveur comme client nuxt)
  // si c'est le serveur qui le reçoit, le client doit le transmettre au navigateur
  // pour ses propres échanges avec le serveur PHP
  // on est ici dans un routing client

  // on récupère le cookie de la session PHP
  const phpSessionCookie = context.store.state.phpSessionCookie
  if (phpSessionCookie) {
    // s'il existe, on affecte le cookie de session PHP au navigateur
    document.cookie = phpSessionCookie
  }

  // où va-t-on ?
  const to = context.route.path
  if (to === '/fin-session') {
    // on nettoie la session
    const session = context.app.$session()
    session.reset(context)
    // on redirige vers la page index
    context.redirect({ name: 'index' })
  }
}

Ce sont les lignes 13-17 qui permettent au client [nuxt] de récupérer le cookie de la session PHP du serveur [nuxt].

Le problème ici c’est que lorsqu’on clique sur le bouton [Valider], il n’y a pas de routage du client [nuxt]. Sa fonction de routage n’est alors pas appelée. On règle le problème en dupliquant les lignes 12-17 au début de la méthode d’authentification de la page [index] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// gestionnaires d'évts
  methods: {
    // ----------- authentification
    async login() {
      // on récupère le cookie de session PHP dans le store
      const phpSessionCookie = this.$store.state.phpSessionCookie
      if (phpSessionCookie) {
        // s'il existe, on affecte le cookie de session PHP au navigateur
        document.cookie = phpSessionCookie
      }
      try {
        // début attente
        this.$emit('loading', true)
        // on n'est pas encore authentifié

Lignes 5-10, on récupère dans le store le cookie de la session PHP initiée par le serveur [nuxt]. Cette modification faite, on récupère bien la page du calcul de l’impôt, signifiant que l’authentification a fonctionné.

étape 8

Nous avons une application fonctionnelle et qui travaille dans un esprit [nuxt]. Comme nous l’avons fait pour l’application [nuxt-13], nous allons nous intéresser à la navigation du serveur [nuxt]. Comme il a déjà été dit, l’utilisateur n’est pas censé taper à la main les URL de l’application. Il est censé utiliser les liens qui lui sont présentés et qui sont exécutés par le client [nuxt] qui fonctionne alors en mode SPA. Néanmoins, on va faire en sorte que la navigation du serveur [nuxt] laisse toujours l’application dans un état stable.

De l’étude faite pour [nuxt-13] (cf paragraphe lien), nous savons qu’il faut :

  • modifier le script [midleware/routing] ;
  • ajouter un script [middleware/server/routing] ;

image38

Le script [middleware/routing] est modifié de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* 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)
  }
}
  • ligne 4 : on importe le script de routage du serveur [nuxt] ;
  • lignes 10-12 : si c’est le serveur [nuxt] qui exécute le code, on utilise sa fonction de routage ;

Le script [middleware/server/routing] est le suivant :

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
/* 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 })
  }
}
  • nous reprenons dans ce script les idées déjà développées et utilisées dans le routage du serveur [nuxt] de l’application [nuxt-13] ;
  • nous ajoutons deux propriétés au store de l’application :
    • [from] : le nom de la dernière page affichée. On sait que le client [nuxt] a cette information mais pas le serveur [nuxt]. On va lui ajouter cette information en stockant dans le store, à chaque routage du serveur [nuxt], le nom de la page qui va être affichée. On fera de même à chaque routage du client [nuxt]. Ainsi au routage suivant du serveur [nuxt], celui-ci trouvera dans le store, le nom de la dernière page affichée par l’application ;
    • [serverRedirection] : lorsqu’une destination de routage sera refusée par le serveur [nuxt], celui-ci opèrera une redirection. Il indiquera alors dans le store que la prochaine destination du serveur [nuxt] est une page de redirection. Cette redirection va provoquer une nouvelle exécution du routeur du serveur [nuxt]. Si celui-ci découvre que la destination en cours est issue d’une redirection, il laissera faire ;
  • lignes 6-11 : on récupère les informations utiles pour le routage ;
  • lignes 13-16 : la cible [/fin-session] n’est pas associée à une page qui s’appellerait [fin-session]. Elle n’a donc pas de nom. On lui en donne un ;
  • ligne 19 : la cible d’une éventuelle redirection ;
  • ligne 21 : [done=true] lorsque les tests du routage sont terminés ;
  • lignes 23-27 : comme il a été dit, si le routage en cours est issu d’une redirection, il n’y a rien à faire. En effet, lors du routage précédent, le routeur a décidé qu’il fallait rediriger le navigateur client. Il n’y a pas lieu de reconsidérer cette décision ;
  • lignes 29-33 : s’il s’agit d’un rechargement de page, on laisse faire. Ce n’est pas un axiome valable pour toute application [nuxt] : il faut regarder pour chaque page les effets d’un rechargement. Ici, il se trouve que le rechargement des pages [index, calcul-impot, liste-des-simulations] ne provoque pas d’effets indésirables ;
  • lignes 35-85 : le routage du serveur [nuxt] reprend le routage du client [nuxt]. Lorsqu’on est sur une page, le routage du serveur [nuxt] doit refléter le menu de navigation offert par le client [nuxt] lorsqu’on est sur cette page ;
  • lignes 38-47 : on traite d’abord le cas de la cible [fin-session] qui ne correspond pas à une page existante. Si les conditions sont réunies (session démarrée, utilisateur authentifié), on nettoie la session et on redirige l’utilisateur vers la page [index] ;
  • lignes 49-55 : si la session jSON avec le serveur de calcul de l’impôt n’a pas commencé, alors la seule destination possible est la page [index] ;
  • lignes 57-62 : si la session jSON a été démarrée et que l’utilisateur n’est pas authentifié et qu’il n’a pas demandé la page d’authentification, alors on redirige vers la page d’authentification qui est la page [index] ;
  • lignes 64-70 : si l’utilisateur est authentifié mais que la donnée [adminData] n’a pas été obtenue, alors on redirige vers la page d’authentification. L’authentification fait deux choses : elle authentifie et si l’authentification a réussi elle demande de plus la donnée [adminData]. Si cette dernière n’a pas été obtenue alors il faut recommencer l’authentification ;
  • lignes 72-85 : si la donnée [adminData] a été obtenue, alors les seules cibles possibles sont [calcul-impot] et [liste-des-simulations]. Si ce n’est pas le cas, on refuse le routage ;
  • lignes 88-95 : on met à jour le store selon qu’il va y avoir redirection ou non ;
  • ligne 94 : il n’y a pas redirection. Aussi le [to] actuel va devenir le [from] du prochain routage ;
  • lignes 96-99 : les informations du store sont sauvegardées dans le cookie de la session [nuxt] ;
  • lignes 100-103 : si on doit faire une redirection, on la fait ;

Pour faire les tests, il faut prendre soin de partir d’une situation vierge en supprimant le cookie de la session [nuxt] et le cookie de la session PHP avec le serveur de calcul de l’impôt :

image39

Pour tester le routage du serveur [nuxt], sur chaque page essayez toutes les URL possibles [/, /calcul-impot, /liste-des-simulations]. A chaque fois, l’application doit rester dans un état cohérent.

étape 9

L’étape 9 est celle du déploiement de l’application [nuxt-20]. Celle-ci nécessite un hébergement offrant un environnement [node.js] pour exécuter le serveur [nuxt]. Je ne l’ai pas. Le lecteur pourra suivre les procédures décrites au paragraphe lien, pour déployer l’application [nuxt-20] sur sa machine de développement et la sécuriser avec un protocole HTTPS.

Conclusion

Le portage de l’application [vuejs-22] vers l’application [nuxt-20] est désormais terminé. Retenons quelques points de ce portage :

  • les pages de [vuejs-22] ont été gardées ;
  • les opérations asynchrones qui existaient dans les pages de [vuejs-22] ont été migrées dans une fonction [asyncData] ;
  • dans [nuxt-20] il a fallu gérer deux entités : le client [nuxt] et le serveur [nuxt]. Cette dernière entité n’existait pas dans [vuejs-22]. Pour garder une cohérence entre les deux entités, nous avons eu besoin d’une session [nuxt] ;
  • il nous a fallu gérer le routage du serveur [nuxt] ;

Dans la pratique, il est sans doute préférable de démarrer directement sur une architecture [nuxt] que de faire une architecture [vue.js] portée ensuite dans un environnement [nuxt].