Introduction au framework NUXT.JS par l’exemple¶
Auteur¶
Serge Tahé, octobre 2019, https://sergetahe.com
Licence¶
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 :
- [Introduction au langage PHP7 par l’exemple] ;
- [Introduction au langage ECMASCRIPT 6 par l’exemple] ;
- [Introduction au framework VUE.JS par l’exemple] ;
- [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 :
- 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 :
Nous sauvegardons l’espace de travail sous le nom [intro-nuxtjs] [3-5] :
Nous ouvrons un terminal [6-7] :
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] :
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 :
- 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 :
Description de l’arborescence d’une application [nuxt]¶
Reprenons l’arborescence de l’application créée :
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 :
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 ;
Le dossier [layouts]
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]¶
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 :
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.
Le composant [Logo]¶
Le composant [Logo.vue] est le suivant :
<template>
<div class="VueToNuxtLogo">
<div class="Triangle Triangle--two" />
<div class="Triangle Triangle--one" />
<div class="Triangle Triangle--three" />
<div class="Triangle Triangle--four" />
</div>
</template>
<style>
.VueToNuxtLogo {
display: inline-block;
animation: turn 2s linear forwards 1s;
transform: rotateX(180deg);
position: relative;
overflow: hidden;
height: 180px;
width: 245px;
}
.Triangle {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
}
.Triangle--one {
border-left: 105px solid transparent;
border-right: 105px solid transparent;
border-bottom: 180px solid #41b883;
}
.Triangle--two {
top: 30px;
left: 35px;
animation: goright 0.5s linear forwards 3.5s;
border-left: 87.5px solid transparent;
border-right: 87.5px solid transparent;
border-bottom: 150px solid #3b8070;
}
.Triangle--three {
top: 60px;
left: 35px;
animation: goright 0.5s linear forwards 3.5s;
border-left: 70px solid transparent;
border-right: 70px solid transparent;
border-bottom: 120px solid #35495e;
}
.Triangle--four {
top: 120px;
left: 70px;
animation: godown 0.5s linear forwards 3s;
border-left: 35px solid transparent;
border-right: 35px solid transparent;
border-bottom: 60px solid #fff;
}
@keyframes turn {
100% {
transform: rotateX(0deg);
}
}
@keyframes godown {
100% {
top: 180px;
}
}
@keyframes goright {
100% {
left: 70px;
}
}
</style>
Ce composant est essentiellement constitué de styles et d’animations pour créer une image animée.
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 :
- 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 :
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] :
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 :
<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 :
On exécute le projet :
- 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 :
- 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 ;
- 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 :
- [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] :
- 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 :
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 :
- 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] :
Nous copions les dossiers [.nuxt, node_modules] et les fichiers [package.json, nuxt.config.js] dans un dossier séparé :
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] :
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].
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 :
- 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 :
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 :
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].
Un dossier [store] est ajouté au projet ainsi que deux nouvelles pages. Nous y reviendrons.
La page [index]
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 ;
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] :
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.
La page [page1]
Le store [Vuex]
Nous avons ajouté un dossier [store] au projet [nuxt-02] :
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 :
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 ;
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 :
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 :
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.
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] ;
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 :
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 :
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.
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 ;
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 :
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 :
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.
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] ;
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 ;
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 :
- 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 :
- 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 :
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] :
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 :
En utilisant plusieurs fois le bouton [3], on a la nouvelle page suivante :
Si on utilise le lien [3], on a la page suivante :
- 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 :
- 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] :
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-05] : persistance du store avec un cookie de session¶
Objectif : on voudrait que le store [Vuex] ne soit pas réinitialisé à chaque requête vers le serveur. Pour ce faire nous allons utiliser un cookie de session :
le store sera initialisé par le serveur et mis par celui-ci dans un cookie de session ;
le navigateur client recevra ce cookie de session et mécaniquement l’enverra à chaque nouvelle requête vers le serveur ;
le serveur pourra alors récupérer ce cookie de session et travailler avec le store qu’il contient, un store mis à jour par le client ;
Présentation
Le projet [nuxt-05] est obtenu initialement par recopie du projet [nuxt-04] :
Nous allons voir que seul le fichier [store / index.js] va changer.
Pour utiliser des cookies avec [nuxt], nous allons utiliser le module [cookie-universal-nuxt] que nous installlons avec [yarn] dans un terminal VSCode :
- en [4], on tape la commande [yarn add cookie-universal-nuxt] ;
Un nouveau module est ainsi ajouté au fichier [package.json] du projet [dvp] :
1 2 3 4 5 6 7 8 9 | ...
},
"dependencies": {
"@nuxtjs/axios": "^5.3.6",
"bootstrap": "^4.1.3",
"bootstrap-vue": "^2.0.0",
"cookie-universal-nuxt": "^2.0.19",
"nuxt": "^2.0.0"
},
|
Le fichier de configuration [nuxt.config.js]¶
Pour que [nuxt] puisse utiliser les cookies de [cookie-universal-nuxt], il faut déclarer ce module dans le fichier de configuration [nuxt.config.js] :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ...
],
/*
** 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'
],
...
|
- ligne 12, le module [cookie-universal-nuxt] est ajouté au tableau des modules [6] de [nuxt] ;
Le fichier [nuxt.config.js] est au final 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 | 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',
// 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-05',
// routeur
router: {
// racine des URL de l'application
base: '/nuxt-05/'
},
// 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: {
maxAge: 60 * 5
}
}
|
ligne 79 : on a ajouté la clé [env] au fichier. Cette clé est un mot réservé. Les éléments déclarés dans cet objet sont disponibles à partir de l’objet [context.env] dans les éléments de l’application ;
ligne 80 : l’attribut [maxAge] sera la durée de vie maximale du cookie de session, durée qui se mesure à partir de la dernière fois que le cookie a été initialisé. Cette durée s’exprime en secondes. On a mis ici une durée de vie de 5 minutes ;
Le principe de la persistance du store
Les cookies échangés entre le client et le serveur sont disponibles de chaque côté (client et serveur) dans :
- [context.app.$cookies] là ou l’objet [context] est disponible, ç-à-d à peu près partout ;
- [this.$cookies] à l’intérieur d’une vue ;
On obtient un cookie particulier avec l’expression […$cookies.get(‘nom_du_cookie’)]. On fixe la valeur d’un cookie avec l’expression […$cookies.set(‘nom_du_cookie’, valeur_du_cookie)].
Le principe du cookie de persistance du store sera le suivant :
lorsque le serveur va initialiser le store dans la fonction [nuxtServerInit], l’état du store sera stocké dans un cookie nommé ‘session’ ;
le cookie ‘session’ fera alors partie de la réponse HTTP du serveur. On sait qu’un navigateur renvoie au serveur les cookies que celui-ci lui a envoyés. Il le fait à chaque nouvelle requête qu’il fait au serveur. On sait également que le serveur envoie le store à l’intérieur la page qu’il envoie au client ;
au sein du navigateur, l’application cliente récupère le store envoyé par le serveur et va ensuite faire son travail. On fera en sorte qu’à chaque fois qu’elle modifie le store, le nouvel état de celui-ci soit stocké dans le cookie ‘session’ enregistré par le navigateur ;
si l’utilisateur force un appel au serveur, le navigateur client renverra automatiquement tous les cookies que le serveur lui a précédemment envoyés, notamment le cookie nommé ‘session’ ;
lorsque suite à cet appel, le serveur va réinitialiser de nouveau le store, il récupèrera le cookie nommé ‘session’ et initialisera l’état du store avec la valeur de celui-ci ;
il y aura donc continuité du store entre le client et le serveur ;
Initialisation du store
Le store est implémenté dans le fichier [store / index.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 | /* 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
initStore(store, context)
// succès
resolve()
}, 1000)
})
}
}
function initStore(store, context) {
// y-a-t-il un cookie de session dans la requête en cours
const cookies = context.app.$cookies
const session = cookies.get('session')
if (!session) {
// pas de session existante
console.log("nuxtServerInit, initialisation d'une nouvelle session")
// on initialise le store
store.commit('increment', 77)
} else {
console.log("nuxtServerInit, reprise d'une session existante")
// on met à jour le store avec le cookie de session
store.commit('replace', session.store)
}
// on met le store dans le cookie de session
cookies.set('session', { store: store.state }, { path: context.base, maxAge: context.env.maxAge })
// log
console.log('initStore terminé, store=', store.state)
}
|
Commentaires
- lignes 2-5 : le store sera constitué d’un compteur ;
- lignes 9-11 : ce compteur pourra être incrémenté ;
- lignes 13-17 : l’état du store pourra être initialisé à partir d’un nouvel état. Cette fonction est là pour montrer une initialisation possible du store lorsque celui-ci n’est pas limité au seul compteur comme ici ;
- lignes 21-35 : la fonction [nuxtServerInit] n’a pas changé ;
- ligne 30 : lorsque le temps d’attente d’une seconde est écoulé, on initialise le store à l’aide de la fonction des lignes 38-56 ;
- lignes 40-41 : on commence par récupérer le cookie nommé ‘session’ :
- lors de la 1ère exécution de l’application et lors de la 1ère requête faite au serveur, ce cookie n’existera pas encore. Il sera alors créé (ligne 53) et sera envoyé au navigateur client ;
- lors de la même exécution de l’application et lors des requêtes n°s 2, 3, … faites au serveur, ce cookie existera car le navigateur client le renverra avec chaque nouvelle requête faite au serveur ;
- lors d’une seconde exécution de l’application et lors de la 1ère requête faite au serveur, ce cookie peut exister également. En effet, à l’issue de l’étape 1, le cookie a été stocké sur le navigateur avec une certaine durée de vie. Si cette durée de vie n’est pas dépassée, le cookie nommé ‘session’ sera envoyé avec la 1ère requête faite au serveur
En résumé, pour chaque requête faite au serveur : si le cookie ‘session’ est déjà stocké sur le navigateur client alors le serveur va le recevoir, sinon il ne le recevra pas.
- lignes 42-47 : si le serveur ne reçoit pas le cookie de session,
alors le store est initialisé par la ligne 46 ;
- puis ligne 53, un cookie nommé ‘session’ sera créé et placé dans la réponse HTTP du serveur. La valeur du cookie est l’objet [{ store: store.state }]. C’est donc l’état du store et non le store lui-même qui est mis dans le cookie de session ;
- le 3ième paramètre de la fonction [set] est un objet d’options :
- [path] indique à quelle URL ce cookie devra être renvoyé. [context.base] est l’URL de base de l’application [nuxt-05]. Celle-ci est définie dans le fichier [nuxt.config.js] :
1 2 3 4 5 | // routeur
router: {
// racine des URL de l'application
base: '/nuxt-05/'
},
|
- [maxAge] est la durée de vie en secondes du cookie sur le navigateur. Passée cette durée, le navigateur ne le renvoie plus au serveur. [context.env.maxAge] renvoie là encore une valeur inscrite dans le fichier [nuxt.config.js] :
1 2 3 4 | // environnement
env: {
maxAge: 60 * 5
}
|
[env] est un mot clé réservé du fichier de configuration. On fixe ici la durée de vie à 5 minutes. Cette durée est mesurée par rapport à la dernière fois où le navigateur a reçu le cookie de session. Passée cette durée, le cookie ne sera pas renvoyé au serveur qui devra alors démarrer une nouvelle session ;
lignes 48-50 : si le serveur reçoit le cookie de session, alors l’état du store est initialisé avec l’objet [store] du cookie de session. On se rappelle que cet objet contient l’état sauvegardé du store ;
- puis ligne 53, le cookie de session sera placé dans la réponse
faite au navigateur client :
- la fonction [get] va chercher le cookie de session dans la requête reçue par le serveur ;
- la fonction [set] met le cookie de session dans la réponse que le serveur fait au navigateur client ;
Incrémentation du compteur du store
- puis ligne 53, le cookie de session sera placé dans la réponse
faite au navigateur client :
L’incrémentation du compteur dans la page [index.vue] évolue de la façon suivante :
1 2 3 4 5 6 7 8 9 10 11 12 | // 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
// sauvegarde du store dans le cookie de session
this.$cookies.set('session', { store: this.$store.state }, { path: this.$nuxt.context.base, maxAge: this.$nuxt.context.env.maxAge })
}
}
|
Du côté client, à chaque fois qu’on modifie le store, il faut le sauvegarder dans le cookie de session. En effet, l’utilisateur peut demander une URL à la main à tout moment et on doit être alors capable d’envoyer au serveur un store à jour. C’est pourquoi, ligne 10, après l’incrémentation du compteur du store, on sauvegarde l’état de celui-ci dans le cookie de session :
les cookies sont disponibles dans la propriété [this.$cookies] ;
l’état du store [this.$store.state] est sauvegardé dans le cookie associé à la clé [store] ;
le chemin du cookie est [context.base]. Dans une vue, le contexte est disponible dans [this.$nuxt.context] ;
la durée de vie du cookie est [context.env.maxAge] disponible ici dans la propriété [this.$nuxt.context.env.maxAge] ;
Exécution de l’exemple [nuxt-05]
Nous lançons l’application [nuxt-05] :
Les copies d’écran qui suivent sont celles d’un navigateur Chrome. Nous demandons l’URL [http://localhost:81/nuxt-05/]. N’oubliez pas le dernier / derrière /nuxt-05, sinon vous n’aurez pas les résultats escomptés :
- en [4], nous avons obtenu la valeur initiale du store (77) ;
Examinons les logs du navigateur (F12) :
- en [5-6], les logs du serveur ;
- en [7], on voit que le serveur démarre une nouvelle session. Cela veut dire qu’il n’a pas reçu de cookie de session ;
- en [8], initialisation du compteur avec la valeur 77 ;
- en [9], la page [index] du serveur (9) et celle du client (10) affichent bien la même valeur du compteur ;
Maintenant regardons les cookies reçus par le navigateur :
- en [1], choisissez l’onglet [Application] puis l’option [Cookies] [2]. Parmi tous les cookies de votre navigateur, choisissez celui du domaine [http://localhost:81];
- en [4], le cookie nommé ‘session’. Si vous ne l’avez pas, rechargez la page [F5] : peut-être avez-vous dépassé sa durée de vie qui est de 5 mn ;
- en [5], la valeur du cookie. Bien que ce ne soit pas très lisible à cause de l’encodage des caractères { :, on distingue la valeur 77 du compteur ;
- en [6], l’URL du cookie : à chaque fois que cette URL sera demandée, le navigateur enverra le cookie au serveur ;
- en [7], l’heure de fin de validité du cookie. Lorsque cette heure sera dépassée, le cookie sera détruit sur le navigateur ;
Assurez-vous d’avoir ce cookie. Si vous ne l’avez pas, rechargez la page (F5). Lorsque vous avez la page avec son cookie, rechargez de nouveau la page (F5). Les logs deviennent alors les suivants :
Cette fois-ci en [3], le serveur a bien récupéré le cookie de session. C’est le navigateur client qui le lui a envoyé.
Maintenant, procédez à des incrémentations du compteur, puis de temps en temps rechargez la page courante (F5), que ce soit [index] ou [page1], vous devez constater que le compteur ne revient pas à 77 comme dans l’exemple [nuxt-04] mais garde la valeur qu’il avait sur le navigateur client avant le rechargement de la page :
Les logs du navigateur sont alors les suivants :
Note : pour les tests vous pouvez avoir besoin de supprimer [5] le cookie de session stocké sur le navigateur pour repartir avec une nouvelle session, initialisée par le serveur, lors de la prochaine requête vers celui-ci.
Enfin montrons l’influence de la fonction [incrementCounter] de la page [index] sur le cookie de session stocké sur le navigateur client :
1 2 3 4 5 6 7 8 9 10 11 12 | // 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
// sauvegarde du store dans le cookie de session
this.$cookies.set('session', { store: this.$store.state }, { path: this.$nuxt.context.base, maxAge: this.$nuxt.context.env.maxAge })
}
}
|
- ligne 10 : la modification du compteur est répercutée sur le cookie de session ;
Vérifions ce point. On part de la situation suivante :
- en [4], le compteur du cookie de session reflète bien la valeur affichée [1] ;
Maintenant, incrémentons le compteur une fois [5]. Le cookie de session en [4] évolue de la façon suivante :
- en [7], le compteur du cookie de session est bien passé à 84. Pour le voir, il faut rafraîchir la vue [8]. Pour cela sélectionnez une autre option du [Storage] [9], puis resélectionnez l’option [8]. La nouvelle valeur du cookie de session devrait alors apparaître ;
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] :
- 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 ;
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 :
Maintenant demandons l’URL [http://localhost:81/nuxt-06/] :
Les logs dans le navigateur sont alors les suivants :
- 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 :
- 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 :
- 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 ;
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 :
Puis utilisons le bouton [Incrémenter] trois fois. La page devient la suivante :
Cette fois-ci la session s’affiche correctement en [2]. Elle est ici réactive. Cela se voit dans les logs :
- 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 :
Puis cliquons deux fois sur le bouton [Incrémenter] [4]. La page devient la suivante :
On constate que là également la session est devenue réactive [2].
Demandons la valeur rendue par la fonction [this.$session()] :
- 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()] :
- 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] :
- 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] :
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 ;
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 ;
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-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] :
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] ;
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 ;
Exécution
Lançons l’application, puis demandons, à la main, au serveur la page [page1] :
Les logs sont les suivants :
- 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 :
- en [1], on voit apparaître la barre de progression ;
Au bout de 5 secondes, on a la page [Page 1] :
Les logs sont les suivants :
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 :
L’exemple [nuxt-11] montre également comment gérer des erreurs de chargement.
L’exemple [nuxt-11] est obtenu initialement par recopie de l’exemple [nuxt-10] :
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 ;
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 ;
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] :
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’ ;
Exécution
[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 ;
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 :
Les logs sont les suivants :
- 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 : {"statusCode":500,"message":"le serveur n'a pas répondu assez vite"}</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] ;
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 :
Les logs affichés sont les suivants :
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 ;
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 :
Examinons les logs affichés dans le navigateur :
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] ;
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 :
puis au bout de 5 secondes, on obtient la page suivante :
La page finale est donc identique à celle obtenue côté serveur.
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 :
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];
La page [page1] exécutée par le client
Après l’attente de 5 secondes, le client affiche la page suivante :
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 :
La page [page2] exécutée par le client¶
Après l’attente de 5 secondes, le client affiche la page suivante :
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 :
- 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 :
- 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 :
- 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¶
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¶
Nous allons donner à l’application [nuxt] l’accès à l’API du serveur de calcul de l’impôt via la vue suivante :
- 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 :
- 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]
hp?action=init-session&type=json |
{
"action": "init-session",
"état": 700,
"réponse": "se
|
[au
est stockée dans la session PHP]
?action=authentifier-utilisateur
|
{
"acti
|
[demande des donn
nt stockées dans la session PHP]
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
],
enusCouplePourReduction »: 42074,
plafondDecoteCelibataire »: 1196,
battementDixPourcentMax »: 12502,
|
[fin de la session PHP avec
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é]
|
{
"action": "fin-session",
"état": 400,
|
Le couche [dao] du serveur [nuxt]¶
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 :
- celui échangé entre le client [nuxt] et le serveur PHP 7 ;
- celui échangé entre le serveur [nuxt] et le serveur PHP 7 ;
- 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 :
- l’application [nuxt] est lancée ;
- 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] ;
- 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] ;
- 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] :
- 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 ;
- 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]¶
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 :
- en le demandant au navigateur ;
- 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]¶
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]¶
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]¶
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¶
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]¶
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]¶
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]¶
[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]¶
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 :
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) :
- 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 :
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] :
- le client [nuxt] s’exécute après que le serveur [nuxt] ait envoyé au navigateur du client [nuxt] la page [authentification] ;
- le client [nuxt] parce que l’utilisateur a cliqué sur le lien [Authentification] du menu de navigation :
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) :
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é :
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] :
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 :
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] :
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 :
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-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 :
La seconde vue est celle du calcul de l’impôt :
La 3ième vue est celle qui affiche la liste des simulations faites par l’utilisateur :
L’écran ci-dessus montre qu’on peut supprimer la simulation n° 1. On obtient alors la vue suivante :
Si on supprime maintenant la dernière simulation, on obtient la nouvelle vue suivante :
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] :
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] :
- 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 :
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] :
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 :
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
|
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 :
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] :
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 :
é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 :
Regardons les logs du navigateur :
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] :
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 :
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] :
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 :
La page obtenue est la suivante :
On a bien obtenu la page de calcul de l’impôt. Maintenant regardons les logs :
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 :
On obtient la réponse suivante :
Si on regarde les logs :
- 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 :
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 :
On obtient alors la page suivante :
Regardons les logs :
- en [6], on voit que le tableau des simulations est vide ;
Maintenant revenons au formulaire de calcul de l’impôt :
On obtient la page suivante :
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] :
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 :
- 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 :
Nous rechargeons la même page après avoir lancé le serveur de calcul de l’impôt :
Regardons les logs :
- 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 :
Nous obtenons la page suivante :
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] ;
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 :
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].