INTRODUCTION AU LANGAGE ECMASCRIPT6 PAR L’EXEMPLE

Auteur

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

Téléchargements

Téléchargement du PDF du cours

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

Présentation du cours

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

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

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] ;

La dernière version du serveur de calcul de l’impôt développée dans le document [1] peut être améliorée de diverses manières :

  • la version écrite est centrée sur le serveur. La tendance est désormais (juillet 2019) au client / serveur :
    • le serveur fonctionne en service jSON ;
    • une page statique ou non est le point d’entrée de l’application web. Cette page contient du HTML /CSS mais aussi du Javascript ;
    • les autres pages de l’application web sont obtenues dynamiquement par le Javascript :
      • la page HTML peut être obtenue par assemblage de fragments statiques, fournis par le même serveur qui a fourni la page d’accueil ou bien entièrement construite par le Javascript ;
      • ces différentes pages affichent des données qui sont demandées au service jSON ;

Ainsi le travail est réparti sur le client et le serveur. Le serveur ainsi déchargé peut servir davantage d’utilisateurs.

L’architecture correspondant à ce modèle est le suivant :

image0

JS : Javascript

Le code javascript est client :

  • d’un service de pages ou fragments statiques ou non ;
  • d’un service jSON ;

Le code Javascript est donc un client jSON et à ce titre peut être organisé en couches [UI, métier, dao] (UI : User Interface) comme l’ont été nos clients jSON écrits en PHP. Au final, le navigateur ne charge qu’une unique page, la page d’accueil. Toutes les autres sont obtenues et construites par le Javascript. On appelle ce type d’application SPA : Single Page Application ou encore APU : Application à Page Unique.

Ce type d’application fait également partie des applications dites AJAX : Asynchronous Javascript And XML

  • Asynchronous : parce que les appels du client Javascript au serveur jSON sont asynchrones ;
  • XML : parce que XML était la technologie utilisée avant l’avènement du jSON. On a cependant gardé l’acronyme AJAX ;

Nous allons étudier une telle architecture dans les chapitres à venir. Côté client, nous utiliserons le framework Javascript [Vue.js] [https://vuejs.org/] pour écrire le client Javascript du serveur jSON PHP que nous avons écrit dans le document [1].

[Vue.js] est un framework Javascript. Pour le comprendre, il faut maîtriser ce langage. Nous présentons dans ce document la norme ECMAScript 6 qui est la normalisation la plus récente (en 2019) de ce langage. On trouvera l’historique et le rôle d’ECMAScript sur Wikipedia [https://fr.wikipedia.org/wiki/ECMAScript].

Ce document propose une liste de scripts console Javascript dans différents domaines (structures du langage, accès aux bases de données, au réseau internet, programmation en couches, programmation par interfaces). Le document se termine avec deux applications :

Une application console qui sera un client du serveur de calcul de l’impôt construit dans le document [1]. Ce client aura la même architecture que celle du client console PHP construit dans le document [1] :

image1

  • les couches [7-9] seront celles du client Javascript s’exécutant dans une console ;
  • les couches [1-4] sont celles du serveur PHP 7 construit dans le document [1] ;
  • contrairement au client PHP console construit dans le document [1], il n’y aura pas d’interactions avec le système de fichiers local [6] ;

On a là une application client / serveur où le client est une application console. Une seconde application sera écrite où le code des couches [7-9] sera porté dans un navigateur. Nous serons alors prêts à aborder les frameworks Javascript des navigateurs dans les documents [3] et [4].

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é, octobre 2019

Installation d’un environnement de travail

Nous allons utiliser les outils suivants (sous Windows 10 x 64 bits) :

  • [Laragon] pour exécuter le serveur web PHP ;

  • [Netbeans] pour modifier le code PHP ;

  • [Visual Studio Code] pour écrire les codes Javascript ;

  • [node.js] pour les exécuter ;

  • [npm] pour télécharger et installer les bibliothèques Javascript dont nous aurons besoin ;

    1. Environnement de travail pour le serveur web

Les scripts PHP ont été écrits et testés dans l’environnement suivant :

  • un environnement serveur web Apache / SGBD MySQL / PHP 7.3 appelé Laragon ;

  • l’IDE de développement Netbeans 10.0 ;

    1. Installation de Laragon

Laragon est un package réunissant plusieurs logiciels :

  • un serveur web Apache. Nous l’utiliserons pour l’écriture de scripts web en PHP ;
  • le SGBD MySQL ;
  • le langage de script PHP ;
  • un serveur Redis implémentant un cache pour des applications web :

Laragon peut être téléchargé (mars 2019) à l’adresse suivante :

https://laragon.org/download/

image0

image1

image2

  • l’installation [1-5] donne naissance à l’arborescence suivante :

image3

  • en [6] le dossier d’installation de PHP ;

Le lancement de [Laragon] affiche la fenêtre suivante :

image4

  • [1] : le menu principal de Laragon ;
  • [2] : le bouton [Start All] lance le serveur web Apache et le SGBD MySQL ;
  • [3] : le bouton [WEB] affiche la page web [http://localhost] qui correspond au fichier PHP [<laragon>/www/index.php] où <laragon> est le dossier d’installation de Laragon ;
  • [4] : le bouton [Database] permet de gérer le SGBD MySQL avec l’outil [phpMyAdmin]. Il faut auparavant installer celui-ci ;
  • [5] : le bouton [Terminal] ouvre un terminal de commandes ;
  • [6] : le bouton [Root] ouvre un explorateur Windows positionné sur le dossier [<laragon>/www] qui est la racine du site web [http://localhost]. C’est là qu’il faut placer toutes les applications web gérées par le serveur Apache de Laragon ;

Ouvrons un terminal Laragon [5] :

image5

  • en [1], le type du terminal. Trois types de terminaux sont disponibles en [6] ;

  • en [2, 3] : le dossier courant ;

  • en [4], on tape la commande [echo %PATH%] qui affiche la liste des dossiers explorés lors de la recherche d’un exécutable. Tous les principaux dossiers de Laragon sont inclus dans ce chemin des exécutables, ce qui ne serait pas le cas si on ouvrait une fenêtre de commandes [cmd] dans Windows. Dans ce document, lorsqu’on est amené à taper des commandes pour installer tel ou tel logiciel, c’est en général dans un terminal Laragon que ces commandes sont tapées ;

    1. Installation de l’IDE Netbeans 10.0

L’IDE Netbeans 10.0 peut être téléchargé à l’adresse suivante (mars 2019) :

https://netbeans.apache.org/download/index.HTML

image6

Le fichier téléchargé est un zip qu’il suffit de dézipper. Une fois Netbeans installé et lancé, on peut créer un premier projet PHP.

image7

  • en [1], prendre l’option File / New Project ;
  • en [2], prendre la catégorie [PHP] ;
  • en [3], prendre le type de projet [PHP Application] ;

image8

  • en [4], donner un nom au projet ;
  • en [5], choisir un dossier pour le projet ;
  • en [6], choisir la version de PHP téléchargée ;
  • en [7], choisir l’encodage UTF-8 pour les fichiers PHP ;
  • en [8], choisir le mode [Script] pour exécuter les scripts PHP en mode ligne de commande. Choisir [Local WEB Server] pour exécuter un script PHP dans un environnement web ;
  • en [9,10], indiquer le répertoire d’installation de l’interpréteur PHP du package Laragon :

image9

  • choisir [Finish] pour terminer l’assistant de création du projet PHP ;

image10

  • en [11], le projet est créé avec un script [index.php] ;
  • en [12], on écrit un script PHP minimal ;
  • en [13], on exécute [index.php] ;

image11

  • en [14], les résultats dans la fenêtre [output] de Netbeans ;
  • en [15], on crée un nouveau script ;
  • en [16], le nouveau script ;

Le lecteur pourra créer tous les scripts qui vont suivre dans différents dossiers du même projet PHP. Les codes source des scripts de ce document sont disponibles sous la forme de l’arborescence Netbeans suivante :

image12

Les scripts de ce document sont placés dans l’arborescence du projet [scripts-console] [1]. Nous allons utiliser également des bibliothèques PHP qui seront placées dans le dossier [<laragon-lite>/www/vendor] [2] où <laragon-lite> est le dossier d’installation du logiciel Laragon. Pour que Netbeans reconnaisse les bibliothèques de [2] comme faisant partie du projet [scripts-console], il nous faut inclure le dossier [vendor] [2] dans la branche [Include Path] [3] du projet. Nous allons configurer Netbeans pour que le dossier [<laragon-lite>/www/vendor] [2] soit inclus dans tout nouveau projet PHP et pas seulement dans le projet [scripts-console] :

image13

  • en [1-2], on va dans les options de Netbeans ;
  • en [3-4], on configure les options de PHP ;
  • en [5-7], on configure le [Global Include Path] de PHP : les dossiers indiqués en [7] sont automatiquement inclus dans le [Include Path] de tout projet PHP ;

image14

  • en [9], on accède aux propriétés de la branche [Include Path] ;
  • en [10-11], les nouvelles bibliothèques explorées par Netbeans. Netbeans explore le code PHP de ces bibliothèques et mémorise leurs classes, interfaces, fonctions… afin de pouvoir proposer de l’aide au développeur ;

image15

  • en [12], un code utilise la classe [PhpMimeMailParserParser] de la bibliothèque [vendor/php-mime-mail-parser] ;
  • en [13], Netbeans propose les méthodes de cette classe ;
  • en [14-15], Netbeans affiche la documentation de la méthode sélectionnée ;

La notion d’[Include Path] est ici propre à Netbeans. PHP a également cette notion mais ce sont a priori deux notions différentes.

Maintenant que l’environnement de travail a été installé, nous pouvons aborder les bases de PHP.

Environnement de travail pour JavaScript

Ces outils peuvent être installés à partir de Laragon (cf. paragraphe lien) :

image16

En [4], on installe [node.js]. Une fois l’installation terminée, on ouvre un terminal Laragon (cf. paragraphe lien) et on demande la version de [node.js] installée (1) ainsi que celle de [npm] (2) :

image17

Ensuite nous installons Visual Studio Code appelé fréquemment [code] ou [VSCode] [3-6]. Ceci fait, nous pouvons lancer cet outil de développement :

image18

image19

Configuration de Visual Studio Code

Nous montrons maintenant comment nous avons configuré [VSCode] afin que le lecteur comprenne les copie d’écran qui apparaîtront de temps en temps. Le lecteur est lui libre de configurer [VSCode] comme il l’entend. Il peut même installer son environnement de travail favori. Celui-ci importe peu pour ce que nous allons faire par la suite.

Tout d’abord, nous changeons l’apparence de la fenêtre [VSCode] pour avoir un fond clair plutôt que noir :

image20

Puis nous cachons la barre de gauche [1-2] dont les éléments sont également disponibles dans le menu. En [3-6], nous demandons un formatage du code à chaque sauvegarde du fichier et à chaque copier / coller.

image21

Après avoir sauvegardé la configuration [Ctrl-S], on peut fermer la fenêtre [Settings] [7]. On peut revenir à tout moment à la configuration de [VSCode] [8-10] :

image22

Ces configurations sont sauvegardées dans un fichier [settings.json] que l’on peut éditer directement. Ouvrons la fenêtre de configuration [Settings] comme il a été vu :

image23

En [4], on peut éditer directement le fichier [settings.json] :

image24

  • en [1], le chemin du fichier [settings.json]. Une façon de revenir à la configuration par défaut est de supprimer ce fichier ;
  • en [2], les configurations que nous venons de faire ;

Maintenant, ouvrons un terminal à l’intérieur de [VSCode] [1-2] :

image25

  • en [3], le type de terminal ouvert, ici PowerShell ;
  • en [4], on peut taper des commandes Windows ;
  • en [6], on peut ouvrir d’autres terminaux ;
  • en [5], la liste des terminaux ouverts ;
  • en [7], supprime le terminal actif ;

Nous utiliserons le terminal de [VSCode] pour installer des packages (bibliothèques) Javascript avec l’outil [npm] (Node Package Manager). Demandons, comme nous l’avons fait précédemment dans un terminal Laragon, la version de [npm] installée :

image26

On voit que la commande [npm] n’a pas été reconnue. Cela signifie qu’elle n’appartient pas au PATH (liste des dossiers à explorer pour chercher un exécutable, ici [npm]) du terminal. On peut connaître le PATH utilisé par le terminal :

image27

L’exécutable [npm] ne se trouve pas parmi ces dossiers. Comme les autres outils installés par Laragon, il se trouve dans le dossier [<laragon>bin] de Laragon et plus précisément dans le dossier de [nodejs] [4-6].

image28

Pour lancer [VSCode] et avoir accès à [npm], nous lancerons [VSCode] à partir d’un terminal Laragon. Lancé de cette façon, [VSCode] va hériter du PATH du terminal Laragon qui lui, contient le dossier des exécutables [node] et [npm] :

image29

  • en [1] : on tape la commande [path] ;
  • en [2] : la liste des dossiers du PATH. On y voit le dossier [node-v10] [2], ce qui nous garantit que les exécutables [node] et [npm] seront trouvés ;

[VSCode] est lancé à partir d’un terminal Laragon avec la commande [code] :

image30

  • en [2], on ouvre un terminal PowerShell dans [VSCode] ;
  • en [3-4], on voit que les exécutables [node] et [npm] sont accessibles ;

Note : il ne faut pas fermer le terminal Laragon qui a lancé l’environnement de développement [VSCode], sinon VSCode lui-même se ferme.

Nous allons faire une dernière configuration : nous allons changer le terminal par défaut de [VSCode] :

image31

image32

Le fichier [settings.json] se met aussitôt à jour :

image33

Maintenant, ouvrons un nouveau terminal [VSCode] [1] :

image34

  • en [2], un terminal [cmd] (pas PowerShell) ;

  • en [3], la commande [path] donne le PATH du terminal ;

  • en [4], on y voit bien le dossier des exécutables [node] et [npm]

    1. Ajout d’extensions à Visual Studio Code

Créons un fichier Javascript avec [VSCode] :

image35

image36

  • en [3-4], on crée un dossier ;
  • en [5], on en fait le dossier courant de [VSCode] ;
  • en [6], on ouvre un terminal ;
  • en [7], on voit qu’on est positionné sur le dossier choisi. Les opérations à suivre vont se faire dans celui-ci ;

image37

  • en [1-3] : on crée un nouveau dossier ;
  • en [4] : on ajoute un fichier dans ce dossier ;

image38

  • en [5-7] : on crée notre 1er programme Javascript ;

image39

  • en [8-9] : on exécute le programme Javascript ;
  • le résultat apparaît dans la console d’exécution [10]. On voit en [11] la commande qui a été exécutée : c’est l’application [node] qui a exécuté le script [test-01.js]. C’est parce que cet exécutable est dans le PATH de [VSCode] que cela a pu être possible, sinon on aurait eu une erreur indiquant que la commande [node] n’était pas connue ;

Procédons de la même façon pour exécuter un second script [test-02.js] :

image40

  • en [1-3], on définit le nouveau script. L’instruction [use strict] de la ligne 1 demande une vérification stricte de la syntaxe. Dans ce contexte, toute variable doit être déclarée avec l’un des mots clés [let, const, var]. Ce n’est pas le cas de la variable [x] de la ligne 2 ;
  • lorsqu’on exécute ce code par [Ctrl-F5], on obtient l’erreur [5]. Il est possible d’être averti de ce type d’erreur avant l’exécution. C’est préférable. Nous allons faire deux choses :
    • installer avec [npm] une bibliothèque appelée [eslint] qui vérifie que la syntaxe du script est conforme à la norme ECMAScript 6 ;
    • installer une extension à Visual Studio Code, appelée elle-aussi ESLINT qui facilite l’utilisation de la bibliothèque [eslint] au sein de [VSCode] ;

Installons tout d’abord la bibliothèque Javascript [eslint] à l’aide de l’outil [npm]. Pour installer une bibliothèque (on dit un package) [npm], il faut connaître son nom exact. Si ce n’est pas le cas, on peut aller sur le site de [npm] à l’URL (2019) [https://www.npmjs.com/] :

image41

  • en [3], les packages commençant par [esl] ;
  • en [4-6], on trouve le package [eslint] ;

image42

  • en [7], la commande [npm] pour installer le package [eslint] ;
  • en [8], la configuration du package ;
  • en [9], son utilisation pour vérifier la syntaxe d’un script Javascript ;

Nous installons le package [eslint] dans une fenêtre [Terminal] de [VSCode]. Tout d’abord, il nous faut créer un fichier [package.json] à la racine du dossier de travail de [VSCode]. Ce fichier contiendra la liste des packages jS utilisés par le projet [VSCode] :

image43

  • en [1], cliquer droit dans l’explorateur de projet (pas sur le dossier tests) ;
  • en [3-4], on crée le fichier [package.json] à la racine du projet [javascript], au même niveau que le dossier [tests] (mais pas dans [tests]) ;
  • en [4-6], on met dans le fichier [package.json] un objet jSON vide ;

Puis on ouvre un terminal [VSCode] pour installer [eslint] :

image44

  • en [2], on est à la racine du projet [javascript] ;
  • en [3], la commande qui installe le package [eslint] ;
  • après exécution,
    • en [4-5], le fichier [package.json] a été modifié. Ligne 3, on trouve la version de [eslint] installée. Ligne 2, [devDependencies] correspond à l’argument [–save-dev] de l’installation. Cet argument signifie que la dépendance installée doit être inscrite dans le fichier [package.json] comme élément de la propriété [devDependencies]. Cette propriété liste les dépendances du projet dont on a besoin en mode développement mais pas en mode production. En effet, on a besoin de la dépendance [eslint] uniquement en développement pour vérifier que le code écrit respecte la norme ECMAScript ;
    • en [6], un dossier [node_modules] est apparu dans le projet. C’est le dossier où sont installées les dépendances du projet ;

image45

  • en [7], une partie des packages installés. Ceux-ci sont très nombreux. En effet, non seulement le package [eslint] a été installé mais également tous les packages sur lesquels celui-ci s’appuie ;

image46

  • [1-2], dans un terminal [VSCode] on émet la commande de configuration du package [eslint]. Celle-ci va poser diverses questions [3] pour savoir comment on souhaite utiliser [eslint]. Dans le doute, laissez les options proposées par défaut. Pour sélectionner une option, utilisez les flèches haute et basse du clavier pour choisir l’option puis validez celle-ci ;
  • en [4], un fichier [.eslintrc.js] a été créé à la racine du projet ;
  • en [6], le contenu du fichier. Vous pouvez en copier le contenu dans votre propre fichier ;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module.exports = {
  "env": {
    "browser": true,
    "es6": true
  },
  "extends": "eslint:recommended",
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly"
  },
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module"
  },
  "rules": {
  }
};

Tout ceci n’est pas suffisant pour signaler les erreurs du fichier [test-02.js] :

image47

  • il faut taper la commande [2-3] pour que le fichier [tests/test-02.js] soit analysé ;
  • en [4], l’erreur sur la variable non déclarée est détectée ;

Nous allons ajouter à [VSCode] une extension qui va permettre de voir les erreurs Javascript en temps réel. Cette extension s’appuie sur le package [eslint] que nous avons installé :

image48

  • en [3-5], nous installons l’extension appelée [ESLint] ;

image49

  • en [1], une page d’informations sur l’extension nouvellement installée ;

en [2], on voit que le mode de vérification de [ESLint] est [type], ce qui signifie que la syntaxe des scripts jS sera vérifiée en même temps que la frappe du texte ;

ESLint peut être configuré via le fichier de configuration général de [VSCode] :

image50

  • en [6-7], la configuration de [ESLint]. C’est ici que vous pourrez la modifier ;

Revenons maintenant au fichier [test-02.js] :

image51

  • en [3-4], les erreurs sur la variable [x] sont désormais signalées ;
  • en [5] : le nombre d’erreurs ESLint dans le fichier ;
  • en [6], indique que dans le dossier [tests], il y a des fichiers erronés ;

Si on corrige l’erreur, les avertissements d’ESLint disparaissent :

image52

Installons maintenant une extension appelée [Code Runner] :

image53

  • une fois installée l’extension [Code-Runner] [1-5], on peut la configurer avec [6-7] (ci-dessus) ;

image54

  • en [1-2], les éléments de configuration de [Code-Runner] ;
  • en [3], on demande à ce que le terminal de sortie soit nettoyé avant chaque exécution ;
  • en [4], on localise l’élément [Executor Map] qui liste les outils d’exécution de différents langages ;
  • en [5-6], on copie la configuration dans le presse-papiers ;
  • en [7-8], on modifie le fichier [settings.json] ;

image55

  • en [2], on ajoute la virgule derrière le dernier élément du fichier [settings.json] [1] ;
  • en [3], on colle ce qu’on a copié en [5-6] précédemment : c’est la liste des commandes permettant d’exécuter les différents langages supportés par [VSCode] ;
  • en [4], la commande permettant d’exécuter les fichiers Javascript. Celle-ci ne fonctionne que si [node] est dans le PATH de [VSCode]. Si ce n’est pas le cas, on peut mettre le chemin complet de l’exécutable [5] ;

Ceci fait sauvegardons la configuration (Ctrl-S). Avec l’extension [Code Runner], les fichiers Javascript peuvent être exécutés avec un clic droit sur le code [6] (ci-dessus) :

image56

Quelques commandes [VSCode] utiles

  • pour formater votre code, cliquez droit dessus [1] ;
  • pour fermer les fenêtres ouvertes, cliquez droit sur leurs titres [2-3] ;

image57

  • pour afficher une fenêtre particulière [4-5] ;
  • pour sauvegarder votre projet (Workspace) [6-9] ;
  • pour sauvegarder un projet [10-11] ;

image58

image59

  • pour ouvrir un projet [11-16] :

image60

  • voir les extensions installées [19-20] :

image61

Nous avons désormais de bons outils pour développer en Javascript. Nous allons maintenant présenter ce langage à l’aide de courts extraits de code. Comme cette présentation se fait à la suite d’un cours PHP, nous ferons parfois des comparaisons entre ces deux langages pour signaler des différences entre eux.

Les bases de Javascript

Note : dans la suite, le terme [Javascript] désignera toujours la norme ECMAScript 6.

A l’intérieur du projet Javascript précédent, créez un dossier [bases]. Nous y mettrons les exemples de cette section :

image0

script [bases-01]

Pour introduire les bases de PHP7, nous avions utilisé le code suivant(cf. 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
 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
<?php

// ceci est un commentaire
// variable utilisée sans avoir été déclarée
$nom = "dupont";
// un affichage écran
print "nom=$nom\n";
// un tableau avec des éléments de type différent
$tableau = array("un", "deux", 3, 4);
// son nombre d'éléments
$n = count($tableau);
// une boucle
for ($i = 0; $i < $n; $i++) {
  print "tableau[$i]=$tableau[$i]\n";
}
// initialisation de 2 variables avec le contenu d'un tableau
list($chaine1, $chaine2) = array("chaine1", "chaine2");
// concaténation des 2 chaînes
$chaine3 = $chaine1 . $chaine2;
// affichage résultat
print "[$chaine1,$chaine2,$chaine3]\n";
// utilisation fonction
affiche($chaine1);
// le type d'une variable peut être connu
afficheType("n", $n);
afficheType("chaine1", $chaine1);
afficheType("tableau", $tableau);
// le type d'une variable peut changer en cours d'exécution
$n = "a changé";
afficheType("n", $n);
// une fonction peut rendre un résultat
$res1 = f1(4);
print "res1=$res1\n";
// une fonction peut rendre un tableau de valeurs
list($res1, $res2, $res3) = f2();
print "(res1,res2,res3)=[$res1,$res2,$res3]\n";
// on aurait pu récupérer ces valeurs dans un tableau
$t = f2();
for ($i = 0; $i < count($t); $i++) {
  print "t[$i]=$t[$i]\n";
}
// des tests
for ($i = 0; $i < count($t); $i++) {
  // n'affiche que les chaînes
  if (getType($t[$i]) === "string") {
    print "t[$i]=$t[$i]\n";
  }
}
// opérateurs de comparaison == et ===
if("2"==2){
  print "avec l'opérateur ==, la chaîne 2 est égale à l'entier 2\n";
}else{
  print "avec l'opérateur ==, la chaîne 2 n'est pas égale à l'entier 2\n";
}
if("2"===2){
  print "avec l'opérateur ===, la chaîne 2 est égale à l'entier 2\n";
}
else{
  print "avec l'opérateur ===, la chaîne 2 n'est pas égale à l'entier 2\n";
}
// d'autres tests
for ($i = 0; $i < count($t); $i++) {
  // n'affiche que les entiers >10
  if (getType($t[$i]) === "integer" and $t[$i] > 10) {
    print "t[$i]=$t[$i]\n";
  }
}
// une boucle while
$t = [8, 5, 0, -2, 3, 4];
$i = 0;
$somme = 0;
while ($i < count($t) and $t[$i] > 0) {
  print "t[$i]=$t[$i]\n";
  $somme += $t[$i];   //$somme=$somme+$t[$i]
  $i++;               //$i=$i+1
}//while
print "somme=$somme\n";

// fin programme
exit;

//----------------------------------
function affiche($chaine) {
  // affiche $chaine
  print "chaine=$chaine\n";
}

//affiche
//----------------------------------
function afficheType($name, $variable) {
  // affiche le type de $variable
  print "type[variable $" . $name . "]=" . getType($variable) . "\n";
}

//afficheType
//----------------------------------
function f1($param) {
  // ajoute 10 à $param
  return $param + 10;
}

//----------------------------------
function f2() {
  // rend 3 valeurs
  return array("un", 0, 100);
}
?>

Traduit en Javascript, cela donne le code 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
'use strict';
// ceci est un commentaire
// constante
const nom = "dupont";
// un affichage écran
console.log("nom : ", nom);
// un tableau avec des éléments de type différent
const tableau = ["un", "deux", 3, 4];
// son nombre d'éléments
let n = tableau.length;
// une boucle
for (let i = 0; i < n; i++) {
  console.log("tableau[", i, "] = ", tableau[i]);
}
// initialisation de 2 variables avec le contenu d'un tableau
let [chaine1, chaine2] = ["chaine1", "chaine2"];
// concaténation des 2 chaînes
const chaine3 = chaine1 + chaine2;
// affichage résultat
console.log([chaine1, chaine2, chaine3]);
// utilisation fonction
affiche(chaine1);
// le type d'une variable peut être connu
afficheType("n", n);
afficheType("chaine1", chaine1);
afficheType("tableau", tableau);
// le type d'une variable peut changer en cours d'exécution
n = "a changé";
afficheType("n", n);
// une fonction peut rendre un résultat
let res1 = f1(4);
console.log("res1=", res1);
// une fonction peut rendre un tableau de valeurs
let res2, res3;
[res1, res2, res3] = f2();
console.log("(res1,res2,res3)=", [res1, res2, res3]);
// on aurait pu récupérer ces valeurs dans un tableau
let t = f2();
for (let i = 0; i < t.length; i++) {
  console.log("t[i]=", t[i]);
}
// des tests
for (let i = 0; i < t.length; i++) {
  // n'affiche que les chaînes
  if (typeof (t[i]) === "string") {
    console.log("t[i]=", t[i]);
  }
}
// opérateurs de comparaison == et ===
if ("2" == 2) {
  console.log("avec l'opérateur ==, la chaîne 2 est égale à l'entier 2");
} else {
  console.log("avec l'opérateur ==, la chaîne 2 n'est pas égale à l'entier 2");
}
if ("2" === 2) {
  console.log("avec l'opérateur ===, la chaîne 2 est égale à l'entier 2");
} else {
  console.log("avec l'opérateur ===, la chaîne 2 n'est pas égale à l'entier 2");
}
// d'autres tests
for (let i = 0; i < t.length; i++) {
  // n'affiche que les entiers >10
  if (typeof (t[i]) === "number" && Math.floor(t[i]) === t[i] && t[i] > 10) {
    console.log("t[i]=", t[i]);
  }
}
// une boucle while
t = [8, 5, 0, -2, 3, 4];
let i = 0;
let somme = 0;
while (i < t.length && t[i] > 0) {
  console.log("t[i]=", t[i]);
  somme += t[i];
  i++;
}
console.log("somme=", somme);

// arrêt du programme car il n'y a plus de code exécutable

//affiche
//----------------------------------
function affiche(chaine) {
  // affiche chaine
  console.log("chaine=", chaine);
}

//afficheType
//----------------------------------
function afficheType(name, variable) {
  // affiche le type de variable
  console.log("type[variable ", name, "]=", typeof (variable));
}

//----------------------------------
function f1(param) {
  // ajoute 10 à param
  return param + 10;
}

//----------------------------------
function f2() {
  // rend 3 valeurs
  return ["un", 0, 100];
}

Commentons les différences entre les codes PHP et ECMAScript 6 avec la déclaration [use strict] (ligne 1) :

  • la 1ère différence est qu’en ECMAScript on déclare les variables avec les mots clés suivants :
    • [let] pour déclarer une variable dont la valeur peut changer au cours de l’exécution du code ;
    • [const] pour déclarer une variable dont la valeur ne va pas changer (une constante donc) au cours de l’exécution du code ;
    • on peut également utiliser le mot clé [var] à la place de [let]. C’était le mot clé utilisé avec ECMAScript 5. Nous ne l’utiliserons pas dans ce cours ;
  • ligne 6 : la méthode d’affichage [console.log] peut afficher toutes sortes de données : chaînes, nombres, booléens, tableaux, objets. La méthode PHP [print] ne sait pas afficher nativement des tableaux et objets. Dans l’expression [console.log], [console] est un objet et [log] une méthode de cet objet ;
  • ligne 8 : les tableaux Javascript sont des objets référencés par un pointeur. Lorsqu’on écrit :
1
const tableau = ["un", "deux", 3, 4];
la variable [tableau] est un pointeur sur le tableau littéral [« un », « deux », 3, 4]. Modifier le contenu du tableau ne modifie pas son pointeur. Aussi un tableau sera-t-il le plus souvent déclaré avec le mot clé [const]. En PHP, un tableau n’est pas référencé par un pointeur. C’est une donnée littérale ;
  • ligne 12 : la variable de boucle [i] est déclarée (let) dans la boucle. Le mot clé [let] respecte la portée de bloc (code entre accolades). Ainsi la variable [i] de la ligne 12 n’est-elle connue que dans la boucle ;
  • ligne 18 : l’opérateur de concaténation de chaîne est l’opérateur + en Javascript, . en PHP. Une particularité de cet opérateur est qu’il a précédence sur l’opérateur + d’addition. Ainsi :
    • en PHP, ‘1’ +2 donne le nombre 3 ;
    • en Javascript ‘1’+2 donne la chaîne ‘12’ ;
  • ligne 20 : [console.log] sait afficher des tableaux ;
  • ligne 82 : en Javascript, il n’est pas possible d’indiquer le type des paramètres d’une fonction ;
  • ligne 91 : l’opérateur [typeof] permet de connaître le type d’une donnée. Il y en a quatre : nombre, chaîne de caractères, booléen et objet. On notera qu’en Javascript on n’a pas de type [integer] ni de type [tableau]. Comme il a été dit, les tableaux sont manipulés via des pointeurs et tombe dans la catégorie des objets ;
  • lignes 50-59 : comme en PHP, Javascript a deux opérateurs de comparaison, == et ‘===’ avec la même signification qu’en PHP. ESLint signale le plus souvent l’opérateur == comme une erreur possible. On utilisera systématiquement l’opérateur ‘===’ ;
  • ligne 79 : on aurait pu mettre l’instruction [return] mais ESLint émet l’avertissement que [return] ne doit s’utiliser que dans une fonction ;

Exécutons ce code :

image1

Les résultats de l’exécution :

 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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\bases\bases-01.js"
nom : dupont
tableau[ 0 ] = un
tableau[ 1 ] = deux
tableau[ 2 ] = 3
tableau[ 3 ] = 4
[ 'chaine1', 'chaine2', 'chaine1chaine2' ]
chaine= chaine1
type[variable n ]= number
type[variable chaine1 ]= string
type[variable tableau ]= object
type[variable n ]= string
res1= 14
(res1,res2,res3)= [ 'un', 0, 100 ]
t[ 0 ]= un
t[ 1 ]= 0
t[ 2 ]= 100
t[ 0 ]= un
avec l'opérateur ==, la chaîne 2 est égale à l'entier 2
avec l'opérateur ===, la chaîne 2 n'est pas égale à l'entier 2
t[ 2 ]= 100
t[ 0 ]= 8
t[ 1 ]= 5
somme= 13

[Done] exited with code=0 in 0.316 seconds

Dans le code écrit, ESLINT signale deux erreurs :

image2

  • en passant le curseur sur la ligne rouge de l’avertissement, on a le message d’erreur [3]. Ici ESLint ne comprend pas qu’on compare deux constantes. L’un des deux opérandes devrait être une variable ;
  • en [4], une option [Quick Fix] permet de lever l’avertissement si on décide de ne pas corriger l’erreur ;

image3

  • en [5], on a la possibilité de désactiver l’avertissement pour la ligne courante ou pour l’ensemble du fichier. C’est cette dernière option que nous choisissons ici . La ligne [6] est alors générée au début du fichier ;

script [bases-02]

Le script [bases-02] montre l’utilisation des mots clés [let] et [const] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
'use strict';
// pour initialiser une variable, on utilise let ou const
// let pour les variables
let x = 4;
x++;
console.log(x);
// const pour les constantes
const y = 10;
x += y;
// interdit
y++;
  • la ligne 11 provoque une erreur à l’exécution [1-2]. Elle est signalée par ESLint avant l’exécution [3] :

image4

script [bases-03]

Le script [bases-03] examine la portée des variables en Javascript :

1
2
3
4
5
6
7
8
9
'use strict';
// portée des variables
let count = 1;
function doSomething() {
  // count est ici connu
  console.log("count=",count);
}
// appel
doSomething();
  • la variable [count] déclarée en-dehors de la fonction [doSomething] est pourtant connue dans cette fonction. C’est une différence fondamentale avec PHP ;

Exécution

1
2
3
4
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\bases\bases-03.js"
count= 1

[Done] exited with code=0 in 0.3 seconds

script [bases-04]

Une variable locale cache une variable globale de même nom :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'use strict';
// portée des variables
const count = 1;
function doSomething() {
  // la variable locale cache la variable globale
  const count = 2;
  console.log("count inside function=",count);
}
// variable globale
console.log("count outside function=",count);
// variable locale
doSomething();

Exécution

1
2
3
4
5
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\bases\bases-04.js"
count outside function= 1
count inside function= 2

[Done] exited with code=0 in 0.246 seconds

script [bases-05]

Une variable définie dans une fonction n’est pas connue en-dehors de celle-ci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
'use strict';
// portée des variables
function doSomething() {
  // variable locale à la fonction
  const count = 2;
  console.log("count inside function=", count);
}
// ici count n'est pas connu
console.log("count outside function=", count);
doSomething();

ESLint déclare une erreur sur la ligne 9 :

image5

script [bases-06]

Les mots clés [let] et [const] définissent des variables de portée [bloc] (code entre accolades) mais pas le mot clé [var] :

 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
'use strict';
// le mot clé [let] permet de définir une variable de portée bloc
{
  // la variable [count] n'est connue que dans ce bloc
  let count = 1;
  console.log("count=", count);
}
// ici la variable [count] n'est pas connue
count++;

// le mot clé [const] permet de définir une variable de portée bloc
{
  // la variable [count2] n'est connue que dans ce bloc
  const count2 = 1;
  console.log("count=", count2);
}
// ici la variable [count2] n'est pas connue
count2++;

// le mot clé [var] ne permet pas de définir une variable de portée bloc
{
  // la variable [count3] sera connue globalement
  var count3 = 1;
  console.log("count=", count3);
}
// ici la variable [count3] est connue
count3++;

Commentaires

  • ligne 5 : la variable [count] n’est connue que dans le bloc de code dans laquelle elle est déclarée (lignes 3-7) ;
  • ligne 14 : la constante [count2] n’est connue que dans le bloc de code dans laquelle elle est déclarée (lignes 12-16) ;
  • ligne 23 : la variable [count3] est connue en-dehors du bloc de code dans laquelle elle est déclarée (lignes 21-25) ;

ESLint déclare les erreurs suivantes :

image6

Pour ces raisons de portée de bloc, nous n’utiliserons par la suite que les mots clés [let] et [const].

script [bases-07]

Les types de données en Javascript :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
'use strict';

// type de données jS
const var1 = 10;
const var2 = "abc";
const var3 = true;
const var4 = [1, 2, 3];
const var5 = {
  nom: 'axèle'
};
const var6 = function () {
  return +3;
}
// affichage des types
console.log("typeof(var1)=", typeof (var1));
console.log("typeof(var2)=", typeof (var2));
console.log("typeof(var3)=", typeof (var3));
console.log("typeof(var4)=", typeof (var4));
console.log("typeof(var5)=", typeof (var5));
console.log("typeof(var6)=", typeof (var6));

Exécution

1
2
3
4
5
6
7
8
9
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\bases\bases-07.js"
typeof(var1)= number
typeof(var2)= string
typeof(var3)= boolean
typeof(var4)= object
typeof(var5)= object
typeof(var6)= function

[Done] exited with code=0 in 0.26 seconds

Commentaires

  • ligne 7 (code) : un tableau est un objet. A ce titre [var4] est un pointeur vers le tableau, pas le tableau lui-même ;
  • ligne 8 (code) : [var5] est un pointeur vers un objet littéral. On verra que les objets littéraux de Javascript ressemblent fort aux instances de classe de PHP. Eux-aussi sont référencés via des pointeurs ;
  • ligne 11 (code) : une variable peut être de type [fonction] (ligne 7 des résultats) ;

script [bases-08]

Ce script montre les changements de type possibles en Javascript.

 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
'use strict';

// changements implicites de types
// type -->bool
console.log("---------------[Conversion implicite vers un booléen]------------------------------");
showBool("abcd");
showBool("");
showBool([1, 2, 3]);
showBool([]);
showBool(null);
showBool(0.0);
showBool(0);
showBool(4.6);
showBool({});
showBool(undefined);

function showBool(data) {
  // la conversion de data en booléen se fait automatiquement dans le test qui suit
  console.log("[data=", data, "], [type(data)]=", typeof (data), "[valeur booléenne(data)]=", data ? true : false);
}

// changements implicites de type  vers un type numérique
console.log("---------------[Conversion implicite vers un nombre]------------------------------");
showNumber("12");
showNumber("45.67");
showNumber("abcd");

function showNumber(data) {
  // data + 1 ne marche pas car alors jS fait une concaténation de chaînes plutôt qu'une addition
  const nombre = data * 1;
  console.log("[data=", data, "], [type(data)]=", typeof (data), "[nombre]=", nombre, "[type(nombre)]=", typeof (nombre));
}

// changements explicites de types vers un booléen
console.log("---------------[Conversion explicite vers un booléen]------------------------------");
showBool2("abcd");
showBool2("");
showBool2([1, 2, 3]);
showBool2([]);
showBool2(null);
showBool2(0.0);
showBool2(0);
showBool2(4.6);
showBool2({});
showBool2(undefined);

function showBool2(data) {
  // la conversion de data en booléen se fait explicitement dans le test qui suit
  console.log("[", data, "], [type(data)]=", typeof (data), "[valeur booléenne(data)]=", Boolean(data));
}
// changements explicites de type vers Number
console.log("---------------[Conversion explicite vers un nombre]------------------------------");
showNumber2("12.45");
showNumber2(67.8);
showNumber2(true);
showNumber2(null);

function showNumber2(data) {
  const nombre = Number(data);
  console.log("[data=", data, "], [type(data)]=", typeof (data), "[nombre]=", nombre, "[type(nombre)]=", typeof (nombre));
}

// vers String
console.log("---------------[Conversion explicite vers un string]------------------------------");
showString(5);
showString(6.7);
showString(false);
showString(null);

function showString(data) {
  const chaîne = String(data);
  console.log("[data=", data, "], [type(data)]=", typeof (data), "[chaîne]=", chaîne, "[type(chaîne)]=", typeof (chaîne));
}

// qqs conversions implicites inattendues
console.log("---------------[Autres cas]------------------------------");
const string1 = '1000.78';
// concaténation de chaînes par défaut
const data1 = string1 + 1.034;
console.log("data1=", data1, "type=", typeof (data1));
const data2 = 1.034 + string1;
console.log("data2=", data2, "type=", typeof (data2));
// conversion explicite vers nombre
const data3 = Number(string1) + 1.034;
console.log("data3=", data3, "type=", typeof (data3));
// true est converti en le nombre 1
const data4 = true * 1.18;
console.log("data4=", data4, "type=", typeof (data4));
// false est converti en le nombre 0
const data5 = false * 1.18;
console.log("data5=", data5, "type=", typeof (data5));

Exécution

 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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Data\st-2019\dev\es6\javascript\bases\bases-08.js"
---------------[Conversion implicite vers un booléen]------------------------------
[data= abcd ], [type(data)]= string [valeur booléenne(data)]= true
[data= ], [type(data)]= string [valeur booléenne(data)]= false
[data= [ 1, 2, 3 ] ], [type(data)]= object [valeur booléenne(data)]= true
[data= [] ], [type(data)]= object [valeur booléenne(data)]= true
[data= null ], [type(data)]= object [valeur booléenne(data)]= false
[data= 0 ], [type(data)]= number [valeur booléenne(data)]= false
[data= 0 ], [type(data)]= number [valeur booléenne(data)]= false
[data= 4.6 ], [type(data)]= number [valeur booléenne(data)]= true
[data= {} ], [type(data)]= object [valeur booléenne(data)]= true
[data= undefined ], [type(data)]= undefined [valeur booléenne(data)]= false
---------------[Conversion implicite vers un nombre]------------------------------
[data= 12 ], [type(data)]= string [nombre]= 12 [type(nombre)]= number
[data= 45.67 ], [type(data)]= string [nombre]= 45.67 [type(nombre)]= number
[data= abcd ], [type(data)]= string [nombre]= NaN [type(nombre)]= number
---------------[Conversion explicite vers un booléen]------------------------------
[ abcd ], [type(data)]= string [valeur booléenne(data)]= true
[ ], [type(data)]= string [valeur booléenne(data)]= false
[ [ 1, 2, 3 ] ], [type(data)]= object [valeur booléenne(data)]= true
[ [] ], [type(data)]= object [valeur booléenne(data)]= true
[ null ], [type(data)]= object [valeur booléenne(data)]= false
[ 0 ], [type(data)]= number [valeur booléenne(data)]= false
[ 0 ], [type(data)]= number [valeur booléenne(data)]= false
[ 4.6 ], [type(data)]= number [valeur booléenne(data)]= true
[ {} ], [type(data)]= object [valeur booléenne(data)]= true
[ undefined ], [type(data)]= undefined [valeur booléenne(data)]= false
---------------[Conversion explicite vers un nombre]------------------------------
[data= 12.45 ], [type(data)]= string [nombre]= 12.45 [type(nombre)]= number
[data= 67.8 ], [type(data)]= number [nombre]= 67.8 [type(nombre)]= number
[data= true ], [type(data)]= boolean [nombre]= 1 [type(nombre)]= number
[data= null ], [type(data)]= object [nombre]= 0 [type(nombre)]= number
---------------[Conversion explicite vers un string]------------------------------
[data= 5 ], [type(data)]= number [chaîne]= 5 [type(chaîne)]= string
[data= 6.7 ], [type(data)]= number [chaîne]= 6.7 [type(chaîne)]= string
[data= false ], [type(data)]= boolean [chaîne]= false [type(chaîne)]= string
[data= null ], [type(data)]= object [chaîne]= null [type(chaîne)]= string
---------------[Autres cas]------------------------------
data1= 1000.781.034 type= string
data2= 1.0341000.78 type= string
data3= 1001.814 type= number
data4= 1.18 type= number
data5= 0 type= number

Les tableaux

image0

script [tab-01]

Le script suivant illustre certaines caractéristiques des tableaux Javascript. Ceux-ci ressemblent aux tableaux PHP avec cependant une grande différence : ils sont manipulés via des pointeurs et sont considérés comme des objets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'use strict';

// un tableau est un objet manipulé via son adresse
const tab1 = [1, 2, 3];
// copie d'adresses
const tab2 = tab1;
// tab1 et tab2 pointent sur le même tableau
console.log("tab1===tab2 :", tab1 === tab2);
// on peut modifier le tableau en passant indifféremment par tab1 ou tab2
tab2[1] = 10;
console.log("tab1=", tab1);
console.log("tab2=", tab2);

Exécution

1
2
3
4
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\tableaux\tab-01.js"
tab1===tab2 : true
tab1= [ 1, 10, 3 ]
tab2= [ 1, 10, 3 ]

Commentaires

  • la ligne 2 des résultats montre que [tab1] et [tab2] sont deux entités égales, en fait deux pointeurs égaux ;
  • les lignes 4 et 5 des résultats montrent que ces deux pointeurs pointent sur le même tableau ;

script [tab-02]

Ce script montre que le tableau Javascript est différent des tableaux des langages compilés.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';

// tableau
const tab = [];
console.log("tab=", tab, ", longueur=[", tab.length, "]");
console.log("-------------------------------");
// initialisation d'un élément
tab[3] = 100;
tab[1] = "huit";
// tableau
console.log("tab=", tab, ", longueur=[", tab.length, "]");
console.log("-------------------------------");
// toString
console.log("tab.toString=[", tab.toString(), "]");
console.log("-------------------------------");
// les clés du tableau sont ses indices
for (let key of tab.keys()) {
  console.log("clé=[", key, "], valeur=[", tab[key], "]");
}
console.log("-------------------------------");
// les valeurs du tableau
for (let value of tab.values()) {
  console.log("valeur=[", value, "]");
}
  • ligne 4 : un tableau vide ;
  • ligne 8 : un tableau n’a pas de taille fixe. C’est simplement une suite d’éléments indexée par un n°. On peut initialiser l’élément n° 3 même si les éléments [0, 1, 2] ne sont pas encore définis ;
  • lignes 16-24 : un tableau Javascript a un comportement analogue à celui du tableau PHP ;

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\tableaux\tab-02.js"
tab= [] , longueur=[ 0 ]
-------------------------------
tab= [ <1 empty item>, 'huit', <1 empty item>, 100 ] , longueur=[ 4 ]
-------------------------------
tab.toString=[ ,huit,,100 ]
-------------------------------
clé=[ 0 ], valeur=[ undefined ]
clé=[ 1 ], valeur=[ huit ]
clé=[ 2 ], valeur=[ undefined ]
clé=[ 3 ], valeur=[ 100 ]
-------------------------------
valeur=[ undefined ]
valeur=[ huit ]
valeur=[ undefined ]
valeur=[ 100 ]

script [tab-03]

Ce script montre différentes méthodes de l’objet [tableau].

 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
'use strict';
// un tableau peut contenir différents types de données
const tab = [1, 2, "un", "deux", true, [10, 20], { prop1: 10, prop2: "abc" }];
// console.log sait afficher le contenu d'un tableau
show(1);
console.log("tab=", tab);
show(2);
// parcours du tableau avec foreach
tab.forEach(element => {
  console.log("élément=", element, typeof (element));
});
show("2b");
// une autre écriture pour faire la même chose
tab.forEach(function (element) {
  console.log("élément=", element, typeof (element));
});
show(3);
// parcours du tableau avec for
for (let i = 0; i < tab.length; i++) {
  console.log("i=", i, "tab[i]=", tab[i]);
}
show(4);
// modification tab[i]
tab[5] = [];
// affichage
console.log("tab=", tab);
show(5);
// on enlève le dernier élément
let element = tab.pop(tab);
console.log("élément=", element, "tab=", tab);
show(6);
// on ajoute un élément à la fin du tableau
tab.push('xyz');
console.log("tab=", tab);
show(7);
// on ajoute un élément au début du tableau
tab.unshift(1000);
console.log("tab=", tab);
show(8);
// on enlève le 1er élément du tableau
element = tab.shift();
console.log("élément=", element, "tab=", tab);
show(9);
// on enlève l'élément n° 2 du tableau
element = tab.splice(2, 1);
console.log("élément=", element, "tab=", tab);
show(10);
// on enlève du tableau deux éléments à partir de l'élément n° 1
element = tab.splice(1, 2);
console.log("élément=", element, "tab=", tab);

// fonction
function show(param) {
  console.log("[", param, ":::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ]");
}

Commentaires

  • la différence entre tableaux PHP et tableaux Javascript est illustrée par les lignes 3 et 24 :
    • la ligne 3 déclare la variable [tab] comme une constante ;
    • la ligne 24 modifie l’élément tab[5] ;
Ligne 3, c’est le pointeur qui pointe sur le tableau qui est déclaré constant, ce n’est pas le tableau lui-même. Celui-ci peut être modifié.
  • lignes 14-16 : le tableau [tab] est parcouru à l’aide d’une méthode [forEach] du tableau [tab]. Cette méthode reçoit en paramètre la définition d’une fonction, qu’on pourrait appeler une fonction littérale. Cette fonction paramètre reçoit un paramètre : l’élément courant du tableau [tab]. La fonction est appelée pour chaque élément du tableau. Ce type d’écriture est courant en Javascript ;
  • lignes 9-10 : on utilise une autre syntaxe pour définir la fonction paramètre. Au lieu d’écrire :
    • function(p1, p2, …, pn){….}
on écrit :
  • (p1,p2, ..,pn)=>{.…}. On appelle cela, la notation « flèche » ou « arrow » ;
  • le reste du code est expliqué par les commentaires ;

De ce script, on notera que :

  • un tableau est un objet référencé par un pointeur ;
  • que cet objet a des méthodes [forEach, pop, push, shift, unshift] ;

script [tab-04]

Ce script présente d’autres méthodes des objets tableau.

 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
'use strict';

// méthode de manipulation de tableaux

// un tableau
const tab = [];
for (let i = 0; i < 10; i++) {
  tab[i] = i * 10;
}
// affichage
console.log("tab=", tab);
// map
const tab2 = tab.map(element => {
  return { prop1: element, prop2: element * element }
});
// affichage
console.log("tab=", tab);
console.log("tab2=", tab2);
// reduce sans valeur initiale
const somme = tab.reduce((accumulator, currentValue) => accumulator + currentValue);
console.log("somme tab=", somme);
// reduce avec valeur initiale
const somme2 = tab.reduce((accumulator, currentValue) => accumulator + currentValue, 10);
console.log("somme2 tab=", somme2);
// filter
const tab4 = tab.filter((element) => {
  if (element > 50) {
    return element;
  }
});
console.log("tab4=", tab4);
// find
const element1 = tab.find((element) => (element > 20));
console.log("élément1=", element1);
// findIndex
const index1 = tab.findIndex((element) => (element === 20));
console.log("index1 20=", index1);
// indexOf
const index2 = tab.indexOf(30);
console.log("index2 30=", index2);
const index3 = tab.indexOf(31);
console.log("index3 31=", index3);
// lastIndexOf
const index4 = [4, 5, 4, 2].lastIndexOf(4);
console.log("index4 4=", index4);
// sort
const tab5 = [4, 5, 4, 2].sort();
console.log("tab5=", tab5);
// sort inverse
const tab6 = [4, 5, 4, 2].sort((e1, e2) => {
  if (e1 > e2) {
    return -1;
  }
  else if (e1 === e2) {
    return 0;
  } else {
    return +1;
  }
});
console.log("tab6=", tab6);

Commentaires

  • lignes 13-15 : la méthode [map] admet une fonction de transformation comme paramètre. Celle-ci est appelée de façon répétée pour chaque élément du tableau. Elle est chargée de transformer celui-ci en autre chose, ici un objet avec les propriétés [prop1, prop2]. La méthode [map] rend un nouveau tableau. L’ancien n’est pas modifié :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
tab= [ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 ]
tab2= [ { prop1: 0, prop2: 0 },
{ prop1: 10, prop2: 100 },
{ prop1: 20, prop2: 400 },
{ prop1: 30, prop2: 900 },
{ prop1: 40, prop2: 1600 },
{ prop1: 50, prop2: 2500 },
{ prop1: 60, prop2: 3600 },
{ prop1: 70, prop2: 4900 },
{ prop1: 80, prop2: 6400 },
{ prop1: 90, prop2: 8100 } ]
  • ligne 20 : la méthode [reduce] admet pour paramètre une fonction à deux paramètres appelée de façon répétée pour chaque élément du tableau. Cette fonction admet deux paramètres :
    • [currentValue] est l’élément courant du tableau ;
    • [accumulator] est le dernier résultat obtenu par la fonction. Si aucune valeur initiale n’est prévue pour cet accumulateur, alors celle-ci sera 0 ;
    • la 1ère fois que la fonction d’accumulation est appelée, elle rend [0+tab[0]]. C’est cette valeur qui est affectée à l’accumulateur ;
    • la seconde fois, elle rend accumulateur+tab[1], donc tab[0]+tab[1] ;
    • la troisème fois, elle rend accumulateur+tab[2], donc tab[0]+tab[1]+tab[2] ;
    • etc. Au final, l’accumulateur représentera la somme de tous les éléments du tableau [tab] ;
  • ligne 26 : la méthode [filter] a pour paramètre une fonction de filtrage. Celle-ci est appelée de façon répétée pour chaque élément du tableau et reçoit celui-ci en paramètre. Elle doit rendre :
    • [true] si l’élément doit être gardé ;
    • [false] sinon ;
  • ligne 33 : la méthode [find] a pour paramètre une fonction de recherche. Celle-ci est appelée de façon répétée pour chaque élément du tableau et reçoit celui-ci en paramètre. Elle doit rendre [true] si l’élément reçu satisfait le critère de recherche. Celle-ci s’arrête alors. La méthode [find] rend donc 0 ou 1 élément ;
  • ligne 36 : la méthode [findIndex] fonctionne comme la méthode [find] mais au lieu de rendre l’élément trouvé, elle rend son index dans le tableau ;
  • lignes 39, 41, la méthode [indexOf(valeur)] recherche [valeur] dans le tableau et rend son index, ou -1 s’il n’est pas trouvé ;
  • ligne 44 : la méthode [lastIndexOf(valeur)] fonctionne comme la méthode [indexOf(valeur)] mais commence sa recherche par la fin du tableau ;
  • ligne 47 : la méthode [sort] sans paramètres rend un tableau trié dans l’ordre naturel (nombres, chaînes) ;
  • ligne 50 : lorsque l’ordre naturel ne convient pas, il faut passer à la méthode [sort] une fonction à deux paramètres (e1,e2) qui rend :
    • +1 si e1 doit être classé après e2 ;
    • -1 si e1 doit être classé avant e2 ;
    • 0 si les deux éléments doivent avoir le même classement ;
La fonction passée en paramètre à la méthode [sort] est appelée de façon répétée par celle-ci pour comparer deux éléments du tableau ;

Les objets littéraux

Nous appelons ici ‘objets littéraux’ des objets définis littéralement dans le code. Javascript a la notion de classe, et d’objet instance de classe. Ce n’est donc pas ce type d’objet dont nous parlons maintenant.

image0

script [obj-01]

Nous présentons ici les premières propriétés des objets littéraux. La principale est que l’objet est manipulé via un pointeur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';
// un objet vide
const obj1={};
// on peut créer dynamiquement les propriétés de l'objet
obj1.prop1="abcd";
console.log('obj1=',obj1);
// autre propriété
obj1.prop2=[1,2,3];
console.log("obj1=",obj1);
// autre propriété avec une notation différente
obj1['prop3']=true;
console.log("obj1=",obj1);
// obj1 est une référence sur l'objet (pointeur), pas l'objet lui-même
const obj2=obj1;
// obj2 et obj1 pointent sur le même objet
obj2.prop1="xyzt";
console.log("obj1=",obj1);
console.log("obj2=",obj2);
// les propriété peuvent être des variables
const var1='prop1';
console.log('prop1=',obj1[var1]);

Exécution

1
2
3
4
5
6
7
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\objets\obj-01.js"
obj1=[object Object]
obj1= { prop1: 'abcd', prop2: [ 1, 2, 3 ] }
obj1= { prop1: 'abcd', prop2: [ 1, 2, 3 ], prop3: true }
obj1= { prop1: 'xyzt', prop2: [ 1, 2, 3 ], prop3: true }
obj2= { prop1: 'xyzt', prop2: [ 1, 2, 3 ], prop3: true }
prop1= xyzt

Commentaires

  • ligne 3 du code : un objet est manipulé via un pointeur. Donc [obj1] est un pointeur. Modifier l’objet pointé ne modifie pas le pointeur [obj1]. C’est pourquoi, comme pour les tableaux, une référence d’objet est déclarée avec le mot clé [const] ;
  • ligne 6 du code : comme pour les tableaux, [console.log] sait afficher des objets ;
  • ligne 11 du code : obj1.prop3 peut être réécrit obj1.[‘prop3’]. Cette dernière notation est utile lorsque ‘prop3’ est en fait une variable (lignes 20-21) ;
  • lignes 13-18 du code : montrent que l’instruction [obj2=obj1] est une copie de référence d’objet et non de l’objet lui-même ;

script [obj-02]

Ce script montre que les propriétés d’un objet peuvent avoir pour valeur un objet. On a alors des objets multi-niveaux. On introduit également l’objet global [JSON] qui permet de faire des conversions objet ↔ chaîne de caractères.

 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
'use strict';
// un objet à plusieurs niveaux
const personne = {
  prénom: "martin",
  âge: 12,
  père: {
    prénom: "paul",
    âge: 45
  },
  mère: {
    prénom: "micheline",
    âge: 42
  }
}
// accès aux propriétés
console.log("prénom personne=", personne.prénom);
console.log("prénom mère=", personne.mère.prénom);
personne.mère.âge = 40;
console.log("âge mère=", personne.mère.âge);
// console.log sait afficher des objets
console.log("personne=", personne);
console.log("mère=", personne.mère);
// on peut aussi afficher la chaîne jSON de l'objet
let json = JSON.stringify(personne);
console.log("jSON=", json);
// on peut relire le jSON
let personne2 = JSON.parse(json);
console.log("père=", personne2.père);

Commentaires

  • ligne 24 : transformation d’un objet Javascript en chaîne jSON ;
  • ligne 27 : transformation d’une chaîne jSON en objet Javascript ;

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\objets\obj-02.js"
prénom personne= martin
prénom mère= micheline
âge mère= 40
personne= { 'prénom': 'martin',
'âge': 12,
'père': { 'prénom': 'paul', 'âge': 45 },
'mère': { 'prénom': 'micheline', 'âge': 40 } }
mère= { 'prénom': 'micheline', 'âge': 40 }
jSON= {"prénom":"martin","âge":12,"père":{"prénom":"paul","âge":45},"mère":{"prénom":"micheline","âge":40}}
père= { 'prénom': 'paul', 'âge': 45 }

Commentaires

  • ligne 10 : dans une chaîne jSON, les propriétés sont obligatoirement entourées de guillemets ainsi que les valeurs de type chaîne de caractères ;

script [obj-03]

Ce script introduit la notion de getter / setter d’une propriété d’un objet :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use strict';
// getters et setters d'un objet
const personne = {
  // getter
  get nom() {
    console.log("getter nom");
    return this._nom;
  },
  // setter
  set nom(unNom) {
    console.log("setter nom");
    this._nom = unNom;
  }
};
// setter
personne.nom = "Hercule";
// getter
console.log(personne.nom);
// l'objet lui-même
console.log("personne=", personne);
// ça n'empêche pas d'accéder à la propriété [_nom] directement
personne._nom = "xyz";
console.log("personne=", personne);

Commentaires

  • lignes 5-7 : définition d’un [getter], une fonction qui rend généralement la valeur d’une propriété de l’objet mais qui en fait peut rendre n’importe quoi. Le mot clé [function] est remplacé par le mot clé [get] ;
  • ligne 7 : le getter rend la propriété [_nom]. On voit que celle-ci n’a pas besoin d’être déclarée ;
  • lignes 10-13 : définition d’un [setter], une fonction qui affecte généralement la valeur reçue à une propriété de l’objet mais qui en fait peut faire n’importe quoi. Le mot clé [function] est remplacé par le mot clé [set]. Le [setter] peut être utilisé pour vérifier la validité de la valeur passée en paramètre au [setter] ;
  • ligne 16 : la fonction [set nom] va être appelée implicitement ;
  • ligne 18 : la fonction [get nom] va être appelée implicitement ;
  • ligne 22 : montre que l’utilisation des getter / setter dépend de la bonne volonté du développeur. Si celui-ci connaît le nom de la propriété gérée par ceux-ci, il peut y accéder directement ;

Exécution

1
2
3
4
5
6
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\objets\obj-03.js"
setter nom
getter nom
Hercule
personne= { nom: [Getter/Setter], _nom: 'Hercule' }
personne= { nom: [Getter/Setter], _nom: 'xyz' }

On notera lignes 5-6, que [console.log] affiche également les propriétés qui sont des fonctions.

script [obj-04]

Ce script montre trois façons d’écrire les noms des propriétés d’un objet et deux façons d’y accéder.

 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
'use strict';
// les noms des propriétés d'un objet  peuvent être littéraux [nom], être entourés d'apostrophes ['nom']
// ou de guillements ["nom"]

// littéraux
const obj1 = {
  nom: "martin",
  prénom: "jean"
};
console.log("prénom=", obj1.prénom);

// entourés d'apostrophes
const obj2 = {
  'nom': "martin",
  'prénom': "jean"
};
console.log("nom=", obj2.nom);

// entourés de guillemets
const obj3 = {
  "nom": "martin",
  "prénom": "jean"
};

// deux syntaxes possibles pour accéder à la propriété [nom]
console.log("nom=", obj3.nom);
console.log("nom=", obj3['nom']);

Exécution

1
2
3
4
5
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\objets\obj-04.js"
prénom= jean
nom= martin
nom= martin
nom= martin

script [obj-05]

Le script montre que les propriétés d’un objet littéral peuvent être des fonctions. On est alors très proche de l’objet instance de classe, où on a des propriétés et des méthodes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';

// un objet peut avoir des propriétés de type [function]
const personne = {
  // propriétés
  prénom: "martin",
  âge: 12,
  père: {
    prénom: "paul",
    âge: 45
  },
  mère: {
    prénom: "micheline",
    âge: 42
  },
  // méthode
  toString: function () {
    return JSON.stringify(this);
  }
}

// usage
console.log("personne=", personne);
console.log("personne.toString=", personne.toString());
  • lignes 17-19 : une méthode interne à l’objet. Dans celle-ci, on accède aux propriétés de l’objet via le mot clé [this] (ligne 18). [this] désigne l’objet lui-même, [this.prénom], la propriété [prénom] de celui-ci ;

Exécution

1
2
3
4
5
6
7
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\objets\obj-05.js"
personne= { 'prénom': 'martin',
'âge': 12,
'père': { 'prénom': 'paul', 'âge': 45 },
'mère': { 'prénom': 'micheline', 'âge': 42 },
toString: [Function: toString] }
personne.toString= {"prénom":"martin","âge":12,"père":{"prénom":"paul","âge":45},"mère":{"prénom":"micheline","âge":42}}

script [obj-06]

Ce script montre comment avoir accès aux propriétés d’un objet lorsqu’on ne connaît pas a priori le nom de celles-ci.

 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
'use strict';

// un objet peut avoir des propriétés de type [function]
let personne = {
  // propriétés
  prénom: "martin",
  âge: 12,
  père: {
    prénom: "paul",
    âge: 45
  },
  mère: {
    prénom: "micheline",
    âge: 42
  },
  // méthode
  toString: function () {
    return JSON.stringify(this);
  }
}

// usage
console.log(personne);
// propriétés
console.log("-----------------------");
for (const key in personne) {
  if (personne.hasOwnProperty(key)) {
    const element = personne[key];
    console.log(key, "=", element);
  }
}
// pour échapper à l'avertissement eslint (1)
console.log("-----------------------");
for (const key in personne) {
  if (Object.prototype.hasOwnProperty.call(personne, key)) {
    const element = personne[key];
    console.log(key, "=", element);
  }
}
// pour échapper à l'avertissement eslint (2)
console.log("-----------------------");
for (const key in personne) {
  // eslint-disable-next-line no-prototype-builtins
  if (personne.hasOwnProperty(key)) {
    const element = personne[key];
    console.log(key, "=", element);
  }
}

Commentaires

  • lignes 26-31 : le code qui permet d’avoir la liste des propriétés, sans les méthodes, d’un objet. Ce code fait l’objet d’un avertissement d’ESLint :

image1

  • lignes 32-39 : le code qui permet d’échapper à l’avertissement d’ESLint. On passe par le prototype de la classe [Object] ;
  • lignes 41-47 : ou bien on se contente de désactiver l’avertissement (ligne 43) ;

script [obj-07]

Le script [obj-07] montre la possibilité de déstructurer un objet :

 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
'use strict';
// déstructuration

// littéraux
const obj1 = {
  nom: "martin",
  prénom: "jean"
};

// déstructuration obj1 dans variables [n,p]
const { nom: n, prénom: p } = obj1;
console.log("n=", n, "p=", p);

// déstructuration obj1 dans variables [n2,p2]
function f({ nom: n2, prénom: p2 }) {
  console.log("f-n2=", n2, "f-p2=", p2);
}
f(obj1);

// déstructuration obj1 dans variables [nom,prénom]
function g({ nom: nom, prénom: prénom }) {
  console.log("g-nom=", nom, "g-prénom=", prénom);
}
g(obj1);

// déstructuration obj1 dans variables [nom,prénom]
// avec notation raccourcie équivalente à h({nom:nom,prénom:prénom})
function h({ nom, prénom }) {
  console.log("h-nom=", nom, "h-prénom=", prénom);
}
h(obj1);

Commentaires

  • ligne 11 : ce sont les accolades {} qui permettent la déstructuration. La syntaxe
1
const { nom: n, prénom: p } = obj1
crée deux variables [n] et [p] et est équivalente à :
1
2
const n = obj1.nom
const p = obj1.prénom
La déclaration pourrait se lire de la façon suivante :
1
const { nom => n, prénom => p } = obj1
pour rappeler que les valeurs des attributs [nom, prénom] vont dans les variables [n, p] ;
  • l’opération de déstructuration se répète aux lignes 15, 21 et 28. A chaque fois, c’est la présence des accolades {} qui indique qu’il va y avoir déstructuration d’un objet dans des variables ;
  • la ligne 28 peut être déconcertante. C’est un raccourci pour la notation :
1
function h({ nom : nom, prénom : prénom })

Les résultats de l’exécution sont les suivants :

1
2
3
4
n= martin p= jean
f-n2= martin f-p2= jean
g-nom= martin g-prénom= jean
h-nom= martin h-prénom= jean

script [obj-08]

Le script [obj-08] montre comment obtenir une copie d’un objet :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'use strict'

// clônage d'objets
const obj1 = {
  nom: "martin",
  prénom: "jean"
};

// clône (copie) de obj1 avec l'opérateur de spread
const obj2 = { ...obj1 }

// vérifications
// obj2 pointe sur une copie de obj1
console.log("obj2===obj1 :", obj1 === obj2)
console.log("obj2=", obj2)
  • ligne 10 : l’opération de copie de l’objet [obj1]. L’opérateur … est appelé opérateur de spread ;

Les résultats de l’exécution sont les suivants :

1
2
obj2===obj1 : false
obj2= { nom: 'martin', 'prénom': 'jean' }
  • ligne 1 : montre que les références [obj1] et [obj2] ne pointent pas sur le même objet ;
  • ligne 2 : montre que l’objet pointé par [obj2] est une copie de l’objet pointé par [obj1] ;

Conclusion

Les scripts de cette section ont montré que l’objet littéral de Javascript est proche de l’objet instance de classe des langages à objets. On peut y définir propriétés, méthodes et getters / setters. C’est un objet dynamique dont on peut définir les propriétés à l’exécution. Il se comporte alors comme un dictionnaire dont les éléments peuvent être de tout type et notamment de type [fonction].

Les chaînes de caractères

Les chaînes de caractères de Javascript sont très semblables à celles de PHP.

image0

script [str-01]

La première chose à comprendre est qu’une fois une chaîne créée, elle n’est plus modifiable. On dispose de nombreuses méthodes pour produire une nouvelle chaîne à partir de la chaîne initiale mais celle-ci reste toujours inchangée. Par ailleurs, une chaîne de caractères peut être de deux types :

  • [string] lorsqu’elle est initialisée avec une chaîne littérale ;
  • [object] lorsqu’elle est créée comme instance de la classe [String] ;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'use strict';

// les chaînes de caractères sont en lecture seule (on ne peut pas les modifier)

// une chaîne
const chaîne1 = "abcd ";
// type
console.log("typeof(chaîne1)=", typeof (chaîne1));
// caractère n° 2
console.log("chaîne1[2]=", chaîne1[2]);
// provoque une erreur
chaîne1[2] = "0";

Exécution

1
2
3
4
5
6
7
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Temp\19-09-01\javascript\strings\str-01.js"
typeof(chaîne1)= string
chaîne1[2]= c
c:\Temp\19-09-01\javascript\strings\str-01.js:1
TypeError: Cannot assign to read only property '2' of string 'abcd '
at Object.<anonymous> (c:\Temp\19-09-01\javascript\strings\str-01.js:12:12)
at Generator.next (<anonymous>)

script [str-02]

Ce script montre qu’on peut construire une chaîne de caractères de deux façons.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
'use strict';

// les chaînes de caractères peuvent être de deux types

// une chaîne littérale
const chaîne1 = "abcd ";
// type
console.log("typeof(chaîne1)=", typeof (chaîne1));
// instance de String
const chaîne2 = new String("xyzt");
// type
console.log("typeof(chaîne2)=", typeof (chaîne2));
// autre écriture (sans new) – type [string] et non [object]
const chaîne3 = String("12 34");
// type
console.log("typeof(chaîne3)=", typeof (chaîne3));
// le type [string] et le type [object] offrent les mêmes méthodes, celles de la classe String
console.log("chaîne1.length=", chaîne1.length);
console.log("chaîne2.length=", chaîne2.length);

Commentaires

  • ligne 6 : la méthode usuelle de définition d’une chaîne. [chaîne1] sera de type [string] ;
  • ligne 10 : on peut construire une chaîne à l’aide du constructeur de la classe [String]. [chaîne2] sera de type [object] ;

Exécution

1
2
3
4
5
6
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\strings\str-02.js"
typeof(chaîne1)= string
typeof(chaîne2)= object
typeof(chaîne3)= string
chaîne1.length= 5
chaîne2.length= 4

Le type [string] bénéficie des méthodes de la classe [String].

script [str-03]

Ce script montre une chaîne particulière avec interpolation de variables.

1
2
3
4
5
6
7
'use strict';

// chaine
const chaîne = "Introduction à Javascript par l'exemple";
// chaîne avec interpolation de variables
const str = `[${chaîne}].substr(3, 2)=` + chaîne.substr(3, 2)
console.log(str);

Commentaires

  • ligne 6 : il est possible d’avoir des chaînes de caractères contenant des expression ${variable} qui sont remplacées par la valeur de la variable. On est là dans la même logique que les variables $ dans les chaînes de caractères PHP. On notera la notation d’une telle chaîne : elle est entourée de « backstick » ou apostrophe inverse (AltGr-7 sur un clavier français) ;

Exécution

1
2
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Temp\19-09-01\javascript\strings\tempCodeRunnerFile.js"
[Introduction à Javascript par l'exemple].substr(3, 2)=ro

script [str-04]

La chaîne avec interpolation de variables reste insuffisante. Il n’est en effet pas possible de mettre une expression à la place de la variable dans l’expression ${variable}. Pour ceux qui ont programmé en C, il n’y a rien de tel que les fonctions [printf, sprintf] pour écrire ou construire des chaînes formatées. Des centaines de développeurs ont développé des milliers de packages Javascript formant un écosystème immense. Lorsqu’on a un besoin non satisfait nativement par Javascript, il est temps de chercher un package qui le satisfasse. Pour cela nous utilisons le gestionnaire de packages [npm]. Celui-ci dispose d’une option [search] qui permet de chercher une chaîne de caractères dans la description des packages. [npm] renvoie la liste des packages satisfaisant à la recherche. Nous allons donc chercher la chaîne [sprintf] dans la description des packages :

image1

  • dans la colonne [4], les mots clés des packages de la colonne [3] ;
  • dans la colonne [5], la description des packages de la colonne [3] ;

L’étape suivante est d’aller sur le site de l’outil [npm], [https://www.npmjs.com/] et de lire la description des packages :

image2

En [3], on examine la liste des packages et on en choisit un.

image3

Dans la description du package, on trouve les informations pour l’installer et l’utiliser :

image4

Nous installons le package [sprintf-js] dans un terminal de [VSCode] :

image5

Cette installation va modifier le fichier [package.json] situé à la racine du dossier [javascript] [2] :

image6

On voit ci-dessus que le package a été installé dans les [dependencies], ç-à-d dans les packages nécessaires à l’exécution du projet. On rappelle qu’on met dans [devDependencies] les packages nécessaires uniquement pendant le développement du projet. Ils ne sont pas utilisés pendant l’exécution. Cette différence est importante lorsqu’il faut créer la version finale du projet pour sa mise en production. Il existe des outils pour :

  • rassembler tous les fichiers jS nécessaires à l’exécution dans un seul fichier. Les packages des [devDependencies] ne sont donc pas inclus dans ce fichier final ;
  • minifier celui-ci, ç-à-d rendre sa taille la plus petite possible. Pour cela par exemple, tous les commentaires sont éliminés ;
  • « obscurcir » le code pour le rendre difficilement compréhensible. Par exemple, les variables taux, salaire, impôt vont être remplacées par des variables a, b, c ;
  • faire d’autres optimisations ;

Cette optimisation du fichier final d’un projet jS est utilisée en programmation web. Une application web peut dépendre de très nombreux fichiers Javascript. Le chargement de ceux-ci par un navigateur peut ralentir l’affichage de la première page de l’application. L’optimisation précédente vise à améliorer ce temps de chargement. Si le temps de chargement est jugé trop long par les utilisateurs, l’application ne sera pas utilisée.

Maintenant que nous disposons du package [sprintf-js], il nous faut l’utiliser. C’est le script [str-04] :

1
2
3
4
5
6
7
'use strict';
// utilisation d'un package externe pour disposer de la fonction sprintf
import { sprintf } from 'sprintf-js';
// chaine
const chaîne = "Introduction à Javascript par l'exemple";
// méthode
console.log(sprintf("[%s].substr(3,2)=[%s]", chaîne, chaîne.substr(3, 2)));

Avec ECMAScript 6, on utilise le mot clé [import] pour importer un objet exporté par un package. Pour savoir ce qu’exporte le package, on peut aller voir son code :

image7

  • en [1], clic droit sur le package importé ;
  • en [2], on veut en voir la définition ;
  • en [3-4], on voit que le package exporte une fonction appelée [sprintf] ;

La fonction [sprintf] du package [sprintf-js] est importée avec l’instruction :

1
import { sprintf } from 'sprintf-js';

Le code complet :

1
2
3
4
5
6
7
'use strict';
// utilisation d'un package externe pour disposer de la fonction sprintf
import { sprintf } from 'sprintf-js';
// chaine
const chaîne = "Introduction à Javascript par l'exemple";
// méthode
console.log(sprintf("[%s].substr(3,2)=[%s]", chaîne, chaîne.substr(3, 2)));

produit les résultats suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe "c:\Temp\19-09-01\javascript\strings\str-04.js"
c:\Temp\19-09-01\javascript\strings\str-04.js:3
import { sprintf } from 'sprintf-js';
^

SyntaxError: Unexpected token {
at new Script (vm.js:79:7)
at createScript (vm.js:251:10)
at Object.runInThisContext (vm.js:303:10)
at Module._compile (internal/modules/cjs/loader.js:657:28)

Ligne 3, l’instruction [import] n’est pas comprise. Cela vient du fait que la version 10.15.1 de [node.js] utilisée dans ce cours (sept 2019) n’observe pas encore la norme ECMAScript pour l’importation de packages appelés modules. En 2019, [node.js] observe une norme de modules appelée CommonJS. L’intégration des modules ECMAScript par [node.js] est prévue en 2020. Là encore des développeurs se sont mis à la tâche et ont produit des packages permettant l’utilisation de modules ES6 avec [node.js] dès maintenant (2019).

Nous allons utiliser un package appelé [esm] (ECMAScript Modules). Nous l’installons dans un terminal du projet [javascript] :

image8

En [4], on constate que l’installation du package [esm] [1-3] a modifié le fichier [javascript/package.json].

Nous n’avons pas fini. Pour que le module [esm] soit utilisé par [node.js], il faut lancer celui-ci avec l’argument [-r esm].

Nous modifions dons la configuration de l’extension [Code Runner] de [VSCode] :

image9

image10

En [11], on ajoute l’argument [-r esm] et on sauvegarde (Ctrl-S) la configuration.

Maintenant, nous pouvons exécuter le script [str-04] :

1
2
3
4
5
6
7
'use strict';
// utilisation d'un package externe pour disposer de la fonction sprintf
import { sprintf } from 'sprintf-js';
// chaine
const chaîne = "Introduction à Javascript par l'exemple";
// méthode substr
console.log(sprintf("[%s].substr(3,2)=[%s]", chaîne, chaîne.substr(3, 2)));

image11

script [str-05]

Voici ce que dit la documentation sur la fonction [sprintf] :

The placeholders in the format string are marked by % and are followed by one or more of these elements, in this order:

  • An optional number followed by a $ sign that selects which argument index to use for the value. If not specified, arguments will be placed in the same order as the placeholders in the input string.
  • An optional + sign that forces to preceed the result with a plus or minus sign on numeric values. By default, only the - sign is used on negative numbers.
  • An optional padding specifier that says what character to use for padding (if specified). Possible values are 0 or any other character precedeed by a “ (single quote). The default is to pad with spaces.
  • An optional - sign, that causes sprintf to left-align the result of this placeholder. The default is to right-align the result.
  • An optional number, that says how many characters the result should have. If the value to be returned is shorter than this number, the result will be padded. When used with the j (JSON) type specifier, the padding length specifies the tab size used for indentation.
  • An optional precision modifier, consisting of a . (dot) followed by a number, that says how many digits should be displayed for floating point numbers. When used with the g type specifier, it specifies the number of significant digits. When used on a string, it causes the result to be truncated.
  • A type specifier that can be any of:
  • % — yields a literal % character
  • b — yields an integer as a binary number
  • c — yields an integer as the character with that ASCII value
  • d or i — yields an integer as a signed decimal number
  • e — yields a float using scientific notation
  • u — yields an integer as an unsigned decimal number
  • f — yields a float as is; see notes on precision above
  • g — yields a float as is; see notes on precision above
  • o — yields an integer as an octal number
  • s — yields a string as is
  • t — yields true or false
  • *T — yields the type of the argument*1
  • v — yields the primitive value of the specified argument
  • x — yields an integer as a hexadecimal number (lower-case)
  • X — yields an integer as a hexadecimal number (upper-case)
  • j — yields a JavaScript object or array as a JSON encoded string

Le script [script-05] met en œuvre quelques-uns de ces formats :

 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
'use strict';
// utilisation d'un package externe pour disposer de la fonction sprintf
import { sprintf } from 'sprintf-js';
// chaine
const chaîne = "Javascript";
// chaînes de caractères
console.log(sprintf("[%s, %%s]=>[%s]", chaîne, chaîne));
console.log(sprintf("[%s, %%20s]=>[%20s]", chaîne, chaîne));
console.log(sprintf("[%s, %%-20s]=>[%-20s]", chaîne, chaîne));
// entiers
console.log(sprintf("[%d, %%d]=>[%d]", 10, 10));
console.log(sprintf("[%d, %%4d]=>[%4d]", 10, 10));
console.log(sprintf("[%d, %%-4d]=>[%-4d]", 10, 10));
console.log(sprintf("[%d, %%04d]=>[%04d]", 10, 10));
// réels
console.log(sprintf("[%f, %%f]=>[%f]", -10.5, -10.5));
console.log(sprintf("[%f, %%10.2f]=>[%10.2f]", -10.5, -10.5));
console.log(sprintf("[%f, %%-10.2f]=>[%-10.2f]", -10.5, -10.5));
console.log(sprintf("[%f, %%010.3f]=>[%010.3f]", -10.5, -10.5));
// json
console.log(sprintf("personne (%%j)=%j", { nom: "mathieu", âge: 34 }));
// type
console.log(sprintf("type personne (%%T)=%T", { nom: "mathieu", âge: 34 }));
// booléen
console.log(sprintf("booléen (%%t)=%t", 4 === 4));

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\strings\str-05.js"
[Javascript, %s]=>[Javascript]
[Javascript, %20s]=>[ Javascript]
[Javascript, %-20s]=>[Javascript ]
[10, %d]=>[10]
[10, %4d]=>[ 10]
[10, %-4d]=>[10 ]
[10, %04d]=>[0010]
[-10.5, %f]=>[-10.5]
[-10.5, %10.2f]=>[ -10.50]
[-10.5, %-10.2f]=>[-10.50 ]
[-10.5, %010.3f]=>[-00010.500]
personne (%j)={"nom":"mathieu","âge":34}
type personne (%T)=object
booléen (%t)=true

script [str-06]

Le script [str-06] présente quelques méthodes de la classe [String] utilisables également sur le type [string] :

 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
'use strict';
// utilisation d'un package externe pour disposer de la fonction sprintf
import { sprintf } from 'sprintf-js';
// chaine
const chaîne = "  Introduction à Javascript ";
// quelques méthodes
// substr(10,2) : 2 caractères à partir du n° 10
console.log(sprintf("[%s].substr(10,2)=[%s]", chaîne, chaîne.substr(10, 2)));
// trim : élimination des blancs de début et fin de chaîne (blanc=\b \t \r \n \f)
console.log(sprintf("[%s].trim()=[%s]", chaîne, chaîne.trim()));
// toLowerCase : transformation en minuscules
console.log(sprintf("[%s].toLowerCase=[%s]", chaîne, chaîne.toLowerCase()));
// toUpperCase : transformation en majuscules
console.log(sprintf("[%s].toUpperCase=[%s]", chaîne, chaîne.toUpperCase()));
// indexOf : position d'une chaîne  cherchée dans la chaîne, -1 si la sous-chaîne n'existe pas
console.log(sprintf("[%s].indexOf('Java')=[%s]", chaîne, chaîne.indexOf('Java')));
console.log(sprintf("[%s].trim().indexOf('abcd')=[%s]", chaîne, chaîne.indexOf('abcd')));
// includes : vrai si la chaîne cherchée est dans la chaîne
console.log(sprintf("[%s].includes('Java')=[%s]", chaîne, chaîne.includes('Java')));
// length : longueur de la chaîne - n'est pas une méthode mais une propriété
console.log(sprintf("[%s].length=[%s]", chaîne, chaîne.length));
// slice (7,10) : chaînes des caractères n° 7 à 9
console.log(sprintf("[%s].slice(7,10)=[%s]", chaîne, chaîne.slice(7, 10)));
// match : cherche une expression dans la chaîne - cette expression peut être une expression régulière
// /intro/i : expression régulière désignant la chaîne [intro] en majuscules ou minuscules
// rend la chaîne trouvée
console.log(sprintf("[%s].match(/intro/i)=[%s]", chaîne, chaîne.match(/intro/i)));
// replace : remplace chaine1 par chaine2 dans chaîne
// remplace la 1ère occurrence de i par x
console.log(sprintf("[%s].replace('i','x')=[%s]", chaîne, chaîne.replace('i', 'x')));
// remplace toutes les occurrences de i par x
// /i/g est une expression régulière désignant toutes (g) les occurrences de i
console.log(sprintf("[%s].replace(/i/g,'x')=[%s]", chaîne, chaîne.replace(/i/g, 'x')));
// split : divise la chaîne en mots séparés par le paramètre de split
// rend le tableau de ces mots
// /\s*/ : mots séparés par 0 ou plusieurs espaces
console.log(sprintf("[%s].split(/\\s*/)=[%s]", chaîne, chaîne.split(/\s*/)));
// /\s+/ : mots séparés par un ou plusieurs espaces
console.log(sprintf("[%s].split(/\\s+/)=[%s]", chaîne, chaîne.split(/\s+/)));

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\strings\str-06.js"
[ Introduction à Javascript ].substr(10,2)=[ti]
[ Introduction à Javascript ].trim()=[Introduction à Javascript]
[ Introduction à Javascript ].toLowerCase=[ introduction à javascript ]
[ Introduction à Javascript ].toUpperCase=[ INTRODUCTION À JAVASCRIPT ]
[ Introduction à Javascript ].indexOf('Java')=[17]
[ Introduction à Javascript ].trim().indexOf('abcd')=[-1]
[ Introduction à Javascript ].includes('Java')=[true]
[ Introduction à Javascript ].length=[28]
[ Introduction à Javascript ].slice(7,10)=[duc]
[ Introduction à Javascript ].match(/intro/i)=[Intro]
[ Introduction à Javascript ].replace('i','x')=[ Introductxon à Javascript ]
[ Introduction à Javascript ].replace(/i/g,'x')=[ Introductxon à Javascrxpt ]
[ Introduction à Javascript ].split(/\s*/)=[,I,n,t,r,o,d,u,c,t,i,o,n,à,J,a,v,a,s,c,r,i,p,t,]
[ Introduction à Javascript ].split(/\s+/)=[,Introduction,à,Javascript,]

Expressions régulières

image0

script [regex-01]

Dans le cours PHP, nous avions utilisé le code suivant pour illustrer les expressions régulières de PHP 7 :

 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
<?php

// type strict pour les paramètres de fonctions
declare (strict_types=1);

// expressions régulières en php
// récupérer les différents champs d'une chaîne
// le modèle : une suite de chiffres entourée de caractères quelconques
// on ne veut récupérer que la suite de chiffres
$modèle = "/(\d+)/";
// on confronte la chaîne au modèle
compareModele2Chaine($modèle, "xyz1234abcd");
compareModele2Chaine($modèle, "12 34");
compareModele2Chaine($modèle, "abcd");

// le modèle : une suite de chiffres entourée de caractères quelconques
// on veut la suite de chiffres ainsi que les champs qui suivent et précèdent
$modèle = "/^(.*?)(\d+)(.*?)$/";
// on confronte la chaîne au modèle
compareModele2Chaine($modèle, "xyz1234abcd");
compareModele2Chaine($modèle, "12 34");
compareModele2Chaine($modèle, "abcd");

// le modèle - une date au format jj/mm/aa
$modèle = "/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/";
compareModele2Chaine($modèle, "10/05/97");
compareModele2Chaine($modèle, "  04/04/01  ");
compareModele2Chaine($modèle, "5/1/01");

// le modèle - un nombre décimal
$modèle = "/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*/";
compareModele2Chaine($modèle, "187.8");
compareModele2Chaine($modèle, "-0.6");
compareModele2Chaine($modèle, "4");
compareModele2Chaine($modèle, ".6");
compareModele2Chaine($modèle, "4.");
compareModele2Chaine($modèle, " + 4");

// fin
exit;

// --------------------------------------------------------------------------
function compareModele2Chaine(string $modèle, string $chaîne): void {
  // compare la chaîne $chaîne au modèle $modèle
  // on confronte la chaîne au modèle
  $champs = [];
  $correspond = preg_match($modèle, $chaîne, $champs);
  // affichage résultats
  print "\nRésultats($modèle,$chaîne)\n";
  if ($correspond) {
    for ($i = 0; $i < count($champs); $i++) {
      print "champs[$i]=$champs[$i]\n";
    }
  } else {
    print "La chaîne [$chaîne] ne correspond pas au modèle [$modèle]\n";
  }
}

Nous transposons ce code en Javascript 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
'use strict';

/// expressions régulières en javascript
// récupérer les différents champs d'une chaîne
// le modèle : une suite de chiffres entourée de caractères quelconques
// on ne veut récupérer que la suite de chiffres
let modèle = /(\d+)/;
// on confronte la chaîne au modèle
compareModèleToChaîne(modèle, "xyz1234abcd");
compareModèleToChaîne(modèle, "12 34");
compareModèleToChaîne(modèle, "abcd");

// le modèle : une suite de chiffres entourée de caractères quelconques
// on veut la suite de chiffres ainsi que les champs qui suivent et précèdent
modèle = /^(.*?)(\d+)(.*?)$/;
// on confronte la chaîne au modèle
compareModèleToChaîne(modèle, "xyz1234abcd");
compareModèleToChaîne(modèle, "12 34");
compareModèleToChaîne(modèle, "abcd");

// le modèle - une date au format jj/mm/aa
modèle = /^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/;
compareModèleToChaîne(modèle, "10/05/97");
compareModèleToChaîne(modèle, "  04/04/01  ");
compareModèleToChaîne(modèle, "5/1/01");

// le modèle - un nombre décimal
modèle = /^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/;
compareModèleToChaîne(modèle, "187.8");
compareModèleToChaîne(modèle, "-0.6");
compareModèleToChaîne(modèle, "4");
compareModèleToChaîne(modèle, ".6");
compareModèleToChaîne(modèle, "4.");
compareModèleToChaîne(modèle, " + 4");

// --------------------------------------------------------------------------
function compareModèleToChaîne(modèle, chaîne) {
  // compare la chaîne [chaîne] au modèle [modèle]
  console.log(`----------- chaîne=${chaîne}, modèle=${modèle}`)
  // on confronte la chaîne au modèle
  const result1 = modèle.exec(chaîne);
  console.log(`comparaison avec exec=`, result1);
  // une autre façon de faire
  const result2 = chaîne.match(modèle);
  console.log(`comparaison avec match=`, result2);
}

Commentaires

  • les codes PHP et Javascript sont très proches l’un de l’autre ;
  • ligne 7 : on notera qu’en Javascript l’expression régulière n’est pas une chaîne de caractères mais un objet. On ne met pas de guillemets ou d’apostrophes autour de l’expression ;
  • lignes 41 et 44 : il y a deux méthodes pour obtenir le même résultat ;

Exécution

  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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\regexp\regexp-01.js"
type d'une expression régulière : object
----------- chaîne=xyz1234abcd, modèle=/(\d+)/
comparaison avec exec= [ '1234',
'1234',
index: 3,
input: 'xyz1234abcd',
groups: undefined ]
comparaison avec match= [ '1234',
'1234',
index: 3,
input: 'xyz1234abcd',
groups: undefined ]
----------- chaîne=12 34, modèle=/(\d+)/
comparaison avec exec= [ '12', '12', index: 0, input: '12 34', groups: undefined ]
comparaison avec match= [ '12', '12', index: 0, input: '12 34', groups: undefined ]
----------- chaîne=abcd, modèle=/(\d+)/
comparaison avec exec= null
comparaison avec match= null
----------- chaîne=xyz1234abcd, modèle=/^(.*?)(\d+)(.*?)$/
comparaison avec exec= [ 'xyz1234abcd',
'xyz',
'1234',
'abcd',
index: 0,
input: 'xyz1234abcd',
groups: undefined ]
comparaison avec match= [ 'xyz1234abcd',
'xyz',
'1234',
'abcd',
index: 0,
input: 'xyz1234abcd',
groups: undefined ]
----------- chaîne=12 34, modèle=/^(.*?)(\d+)(.*?)$/
comparaison avec exec= [ '12 34',
'',
'12',
' 34',
index: 0,
input: '12 34',
groups: undefined ]
comparaison avec match= [ '12 34',
'',
'12',
' 34',
index: 0,
input: '12 34',
groups: undefined ]
----------- chaîne=abcd, modèle=/^(.*?)(\d+)(.*?)$/
comparaison avec exec= null
comparaison avec match= null
----------- chaîne=10/05/97, modèle=/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/
comparaison avec exec= [ '10/05/97',
'10',
'05',
'97',
index: 0,
input: '10/05/97',
groups: undefined ]
comparaison avec match= [ '10/05/97',
'10',
'05',
'97',
index: 0,
input: '10/05/97',
groups: undefined ]
----------- chaîne= 04/04/01 , modèle=/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/
comparaison avec exec= [ ' 04/04/01 ',
'04',
'04',
'01',
index: 0,
input: ' 04/04/01 ',
groups: undefined ]
comparaison avec match= [ ' 04/04/01 ',
'04',
'04',
'01',
index: 0,
input: ' 04/04/01 ',
groups: undefined ]
----------- chaîne=5/1/01, modèle=/^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$/
comparaison avec exec= null
comparaison avec match= null
----------- chaîne=187.8, modèle=/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/
comparaison avec exec= [ '187.8',
'',
'187.8',
index: 0,
input: '187.8',
groups: undefined ]
comparaison avec match= [ '187.8',
'',
'187.8',
index: 0,
input: '187.8',
groups: undefined ]
----------- chaîne=-0.6, modèle=/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/
comparaison avec exec= [ '-0.6', '-', '0.6', index: 0, input: '-0.6', groups: undefined ]
comparaison avec match= [ '-0.6', '-', '0.6', index: 0, input: '-0.6', groups: undefined ]
----------- chaîne=4, modèle=/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/
comparaison avec exec= [ '4', '', '4', index: 0, input: '4', groups: undefined ]
comparaison avec match= [ '4', '', '4', index: 0, input: '4', groups: undefined ]
----------- chaîne=.6, modèle=/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/
comparaison avec exec= [ '.6', '', '.6', index: 0, input: '.6', groups: undefined ]
comparaison avec match= [ '.6', '', '.6', index: 0, input: '.6', groups: undefined ]
----------- chaîne=4., modèle=/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/
comparaison avec exec= [ '4.', '', '4.', index: 0, input: '4.', groups: undefined ]
comparaison avec match= [ '4.', '', '4.', index: 0, input: '4.', groups: undefined ]
----------- chaîne= + 4, modèle=/^\s*([+|-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/
comparaison avec exec= [ ' + 4', '+', '4', index: 0, input: ' + 4', groups: undefined ]
comparaison avec match= [ ' + 4', '+', '4', index: 0, input: ' + 4', groups: undefined ]

Les méthodes [regexp.exec] et [string.match] donnent les mêmes résultats :

  • [null] s’il n’y a pas de correspondances entre la chaîne et son modèle ;
  • un tableau t, s’il y a correspondance avec :
    • t[0] : la chaîne correspondant au modèle ;
    • t[1] : la chaîne correspondant à la 1ère parenthèse du modèle ;
    • t[2] : la chaîne correspondant à la 2ième parenthèse du modèle ;
    • t[input] : la chaîne entière dans laquelle on a cherché le modèle ;

script [regexp-02]

Parfois on ne souhaite pas récupérer des éléments de la chaîne testée mais seulement savoir si elle correspond au modèle :

 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
'use strict';

/// expressions régulières en javascript
// récupérer les différents champs d'une chaîne
// le modèle : une suite de chiffres entourée de caractères quelconques
// on ne veut récupérer que la suite de chiffres
let modèle = /\d+/;
console.log("type d'une expression régulière : ", typeof (modèle));
// on confronte la chaîne au modèle
compareModèleToChaîne(modèle, "xyz1234abcd");
compareModèleToChaîne(modèle, "12 34");
compareModèleToChaîne(modèle, "abcd");

// le modèle : une suite de chiffres entourée de caractères quelconques
// on veut la suite de chiffres ainsi que les champs qui suivent et précèdent
modèle = /^.*?\d+.*?$/;
// on confronte la chaîne au modèle
compareModèleToChaîne(modèle, "xyz1234abcd");
compareModèleToChaîne(modèle, "12 34");
compareModèleToChaîne(modèle, "abcd");

// le modèle - une date au format jj/mm/aa
modèle = /^\s*\d\d\/\d\d\/\d\d\s*$/;
compareModèleToChaîne(modèle, "10/05/97");
compareModèleToChaîne(modèle, "  04/04/01  ");
compareModèleToChaîne(modèle, "5/1/01");

// le modèle - un nombre décimal
modèle = /^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/;
compareModèleToChaîne(modèle, "187.8");
compareModèleToChaîne(modèle, "-0.6");
compareModèleToChaîne(modèle, "4");
compareModèleToChaîne(modèle, ".6");
compareModèleToChaîne(modèle, "4.");
compareModèleToChaîne(modèle, " + 4");

// --------------------------------------------------------------------------
function compareModèleToChaîne(modèle, chaîne) {
  // test
  const correspond = modèle.test(chaîne);
  // compare la chaîne [chaîne] au modèle [modèle]
  console.log(`----------- chaîne=${chaîne}, modèle=${modèle}, correspond=${correspond}`);
}

Commentaires

  • [regexp-02] reprend le code de [regexp-01] avec les différences suivantes :
    • on ne souhaite pas récupérer des éléments de la chaîne testée. Aussi a-t-on enlevé les parenthèses dans les expression régulières utilisées ;
    • ligne 40 : on utilise la méthode [Regexp.test] pour savoir si une chaîne de caractères vérifie une expression régulière ;

Les résultats de l’exécution sont les suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\cours\regexp\regexp-02.js"
type d'une expression régulière :  object
----------- chaîne=xyz1234abcd, modèle=/\d+/, correspond=true
----------- chaîne=12 34, modèle=/\d+/, correspond=true
----------- chaîne=abcd, modèle=/\d+/, correspond=false
----------- chaîne=xyz1234abcd, modèle=/^.*?\d+.*?$/, correspond=true
----------- chaîne=12 34, modèle=/^.*?\d+.*?$/, correspond=true
----------- chaîne=abcd, modèle=/^.*?\d+.*?$/, correspond=false
----------- chaîne=10/05/97, modèle=/^\s*\d\d\/\d\d\/\d\d\s*$/, correspond=true
----------- chaîne=  04/04/01  , modèle=/^\s*\d\d\/\d\d\/\d\d\s*$/, correspond=true
----------- chaîne=5/1/01, modèle=/^\s*\d\d\/\d\d\/\d\d\s*$/, correspond=false
----------- chaîne=187.8, modèle=/^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/, correspond=true
----------- chaîne=-0.6, modèle=/^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/, correspond=true
----------- chaîne=4, modèle=/^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/, correspond=true
----------- chaîne=.6, modèle=/^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/, correspond=true
----------- chaîne=4., modèle=/^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/, correspond=true
----------- chaîne= + 4, modèle=/^\s*[+|-]?\s*\d+\.\d*|\.\d+|\d+\s*$/, correspond=true

[Done] exited with code=0 in 0.269 seconds

Les fonctions

image0

script [func-01]

Le script s’intéresse au mode de passage des paramètres d’une fonction :

  • passage par valeur pour nombres, chaînes et booléens ;
  • passage par référence pour les tableaux, objets littéraux et fonctions ;
 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
'use strict';
// mode de passage des paramètres d'une fonction
// -----------------------nombre - passage par valeur
function doSomethingWithNumber(param) {
  param++;
  console.log("[param inside function]=", param, "[type]=", typeof (param), "[passage par référence]=", param === count);
}
// code d'appel
let count = 10;
doSomethingWithNumber(count);
console.log("[count outside function]=", count);

// --------------------- chaîne - passage par valeur
function doSomethingWithString(param) {
  param += " xyz"
  console.log("[param inside function]=", param, "[type]=", typeof (param), "[passage par référence]=", param === text);
}
// code d'appel
let text = "abcd";
doSomethingWithString(text);
console.log("[text outside function]=", text);

// --------------------- booléen - passage par valeur
function doSomethingWithBoolean(param) {
  param = !param;
  console.log("[param inside function]=", param, "[type]=", typeof (param), "[passage par référence]=", param === bool);
}
// code d'appel
let bool = true;
doSomethingWithBoolean(bool);
console.log("bool [outside function]=", bool);

// --------------------- tableau - passage par référence
function doSomethingWithArray(param) {
  param.push(1000);
  console.log("[param inside function]=", param, "[type]=", typeof (param), "[passage par référence]=", param === tab);
}
// code d'appel
const tab = [10, 20, 30];
doSomethingWithArray(tab);
console.log("[tab outside function]=", tab);

// --------------------- objet - passage par référence
function doSomethingWithObject(param) {
  param.unePropriétéNouvelle = "xyz";
  console.log("[param inside function]=", param, "[type]=", typeof (param), "[passage par référence]=", param === obj);
}
// code d'appel
const obj = [10, 20, 30];
doSomethingWithObject(obj);
console.log("[obj outside function]=", obj);

// --------------------- fonction - passage par référence
function doSomethingWithFunction(param) {
  // une chose plutôt bizarre qui marche pourtant
  param.unePropriétéNouvelle = "xyz";
  console.log("[param inside function]=", param, "[type]=", typeof (param), "[passage par référence]=", param === f);
}
// code d'appel
const f = x => x + 4;
doSomethingWithFunction(f);
console.log("[f outside function]=", f, f.unePropriétéNouvelle, typeof (f));

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\fonctions\func-01.js"
[param inside function]= 11 [type]= number [passage par référence]= false
[count outside function]= 10
[param inside function]= abcd xyz [type]= string [passage par référence]= false
[text outside function]= abcd
[param inside function]= false [type]= boolean [passage par référence]= false
bool [outside function]= true
[param inside function]= [ 10, 20, 30, 1000 ] [type]= object [passage par référence]= true
[tab outside function]= [ 10, 20, 30, 1000 ]
[param inside function]= [ 10, 20, 30, 'unePropriétéNouvelle': 'xyz' ] [type]= object [passage par référence]= true
[obj outside function]= [ 10, 20, 30, 'unePropriétéNouvelle': 'xyz' ]
[param inside function]= x => x + 4 [type]= function [passage par référence]= true
[f outside function]= x => x + 4 xyz function

script [func-02]

Le script suivant montre que le type [function] est un type de donnée comme un autre et qu’une variable peut avoir ce type. Il montre également deux façons de définir une fonction :

  • l’une avec le mot clé [function] ;
  • l’autre avec la notation « flèche » => ;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
'use strict';
// on peut affecter une fonction à une variable
const variable1 = function (a, b) {
return a + b;
};
console.log("typeof(variable1)=", typeof (variable1));
// la variable peut ensuite s'utiliser comme une fonction
console.log("variable1(10,12)=", variable1(10, 12));
// la définition de la fonction peut se faire avec la notation =>
const variable2 = (a, b, c) => {
return a - b + c;
};
console.log("variable2(10,12,14)=", variable2(10, 12, 14));
// on peut ne pas mettre les accolades s'il n'y a qu'une expression dans le code de la fonction
// cette expression est alors la valeur de retour de la fonction
const variable3 = (a, b, c) => a + b + c;
console.log("variable3(10,12,14)=", variable3(10, 12, 14));

Exécution

1
2
3
4
5
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\fonctions\func-02.js"
typeof(variable1)= function
variable1(10,12)= 22
variable2(10,12,14)= 12
variable3(10,12,14)= 36

script [func-03]

Ce script revient sur la possibilté de passer une fonction en paramètre à une autre fonction. Ce procédé est abondamment utilisé dans les frameworks Javascript.

 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
'use strict';
// les paramètres d'une fonction peuvent être de type [fonction]

// fonction f1
function f1(param1, param2) {
  return param1 + param2 + 10;
}
// fonction f2
function f2(param1, param2) {
  return param1 + param2 + 20;
}
// fonction g avec une fonction f en paramètre
function g(param1, param2, f) {
  return f(param1, param2) + 100;
}
// utilisations de g
console.log(g(0, 10, f1));
console.log(g(0, 10, f2));
// le paramètre effectif de type fonction peut être passé en direct - forme 1
console.log(g(0, 10, (param1, param2) => {
  return param1 + param2 + 30;
}));
// le paramètre effectif de type fonction peut être passé en direct - forme 2
console.log(g(0, 10, function (param1, param2) {
  return param1 + param2 + 40;
}));

Exécution

1
2
3
4
5
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\fonctions\func-03.js"
120
130
140
150

script [func-04]

Le script suivant montre qu’une fonction Javascript peut se comporter comme une classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';
// une fonction peut être utilisée comme un objet

// une coquille vide
function f() {

}
// à qui on attribue des propriétés de l'extérieur
f.prop1 = "val1";
f.show = function () {
  console.log(this.prop1);
}
// utilisation de f
f.show();

// une fonction g fonctionnant comme une classe
function g() {
  this.prop2 = "val2";
  this.show = function () {
    console.log(this.prop2);
  }
}
// instanciation de la fonction avec [new]
new g().show();

Commentaires

  • lignes 5-7 : le corps de la fonction f ne définit aucune propriété ;
  • lignes 9-12 : on donne de l’extérieur des propriétés à la fonction f ;
  • ligne 14 : utilisation de la function (objet) f. Notez qu’on n’écrit pas [f()] mais simplement [f]. On a là la notation d’un objet ;
  • lignes 17-22 : on définit une fonction [g] comme si c’était une classe avec propriétés et méthodes ;
  • ligne 24 : la fonction [g] est instanciée par [new g()] ;

Résultats de l’exécution

1
2
3
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\classes\class-00.js"
val1
val2

ES6 a introduit la notion de classe qui nous permet désormais d’éviter de passer par des fonctions pour avoir des classes.

script [func-05]

Le script [func-05] montre l’usage d’un opérateur appelé [rest operator] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
'use strict';
// rest operator
function f(arg1, ...otherArgs) {
  // 1er argument
  console.log("arg1=", arg1);
  // les autres arguments
  let i = 0;
  otherArgs.forEach(element => {
    console.log("otherArguments[", i, "]=", element);
    i++;
  });
}

// appel
f(1, "deux", "trois", { x: 2, y: 3 })
  • ligne 3 : la notation […otherArgs] fait qu’avec un appel de type f(param1, param2, param3) on aura ligne 3 :
    • arg1=param1
    • otherArgs=[param2, param3]. [otherArgs] est donc un tableau qui rassemble tous les paramètres effectifs passés derrière [param1] ;

Les résultats de l’application sont les suivants :

1
2
3
4
arg1= 1
otherArguments[ 0 ]= deux
otherArguments[ 1 ]= trois
otherArguments[ 2 ]= { x: 2, y: 3 }

Les erreurs et exceptions

image0

Javascript n’a pas un système d’exceptions très évolué. Il offre néanmoins l’instruction [throw] qui permet de signaler une erreur ainsi que la structure try / catch / finally qui permet d’intercepter ces erreurs.

script [excep-01]

Dans le script qui suit, nous allons afficher la date du moment sous la forme [heures:minutes:secondes:millisecondes]. Pour ce faire, nous allons utiliser une bibliothèques jS appelée [moment.js]. Nous l’installons, comme d’habitude, avec l’outil [npm] :

image1

Le code du script est le suivant :

1
2
3
4
5
6
'use strict';

// package moment
import moment from 'moment';

...

Pour savoir comment écrire la ligne [import] de la ligne 4, on peut regarder la définition du module [moment] :

image2

  • en [1-2], on va à la définition du module ;
  • en [3], on a un fichier Typescript, pas Javascript. A l’exécution, ce fichier Typescript est compilé en fichier Javascript avant d’être utilisé ;
  • en [4], on cherche les instructions [export] (Ctrl-F) ;
  • en [5], l’instruction exporte l’objet [moment]. Celui-ci peut être importé en ES6 de la façon suivante :
1
import moment from 'moment';
On peut utiliser n’importe quel nom pour importer l’objet [moment], par exemple :
1
import m from 'moment';

Revenons au code du script :

 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
'use strict';

// package moment
import moment from 'moment';

// principe du try / catch / finally
for (let i = 0; i < 10; i++) {
  // date - heure du moment courant
  const now = Date.now();
  // formatage heure pour avoir les millisecondes
  const time = moment(now).format("HH:mm:ss:SSS");
  // les millisecondes
  const milli = Number(time.substr(time.length - 3));
  // affichage
  console.log("--------------------itération n° ", i, "à", time);
  try {
    // nbre variant selon l’heure du moment
    const nbre = milli % 2;
    if (nbre === 0) {
      // lancer un msg d'erreur
      throw "erreur";
    }
    // si on arrive ici c'est qu'il n'y a pas eu d'erreur
    console.log("pas d'erreur");
  } catch (error) {
    // si on arrive ici, c'est qu'il y a eu erreur
    console.log("erreur1=", error);
  } finally {
    // exécuté dans tous les cas erreur ou pas
    console.log("finally")
  }
}

Commentaires

  • ligne 4 : import de la bibliothèque [moment] ;
  • le principe du script est de boucler 10 fois (ligne 7). A chaque tour de boucle, on récupère l’heure du moment sous la forme [heures:minutes:secondes:millisecondes] (lignes 8-13) ;
  • si le nombre de millisecondes est pair, on lance un message d’erreur (lignes 19-22) ;
  • il s’agit ici de comprendre le fonctionnement du try / catch / finally

Exécution

 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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\exceptions\excep-01.js"
--------------------itération n° 0 à 17:57:04:440
erreur1= erreur
finally
--------------------itération n° 1 à 17:57:04:447
pas d'erreur
finally
--------------------itération n° 2 à 17:57:04:448
erreur1= erreur
finally
--------------------itération n° 3 à 17:57:04:448
erreur1= erreur
finally
--------------------itération n° 4 à 17:57:04:448
erreur1= erreur
finally
--------------------itération n° 5 à 17:57:04:448
erreur1= erreur
finally
--------------------itération n° 6 à 17:57:04:448
erreur1= erreur
finally
--------------------itération n° 7 à 17:57:04:448
erreur1= erreur
finally
--------------------itération n° 8 à 17:57:04:449
pas d'erreur
finally
--------------------itération n° 9 à 17:57:04:449
pas d'erreur
finally

On voit que la clause [finally] est toujours exécutée qu’il y ait erreur ou pas.

script [excep-02]

Ce script que l’instruction [throw] peut lancer n’importe quel type de donnée et que cette donnée est récupérée intégralement par la clause [catch].

 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
'use strict';

// on peut "lancer" (throw) à peu près n'importe quoi pour signaler une erreur
let i = 0;
console.log("--------------------essai n° ", i);
// lancer une chaîne de caractères
try {
  throw "msg d'erreur";
} catch (error) {
  // il y a eu erreur
  console.log("erreur=[", error, "], type=", typeof (error));
}
// lancer un tableau
i++;
console.log("--------------------essai n° ", i);
try {
  throw [1, 2, 3]
} catch (error) {
  // il y a eu erreur
  console.log("erreur=[", error, "], type=", typeof (error));
}
// lancer un objet littéral
i++;
console.log("--------------------essai n° ", i);
try {
  throw { nom: "hercule", pays: "grèce antique" }
} catch (error) {
  // il y a eu erreur
  console.log("erreur=[", error, "], type=", typeof (error));
}
// lancer un type Error
i++;
console.log("--------------------essai n° ", i);
try {
  throw new Error("erreur de connexion au réseau");
} catch (error) {
  // il y a eu erreur
  console.log("erreur=[", error, "], type=", typeof (error));
}
// lancer un type Error
i++;
console.log("--------------------essai n° ", i);
try {
  throw new Error("erreur de connexion au réseau");
} catch (error) {
  // il y a eu erreur - le message est dans [error.message]
  console.log("erreur.message=[", error.message, "], type(error)=", typeof (error));
}

Commentaires

  • lignes 35, 44 : [Error] est une classe Javascript dont le constructeur admet comme 1er paramètre facultatif un message d’erreur. Ce message peut être récupéré dans la propriété [Error.message] (ligne 47) ;
  • il existe d’autres classes que [Error] pour signaler une erreur : [EvalError, InternalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError) ;

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\exceptions\excep-02.js"
--------------------essai n° 0
erreur=[ msg d'erreur ], type= string
--------------------essai n° 1
erreur=[ [ 1, 2, 3 ] ], type= object
--------------------essai n° 2
erreur=[ { nom: 'hercule', pays: 'grèce antique' } ], type= object
--------------------essai n° 3
erreur=[ Error: erreur de connexion au réseau
at Object.<anonymous> (c:\Data\st-2019\dev\es6\javascript\exceptions\excep-02.js:35:9)
at Object.<anonymous> (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:251206)
at c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:245054
at Generator.next (<anonymous>)
at bl (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:245412)
at kl (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:247659)
at Object.u (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:287740)
at Object.o (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:287137)
at Object.<anonymous> (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:284879)
at Object.apply (c:\Temp\19-09-01\javascript\node_modules\esm\esm.js:1:199341) ], type= object
--------------------essai n° 4
erreur.message=[ erreur de connexion au réseau ], type(error)= object

script [excep-03]

Ce script montre qu’on peut, dans un [catch], différentier le type d’instance [Error] interceptée par le [catch] :

 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
'use strict';

// package moment
import moment from 'moment';

// différencier l'instance d'Error reçue dans un [catch]
for (let i = 0; i < 10; i++) {
  // date - heure du moment courant
  const now = Date.now();
  // formatage heure pour avoir les millisecondes
  const time = moment(now).format("HH:mm:ss:SSS");
  // les millisecondes
  const milli = Number(time.substr(time.length - 3));
  console.log("--------------------itération n° ", i);
  try {
    // nbre [0, 1, 2]
    const nbre = milli % 3;
    switch (nbre) {
      case 0:
        throw new ReferenceError("erreur 1");
      case 1:
        throw new RangeError("erreur 2");
      default:
        throw new EvalError("erreur 3");
    }
  } catch (error) {
    // il y a eu erreur
    if (error instanceof ReferenceError) {
      console.log("ReferenceError :", error.message);
    } else {
      if (error instanceof RangeError) {
        console.log("RangeError :", error.message);
      }
      else {
        if (error instanceof EvalError) {
          console.log("EvalError :", error.message);
        }
      }
    }
  }
}

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\exceptions\excep-03.js"
--------------------itération n° 0
ReferenceError : erreur 1
--------------------itération n° 1
RangeError : erreur 2
--------------------itération n° 2
RangeError : erreur 2
--------------------itération n° 3
RangeError : erreur 2
--------------------itération n° 4
RangeError : erreur 2
--------------------itération n° 5
RangeError : erreur 2
--------------------itération n° 6
EvalError : erreur 3
--------------------itération n° 7
EvalError : erreur 3
--------------------itération n° 8
EvalError : erreur 3
--------------------itération n° 9
EvalError : erreur 3

Les modules ES6

image0

Les modules ES6 permettent de construire des applications Javascript structurées en modules indépendants et réutilisables.

scripts [import-01, export-01]

Le script [import-01] va utiliser le module [export-01]. Celui-ci est défini de la façon suivante :

1
2
3
4
5
6
7
// export par défaut d'un objet non nommé
export default {
  data: 2,
  do() {
    console.log(this.data);
  }
};

Commentaires

  • les lignes [2-7] définissent un objet non nommé avec les propriétés [data, do] ;
  • ligne 2 : l’instruction [export default] exporte cet objet. Celui-ci pourra donc être importé ;

Le module [export-01] est utilisé par le script [import-01] de la façon suivante :

1
2
3
4
5
6
7
8
'use strict';
// import d'un objet exporté par défaut
import export01 from './export-01';
// utilisation de cet objet
export01.do();
// on peut importer un export par défaut sous n'importe quel nom
import data from './export-01';
console.log(data.data);

Commentaires

  • les lignes 3 et 7 importent l’objet exporté par défaut du module [export-01] sous deux noms différents ;
  • une fois un objet importé, on peut l’utiliser comme s’il avait été défini localement dans le script ;

Exécution

1
2
3
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\modules\import-01.js"
2
2

scripts [import-02, export-02]

Ces scripts montrent un export d’un objet nommé.

Le script [export-02] est le suivant :

1
2
3
4
5
6
7
8
9
// export par défaut d'un objet nommé
const data = {
  data: 2,
  do() {
    console.log(this.data);
  }
};
// export
export default data;
  • ligne 9 : on exporte l’objet [data] ;

Que l’objet exporté soit nommé ou non ne change rien à l’opération d’import. Le script [import-02] est le suivant :

1
2
3
4
5
6
7
8
'use strict';
// import d'un objet exporté par défaut
import module1 from './export-02';
// utilisation de cet objet
module1.do();
// on peut importer un export par défaut sous n'importe quel nom
import module2 from './export-02';
console.log(module2.data);

Exécution

1
2
3
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\modules\import-02.js"
2
2

scripts [import-03, export-03]

Un module peut exporter plusieurs éléments.

Le script [export-03] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// multi-exports
// export objet
const data = {
  data: 2,
  do() {
    console.log(this.data);
  }
};
// export fonction
export { data };
function doSomething() {
  console.log("doSomething");
}
export { doSomething };

Commentaires

  • lignes 10 et 14 : export de deux éléments. L’export d’un élément se fait avec la syntaxe [export {élément}] ;

Le script [import-03] utilise le module [export-03] de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
'use strict';
// import d'un module [export03]
import {data, doSomething} from './export-03';
// utilisation des imports
data.do();
doSomething();
// autre écriture
import * as module from './export-03';
// utilisation de l'import
console.log(module.data);
module.doSomething();
  • ligne 3 : les [imports] se font avec les noms exacts des objets exportés ;
  • ligne 8 : on peut importer tous les objets exportés dans un objet nommé (ici [module]) ;

Exécution

1
2
3
4
5
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\modules\import-03.js"
2
doSomething
{ data: 2, do: [Function: do] }
doSomething

Programmation événementielle et fonctions asynchrones

image0

Une fonction asynchrone est une fonction dont l’exécution est lancée mais dont on n’attend pas le résultat. Lorsque l’exécution est terminée, la fonction asynchrone émet un événement et transmet son résultat via celui-ci.

Ce mode de fonctionnement est bien adapté à l’exécution au sein d’un navigateur web. En effet, une application exécutée au sein d’un navigateur est une application à événements : l’application réagit à des événements, principalement provoqués par l’utilisateur (clics, déplacements de souris, frappe de texte, …). Les applications Javascript exécutées au sein d’un navigateur sont amenées à dialoguer avec des services externes via le protocole HTTP. Les fonctions natives HTTP de Javascript sont asynchrones : elles sont lancées et l’obtention de la réponse du service externe sollicité est signalé par un événement qui s’ajoute à l’ensemble des événements gérés par l’application.

Les scripts suivants vont être exécutés par [node.js] et non par un navigateur. [node.js] a lui également un mode d’exécution par événement :

  • [node.js], comme un navigateur, utilise une boucle d’événements pour exécuter un script ;
  • l’exécution du code principal du script est le 1er événement exécuté ;
  • si ce code principal a lancé des tâches asynchrones alors l’exécution se poursuit tant que ces tâches asynchrones ne sont pas terminées. Celles-ci émettent un événement lorsqu’elles sont terminées. Ces événements sont mis en file d’attente dans la boucle d’événements ;
  • le script principal doit s’abonner à ces événements s’il veut récupérer les résultats des actions asynchrones ;
  • le script n’est terminé que lorsque tous les événements émis par celui-ci ont été traités ;

script [async-01]

Le script suivant montre le comportement d’un script comportant une action asynchrone.

 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
'use strict';

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// setTimeout arme un timer de 1000 ms (2ième paramètre) et retourne immédiatement le n° de ce timer
// lorsque le timer a épuisé les 1000 ms il émet un événement qui est mis en file d'attente du runtime
// lorsque l'événement est traité par le runtime, la fonction (1er paramètre) est exécutée
setTimeout(function () {
  // ce code sera exécuté lorsque le timer aura atteint la valeur 0
  console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)

// s'affichera avant le msg de la fonction interne au timer
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire d'affichage heure et durée
function heure(début) {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // faut-il calculer une durée ?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage heure + durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}
  • ligne 4 : on importe la bibliothèque [moment] pour pouvoir formater les dates (ligne 27) ;
  • ligne 5 : on importe la bibliothèque [sprintf-js] pour pouvoir formater les durées (ligne 34) ;
  • ligne 8 : on note l’heure de début du script ;
  • ligne 9 : on l’affiche à l’aide de la méthode [heure] des lignes 20-34 ;
  • lignes 14-17 : la fonction [setTimeout] a deux paramètres (f, durée) : f est une fonction qui est exécutée lorsque [durée] millisecondes se sont écoulées ;
  • ligne 14 : à l’exécution du script, la fonction [setTimeout] est exécutée :
    • ligne 17 : un minuteur de 1000 ms est armé et le décompte commence jusqu’à atteindre ultérieurement 0. La fonction [setTimeout] est terminée dès que le minuteur est initialisé et le décompte commencé. Elle n’attend pas la fin de ce décompte. Elle rend un n° identifiant le minuteur utilisé et l’exécution passe à l’instruction suivante, ligne 20. Ici, le résultat de [setTimeout] n’est pas utilisé ;
  • ligne 16 : ce message va s’afficher à la fin du délai de 1000 ms de la fonction [setTimeout] ;
  • lignes 15-16 : la fonction f, 1er paramètre de la fonction [setTimeout], va être exécutée à la fin du délai des 1000 ms. Le message de la ligne 16 va alors s’afficher ;
  • ligne 20 : ce message va s’afficher avant celui de la ligne 16 ;

Fonction heure :

  • ligne 23 : la fonction admet un paramètre facultatif [heure] qui est l’heure de début d’une opération dont elle doit afficher la durée ;
  • lignes 25-27 : on calcule et formate l’heure du moment ;
  • ligne 29 : si le paramètre [début] est présent alors il faut calculer une durée ;
  • ligne 30 : la durée de l’opération. On obtient un nombre de millisecondes ;
  • lignes 31-32 : ce nombre de millisecondes est décomposé en secondes et millisecondes ;
  • ligne 34 : la durée est ajoutée à l’heure ;

Exécution

1
2
3
4
5
6
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-01.js"
[début du script], heure=09:26:40:238
[fin du code principal du script], heure=09:26:40:246, durée= 0 seconde(s) et 11 millisecondes
[fin de l'action asynchrone setTimeout], heure=09:26:41:249, durée= 1 seconde(s) et 14 millisecondes

[Done] exited with code=0 in 1.672 seconds
  • ligne 4, on voit que l’action asynchrone [setTimeOut] s’est terminée 1s environ après la fin du code principal du script ;
  • ligne 6 : l’heure affichée ligne 3 est celle de la fin du code principal. Si celui-ci a lancé des tâches asynchrones, le script n’est terminé que lorsque toutes les tâches asynchrones ont été exécutées. La durée affichée ligne 6, est la durée totale d’exécution du script (code principal + tâches asynchrones) ;

La fonction [setTimeout] va nous permettre de simuler des tâches asynchrones dans un environnement [node.js]. En effet, la fonction [setTimeout] se comporte comme une tâche asynchrone :

  • elle rend un résultat immédiatement, ici un n° de timer, par le mécanisme usuel des fonctions (return) ;
  • elle peut rendre ultérieurement (ce n’est pas encore le cas ci-dessus) d’autres résultats via des événements qui sont alors traités par la boucle d’événements de [node.js] ;
  • dans la plupart des cas qui vont suivre, ces événements seront au nombre de deux :
    • un événement qu’on pourrait appeler [success] qui sera émis par la tâche asynchrone qui a réussi ce qu’elle devait faire. Une donnée, le résultat de la tâche, est associée à l’événement émis ;
    • un événement qu’on pourrait appeler [failure] qui sera émis par la tâche asynchrone qui a échoué à faire ce qu’elle devait faire. Une donnée, un objet décrivant l’erreur généralement, est associé à l’événement émis. Des erreurs possibles par exemple avec une tâche internet asynchrone seraient ‘réseau indisponible’, ‘machine serveur inexistante’, ‘timeout dépassé’, …
  • le code principal qui a lancé une tâche asynchrone peut s’abonner aux événements que cette tâche est susceptibe d’émettre. Lorsqu’un de ceux-ci est émis, le code principal en est averti et peut déclencher l’exécution d’une fonction particulière destinée à traiter l’événement. Cette fonction reçoit en paramètre, la donnée que la tâche asynchrone a associée à l’événement émis ;

script [async-02]

Dans ce script, la fonction asynchrone [setTimeout] va émettre des événements pour communiquer des données aux codes qui se seront abonnés à ceux-ci.

L’accès aux événements de [node.js] nécessite des bibliohèques supplémentaires. Nous choisissons la bibliothèque [events] que nous installons avec [npm] :

image1

Le script [async-02] 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
'use strict';

// les fonctions asynchrones peuvent rendre un résultat en émettant un événement
// le code principal peut récupérer ces résultats en s'abonnant aux événements émis

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// un émetteur d'événements
const eventEmitter = new EventEmitter();

// setTimeout arme un timer de 1000 ms (2ième paramètre) et retourne immédiatement le n° de ce timer
// lorsque le timer a épuisé les 1000 ms il émet un événement qui est mis en file d'attente du runtime
// lorsque l'événement est traité par le runtime, la fonction (1er paramètre) est exécutée
setTimeout(function () {
  // ce code sera exécuté lorsque le timer aura atteint la valeur 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // on émet un événement pour dire qu'un résultat est disponible
  eventEmitter.emit("timer1Success", { success: 4 });
  // on émet un autre événement pour dire qu'un autre résultat est disponible
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// on s'abonne à l'évt [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// on s'abonne à l'évt [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// s'affichera avant les msg des evts émis par la fonction associée à [timer1]
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire d'affichage heure et durée
function heure(début) {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // faut-il calculer une durée ?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage heure + durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}

Commentaires

  • ligne 9, on importe la classe [EventEmitter] de la bibliothèque [events]. C’est une nouveauté : nous n’avions jusqu’à maintenant importé que des objets littéraux et des fonctions ;
  • ligne 15 : on crée un émetteur d’événements [node.js] en instanciant la classe [EventEmitter] avec le mot clé [new] ;
  • lignes 20-27 : la fonction asynchrone [setTimeout]. Elle va émettre deux événements lors de son exécution :
    • ligne 24, l’événement [timer1Success] avec comme valeur associée l’objet {success : 4} ;
    • ligne 26, l’événement [timer1Failure] avec comme valeur associée l’objet {failure : 6} ;
    • une fonction asynchrone peut émettre autant d’événements qu’elle veut. On a dit précédemment que le plus souvent elle émettait l’un des deux événements [success, failure], pas les deux comme on le fait ici ;
  • ligne 20 : l’exécution de [setTimeout] est instantanée : un timer est armé et le n° de celui-ci rendu au code appelant. L’émission des événements se fera plus tard, ici 1 seconde plus tard ;
  • l’émission d’événements est inutile s’il n’y a aucun code pour les exploiter lorsqu’ils surviendront. C’est pourquoi le code principal doit s’abonner aux deux événements [timer1Success, timer1Failure] s’il veut les gérer, notamment récupérer les données associées à ces événements ;
  • lignes 30-32 : le code principal s’abonne à l’événement [timer1Success]. Lorsque la bouche d’événements de [node.js] traitera cet événement, il appellera la fonction qui est le second paramètre de la méthode [eventEmitter.on] en lui passant la donnée (ici appelée [result]) associée à l’événement [timer1Success] ;
  • ligne 31 : la fonction de traitement de l’événement affichera le jSON de la donnée associée à l’événement ainsi que l’heure du moment ;
  • lignes 35-37 : avec un code analogue, le code principal s’abonne à l’événement [timer1Failure] ;
  • l’abonnement à un événement (1er paramètre) n’exécute pas immédiatement le code de la fonction [callback] (2ième paramètre). Celle-ci ne sera exécutée qu’après que l’événement ait eu lieu ;
  • ligne 40 : le code principal du script est terminé mais pas le script lui-même puisque le code principal a lancé une tâche asynchrone. Le script global ne sera terminé qu’après la fin de cette tâche asynchrone ;

C’est ce que montrent les résultats obtenus :

1
2
3
4
5
6
7
8
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-02.js"
[début du script], heure=09:34:58:909
[fin du code principal du script], heure=09:34:58:916, durée= 0 seconde(s) et 10 millisecondes
[setTimeout, fin du timer d'1 s], heure=09:34:59:929, durée= 1 seconde(s) et 23 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=09:34:59:931, durée= 1 seconde(s) et 25 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=09:34:59:932, durée= 1 seconde(s) et 26 millisecondes, via l'événement [timer1Failure]

[Done] exited with code=0 in 1.627 seconds
  • ligne 3 : fin du code principal 10 ms après le début du script ;
  • ligne 4 : début de la fonction encapsulée dans le timer de 1000 ms, 1 seconde environ après le début du script ;
  • ligne 5 : traitement de l’événement [‘timer1Success’], 2 ms plus tard ;
  • ligne 6 : traitement de l’événement [‘timer1Failure’], 1 ms plus tard que l’événement [‘timer1Success’] ;
  • ligne 8 : fin du script global avec une durée totale de 1,627 seconde ;

script [async-03]

Le script suivant montre un autre aspect de la boucle événementielle de [node.js] :

  • la boucle exécute les événements les uns après les autres, généralement dans leur ordre d’arrivée. Certains OS accordent des priorités aux événements qui sont alors traités par ordre de priorité et non par ordre d’arrivée ;
  • la boucle n’exécute qu’un événement à la fois. Le suivant n’est traité que lorsque le traitement du précédent est terminé. Dans un système événementiel, il faut donc éviter d’écrire du code qui monopolise longtemps le processeur car alors les événements ne sont pas traités lorsqu’ils se produisent mais plus tard lorsque la boucle événementielle arrive à eux. On a alors une application peu « réactive » ;

Le script [async-03] montre un exemple de ce phénomène :

 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
'use strict';

// les fonctions asynchrones peuvent rendre un résultat en émettant un événement
// le code principal peut récupérer ces résultats en s'abonnant aux événements émis

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// un émetteur d'événements
const eventEmitter = new EventEmitter();

// setTimeout arme un timer de 1000 ms (2ième paramètre) et retourne immédiatement le n° de ce timer
// lorsque le timer a épuisé les 1000 ms il émet un événement qui est mis en file d'attente du runtime
// lorsque l'événement est traité par le runtime, la fonction (1er paramètre) est exécutée
setTimeout(function () {
  // ce code sera exécuté lorsque le timer aura atteint la valeur 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // on émet un événement pour dire qu'un résultat est disponible
  eventEmitter.emit("timer1Success", { success: 4 });
  // on émet un autre événement pour dire qu'un autre résultat est disponible
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)

// on s'abonne à l'évt [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});

// on s'abonne à l'évt [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// un code synchrone un peu intensif qui a empêcher le code principal de s'achever avant la fin de [timer1]
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 10000; j++) {
    i + i ^ 2 + i ^ 3;
  }
}

// s'affichera avant les msg des evts émis par la fonction associée à [timer1]
console.log("[fin du script],", heure(débutScript));

// utilitaire d'affichage heure et durée
function heure(début) {
 ...
}

Commentaires

  • ce code est celui de l’exemple précédent [async-02] auquel on a ajouté les lignes 39-44 ;
  • lignes 20-27 : la fonction [setTimeout] a été programmée pour exécuter une fonction asynchrone interne au bout d’un délai d’une seconde. Au bout de cette seconde, l’exécution de la fonction asynchrone du timer n’a pas lieu immédiatement : un événement est placé dans la boucle d’exécution pour demander celle-ci. Si la boucle d’exécution est occupée à traiter un autre événement, l’exécution de la fonction asynchrone du timer devra attendre ;
  • lignes 20-27 : dès que la la fonction [setTimeout] a armé son timer d’un délai d’une seconde, elle lâche le processeur et rend la main au code appelant. Celui-ci continue avec les 30-37 qui sont des abonnements à des événements et qui ont un temps d’exécution négligeable ;
  • le code principal continue avec les lignes 40-44 qui forment une boucle de 1010 itérations. Ce code sera en cours d’exécution lorsque le timer va émettre son événement de « fin du délai d’1 seconde ». Cet événement est alors mis dans la boucle événementielle mais devra attendre la fin d’exécution du code principal du script pour avoir une chance d’être traité ;
  • ligne 47 : fin du code principal du script. C’est après ce dernier affichage que l’événement de fin de timer va pouvoir être traité et la fonction asynchrone interne à [setTimeout] va pouvoir être exécutée ;

Le script donne les résultats suivants :

1
2
3
4
5
6
7
8
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-03.js"
[début du script], heure=08:55:02:665
[fin du code principal du script], heure=08:55:11:789, durée= 9 seconde(s) et 131 millisecondes
[setTimeout, fin du timer d'1 s], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Failure]

[Done] exited with code=0 in 9.796 seconds

Commentaires

  • ligne 3 : on voit que le code principal du script a mis 9 secondes à s’exécuter. Les événements qui ont pu se produire pendant ce temps ont été mis en attente dans la boucle événementielle ;
  • ligne 4 : on voit que l’événement [fin du timer] a été traité 5 ms après la fin du code principal. Il a été émis environ 1 s après le début du script mais a du attendre 8s supplémentaires pour être finalement traité ;

On retiendra de cet exemple que dans un système événementiel, un code ne doit jamais occuper le processeur très longtemps. Si on a un code synchrone long à exécuter, on doit se « débrouiller » pour le décomposer en tâches asynchrones plus courtes qui signaleront leur fin avec un événement.

script [async-04]

Le script [async-04] montre un autre mécanisme, appelé [Promise], une promesse de résultat. Ce mécanisme évite de gérer explicitement des événements [node.js]. C’est fait implicitement et le développeur peut alors ignorer l’existence de ces événements. Les comprendre lui permettra cependant de mieux appréhender le fonctionnement des [Promise] qui est de prime abord complexe.

Le type [Promise] est une classe Javascript. Son constructeur admet comme paramètre une fonction asynchrone à qui elle passe deux paramètres appelés traditionnellement [resolve] et [reject]. Ils pourraient porter un autre nom ;

1
2
3
4
5
6
7
const promise=new Promise(function(resolve, reject){
     // une tâche asynchrone est lancée
     …
     // si réussite : appeler resolve(result) où [result] est le résultat de la tâche asynchrone ;
     // si échec : appeler reject(error) où [error] est un objet encapsulant l’erreur rencontrée ;
}
// s’abonner aux événements émis par la tâche asynchrone de la [Promise]
  • le constructeur de [Promise] fait deux choses :
    • il crée un événement pour lancer l’exécution de la fonction [function(resolve, reject)] qu’on lui a passée en paramètre mais n’attend pas son résultat et rend immédiatement un objet [Promise] au code appelant. Celui-ci peut avoir quatre états :
      • [pending] : l’action asynchrone qui a rendu la [Promise] n’est pas encore terminée ;
      • [fulfilled] : l’action asynchrone qui a rendu la [Promise] s’est terminée avec succès ;
      • [rejected] : l’action asynchrone qui a rendu la [Promise] s’est terminée sur un échec ;
      • [settled] : l’action asynchrone qui a rendu la [Promise] est terminée ;
Lorsque le constructeur rend son résultat, l’objet [Promise] créé est dans l’état [pending], en attente des résultats de la fonction asynchrone ;
  • la tâche asynchrone des lignes 2-5 est lancée immédiatement. Les tâches asynchrones sont le plus souvent des tâches asynchrones d’entrée / sortie qui se décomposent de la façon suivante :
    1. exécution d’un code synchrone pour lancer l’opération d’E/S avec un autre organe, par exemple un serveur distant ;
    2. attente de la réponse de cet organe ;
    3. traitement de cette réponse ;
C’est la phase 2 d’attente de l’organe extérieur au processeur qui est le plus coûteux. Plutôt que d’attendre :
  • la réception de la donnée demandée à l’organe extérieur va être signalée par un événement ;
  • dans le code synchrone qui va suivre la phase 1 (ligne 7 du code exemple), on va s’abonner à cet événement puis à un moment retourner dans la boucle d’événements de [node.js]. L’événement suivant dans la liste des événements en attente va alors être traité ;
  • pendant la phase 2, il y a parallélisme d’exécution mais sur des périphériques différents :
    • le processeur pour la boucle d’événements ;
    • un organe extérieur (disque, base de données, serveur distant) pour la recherche de la donnée demandée ;
  • à la fin de la phase 2, lorsque l’opération d’E/S a obtenu la donnée qu’elle demandait, un événement va être émis pour indiquer que le résultat de l’E/S est disponible. Cet événement va alors rejoindre les autres dans la liste d’attente des événements ;
  • lorsque son tour viendra, il sera traité. La fonction associée à cet événement (ligne 7 du code exemple) va alors être exécutée ;
Ce mode de fonctionnement permet d’éviter les temps morts : celui où le processeur attend la réponse d’un périphérique plus lent que lui ;
  • lorsque la tâche asynchrone des lignes 2 et 5 a été lancée et a terminé son travail, elle a la possibilité de rendre un résultat au code appelant, grâce aux deux fonctions [resolve, reject] que le constructeur [Promise] lui a passé en paramètres. La convention est la suivante :
    • la tâche asynchrone signale un succès par [resolve(result)]. Cela revient à mettre dans la boucle d’événements de [node.js], un événement qu’on pourrait appeler [resolved] avec [result] comme donnée associée ;
    • la tâche asynchrone signale un échec par [reject(error)]. Cela revient à mettre dans la boucle d’événements de [node.js], un événement qu’on pourrait appeler [rejected] avec [error] comme donnée associée, en général un objet détaillant l’erreur qui s’est produite ;
    • il faut donc que le code appelant s’abonne à ces deux événements pour être prévenu de la disponibilité du résultat de la fonction asynchrone ;

Après l’exécution terminée de la tâche asynchrone encapsulée dans la [Promise], l’état de l’objet [promise] rendu par le constructeur [Promise(…)] change :

  • l’événement [resolved] le fait passer de l’état [pending] à [resolved] ;
  • l’événement [rejected] le fait passer de l’état [pending] à [rejected] ;

L’abonnement aux événements [resolved] et [rejected] de la tâche asynchrone se fait avec des méthodes de la classe [Promise] avec la syntaxe suivante :

1
promise.then(f1).catch(f2).finally(f3) ;

où :

  • f1 est une fonction exécutée lorsque l’état de [promise] passe de [pending] à [resolved], donc lorsque la tâche asynchrone a réussi son travail. Elle reçoit pour paramètre la valeur [result], transmise par l’instruction [resolve(result)] de la tâche asynchrone ;
  • f2 est une fonction exécutée lorsque l’état de [promise] passe de [pending] à [rejected], donc lorsque la tâche asynchrone a échoué à faire son travail. Elle reçoit pour paramètre la valeur [error], transmise par l’instruction [reject(errror)] de la tâche asynchrone ;
  • f3 est une fonction exécutée après exécution des méthodes [then] ou [catch], donc tout le temps exécutée. Elle ne reçoit aucun paramètre ;

Cette syntaxe cache complètement les événements auxquels on s’abonne. C’est pourtant un abonnement et comme celui de l’exemple précédent, il n’exécute pas immédiatement les fonctions [f1, f2, f3]. Celles-ci seront exécutées ou pas lorsque l’un des événements [resolved, rejected] auxquels on s’abonne va se produire.

Le script [async-04] montre cette mécanique :

 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
'use strict';

// il est possible d'obtenir les résultats (succes, failure) d'une fonction asynchrone
// sans utiliser explicitement des événements grâce à la classe [Promise]
// cette classe utilise implicitement des événements mais ceux-ci ne se voient pas dans le code

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));

// définition d'une tâche asynchrone à l'aide d'une promesse [Promise]
// la tâche asynchrone est le paramètre du constructeur [Promise]
const débutPromise1 = moment(Date.now());
const promise1 = new Promise(function (resolve) {
  // log
  console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
  // code asynchrone
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
    // la tâche asynchrone rend un résultat avec la fonction [resolve]
    // la promesse est alors réussie
    resolve('[réussite]');
  }, 1000)
});

// on peut connaître le résultat de la promesse [promise1]
// lorsque celle-ci a été résolue (resolve) ou rejetée (reject)
// l'instruction qui suit est un abonnement à l'évt [resolved] via la méthode [then]
// et à l'évt [rejected] via la méthode [catch]
// la méthode [finally] est exécutée que ce soit après un then ou un catch
promise1.then(result => {
  // cas de réussite de la promesse  [evt resolved]
  console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // cas d'erreur  [evt rejected]
  console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // exécuté dans tous les cas
  console.log("[promise1.finally]", heure(débutPromise1));
});

// définition d'une tâche asynchrone à l'aide d'une promesse [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise(function (resolve, reject) {
  // log
  console.log("[début fonction asynchrone de promise2],", heure(débutPromise1));
  // tâche asynchrone
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
    // la tâche asynchrone rend un résultat avec la fonction [reject]
    // la promesse est alors ratée
    reject('[échec]');
  }, 2000)
});

// on peut connaître le résultat de la promesse [promise2]
// lorsque celle-ci a été résolue (resolve) ou rejetée (reject)
promise2.then(result => {
  // cas de réussite de la promesse [evt resolved]
  console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // cas d'erreur [evt rejected]
  console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // exécuté dans tous les cas
  console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});

// s'affichera avant les msg des fonctions asynchrones et ceux des évts associés
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire
function heure(début) {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}

Commentaires

  • lignes 18-28 : création d’une [Promise promise1]. Sa fonction asynchrone rend son résultat via un événement au bout d’une seconde. Une fois cet opération asynchrone lancée (armement d’un timer), on n’attend pas que celle-ci rend son résultat et on passe tout de suite au code de la ligne 35 ;
  • lignes 35-44 : on s’abonne aux deux événements [resolved, rejected] que la fonction asynchrone interne à [promise1] peut émettre ;
  • lignes 46-71 : on répète la même séquence de code que précédemment pour une seconde promesse [promise2] ;
  • ligne 74 : le code principal du script est terminé mais pas le script dans son ensemble car deux actions asynchrones ont été lancées. On retourne dans la boucle événementielle où à un moment l’un des événements [resolved, rejected] des promesses [promise1, promise2] va se produire. Il sera alors traité ;
  • puis il y aura retour à la boucle événementielle. Et là le second événement [resolved, rejected] des promesses [promise1, promise2] sera traité lorsqu’il se produira ;

Exécution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-04.js"
[début du script], heure=09:39:05:950, durée= 0 seconde(s) et 3 millisecondes
[début fonction asynchrone de promise1], heure=09:39:05:958, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=09:39:05:959, durée= 0 seconde(s) et 1 millisecondes
[fin du code principal du script], heure=09:39:05:960, durée= 0 seconde(s) et 13 millisecondes
[fin fonction asynchrone de promise1], heure=09:39:06:977, durée= 1 seconde(s) et 19 millisecondes
[promise1.then], heure=09:39:06:980, durée= 1 seconde(s) et 21 millisecondes, result=[réussite]
[promise1.finally] heure=09:39:06:982, durée= 1 seconde(s) et 24 millisecondes
[fin fonction asynchrone de promise2], heure=09:39:07:976, durée= 2 seconde(s) et 17 millisecondes
[promise2.catch], heure=09:39:07:978, durée= 2 seconde(s) et 19 millisecondes, result=[échec]
[promise2.finally], heure=09:39:07:980, durée= 2 seconde(s) et 21 millisecondes

[Done] exited with code=0 in 2.589 seconds

Commentaires

  • ligne 3 : la fonction asynchrone de [promise1] est lancée mais on n’attend pas sa fin qui sera signalée par un événement ;
  • ligne 4 : la fonction asynchrone de [promise2] est lancée mais on n’attend pas sa fin qui sera signalée par un événement ;
  • ligne 5 : fin du code principal et retour à la boucle événementielle ;
  • ligne 6 : traitement de l’événement [fin fonction asynchrone de promise1]. L’état de [promise1] va passer à [resolved]. Un événement le signale ;
  • ligne 7 : [promise2] n’ayant toujours pas fini son travail, l’événement [promise1 resolved] qui vient d’être mis dans la boucle va être traité par la méthode [promise1.then] puis par la méthode [promise.finally] (ligne 8) ;
  • lignes 9-11 : le même mécanisme se déroule lorsque [promise2] passe de l’état [pending] à [resolved] ;

script [async-05]

Revenons au code du constructeur d’un objet [Promise] :

1
2
3
4
5
6
7
const promise=new Promise(function(resolve, reject){
     // une tâche asynchrone est lancée
     …
     // si réussite : appeler resolve(result) où [result] est le résultat de la tâche asynchrone ;
     // si échec : appeler reject(error) où [error] est un objet encapsulant l’erreur rencontrée ;
}
// on s’abonne aux événements émis par la tâche asynchrone

Ligne 2, la tâche asynchrone de la [Promise] est lancée. Elle a souvent besoin de davantage de paramètres que les seuls paramètres [resolve, reject] que la fonction qui l’encapsule lui passe. Dans ce cas, on encapsule la création de la [Promise] dans une fonction qui va lui passer les paramètres dont sa fonction asynchrone a besoin :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// définition de la fonction asynchrone
function uneFonctionAsynchrone (p1, p2, …, pn){
 return new Promise(function(resolve, reject){
     // une tâche asynchrone est lancée avec les paramètres (P1, p2, …, pn)
     …
     // si réussite : appeler resolve(result) où [result] est le résultat de la tâche asynchrone ;
     // si échec : appeler reject(error) où [error] est un objet encapsulant l’erreur rencontrée ;
}
// on s’abonne aux évts [resolved, rejected] que va émettre la fonction asynchrone [uneFonctionAsynchrone]
…
// qq temps plus tard, la fonction asynchrone [uneFonctionAsynchrone] est appelée
uneFonctionAsynchrone(e1, e2, …, en) ;

Le script suivant :

  • définit deux fonctions asynchrones rendant une [Promise] ;
  • lance leur exécution en parallèle et attend que les deux soient terminées pour faire un certain travail ;
 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
'use strict';

// on peut définir des fonctions asynchrones qui rendent un type [Promise]
// elles peuvent être alors taguées avec le mot clé [async]

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du script],", heure());

// une fonction asynchrone qui rend une promesse [Promise]
function async01(p1) {
  return new Promise((resolve) => {
    console.log("[début de la tâche asynchrone async01]");
    // la tâche asynchrone
    const débutAsync01 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
      // la tâche asynchrone peut rendre un résultat complexe
      resolve({
        prop1: [10, 20, 30],
        prop2: "abcd",
        prop3: p1,
      });
    }, 1000)
  });
}

// une fonction asynchrone qui rend une promesse [Promise]
function async02(p1, p2) {
  return new Promise(resolve => {
    console.log("[début de la tâche asynchrone async02]");
    // tâche asynchrone
    const débutAsync02 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
      // la tâche asynchrone peut rendre un résultat complexe
      resolve({
        prop1: [11, 21, 31],
        prop2: "xyzt",
        prop3: p1 + p2
      });
    }, 2000)
  })
}

// on lance les deux fonctions asynchrones en parallèle
// et on attend qu'elles aient terminé toutes les deux
// le then ne s'exécute que si les deux fonctions ont émis l'évt [resolved]
// le catch s'exécute dès que l'une des deux fonctions émet l'évt [rejected]
Promise.all([async01(10), async02(10, 20)])
  // le résultat est un tableau [result1, result2] où [result1] est le résultat émis par un [resolve] de [async01]
  // et [result2] le résultat émis par un [resolve] de [async02]
  .then(result => {
    console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
  })
  // error est le résultat émis par le premier [reject] de l'une des deux fonctions asynchrones
  .catch(error => {
    console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
  })
  // finally est exécuté après le then ou le catch
  .finally(() => {
    console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
  });

// s'affichera avant les msgs des fonctions asynchrones et des évts associés
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire
function heure(début) {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}

Commentaires

  • lignes 15-30 : on définit une fonction [async01] qui rend son résultat au bout d’1 seconde via un événement de timer. La fonction [async01] qui est utilisée dans son résultat ligne 26 ;
  • lignes 33-47 : on fait de même avec une fonction [async02] qui rend son résultat au bout de 2 secondes via un événement de timer. La fonction [async02] admet deux paramètres qui sont utilisés dans son résultat ligne 44 ;
  • lorsqu’elles seront appelées les deux fonctions [async01, async02] :
    • seront lancées ;
    • rendront au code appelant deux promesses [promise1, promise2] ;
    • l’exécution reviendra alors au code appelant qui continuera sa course ;
    • au bout d’1 seconde environ [async01] émettra un événement pour dire qu’elle a terminé son travail. L’événement en question sera mis en attente dans la boucle événementielle associé au résultat transmis par [async01] avec l’événement ;
    • au bout de 2 secondes environ, le même processus se passera pour [async02] ;
  • ligne 54 : ce n’est que maintenant que les fonctions asynchrones [async01, async02] sont exécutées (notations async01(10) et async02(10,20)). Elles le sont au sein d’un tableau passé en paramètre à la méthode [Promise.all]. On sait que [async01, async02] rendent toutes deux une promesse au code appelant. Aussi le paramètre de [Promise.all] est un tableau de deux promesses ;
  • [Promise.all([promise1, promise2, …, promisen]).then(f1).catch(f2).finally(f3)] est un abonnement à des événements :
    • [Promise.all] est de type [Promise] ;
    • la fonction [f1] de la méthode [then] sera exécutée lorsque toutes les promesses [promise1, promise2, …, promisen] du tableau paramètre de la méthode [all] seront passées de l’état [pending] à l’état [resolved]. Dit autrement, [f1] sera exécutée lorsque toutes les promesses du tableau se seront terminées avec succès ;
    • la fonction [f2] de la méthode [catch] sera exécutée dès que l’une des promesses du tableau passera de l’état [pending] à l’état [rejected]. Dit autrement, [f2] est exécutée dès que l’une des promesses du tableau échoue ;
    • la fonction [f3] de la méthode [finally] sera exécutée après l’exécution d’une des méthodes [then, catch], donc toujours exécutée ;

L’exécution du code donne les résultats suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-05.js"
[début du script], heure=12:17:17:367
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=12:17:17:375, durée= 0 seconde(s) et 10 millisecondes
[fin de la tâche asynchrone async01], heure=12:17:18:391, durée= 1 seconde(s) et 17 millisecondes
[fin de la tâche asynchrone async02], heure=12:17:19:389, durée= 2 seconde(s) et 14 millisecondes
[promise-all success], heure=12:17:19:390, durée= 2 seconde(s) et 25 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=12:17:19:392, durée= 2 seconde(s) et 27 millisecondes

[Done] exited with code=0 in 2.572 seconds
  • lignes 6-7 : les deux tâches asynchrones [async01, async02] sont lancées. Elles fonctionnent en parallèle. Ce n’est pas l’exécution de leur code qui est faite en parallèle mais leurs attentes respectives de la donnée demandée se passent en même temps ;
  • ligne 5 : le code principal du script est terminé. Restent à attendre la fin des deux tâches asynchrones [async01, async02] ;
  • ligne 6 : la tâche asynchrone [async01] se termine environ 1 s après son lancement. Elle rend un résultat avec la fonction [resolve] donc sa promesse dans le tableau de la ligne 56 du code, passe de l’état [pending] à [resolved]. Ce n’est pas suffisant pour déclencher la méthode [then], lignes 59-60 du code ;
  • ligne 7 : la tâche asynchrone [async02] se termine environ 2 s après son lancement. Elle rend un résultat avec la fonction [resolve] donc sa promesse dans le tableau de la ligne 56 du code, passe de l’état [pending] à [resolved]. La méthode [then] va être exécutée dès que la boucle événementielle le permettra ;
  • ligne 8 : la méthode [then] de [Promise.all] est exécutée. Elle reçoit en paramètre un tableau [result1, result2][result1] est le résultat émis par [async01], et [result2] celui émis par [async02] ;
  • ligne 9 : la méthode [finally] de [Promise.all] est exécutée ;

script [async-06]

Ce nouveau script montre comment l’utilisation conjointe des mots clés [async / await] permet d’avoir un code asynchrone ressemblant à un code synchrone. La gestion des événements est complètement cachée et la compréhension du code facilitée.

Nous reprenons l’exemple précédent en y amenant les modification suivantes :

  • on ajoute une troisième fonction asynchrone [async03] qui elle renvoie son résultat avec la méthode [Promise.reject] qui signale donc à la boucle événementielle qu’elle a « échoué » à faire son travail ;
  • on exécute séquentiellement les trois fonctions asynchrones [async01, async02, async03]. Dans l’exemple précédent, on avait exécuté en parallèle les fonctions asynchrones [async01, async02] ;
  • avant l’apparition des mots clés [async/await], l’exécution séquentielle d’actions asynchrones se faisait à l’aide de [Promise] emboîtées les unes dans les autres. Dès qu’il y avait plusieurs actions asynchrones à exécuter ainsi, le nombre de promesses augmentait en conséquence et le code devenait moins lisible ;
  • avec les mots clés [async/await], l’exécution séquentielle de tâches asynchrones se fait avec une syntaxe à celle de l’exécution de tâches synchrones :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// fonction asynchrone - utilisation async / await
async function main() {
  // exécution séquentielle des tâches asynchrones
  try {
    // exécution avec attente de [async01]
    const result1 = await async01(...);
    console.log("[async01 result]=", result1);
    // exécution avec attente de [async02]
    const result2 = await async02(...);
    console.log("[async02 result]=", result2);
    // exécution avec attente de [async03]
    const result3 = await async03(...);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // une des actions asynchrones a échoué
    console.log(sprintf("[sequential error]= %j, %s", error));
  } finally {
    // terminé
    console.log("[fin exécution séquentielle des tâches asynchrones],");
  }
  • ligne 6 : la fonction asynchrone [async01] est lancée (mot clé await) et on attend qu’elle ait publié son résultat par l’une des méthodes [Promise.resolve, Promise.reject]. C’est donc une opération **bloquante **;
  • ligne 6 : le mot clé [await] transforme l’opération asynchrone [async01] en opération bloquante. On sait que l’opération [async01] rend un résultat de deux façons :
    • elle rend au code appelant, quasi immédiatement, un objet [Promise] ;
    • elle publie ultérieurement un résultat sur la boucle événementielle via les méthodes [Promise.resolve, Promise.reject]. C’est ce dernier résultat que récupère [result1], ligne 6. La gestion événementielle de l’action [async01] est devenue invisible ;
    • si le résultat [result] de [async01] est publié par [Promise.resolve(result)], il est affecté à [result1] ligne 6 et l’exécution continue ligne 7 ;
    • si le résultat de [async01] est publié par [Promise.reject], cela provoque une exeption et l’exécution du code passe à la ligne 14, celle du catch. Le paramètre de la clause [catch] est l’objet d’erreur (error) publié par [async01] avec une expression [Promise.reject(error)]. La tâche asynchrone peut également publier l’erreur par un [throw(error)]. L’objet [error] est celui récupéré dans [catch(error)] ;
    • le mot clé [await] doit être obligatoirement dans une fonction précédée du mot clé [async], ligne 2. Ce mot clé indique que la fonction [main] est une fonction asynchrone ;
    • dans l’expression [await f(…)], [f] doit être une fonction asynchrone rendant un objet [Promise] au code appelant ;
  • on refait la même chose pour l’action asynchrone [async02], ligne 9 et [async03], ligne 12 ;

Toujours avec les mots clés [async / await], il est possible de faire l’exécution parallèle de tâches asynchrones avec la syntaxe suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try {
    // exécution parallèle des tâches asynchrones
    const result = await Promise.all([async01(...), async02(...), async03(...)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // une des actions asynchrones a échoué
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // terminé
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
  • ligne 3 : on a une opération bloquante : on attend que les trois tâches asynchrones du tableau [async01(..), async02(..), async03(..)] aient publié leurs résultats sur la boucle événementielle avec l’une des méthodes [Promise.resolve, Promise.reject] ;
  • si les trois tâches asynchrones publient leurs résultats avec [Promise.resolve], la constante [result] est alors le tableau [result1, result2, result3] où :
    • [result1] est le résultat publié par [async01] avec l’expression [Promise.resolve(result1)] ;
    • [result2] est le résultat publié par [async02] avec l’expression [Promise.resolve(result2)] ;
    • [result3] est le résultat publié par [async03] avec l’expression [Promise.resolve(result3)] ;
  • si l’une des trois tâches publie son résultat avec une expression [Promise.reject(error)] alors une exception se produit ;
    • la constante [result] de la ligne 3 ne reçoit pas sa valeur ;
    • l’exécution passe directement au [catch] de la ligne 5 ;
    • le paramètre (error) du catch, est l’objet (error) publié par l’expression [Promise.reject(error)] ;

En mixant ces deux syntaxes, on peut exécuter indifféremment des tâches asynchrones en séquentiel ou en parallèle, tout cela avec une syntaxe analogue à celle d’un code synchrone. Il faut donc privilégier cette syntaxe beaucoup plus lisible que les précédentes. Cette syntaxe [async / await] n’est disponible que depuis la version 6 d’ECMAScript. Il y a encore beaucoup de codes Javascript utilisant des promesses [Promise]. C’est pourquoi il est important de comprendre également le fonctionnement de celles-ci.

Le code complet du script [async-06] 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
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
'use strict';

// exécution parallèle ou séquentielle de plusieurs tâches asynchrones
// avec les mots clés async / await

// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';

// début
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());

// une fonction asynchrone rendant une [Promise]
function async01(débutAsync01) {
  return new Promise(function (resolve) {
    console.log("[début fonction asynchrone async01],", heure());
    // fonction asynchrone
    setTimeout(function () {
      console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
      // l'action asynchrone peut rendre un résultat complexe
      // ici réussite
      resolve({
        prop1: [11, 21, 31],
        prop2: "abcd"
      });
    }, 1000)
  });
}

// une fonction asynchrone rendant une [Promise]
function async02(débutAsync02) {
  console.log("[début fonction asynchrone async02],", heure());
  return new Promise(function (resolve) {
    // fonction asynchrone
    setTimeout(function () {
      console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
      // l'action asynchrone peut rendre un résultat complexe
      // ici réussite
      resolve({
        prop1: [12, 22, 32],
        prop2: "xyzt"
      });
    }, 2000)
  })
}

// une fonction asynchrone rendant une [Promise]
function async03(débutAsync03) {
  console.log("[début fonction asynchrone async03],", heure());
  return new Promise((resolve, reject) => {
    // fonction asynchrone
    setTimeout(function () {
      console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
      // l'action asynchrone peut rendre un résultat complexe
      // ici échec
      reject({
        prop1: [13, 23, 33],
        prop2: "échec"
      });
    }, 3000)
  })
}

// fonction asynchrone - utilisation async / await
async function main() {
  const débutSequential = moment(Date.now());
  // exécution séquentielle des tâches asynchrones
  console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
  try {
    // exécution avec attente de [async01]
    const débutAsync01 = moment(Date.now());
    const result1 = await async01(débutAsync01);
    console.log("[async01 result]=", result1);
    // exécution avec attente de [async02]
    const débutAsync02 = moment(Date.now());
    console.log("début async02-------------", heure());
    const result2 = await async02(débutAsync02);
    console.log("[async02 result]=", result2);
    // exécution avec attente de [async03]
    const débutAsync03 = moment(Date.now());
    console.log("début async03-------------", heure());
    const result3 = await async03(débutAsync03);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // une des actions asynchrones a échoué
    console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
  } finally {
    // terminé
    console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
  }

  const débutParallel = moment(Date.now());
  // exécution en parallèle des tâches asynchrones
  console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------")
  try {
    const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // une des actions asynchrones a échoué
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // terminé
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
  }

  // terminé
  console.log("[fin de la fonction main],", heure(débutSequential));
}
// exécution fonction asynchrone main
main();

// s'affichera avant les différents msgs des fonctions asynchrones et de leurs évts
console.log("[fin du code principal du script],", heure(débutScript));

// utilitaire
function heure(début) {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}

Les résultats de l’exécution sont 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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-06.js"
[début du code principal du script], heure=15:02:00:152
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:00:161
[fin du code principal du script], heure=15:02:00:164, durée= 0 seconde(s) et 15 millisecondes
[fin fonction asynchrone async01], heure=15:02:01:165, durée= 1 seconde(s) et 4 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=15:02:01:253
[début fonction asynchrone async02], heure=15:02:01:254
[fin fonction asynchrone async02], heure=15:02:03:265, durée= 2 seconde(s) et 12 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=15:02:03:268
[début fonction asynchrone async03], heure=15:02:03:268
[fin fonction asynchrone async03], heure=15:02:06:285, durée= 3 seconde(s) et 18 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=15:02:06:289, durée= 6 seconde(s) et 129 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=15:02:06:291, durée= 6 seconde(s) et 131 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:06:292
[début fonction asynchrone async02], heure=15:02:06:293
[début fonction asynchrone async03], heure=15:02:06:294
[fin fonction asynchrone async01], heure=15:02:07:294, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=15:02:08:298, durée= 2 seconde(s) et 6 millisecondes
[fin fonction asynchrone async03], heure=15:02:09:297, durée= 3 seconde(s) et 5 millisecondes
[parallel error], heure=15:02:09:298, durée= 3 seconde(s) et 6 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=15:02:09:299, durée= 3 seconde(s) et 7 millisecondes
[fin de la fonction main], heure=15:02:09:300, durée= 9 seconde(s) et 140 millisecondes

[Done] exited with code=0 in 9.668 seconds

Les fonctions HTTP de Javascript

image0

Choix d’une bibliothèque HTTP

Nous avons fait ici le choix de deux bibliothèques :

EcmaScript 6 a nativement une fonction HTTP appelée [fetch] qui n’est pas implémentée par [node.js] (sept 2019). Il existe une bibliothèque appelée [node-fetch] qui permet d’utiliser la fonction [fetch] sous Node. Cette bibliothèque utilise certaines API propres à [node.js]. Un code [node-fetch] peut être alors non transportable à 100 % dans un environnement non [node], dans un navigateur par exemple ;

Il existe par ailleurs une bibliothèque nommée [axios] dédiée aux requêtes HTTP compatible aussi bien avec [node.js] qu’avec les navigateurs. C’est cette bibliothèque que nous utiliserons au final.

Nous allons présenter un même script écrit avec ces deux bibliothèques pour montrer que la démarche de codage avec elles est analogue.

Mise en place d’un environnement de travail

Installation du serveur de calcul d’impôt

Ultimement, nous allons écrire une application web avec l’architecture suivante :

image1

JS : Javascript

Le code Javascript est client :

  • d’un service de pages ou fragments statiques ;
  • d’un service jSON ;

Le code Javascript est donc un client jSON et à ce titre peut être organisé en couches [UI, métier, dao] (UI : User Interface) comme l’ont été nos clients jSON écrits en PHP.

Le serveur sera celui du calcul de l’impôt dont nous avons déjà écrit 13 versions. Nous allons en écrire une 14ième. Nous commençons donc par dupliquer, sous Netbeans, le dossier de la version 13, dans le dossier de la version 14 :

image2

  • en [6], nous modifions le fichier [config.json] de la version 14 de la façon suivante :
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "databaseFilename": "Config/database.json",
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
    "relativeDependencies": [

        "/Entities/BaseEntity.php",
        "/Entities/Simulation.php",
        ...
    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}
  • ligne 3, nous changeons le dossier racine de l’application ;

Pour accéder à ce serveur, il faut lancer les services [Laragon].

Ceci fait, nous pouvons tester avec [Postman] (cf. article lien), cette nouvelle version du serveur, identique pour l’instant à la version 13. Nous pouvons utiliser la collection de requêtes utilisées pour tester la version 12 du serveur de calcul d’impôt :

image3

  • en [1-4], utiliser la requête [init-session-700] pour initialiser une session jSON ;
  • en [4-5], mettre [version-14] au lieu de [version-12] pour tester la version 14 du projet ;
  • à l’exécution on doit recevoir la réponse jS0N [6] du serveur ;

La version 14 du serveur est désormais opérationnelle. Nous serons amenés à la modifier légèrement. Rappelons l’API de ce serveur :

Action Rôle Contexte d’exécution
init-session Sert à fixer le type (json, xml, html) des réponses souhaitées

Requête GET main.php?action=i nit-session&type=x

peut être émise à tout moment

auth entifier-utilisateur Autorise ou non un utilisateur à se connecter

Requête POST ma in.php?action=authen tifier-utilisateur

La requête doit avoir deux paramètres postés [user, password]

Ne peut être émise que si le type de la session (json, xml, html) est connu

calculer-impot Fait une simulation de calcul d’impôt

Requête POST main.php?act ion=calculer-impot

La requête doit avoir trois paramètres postés [marié, enfants, salaire]

Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié

lister-simulations Demande à voir la liste des simulations opérées depuis le début de la session

Requête GET main.php?action= lister-simulations

La requête n’accepte aucun autre paramètre

Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié

supprimer-simulation Supprime une simulation de la liste des simulations

Requête GET main. php?action=lister-si mulations&numéro=x

La requête n’accepte aucun autre paramètre

Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié

fin-session Termine la session de simulations.

Techniquement l’ancienne session web est supprimée et une nouvelle session est créée

Ne peut être émise que si le type de la session (json, xml, html) est connu et l’utilisateur authentifié

Installation des bibliothèques HTTP du client Javascript

Dans un premier temps, nous travaillerons avec l’architecture suivante :

image4

  • en [1], un script console [node.js] fait une requête HTTP vers le serveur jSON du calcul de l’impôt ;
  • en [4], il reçoit cette réponse et l’affiche sur la console ;

Dans l’exemple n° 1, nous utiliserons les bibliothèques [node-fetch] et [axios] puis nous ne conserverons qu’[axios] pour les exemples suivants. Nous installons maintenant ces deux bibliothèques Javascript à partir du terminal de [VSCode] :

image5

Nous utiliserons également la bibliothèque [qs] qui permet l’encodage URL d’une chaîne de caractères. On se rappelle que cet encodage est utilisé pour encoder les paramètres d’une requête HTTP GET ou POST.

image6

script [fetch-01]

Le script [fetch-01] utilise la bibliothèque [node-fetch] pour initialiser une session jSON avec le serveur de calcul d’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
86
87
88
89
90
'use strict';

// imports
import fetch from 'node-fetch';
import qs from 'qs';
import { sprintf } from 'sprintf-js';
import moment from 'moment';


// URL de base du serveur de calcul d'impôt
const baseUrl = 'http://localhost/php7/scripts-web/impots/version-14/main.php?';
// init session
async function initSession() {
  // options de la requête HHTP [get /main.php?action=init-session&type=json]
  const options = {
    method: "GET",
    timeout: 2000
  };
  // exécution de la requête HTTP [get /main.php?action=init-session&type=json]
  let débutFetch;
  try {
    // requête asynchrone - [fetch] rend une promesse
    débutFetch = moment(Date.now());
    const response = await fetch(baseUrl + qs.stringify({
      action: 'init-session',
      type: 'json'
    }), options);
    // [response] est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // on affiche cette réponse pour voir sa structure
    console.log(sprintf("réponse fetch formatée en json,=%j, %s", response, heure(débutFetch)));
    console.log("réponse fetch en javascript=", response);
    // on peut avoir aux entêtes HTTP
    console.log("entêtes de la réponse=", response.headers);
    // si réponse de type application / json, la réponse json du serveur est obtenue avec la fonction asynchrone [response.json()]
    // dans ce cas le code appelant obtient un objet [Promise]
    // [await] permet d'obtenir la réponse [json] du serveur plutôt que sa promesse
    const débutJson = moment(Date.now());
    const objet = await response.json();
    console.log(sprintf("réponse json=%j, type=%s, %s", objet, typeof (objet), heure(débutJson)));
    return objet;
    // si réponse de type text / plain, la réponse texte du serveur est obtenue avec [response.text()]
    // dans ce cas le code appelant obtient un objet [Promise]
    // [await] permet d'obtenir la réponse [texte] du serveur plutôt que sa promesse
    // const text = await response.text();
    // console.log("réponse texte=", text);
    // return text;
  } catch (error) {
    // on est là parce que le serveur a envoyé un code d'erreur [404 Not Found, ...] accompagné d'un corps vide - on affiche l'erreur pour voir sa structure
    // ou bien parce que le client [fetch] a lancé une exception (réseau inaccesible, ...)
    // on affiche la structure de l'erreur
    console.log(sprintf("error fetch en json=%j, %s", error, heure(débutFetch)));
    console.log("error fetch en javascript=", typeof (error), error);
    // on lance le msg d'erreur reçu
    throw error.message;
  }
}

// la fonction main exécute la fonction asynchrone [initSession]
async function main() {
  try {
    console.log("requête HTTP vers le serveur en cours ---------------------------------------------");
    const response = await initSession();
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response, typeof (response))
  } catch (error) {
    console.log("erreur ---------------------------------------------");
    console.log("erreur=", error, typeof (error));
  }
}

// test
main();

// utilitaire d'affichage heure et durée
function heure(début) {
  // heure du moment courant
  const now = moment(Date.now());
  // formatage heure
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // faut-il calculer une durée ?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatage heure + durée
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // résultat
  return result;
}

Commentaires

  • les fonctions HTTP du Javascript sont des fonctions asynchrones. Nous utilisons ici ce que nous avons appris dans la section précédente (cf. lien) ;
  • ligne 24 : pour attendre que la réponse de la fonction asynchrone [fetch] soit publiée sur la boucle événementielle de [node.js], nous utilisons le mot clé [await]. Nous savons qu’alors que cette instruction doit être dans un code préfixé par le mot clé [async] (ligne 13) ;
  • lignes 13-56 : nous encapsulons le code HTTP dans la fonction asynchrone [initSession] ;
  • lignes 59-69 : une seconde fonction asynchrone [main] est utilisée pour appeler de façon bloquante (async / await) la fonction asynchrone [initSession] ;
  • ligne 72 : la fonction asynchrone [main] est appelée ;
  • bien que l’ensemble du code ressemble à du code synchrone, ce sont bien des fonctions asynchrones qui sont exécutées, mais de façon bloquante ;
  • ligne 19 : pour initialiser une session jSON avec le serveur de calcul d’impôt, il faut lui envoyer la commande HTTP [get /main.php?action=init-session&type=json]. C’est ce que fait le code des lignes 24-27. La syntaxe de [fetch] est la suivante [fetch(URL, options)] avec :
    • [URL] : l’URL interrogée ;
    • [options] : un objet définissant les options de la requête. C’est là notamment qu’on définit les entêtes HTTP qu’on veut envoyer à la machine cible ;
  • lignes 15-18 : on définit les options de la requête qu’on veut faire :
    • [method] : on veut faire un GET ;
    • [timeout] : on veut que le client [fetch] n’attende pas plus de 2 secondes la réponse du serveur Si ce délai est dépassé, [fetch] lancera une exception ;
  • ligne 24 : pour obtenir l’URL [/main.php?action=init-session&type=json], on utilise la bibliohèque [qs] pour obtenir l’encodage URL des paramètres [action,type] du GET. La chaîne obtenue est [init-session&type=json] qu’on aurait pu construire nous-mêmes. On voulait simplement montrer comment obtenir une chaîne URL encodée ;
  • ligne 24 : le mot clé [await] montre que c’est une tâche asynchrone qui est lancée ici et qu’on attend qu’elle publie sa réponse sur la boucle événementielle de [node.js] ;
  • ligne 24 : dans [response], on obtient un objet complexe qui décrit la totalité de la réponse HTTP reçue (entêtes et document) ;
  • lignes 30-31 : on affiche l’objet [response] pour voir sa structure, d’abord comme chaîne de caractères puis comme objet Javascript ;
  • ligne 33 : on affiche les entêtes HTTP envoyés par le serveur ;
  • ligne 38 : on sait que le serveur de calcul d’impôt va envoyer une chaîne jSON. Celle-ci est encapsulée dans l’objet [response]. On peut l’obtenir avec la méthode [response.json()]. Cependant cette méthode est asynchrone. On écrit donc [await response.json()] pour obtenir la chaîne jSON qui va être publiée sur la boucle événementielle de [node.js]. En fait ce n’est pas la chaîne jSON qu’on obtient mais l’objet Javascript représenté par celle-ci ;
  • ligne 39 : affichage de la chaîne jSON reçue ;
  • ligne 40 : on rend l’objet Javascript reçu ;
  • ligne 47 : on intercepte une erreur éventuelle de l’instruction [fetch]. Celle-ci ne lance une exception que si l’opération HTTP n’a pu aboutir et qu’aucune réponse du serveur n’a été reçue. Si une réponse a été reçue, même avec un code HTTP différent de [200 OK], [fetch] ne lance pas d’exception et la réponse du serveur sera disponible ligne 38 ;
  • lignes 51-52 : on affiche l’objet [error] reçu par la clause [catch], d’abord comme une chaîne jSON puis comme un objet Javascript ;
  • ligne 54 : le message d’erreur de [fetch] se trouve dans [error.message] ;
  • lignes 59-69 : la fonction asynchrone [main] lance la fonction asynchrone [initSession] de façon bloquante (await ligne 62) ;
  • ligne 72 : la fonction asynchrone [main] est lancée et le code principal du script est alors terminé. Le script global lui sera terminé lorsque les tâches asynchrones lancées auront publié leurs résultats sur la boucle événementielle ;

Les résultats de l’exécution sont les suivants :

Cas 1 : le serveur Laragon n’est pas lancé

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\fetch-01.js"
requête HTTP vers le serveur en cours ---------------------------------------------
error fetch en json={"message":"network timeout at: http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json","type":"request-timeout"}, heure=10:08:48:180, durée= 2 seconde(s) et 62 millisecondes
error fetch en javascript= object { FetchError: network timeout at: http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json
    at Timeout.<anonymous> (c:\Data\st-2019\dev\es6\javascript\node_modules\node-fetch\lib\index.js:1448:13)
    at ontimeout (timers.js:436:11)
    at tryOnTimeout (timers.js:300:5)
    at listOnTimeout (timers.js:263:5)
    at Timer.processTimers (timers.js:223:10)
  message:
   'network timeout at: http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json',
  type: 'request-timeout' }
erreur ---------------------------------------------
erreur= network timeout at: http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json string

[Done] exited with code=0 in 2.804 seconds

Commentaires

  • ligne 3 : la requête HTTP échoue au bout de 2 secondes et 62 millisecondes à cause du timeout de 2 secondes qu’on avait imposé à la requête HTTP ;
  • lignes 4-9 : l’objet Javascript [error] intercepté par la clause [catch(error)]. Cet objet a deux propriétés :
    • [FetchError] : ligne 4 ;
    • [message] : lignes 10-12 ;
  • ligne 14 : le message d’erreur reçu par la fonction asynchrone [main] ;

Cas 2 : le serveur Laragon est lancé

 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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\fetch-01.js"
requête HTTP vers le serveur en cours ---------------------------------------------
réponse fetch formatée en json,={"size":0,"timeout":2000}, heure=10:13:50:814, durée= 0 seconde(s) et 375 millisecondes
réponse fetch en javascript= Response {
  size: 0,
  timeout: 2000,
  [Symbol(Body internals)]:
   { body:
      PassThrough {
        _readableState: [ReadableState],
        readable: true,
        domain: null,
        _events: [Object],
        _eventsCount: 2,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: false,
        allowHalfOpen: true,
        _transformState: [Object] },
     disturbed: false,
     error: null },
  [Symbol(Response internals)]:
   { url:
      'http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json',
     status: 200,
     statusText: 'OK',
     headers: Headers { [Symbol(map)]: [Object] },
     counter: 0 } }
entêtes de la réponse= Headers {
  [Symbol(map)]:
   [Object: null prototype] {
     date: [ 'Sat, 14 Sep 2019 08:13:50 GMT' ],
     server: [ 'Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11' ],
     'x-powered-by': [ 'PHP/7.2.11' ],
     'cache-control': [ 'max-age=0, private, must-revalidate, no-cache, private' ],
     'set-cookie': [ 'PHPSESSID=99q2iinusmhl55fa600aie2mmu; path=/' ],
     'content-length': [ '86' ],
     connection: [ 'close' ],
     'content-type': [ 'application/json' ] } }
réponse json={"action":"init-session","état":700,"réponse":"session démarrée avec type [json]"}, type=object, heure=10:13:50:825, durée= 0 seconde(s) et 1 millisecondes
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 700,
  'réponse': 'session démarrée avec type [json]' } object

[Done] exited with code=0 in 1.022 seconds

Commentaires

  • ligne 3 : [fetch] reçoit la réponse du serveur au bout de 375 ms ;
  • lignes 4-39 : la structure de l’objet Javascript [response] encapsulant la réponse du serveur. Parmi ses propriétés, certaines peuvent nous intéresser :
    • [status] (ligne 25) : code HTTP de la réponse du serveur ;
    • [statusText] (ligne 26) : texte associé à ce code ;
    • [headers] (ligne 27) : les entêtes HTTP de la réponse du serveur ;
    • [body] (ligne 8) : représente le document envoyé par le serveur. L’instruction [fetch] offre des méthodes pour l’exploiter ;
  • lignes 29-39 : les entêtes HTTP de la réponse du serveur ;
  • ligne 40 : la fonction asynchrone [response.json()] a publié sa réponse au bout d’1 milliseconde ;
  • lignes 42-44 : l’objet Javascript reçu par la fonction asynchrone [main] ;

Cas 3 : le serveur Laragon est lancé mais on lui envoie une commande erronée :

image7

  • ci-dessus, ligne 26, on passe un type erroné de session au serveur ;

Les résultats de l’exécution sont 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
34
35
36
37
38
39
40
41
42
43
44
45
requête HTTP vers le serveur en cours ---------------------------------------------
réponse fetch formatée en json,={"size":0,"timeout":2000}, heure=10:27:54:114, durée= 0 seconde(s) et 136 millisecondes
réponse fetch en javascript= Response {
  size: 0,
  timeout: 2000,
  [Symbol(Body internals)]:
   { body:
      PassThrough {
        _readableState: [ReadableState],
        readable: true,
        domain: null,
        _events: [Object],
        _eventsCount: 2,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: false,
        allowHalfOpen: true,
        _transformState: [Object] },
     disturbed: false,
     error: null },
  [Symbol(Response internals)]:
   { url:
      'http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=x',
     status: 400,
     statusText: 'Bad Request',
     headers: Headers { [Symbol(map)]: [Object] },
     counter: 0 } }
entêtes de la réponse= Headers {
  [Symbol(map)]:
   [Object: null prototype] {
     date: [ 'Sat, 14 Sep 2019 08:27:54 GMT' ],
     server: [ 'Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11' ],
     'x-powered-by': [ 'PHP/7.2.11' ],
     'cache-control': [ 'max-age=0, private, must-revalidate, no-cache, private' ],
     'set-cookie': [ 'PHPSESSID=5ku9gfok81ikj98hia0meeum57; path=/' ],
     'content-length': [ '79' ],
     connection: [ 'close' ],
     'content-type': [ 'application/json' ] } }
réponse json={"action":"init-session","état":703,"réponse":"paramètre type=[x] invalide"}, type=object, heure=10:27:54:127, durée= 0 seconde(s) et 2 millisecondes
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 703,
  'réponse': 'paramètre type=[x] invalide' } object

[Done] exited with code=0 in 0.712 seconds
  • la réponse du serveur est reçue ligne 2 ;
  • ligne 24 : on peut voir que le code HTTP de la réponse du serveur est 400, un code d’erreur. Néanmoins, [fetch] n’a pas lancé d’exception. Tant que [fetch] reçoit une réponse du serveur, il l’exploite et ne lance pas d’exception ;
  • lignes 41-43 : la réponse obtenue par la fonction asynchrone [main] ;

script [fetch-02]

Le script suivant reprend le script [fetch-01] en le débarrassant de tous les détails inutiles :

 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
'use strict';

// imports
import fetch from 'node-fetch';
import qs from 'qs';

// URL de base du serveur de calcul d'impôt
const baseUrl = 'http://localhost/php7/scripts-web/impots/version-14/main.php?';
// init session
async function initSession() {
  // options de la requête HHTP [get /main.php?action=init-session&type=json]
  const options = {
    method: "GET",
    timeout: 2000
  };
  // exécution de la requête HTTP [get /main.php?action=init-session&type=json]
  const response = await fetch(baseUrl + qs.stringify({
    action: 'init-session',
    type: 'json'
  }), options);
  // résultat reçu en jSON
  return await response.json();
}

// la fonction main exécute la fonction asynchrone [initSession]
async function main() {
  try {
    console.log("requête HTTP vers le serveur en cours ---------------------------------------------");
    const response = await initSession();
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response)
  } catch (error) {
    console.log("erreur ---------------------------------------------");
    console.log("erreur=", error.message);
  }
}

// test
main();

Les résultats d’une exécution normale :

1
2
3
4
5
6
7
8
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\fetch-02.js"
requête HTTP vers le serveur en cours ---------------------------------------------
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 700,
  'réponse': 'session démarrée avec type [json]' }

[Done] exited with code=0 in 0.56 seconds

Les résultats d’une exécution avec exception (on arrête le serveur Laragon) :

1
2
3
4
5
6
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\fetch-02.js"
requête HTTP vers le serveur en cours ---------------------------------------------
erreur ---------------------------------------------
erreur= network timeout at: http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json

[Done] exited with code=0 in 2.701 seconds

script [axios-01]

On reprend ici le script [fetch-01] que l’on réécrit avec la bibliothèque [axios]. On rappelle que notre intérêt pour cette bibliothèque est qu’elle soit portable entre l’environnement [node.js] et ceux des navigateurs usuels. Cela permet :

  • dans une phase 1, de tester nos scripts dans un environnement [node.js] ;
  • dans une phase 2, de les porter sur un navigateur ;

Le script [axios-01] reprend la structure du script [fetch-01] :

 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
'use strict';
import axios from 'axios';

// configuration par défaut d'axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';

// init session
async function initSession(axios) {
  // 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 [get /main.php?action=init-session&type=json]
  try {
    // requête asynchrone
    const response = await axios.request('main.php', options);
    // response est l'ensemble de la réponse HTTP du serveur (entêtes HTTP + réponse elle-même)
    // on affiche cette réponse pour voir sa structure
    console.log("réponse axios=", response);
    // la réponse du serveur est dans [response.data]
    return response.data;
  } catch (error) {
    // on est là parce que le serveur a envoyé un code d'erreur [404 Not Found, 500 Internal Server Error, ...]
    // le paramètre [error] est une instance d'exception - elle peut avoir diverses formes
    // on l'affiche pour voir sa structure
    console.log("axios error=", typeof (error), error);
    if (error.response) {
      // le serveur a signalé une erreur dans le statut HTTP mais il a aussi envoyé une réponse
      // alors celle-ci est trouvée dans [error.response.data]
      // on sait que le serveur envoie des réponses jSON de structure {action, état, réponse}
      // et qu'en cas d'erreur, le msg d'erreur est dans [réponse]
      return error.response.data;
    } else {
      // on lance l'erreur
      throw error;
    }
  }
}

// la fonction main exécute la fonction asynchrone [initSession]
async function main() {
  try {
    console.log("requête HTTP vers le serveur en cours ---------------------------------------------");
    const response = await initSession(axios);
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response, typeof (response))
  } catch (error) {
    console.log("erreur ---------------------------------------------");
    console.log("erreur=", error.message);
  }
}

// test
main();

Commentaires

  • ligne 2 : on importe la bibliothèque [axios] ;
  • lignes 5-6 : configuration par défaut des requêtes HTTP. Les options [axios.defaults] sont valables pour toutes les requêtes HTTP émises par l’objet [axios] sans qu’on ait besoin de les rappeler à chaque nouvelle requête ;
  • ligne 5 : timeout de 2 secondes pour toutes les requêtes ;
  • ligne 6 : toutes les URL seront exprimées relativement à l’URL de base ;
  • ligne 9 : la fonction asynchrone [initSession] ;
  • lignes 11-18 : les options de la requête HTTP qui va être émise (en plus des options par défaut déjà définies aux lignes 5-6) ;
  • lignes 14-17 : les paramètres de l’URL [action=init-session&type=json]. L’objet [params] sera automatiquement transformée en chaîne de caractères URL encodée ;
  • ligne 22 : appel bloquant de la fonction asynchrone [axios.request]. Le 1er paramètre est l’URL cible construite comme [main.php] ajouté à l’URL de base définie ligne 6. Le second paramètre est l’objet [options] des lignes 11-18 ;
  • ligne 25 : [response] est un objet Javascript encapsulant la totalité de la réponse HTTP du serveur (entêtes HTTP + document réponse). On l’affiche pour voir sa structure Javascript ;
  • ligne 27 : si le serveur a envoyé un document, alors il est trouvé dans [response.data]. Ici nous savons que le serveur envoie une réponse jSON accompagnée de l’entête HTTP [Content-type : application/json]. La présence de cet entête fait que [axios] désérialise automatiquement [response.data] en un objet Javascript ;
  • ligne 28 : la fonction [axios] peut lancer une exception. C’est là que [axios] diffère de [fetch]. Une exception est lancée dans les cas suivants :
    • la requête HTTP n’a pas pu être émise (erreur côté client) ;
    • le serveur a envoyé un code HTTP d’erreur (400, 404, 500, …) (ce point est en fait configurable). On rappelle que si ce code HTTP est accompagné d’une réponse, [fetch] ne lançait pas d’exception alors qu’[axios] en lance une. Néanmoins, si le code HTTP d’erreur est accompagné d’un document, celui-ci est mis dans [error.response] ;
  • ligne 32 : on affiche la structure Javascript de l’objet [error] ;
  • lignes 33-38 : si l’objet [error] contient un objet [response] alors c’est cette réponse qu’on rend au code appelant ;
  • lignes 39-42 : dans les autres cas, on remonte l’objet [error] au code appelant ;
  • lignes 47-57 : la fonction asynchrone [main] ;
  • ligne 50 : appel bloquant à la fonction asynchrone [initSession]. On récupère la réponse jSON du serveur comme un objet Javascript ;
  • lignes 53-56 : interception de l’erreur éventuelle. Le message d’erreur est dans [error.message] ;

Les résultats de l’exécution sont les suivants :

Cas 1 : le serveur Laragon n’est pas lancé

  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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\axios-01.js"
requête HTTP vers le serveur en cours ---------------------------------------------
axios error= object { Error: timeout of 2000ms exceeded
    at createError (c:\Data\st-2019\dev\es6\javascript\node_modules\axios\lib\core\createError.js:16:15)
    at Timeout.handleRequestTimeout (c:\Data\st-2019\dev\es6\javascript\node_modules\axios\lib\adapters\http.js:252:16)
    at ontimeout (timers.js:436:11)
    at tryOnTimeout (timers.js:300:5)
    at listOnTimeout (timers.js:263:5)
    at Timer.processTimers (timers.js:223:10)
  config:
   { url:
      'http://localhost/php7/scripts-web/impots/version-14/main.php',
     method: 'get',
     params: { action: 'init-session', type: 'json' },
     headers:
      { Accept: 'application/json, text/plain, */*',
        'User-Agent': 'axios/0.19.0' },
     baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
     transformRequest: [ [Function: transformRequest] ],
     transformResponse: [ [Function: transformResponse] ],
     timeout: 2000,
     adapter: [Function: httpAdapter],
     xsrfCookieName: 'XSRF-TOKEN',
     xsrfHeaderName: 'X-XSRF-TOKEN',
     maxContentLength: -1,
     validateStatus: [Function: validateStatus],
     data: undefined },
  code: 'ECONNABORTED',
  request:
   Writable {
     _writableState:
      WritableState {
        objectMode: false,
        highWaterMark: 16384,
        finalCalled: false,
        needDrain: false,
        ending: false,
        ended: false,
        finished: false,
        destroyed: false,
        decodeStrings: true,
        defaultEncoding: 'utf8',
        length: 0,
        writing: false,
        corked: 0,
        sync: true,
        bufferProcessing: false,
        onwrite: [Function: bound onwrite],
        writecb: null,
        writelen: 0,
        bufferedRequest: null,
        lastBufferedRequest: null,
        pendingcb: 0,
        prefinished: false,
        errorEmitted: false,
        emitClose: true,
        bufferedRequestCount: 0,
        corkedRequestsFree: [Object] },
     writable: true,
     domain: null,
     _events:
      [Object: null prototype] {
        response: [Function: handleResponse],
        error: [Function: handleRequestError] },
     _eventsCount: 2,
     _maxListeners: undefined,
     _options:
      { protocol: 'http:',
        maxRedirects: 21,
        maxBodyLength: 10485760,
        path:
         '/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json',
        method: 'GET',
        headers: [Object],
        agent: undefined,
        auth: undefined,
        hostname: 'localhost',
        port: null,
        nativeProtocols: [Object],
        pathname: '/php7/scripts-web/impots/version-14/main.php',
        search: '?action=init-session&type=json' },
     _redirectCount: 0,
     _redirects: [],
     _requestBodyLength: 0,
     _requestBodyBuffers: [],
     _onNativeResponse: [Function],
     _currentRequest:
      ClientRequest {
        domain: null,
        _events: [Object],
        _eventsCount: 6,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: false,
        sendDate: false,
        _removedConnection: false,
        _removedContLen: false,
        _removedTE: false,
        _contentLength: 0,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [Socket],
        connection: [Socket],
        _header:
         'GET /php7/scripts-web/impots/version-14/main.php?action=init-session&type=json HTTP/1.1\r\nAccept: application/json, text/plain, */*\r\nUser-Agent: axios/0.19.0\r\nHost: localhost\r\nConnection: close\r\n\r\n',
        _onPendingData: [Function: noopPendingOutput],
        agent: [Agent],
        socketPath: undefined,
        timeout: undefined,
        method: 'GET',
        path:
         '/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json',
        _ended: false,
        res: null,
        aborted: 1568528450762,
        timeoutCb: null,
        upgradeOrConnect: false,
        parser: [HTTPParser],
        maxHeadersCount: null,
        _redirectable: [Circular],
        [Symbol(isCorked)]: false,
        [Symbol(outHeadersKey)]: [Object] },
     _currentUrl:
      'http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json' },
  response: undefined,
  isAxiosError: true,
  toJSON: [Function] }
erreur ---------------------------------------------
erreur= timeout of 2000ms exceeded

[Done] exited with code=0 in 2.784 seconds

Commentaires

  • lignes 33-136 : l’objet [error] contient de nombreuses informations ;
    • [Error], lignes 3-9 : une description de l’erreur qui s’est produite ;
    • [config], lignes 10-27 : la configuration de la requête HTTP qui a mené à cette erreur ;
    • [config.url], lignes 11-12 : l’URL cible ;
    • [config.method], ligne 13 : méthode de la requête ;
    • [config.params], ligne 14 : les paramètres de l’URL ;
    • [config.headers], lignes 16-17 : les entêtes HTTP de la requête ;
    • [config.baseURL], ligne 18 : l’URL de base de l’URL cible ;
    • [config.timeout], ligne 21 : le timeout de la requête ;
    • [code], ligne 28 : un code d’erreur ;
    • [request], lignes 29-133 : une description détaillée de la requête HTTP. On remarquera que la plupart des propriétés sont préfixées par l’underscore _ montrant par là que ce sont des propriétés internes à l’objet [request] pas destinées à être exploitées directement par le développeur ;
    • [response], ligne 134 : la réponse du serveur, ici inexistante ;
  • ligne 138 : le message d’erreur affiché par la fonction [main] ;

Cas 2 : le serveur Laragon est lancé

  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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\axios-01.js"
requête HTTP vers le serveur en cours ---------------------------------------------
réponse axios= { status: 200,
  statusText: 'OK',
  headers:
   { date: 'Sun, 15 Sep 2019 07:09:26 GMT',
     server: 'Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11',
     'x-powered-by': 'PHP/7.2.11',
     'cache-control': 'max-age=0, private, must-revalidate, no-cache, private',
     'set-cookie': [ 'PHPSESSID=uas6lugtblstktcifpd8e5irm6; path=/' ],
     'content-length': '86',
     connection: 'close',
     'content-type': 'application/json' },
  config:
   { url:
      'http://localhost/php7/scripts-web/impots/version-14/main.php',
     method: 'get',
     params: { action: 'init-session', type: 'json' },
     headers:
      { Accept: 'application/json, text/plain, */*',
        'User-Agent': 'axios/0.19.0' },
     baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
     transformRequest: [ [Function: transformRequest] ],
     transformResponse: [ [Function: transformResponse] ],
     timeout: 2000,
     adapter: [Function: httpAdapter],
     xsrfCookieName: 'XSRF-TOKEN',
     xsrfHeaderName: 'X-XSRF-TOKEN',
     maxContentLength: -1,
     validateStatus: [Function: validateStatus],
     data: undefined },
  request:
   ClientRequest {
     domain: null,
     _events:
      [Object: null prototype] {
        socket: [Function],
        abort: [Function],
        aborted: [Function],
        error: [Function],
        timeout: [Function],
        prefinish: [Function: requestOnPrefinish] },
     _eventsCount: 6,
     _maxListeners: undefined,
     output: [],
     outputEncodings: [],
     outputCallbacks: [],
     outputSize: 0,
     writable: true,
     _last: true,
     chunkedEncoding: false,
     shouldKeepAlive: false,
     useChunkedEncodingByDefault: false,
     sendDate: false,
     _removedConnection: false,
     _removedContLen: false,
     _removedTE: false,
     _contentLength: 0,
     _hasBody: true,
     _trailer: '',
     finished: true,
     _headerSent: true,
     socket:
      Socket {
        connecting: false,
        _hadError: false,
        _handle: [TCP],
        _parent: null,
        _host: 'localhost',
        _readableState: [ReadableState],
        readable: true,
        domain: null,
        _events: [Object],
        _eventsCount: 7,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: false,
        allowHalfOpen: false,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: null,
        _server: null,
        parser: null,
        _httpMessage: [Circular],
        [Symbol(asyncId)]: 6,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0 },
     connection:
      Socket {
        connecting: false,
        _hadError: false,
        _handle: [TCP],
        _parent: null,
        _host: 'localhost',
        _readableState: [ReadableState],
        readable: true,
        domain: null,
        _events: [Object],
        _eventsCount: 7,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: false,
        allowHalfOpen: false,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: null,
        _server: null,
        parser: null,
        _httpMessage: [Circular],
        [Symbol(asyncId)]: 6,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0 },
     _header:
      'GET /php7/scripts-web/impots/version-14/main.php?action=init-session&type=json HTTP/1.1\r\nAccept: application/json, text/plain, */*\r\nUser-Agent: axios/0.19.0\r\nHost: localhost\r\nConnection: close\r\n\r\n',
     _onPendingData: [Function: noopPendingOutput],
     agent:
      Agent {
        domain: null,
        _events: [Object],
        _eventsCount: 1,
        _maxListeners: undefined,
        defaultPort: 80,
        protocol: 'http:',
        options: [Object],
        requests: {},
        sockets: [Object],
        freeSockets: {},
        keepAliveMsecs: 1000,
        keepAlive: false,
        maxSockets: Infinity,
        maxFreeSockets: 256 },
     socketPath: undefined,
     timeout: undefined,
     method: 'GET',
     path:
      '/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json',
     _ended: true,
     res:
      IncomingMessage {
        _readableState: [ReadableState],
        readable: false,
        domain: null,
        _events: [Object],
        _eventsCount: 3,
        _maxListeners: undefined,
        socket: [Socket],
        connection: [Socket],
        httpVersionMajor: 1,
        httpVersionMinor: 0,
        httpVersion: '1.0',
        complete: true,
        headers: [Object],
        rawHeaders: [Array],
        trailers: {},
        rawTrailers: [],
        aborted: false,
        upgrade: false,
        url: '',
        method: null,
        statusCode: 200,
        statusMessage: 'OK',
        client: [Socket],
        _consuming: false,
        _dumped: false,
        req: [Circular],
        responseUrl:
         'http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json',
        redirects: [] },
     aborted: undefined,
     timeoutCb: null,
     upgradeOrConnect: false,
     parser: null,
     maxHeadersCount: null,
     _redirectable:
      Writable {
        _writableState: [WritableState],
        writable: true,
        domain: null,
        _events: [Object],
        _eventsCount: 2,
        _maxListeners: undefined,
        _options: [Object],
        _redirectCount: 0,
        _redirects: [],
        _requestBodyLength: 0,
        _requestBodyBuffers: [],
        _onNativeResponse: [Function],
        _currentRequest: [Circular],
        _currentUrl:
         'http://localhost/php7/scripts-web/impots/version-14/main.php?action=init-session&type=json' },
     [Symbol(isCorked)]: false,
     [Symbol(outHeadersKey)]:
      [Object: null prototype] { accept: [Array], 'user-agent': [Array], host: [Array] } },
  data:
   { action: 'init-session',
     'état': 700,
     'réponse': 'session démarrée avec type [json]' } }
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 700,
  'réponse': 'session démarrée avec type [json]' } object

[Done] exited with code=0 in 1.115 seconds

Commentaires

  • lignes 3-203 : l’objet Javascript [response] qui encapsule la réponse HTTP du serveur ;
  • ligne 3, [status] : le code HTTP de la réponse ;
  • ligne 4, [statusText] : le texte associé au code HTTP précédent ;
  • lignes 5-13, [headers] : les entêtes HTTP de la réponse :
    • ligne 10, [Set-Cookie] : le cookie de session ;
    • ligne 13, [Content-Type] : le type du document envoyé par le serveur ;
  • lignes 14-31, [config] : la configuration de la requête HTTP émise ;
  • lignes 32-199, [request] : l’objet Javascript détaillant la requête HTTP émise ;
  • lignes 200-203, [request.data] : l’objet Javascript encapsulant la réponse jSON du serveur ;
  • lignes 205-207 : la réponse récupérée par la fonction asynchrone [main] ;

Cas 3 : on fait une requête [init-session] erronée au serveur Laragon ;

image8

Les résultats de l’exécution sont 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
34
35
36
37
38
39
40
41
42
43
44
45
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\axios-01.js"
requête HTTP vers le serveur en cours ---------------------------------------------
axios error= object { Error: Request failed with status code 400
   ...
  config:
   { url:
      'http://localhost/php7/scripts-web/impots/version-14/main.php',
     ...
     data: undefined },
  request:
   ...
     [Symbol(outHeadersKey)]:
      [Object: null prototype] { accept: [Array], 'user-agent': [Array], host: [Array] } },
  response:
   { status: 400,
     statusText: 'Bad Request',
     headers:
      { date: 'Sun, 15 Sep 2019 07:25:58 GMT',
        server: 'Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11',
        'x-powered-by': 'PHP/7.2.11',
        'cache-control': 'max-age=0, private, must-revalidate, no-cache, private',
        'set-cookie': [Array],
        'content-length': '79',
        connection: 'close',
        'content-type': 'application/json' },
     config:
      { url:
         'http://localhost/php7/scripts-web/impots/version-14/main.php',
        ...
        data: undefined },
     request:
      ...
        [Symbol(outHeadersKey)]: [Object] },
     data:
      { action: 'init-session',
        'état': 703,
        'réponse': 'paramètre type=[x] invalide' } },
  isAxiosError: true,
  toJSON: [Function] }
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 703,
  'réponse': 'paramètre type=[x] invalide' } object

[Done] exited with code=0 in 0.69 seconds

Parce que le serveur a répondu avec un code HTTP 400 (ligne 15), [axios] a lancé une exception (encore une fois ce comportement est configurable). Bien que [axios] ait lancé une exception, on obtient bien la réponse HTTP du serveur dans [error.response] (ligne 14) et le document jSON envoyé [error.response.data] (ligne 34). Lignes 41-43 : la fonction [main] récupère correctement la réponse jSON du serveur.

script [axios-02]

Maintenant que nous avons détaillé les objets manipulés par la bibliothèque [axios] lors d’une requête HTTP, on peut réécrire le script [axios-01] 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
'use strict';
import axios from 'axios';

// configuration par défaut d'axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';

// init session
async function initSession(axios) {
  // 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'
    }
  };
  try {
    // exécution de la requête HTTP [get /main.php?action=init-session&type=json]
    const response = await axios.request('main.php', options);
    // la réponse du serveur est dans [response.data]
    return response.data;
  } catch (error) {
    // réponse du serveur
    if (error.response) {
      // la réponse jSON est dans [error.response.data]
      return error.response.data;
    } else {
      // on relance l'erreur
      throw error;
    }
  }
}

// la fonction main exécute la fonction asynchrone [initSession]
async function main() {
  try {
    console.log("requête HTTP vers le serveur en cours ---------------------------------------------");
    const response = await initSession(axios);
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response, typeof (response))
  } catch (error) {
    console.log("erreur ---------------------------------------------");
    console.log("erreur=", error.message);
  }
}

// test
main();

script [axios-03]

Le script [axios-03] reprend la méthodogie du script [axios-02]. On ajoute cette fois la requête HTTP [authentifier-utilisateur] vers le serveur qui se fait à l’aide d’un POST :

 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
'use strict';
import axios from 'axios';
import qs from 'qs'

// configuration axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';


// init session
async function initSession(axios) {
  // 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'
    }
  };
  try {
    // exécution de la requête HTTP [get /main.php?action=init-session&type=json]
    const response = await axios.request('main.php', options);
    // la réponse du serveur est dans [response.data]
    return response.data;
  } catch (error) {
    // réponse du serveur
    if (error.response) {
      // la réponse jSON est dans [error.response.data]
      return error.response.data;
    } else {
      // on relance l'erreur
      throw error;
    }
  }
}

async function authentifierUtilisateur(axios, 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'
    }
  };
  try {
    // exécution de la requête HTTP [post /main.php?action=authentifier-utilisateur]
    const response = await axios.request('main.php', options);
    // la réponse du serveur est dans [response.data]
    return response.data;
  } catch (error) {
    // réponse du serveur
    if (error.response) {
      // la réponse jSON est dans [error.response.data]
      return error.response.data;
    } else {
      // on relance l'erreur
      throw error;
    }
  }
}

// la fonction main exécute les fonctions asynchrones une par une
async function main() {
  try {
    // init-session
    console.log("action init-session en cours ---------------------------------------------");
    const response1 = await initSession(axios);
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response1);
    // authentifier-utilisateur
    console.log("action authentifier-utilisateur en cours ---------------------------------------------");
    const response2 = await authentifierUtilisateur(axios, 'admin', 'admin');
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response2)
  } catch (error) {
    console.log("erreur ---------------------------------------------");
    console.log("erreur=", error);
  }
}

// test
main();

Commentaires

  • lignes 38-70 : la fonction asynchrone [authentifierUtilisateur] ;
  • ligne 39 : il faut faire la requête [POST /main.php?action=authentifier-utilisateur] ;
  • lignes 40-54 : les options de la requête HTTP ;
  • ligne 41 : c’est un POST ;
  • lignes 42-44 : les paramètres du POST seront URL encodés dans un document que le client envoie avec sa requête ;
  • lignes 46-49 : la propriété [data] doit contenir la chaîne du POST URL encodée. Pour cela, on utilise ici la bibliothèque [qs] importée ligne 3 ;
  • lignes 55-69 : pour l’exécution de la requête, on retrouve le même code que dans la méthode [initSession] ;
  • lignes 73-89 : la méthode [asynchrone] appelle successivement, de manière bloquante, les méthodes [initSession, authentifierUtilisateur], lignes 77 et 82 ;
  • ligne 82 : on utilise le couple (admin, admin) comme identifiants de connexion. On sait qu’ils sont reconnus par le serveur ;

Les résultats de l’exécution sont les suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\axios-03.js"
action init-session en cours ---------------------------------------------
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 700,
  'réponse': 'session démarrée avec type [json]' }
action authentifier-utilisateur en cours ---------------------------------------------
succès ---------------------------------------------
réponse= { action: 'authentifier-utilisateur',
  'état': 103,
  'réponse':
   [ 'pas de session en cours. Commencer par action [init-session]' ] }

[Done] exited with code=0 in 0.834 seconds
  • lignes 9-12 : l’authentification utilisateur échoue : le serveur n’a pas retenu le fait qu’on avait initié une session jSON. Cela vient du fait qu’on n’a pas renvoyé le cookie de session envoyé en réponse à la 1ère requête [init-session] ;

script [axios-04]

Le script [axios-04] amène deux améliorations au script [axios-03] :

  • il gère le cookie de session ;
  • il factorise dans une fonction [getRemoteData] ce qui est commun aux fonctions [initSession] et [authentifierUtilisateur] ;
  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';
import axios from 'axios';
import qs from 'qs'

// configuration axios
axios.defaults.timeout = 2000;
axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';

// cookie de session
const sessionCookieName = "PHPSESSID";
let sessionCookie = '';

// init session
async function initSession(axios) {
  // 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 getRemoteData(axios, options);
}

async function authentifierUtilisateur(axios, 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 getRemoteData(axios, options);
}

async function getRemoteData(axios, options) {
  // pour le cookie de session
  if (!options.headers) {
    options.headers = {};
  }
  options.headers.Cookie = sessionCookie;
  // exécution de la requête HTTP
  let response;
  try {
    // requête asynchrone
    response = await 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('^(' + sessionCookieName + '.+?);').exec(setCookie[i]);
      if (results) {
        // on mémorise le cookie de session
        // eslint-disable-next-line require-atomic-updates
        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;
}

// la fonction main exécute les fonctions asynchrones une par une
async function main() {
  try {
    // init-session
    console.log("action init-session en cours ---------------------------------------------");
    const response1 = await initSession(axios);
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response1);
    // authentifier-utilisateur
    console.log("action authentifier-utilisateur en cours ---------------------------------------------");
    const response2 = await authentifierUtilisateur(axios, 'admin', 'admin');
    console.log("succès ---------------------------------------------");
    console.log("réponse=", response2)
  } catch (error) {
    console.log("erreur ---------------------------------------------");
    console.log("erreur=", error.message);
  }
}

// test
main();

Commentaires

  • lignes 14-26 : la fonction [initSession]. Elle se contente désormais de préparer la requête HTTP à envoyer au serveur mais ne l’exécute pas. Elle confie ce rôle à la méthode [getRemoteDate] des lignes 49-95 ;
  • lignes 28-47 : la fonction [authentifierUtilisateur] suit la même démarche ;
  • ligne 49 : la fonction [getRemoteData] reçoit les deux informations qui lui permettent d’exécuter une requête HTTP:
    • [axios], l’objet qui va se charger d’envoyer la requête et de recevoir la réponse ;
    • [options], les options de configuration de la requête à envoyer au serveur ;
  • ligne 59 : exécution de la requête et attente bloquante de sa réponse jSON ;
  • lignes 60-68 : gestion de l’éventuelle exception ;
  • ligne 64 : on récupère la réponse qui peut être encapsulée dans l’objet d’erreur ;
  • ligne 67 : si le serveur a lancé une exception sans y inclure la réponse du serveur, alors on remonte l’erreur reçue au code appelant ;
  • la fonction [getRemoteData] gère le cookie de session :
    • il le mémorise dans la variable [sessionCookie] (ligne 11) lorsqu’il le reçoit la 1ère fois ;
    • il le renvoie ensuite à chaque nouvelle requête HTTP ;
  • ligne 72-92 : [getRemoteData] analyse chaque réponse du serveur pour savoir s’il a envoyé l’entête HTTP [Set-Cookie]. On sait que le serveur envoie un cookie de session nommé [PHPSESSID] (ligne 10). C’est donc ce cookie que l’on recherche (ligne 10) ;
  • ligne 72 : on récupère les entêtes HTTP [Set-Cookie] s’ils existent (la casse n’a pas d’importance). Il peut y avoir en effet plusieurs entêtes [Set-Cookie] et c’est donc un tableau que l’on récupère ;
  • ligne 73 : si on a récupéré un tableau de cookies ;
  • lignes 78-90 : on cherche le cookie de session parmi tous les cookies du tableau ;
  • ligne 80 : l’expression relationnelle qui permet de chercher le cookie de session dans le cookie n° i ;
  • ligne 81 : si la comparaison a ramené des résultats ;
  • ligne 84 : on a dans results[1], la 1ère parenthèse du modèle de l’expression relationnelle, ç-à-d (PHPSESSID=xxxx) jusqu’au ; (non inclus) qui termine le cookie de session ;
  • lignes 50-54 : à chaque requête, le cookie de session est inclus dans les entêtes HTTP de la requête. La 1ère fois, ce cookie est vide et sera alors ignoré par le serveur ;

Les résultats de l’exécution sont les suivants :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\http\axios-04.js"
action init-session en cours ---------------------------------------------
succès ---------------------------------------------
réponse= { action: 'init-session',
  'état': 700,
  'réponse': 'session démarrée avec type [json]' }
action authentifier-utilisateur en cours ---------------------------------------------
succès ---------------------------------------------
réponse= { action: 'authentifier-utilisateur',
  'état': 200,
  'réponse': 'Authentification réussie [admin, admin]' }

[Done] exited with code=0 in 0.982 seconds

Les classes

image0

Nous introduisons ici les classes d’ECMAScript 6. Tout d’abord nous montrons que les fonctions peuvent être déjà utilisées comme des classes.

script [class-00]

Le script suivant montre une utilisation inhabituelle de fonctions. On les utilise ici comme des objets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';
// une fonction peut être utilisée comme un objet

// une coquille vide
function f() {

}
// à qui on attribue des propriétés de l'extérieur
f.prop1 = "val1";
f.show = function () {
  console.log(this.prop1);
}
// utilisation de f
f.show();

// une fonction g fonctionnant comme une classe
function g() {
  this.prop2 = "val2";
  this.show = function () {
    console.log(this.prop2);
  }
}
// instanciation de la fonction avec [new]
new g().show();

Commentaires

  • lignes 5-7 : le corps de la fonction f ne définit aucune propriété ;
  • lignes 9-12 : on donne de l’extérieur des propriétés à la fonction f ;
  • ligne 14 : utilisation de la function (objet) f. Notez qu’on n’écrit pas [f()] mais simplement [f]. On a là la notation d’un objet ;
  • lignes 17-22 : on définit une fonction [g] comme si c’était une classe avec propriétés et méthodes ;
  • ligne 24 : la fonction [g] est instanciée par [new g()] ;

Résultats de l’exécution

1
2
3
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\classes\class-00.js"
val1
val2

ES6 a introduit la notion de classe qui nous permet désormais d’éviter de passer par des fonctions pour avoir des classes.

script [class-01]

Le script [class-01] présente une classe [Personne] :

 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
// classe
class Personne {

  // constructeur
  constructor(nom, prénom, âge) {
    this.nom = nom;
    this.prénom = prénom;
    this.âge = âge;
  }

  // getters et setters
  get nom() {
    return this._nom;
  }
  set nom(value) {
    this._nom = value;
  }

  get prénom() {
    return this._prénom;
  }
  set prénom(value) {
    this._prénom = value;
  }

  get âge() {
    return this._âge;
  }
  set âge(value) {
    this._âge = value;
  }

  // toString en JSON
  toString() {
    return JSON.stringify(this);
  }
}

// appel de la classe
function main() {
  const personne = new Personne("Poirot", "Hercule", 66);
  console.log("personne=", personne.toString(), typeof (personne), personne instanceof (Personne));
}

// appel de main
main();

Commentaires

  • ligne 2 : le mot clé [class] désigne une classe ;
  • lignes 5-9 : le mot clé [constructor] désigne le constructeur de la classe. Il ne peut y en avoir qu’un au plus. Il sert à construire et initialiser une instance de la classe. Notez qu’il n’y a pas de déclaration des propriétés [nom, prénom, âge] ;
  • lignes 11-36 : propriétés de la classe. On retrouve ici des choses déjà vues dans le paragraphe lien. Seule la syntaxe diffère ;
  • ligne 41 : création d’un objet de type [Personne]. A partir de maintenant l’objet [personne] s’utilise comme un objet littéral. [typeof (personne)] vaut « object » et l’expression [personne instanceof (Personne)] est vraie. Il est donc possible de connaître le type exact d’une instance de classe ;

Résultats de l’exécution

1
2
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\classes\class-01.js"
personne= {"_nom":"Poirot","_prénom":"Hercule","_âge":66} object true

script [class-02]

Ce script montre la possibilité d’hériter d’une classe avec le mot clé [extends].

Tout d’abord nous isolons la classe [Personne] dans un module [Personne.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
// classe
class Personne {

  // constructeur
  constructor(nom, prénom, âge) {
    this.nom = nom;
    this.prénom = prénom;
    this.âge = âge;
  }

  // getters et setters
  get nom() {
    return this._nom;
  }
  set nom(value) {
    this._nom = value;
  }

  get prénom() {
    return this._prénom;
  }
  set prénom(value) {
    this._prénom = value;
  }

  get âge() {
    return this._âge;
  }
  set âge(value) {
    this._âge = value;
  }

  // toString en JSON
  toString() {
    return JSON.stringify(this);
  }
}
// export classe
export default Personne;
  • ligne 39 : nous exportons la classe [Personne] pour que des scripts puissent l’importer ;

Le script [class-02] crée une classe [Enseignant] dérivée de la classe [Personne] :

 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
// imports
import Personne from './Personne';

// classe
class Enseignant extends Personne {

  // constructeur
  constructor(nom, prénom, âge, discipline) {
    super(nom, prénom, âge);
    this.discipline = discipline;
  }

  // getters et setters
  get discipline() {
    return this._discipline;
  }
  set discipline(value) {
    this._discipline = value;
  }

}

// appel de la classe
function main() {
  const enseignant = new Enseignant("Poirot", "Hercule", 66, "détective");
  console.log("enseignant=", enseignant.toString(), typeof (enseignant), enseignant instanceof Enseignant);
}

// appel de main
main();

Commentaires

  • ligne 2 : on importe la classe [Personne] à partir du module [Personne.js] qui se trouve dans le même dossier que [class-02] ;
  • ligne 5 : la classe [Enseignant] étend (hérite de) la classe [Personne] avec le mot clé [extends] : elle lui ajoute une propriété [_discipline] avec les getter / setter qui vont avec ;
  • lignes 8-11 : le constructeur de la classe [Enseignant] reçoit quatre valeurs pour initialiser les quatre propriétés de la classe ;
  • ligne 9 : le mot clé [super] appelle le constructeur de la classe parent [Personne] qui va donc initialiser les propriétés [_nom, _prénom, _âge] ;
  • ligne 10 : on initialise la propriété [_discipline] qui lui appartient à la classe [Enseignant] ;
  • lignes 14-19 : le getter et le setter de la propriété [_discipline] ;
  • ligne 25 : on crée un objet de type [Enseignant] ;
  • ligne 26 : on utilise la méthode [enseignant.toString()]. La classe [Enseignant] n’a pas cette méthode. C’est alors celle de sa classe parent qui est automatiquement utilisée. Cette méthode rend l’expression [JSON.stringify(this)][this] va être ici un objet [Enseignant] et non un objet [Personne]. C’est qu’on appelle en programmation objet, le polymorphisme des classes. Un grand mot pour Javascript qui n’est pas un langage orienté objets. Néanmoins, Javascript fait ici ce qu’on attend de lui : il affiche bien un enseignant ;

Les résultats de l’exécution sont les suivants :

1
2
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\classes\class-02.js"
enseignant= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"} object true
  • ligne 2 : Javascript reconnaît bien que la variable [enseignant] est de type [Enseignant] ;

script [class-03]

Le script [class-03] montre qu’une classe fille peut redéfinir propriétés et méthodes de sa classe parent. Ici, nous redéfinissons la méthode [toString] de la classe parent :

 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
// imports
import Personne from './Personne';

// classe
class Enseignant extends Personne {

  // constructeur
  constructor(nom, prénom, âge, discipline) {
    super(nom, prénom, âge);
    this.discipline = discipline;
  }

  // getters et setters
  get discipline() {
    return this._discipline;
  }
  set discipline(value) {
    this._discipline = value;
  }

  // redéfinition de toString
  toString() {
    return "[Enseignant]" + JSON.stringify(this);
  }
}

// appel de la classe
function main() {
  const enseignant = new Enseignant("Poirot", "Hercule", 66, "détective");
  console.log("enseignant=", enseignant.toString(), typeof (enseignant), enseignant instanceof Enseignant);
}

// appel de main
main();

Les résultats de l’exécution sont les suivants :

1
2
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\classes\class-03.js"
enseignant= [Enseignant]{"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"} object true

script [class-04]

Le script [class-04] montre de nouveau le polymorphisme à l’oeuvre : là où une fonction attend un paramètre formel de type [Personne], on peut passer un type dérivé tel que [Enseignant]. En effet, de par la dérivation, le type [Enseignant] a tous les attributs du type [Personne].

Tout d’abord, nous isolons le type [Enseignant] dans un module [Enseignant.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
// imports
import Personne from './Personne';

// classe
class Enseignant extends Personne {

  // constructeur
  constructor(nom, prénom, âge, discipline) {
    super(nom, prénom, âge);
    this.discipline = discipline;
  }

  // getters et setters
  get discipline() {
    return this._discipline;
  }
  set discipline(value) {
    this._discipline = value;
  }

}

// export classe
export default Enseignant;
  • ligne 24 : la classe [Enseignant] est exportée pour que d’autres scripts puissent l’importer ;

Le script [class-04] est le suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// imports
import Enseignant from './Enseignant';
import Personne from './Personne';

// fonction acceptant une personne comme paramètre
function show(personne) {
  // dans tous les cas
  console.log("paramètre=", personne.toString(), typeof (personne));
  // instance de Personne
  if (personne instanceof Personne) {
    console.log("personne=", personne.toString());
  }
  // instance de Enseignant
  if (personne instanceof Enseignant) {
    console.log("enseignant=", personne.toString());
  }
}

// appel de show avec un enseignant
show(new Enseignant("Poirot", "Hercule", 66, "détective"));
show(new Personne("Marple", "Miss", 70));
  • ligne 6 : la fonction [show] attend un type [Personne] ou dérivé ;
  • ligne 8 : on affiche la chaîne du paramètre et son type. On va trouver [object] ;
  • lignes 10-16 : on est capable de savoir si c’est un type [Personne] ou un type [Enseignant]. Le code peut donc être adapté au type réel du paramètre ;

Les résultats de l’exécution sont les suivants :

1
2
3
4
5
6
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\classes\class-04.js"
paramètre= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"} object
personne= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"}
enseignant= {"_nom":"Poirot","_prénom":"Hercule","_âge":66,"_discipline":"détective"}
paramètre= {"_nom":"Marple","_prénom":"Miss","_âge":70} object
personne= {"_nom":"Marple","_prénom":"Miss","_âge":70}
  • lignes 4 et 6 : Javascript connaît correctement le type des instances de classe ;

Clients HTTP Javascript du service de calcul de l’impôt

Introduction

Nous nous proposons ici d’écrire un client [node.js] de la version 14 du service de calcul de l’impôt. L’architecture client / serveur sera la suivante :

image0

Nous étudierons deux versions du client :

  • la version 1 du client aura la structure [main, dao] en couches suivante :

image1

  • la version 2 du client aura une structure [main, métier, dao]. La couche [métier] du serveur sera déportée sur le client :

image2

Client HTTP 1

image3

Comme nous l’avons dit, le client HTTP 1 implémente l’architecture client / serveur suivante :

image4

Nous implémenterons :

  • la couche [dao] sous la forme d’une classe ;
  • la couche [main] sous la forme d’un script utilisant cette classe ;

La couche [dao]

La couche [dao] sera implémentée par la classe suivante [Dao1.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
 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
'use strict';

// imports
import qs from 'qs'

class Dao1 {

  // constructeur
  constructor(axios) {
    // bibliothèque axios pour faire les requêtes HTTP
    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);
  }

  // calcul de l'impôt
  async  calculerImpot(marié, enfants, salaire) {
    // options de la requête HHTP [post /main.php?action=calculer-impot]
    const options = {
      method: "POST",
      headers: {
        'Content-type': 'application/x-www-form-urlencoded',
      },
      // corps du POST [marié, enfants, salaire]
      data: qs.stringify({
        marié: marié,
        enfants: enfants,
        salaire: salaire
      }),
      // paramètres de l'URL
      params: {
        action: 'calculer-impot'
      }
    };
    // exécution de la requête HTTP
    const data = await this.getRemoteData(options);
    // résultat
    return data;
  }

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

  // liste des simulations
  async  supprimerSimulation(index) {
    // options de la requête HHTP  [get /main.php?action=supprimer-simulation&numéro=index]
    const options = {
      method: "GET",
      // paramètres de l'URL
      params: {
        action: 'supprimer-simulation',
        numéro: index
      },
    };
    // 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 Dao1;

Nous utilisons ici ce que nous avons appris au paragraphe lien, où nous avons présenté la bibliothèque [axios] permettant de faire des requêtes HTTP aussi bien sous [node.js] que dans un navigateur. On regardera en particulier le script du paragraphe lien ;

  • lignes 9-15 : le constructeur de la classe. Celle-ci aura trois propriétés :
    • [axios] : l’objet [axios] permettant de faire les requêtes HTTP. Celui-ci est transmis par le code appelant ;
    • [sessionCookieName] : selon les serveurs, le cookie de session porte des noms différents. Ici, c’est [PHPSESSID] ;
    • [sessionCookie] : le cookie de session envoyé par le serveur et mémorisé par le client ;
  • lignes 53-76 : la fonction asynchrone [calculerImpot] fait la requête [post /main.php?action=calculer-impot] en postant les paramètres [marié, enfants, salaire]. Elle rend la chaîne jSON transmise par le serveur sous la forme d’un objet Javascript ;
  • lignes 79-92 : la fonction asynchrone [listeSimulations] fait la requête [get /main.php?action=lister-simulations. Elle rend la chaîne jSON transmise par le serveur sous la forme d’un objet Javascript ;
  • lignes 95-109 : la fonction asynchrone [supprimerSimulation] fait la requête [get /main.php?action=supprimer-simulation&numéro=index]. Elle rend la chaîne jSON transmise par le serveur sous la forme d’un objet Javascript ;
  • ligne 121 : on utilise la notation [this.axios] car ici, l’objet [axios] transmis au constructeur a été mémorisé dans la propriété [this.axios] ;
  • ligne 161 : la classe [Dao1] est exportée pour pouvoir être utilisée ;

Le script [main1.js]

Le script [main1.js] fait une série d’appels au serveur à l’aide de la classe [Dao1] :

  • initialisation d’une session jSON ;
  • authentification avec [admin, admin] ;
  • demande trois calculs d’impôts ;
  • demande la liste des simulations ;
  • supprime l’une d’elles ;

Le 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
// import axios
import axios from 'axios';
// import de la classe Dao1
import Dao from './Dao1';

// fonction asynchrone [main]
async function main() {
  // configuration axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instanciation couche [dao]
  const dao = new Dao(axios);
  // utilisation de la couche [dao]
  try {
    // init session
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // authentification
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // calculs d'impôt
    log("-----------calculer-impot x 3");
    response = await Promise.all([
      dao.calculerImpot("oui", 2, 45000),
      dao.calculerImpot("non", 2, 45000),
      dao.calculerImpot("non", 1, 30000)
    ]);
    log(response);
    // liste des simulations
    log("-----------liste-des-simulations");
    response = await dao.listeSimulations();
    log(response);
    // suppression d'une simulation
    log("-----------suppression simulation n° 1");
    response = await dao.supprimerSimulation(1);
    log(response);
  } catch (error) {
    // on logue l'erreur
    console.log("erreur=", error.message);
  }
}

// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// exécution
main();

Commentaires

  • ligne 2 : on importe la bibliothèque [axios] ;
  • ligne 4 : on importe la classe [Dao] ;
  • ligne 7 : la fonction [main] qui dialogue avec le serveur est asynchrone ;
  • lignes 9-10 : configuration par défaut des requêtes HTTP qui seront faites au serveur :
    • ligne 9 : [timeout] de 2 secondes ;
    • ligne 10 : toutes les URL ont pour préfixe, l’URL base de la version 14 du serveur de calcul de l’impôt ;
  • ligne 12 : la couche [Dao] est construite. On peut désormais l’utiliser ;
  • lignes 46-48 : la fonction [log] a pour objet d’afficher la chaîne jSON d’un objet Javascript sous une forme embellie : sous forme verticale avec une indentation de deux espaces (3ième paramètre) ;
  • lignes 15-18 : initialisation de la session jSON ;
  • lignes 19-22 : authentification ;
  • lignes 23-30 : trois calculs d’impôt sont demandés en parallèle. Grâce à [await Promise.all], l’exécution est bloquée tant que les trois résultats n’ont pas été tous obtenus ;
  • lignes 31-34 : liste des simulations ;
  • lignes 35-38 : suppression d’une simulation ;
  • lignes 39-42 : gestion de l’éventuelle exception ;

Les résultats de l’exécution sont 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
 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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 1\main1.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------calculer-impot x 3"
[
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3
    }
  },
  {
    "action": "calculer-impot",
    "état": 300,
    "réponse": {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14
    }
  }
]
"-----------liste-des-simulations"
{
  "action": "lister-simulations",
  "état": 500,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 3250,
      "surcôte": 370,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.3,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}
"-----------suppression simulation n° 1"
{
  "action": "supprimer-simulation",
  "état": 600,
  "réponse": [
    {
      "marié": "oui",
      "enfants": "2",
      "salaire": "45000",
      "impôt": 502,
      "surcôte": 0,
      "décôte": 857,
      "réduction": 126,
      "taux": 0.14,
      "arrayOfAttributes": null
    },
    {
      "marié": "non",
      "enfants": "1",
      "salaire": "30000",
      "impôt": 1687,
      "surcôte": 0,
      "décôte": 0,
      "réduction": 0,
      "taux": 0.14,
      "arrayOfAttributes": null
    }
  ]
}

[Done] exited with code=0 in 0.516 seconds

Client HTTP 2

image5

L’architecture du client HTTP2 est la suivante :

image6

On a déporté la couche [métier] du serveur vers le client Javascript. Contrairement à ce que nous avons pu faire dans le cours PHP7, la couche [main] n’aura pas ici à passer par la couche [métier] pour atteindre la couche [dao]. Nous utiliserons ces deux couches comme des centres de compétences :

  • la couche [main] passe par la couche [dao] dès qu’elle a besoin de données qui sont sur le serveur ;
  • la couche [main] demande à la couche [métier] de faire les calculs de l’impôt ;
  • la couche [métier] est indépendante de la couche [dao] et ne fait jamais appel à elle ;

La classe Javascript [Métier]

L’essence de la classe [Métier] en PHP a été décrite dans l’article lien. C’est un code plutôt complexe qu’on rappelle ici, non pour l’expliquer, mais pour pouvoir le traduire en Javascript :

  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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
<?php

// espace de noms
namespace Application;

class Metier implements InterfaceMetier {
  // couche Dao
  private $dao;
  // données administration fiscale
  private $taxAdminData;

  //---------------------------------------------
  // setter couche [dao]
  public function setDao(InterfaceDao $dao) {
    $this->dao = $dao;
    return $this;
  }

  public function __construct(InterfaceDao $dao) {
    // on mémorise une référence sur la couche [dao]
    $this->dao = $dao;
    // on récupère les données permettant le calcul de l'impôt
    // la méthode [getTaxAdminData] peut lancer une exception ExceptionImpots
    // on la laisse alors remonter au code appelant
    $this->taxAdminData = $this->dao->getTaxAdminData();
  }

// calcul de l'impôt
// --------------------------------------------------------------------------
  public function calculerImpot(string $marié, int $enfants, int $salaire): array {
    // $marié : oui, non
    // $enfants : nombre d'enfants
    // $salaire : salaire annuel
    // $this->taxAdminData : données de l'administration fiscale
    //
    // on vérifie qu'on a bien les données de l'administration fiscale
    if ($this->taxAdminData === NULL) {
      $this->taxAdminData = $this->getTaxAdminData();
    }
    // calcul de l'impôt avec enfants
    $result1 = $this->calculerImpot2($marié, $enfants, $salaire);
    $impot1 = $result1["impôt"];
    // calcul de l'impôt sans les enfants
    if ($enfants != 0) {
      $result2 = $this->calculerImpot2($marié, 0, $salaire);
      $impot2 = $result2["impôt"];
      // application du plafonnement du quotient familial
      $plafonDemiPart = $this->taxAdminData->getPlafondQfDemiPart();
      if ($enfants < 3) {
        // $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
        $impot2 = $impot2 - $enfants * $plafonDemiPart;
      } else {
        // $PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
        $impot2 = $impot2 - 2 * $plafonDemiPart - ($enfants - 2) * 2 * $plafonDemiPart;
      }
    } else {
      $impot2 = $impot1;
      $result2 = $result1;
    }
    // on prend l'impôt le plus fort
    if ($impot1 > $impot2) {
      $impot = $impot1;
      $taux = $result1["taux"];
      $surcôte = $result1["surcôte"];
    } else {
      $surcôte = $impot2 - $impot1 + $result2["surcôte"];
      $impot = $impot2;
      $taux = $result2["taux"];
    }
    // calcul d'une éventuelle décôte
    $décôte = $this->getDecôte($marié, $salaire, $impot);
    $impot -= $décôte;
    // calcul d'une éventuelle réduction d'impôts
    $réduction = $this->getRéduction($marié, $salaire, $enfants, $impot);
    $impot -= $réduction;
    // résultat
    return ["impôt" => floor($impot), "surcôte" => $surcôte, "décôte" => $décôte, "réduction" => $réduction, "taux" => $taux];
  }

// --------------------------------------------------------------------------
  private function calculerImpot2(string $marié, int $enfants, float $salaire): array {
    // $marié : oui, non
    // $enfants : nombre d'enfants
    // $salaire : salaire annuel
    // $this->taxAdminData : données de l'administration fiscale
    //
    // nombre de parts
    $marié = strtolower($marié);
    if ($marié === "oui") {
      $nbParts = $enfants / 2 + 2;
    } else {
      $nbParts = $enfants / 2 + 1;
    }
    // 1 part par enfant à partir du 3ième
    if ($enfants >= 3) {
      // une demi-part de + pour chaque enfant à partir du 3ième
      $nbParts += 0.5 * ($enfants - 2);
    }
    // revenu imposable
    $revenuImposable = $this->getRevenuImposable($salaire);
    // surcôte
    $surcôte = floor($revenuImposable - 0.9 * $salaire);
    // pour des pbs d'arrondi
    if ($surcôte < 0) {
      $surcôte = 0;
    }
    // quotient familial
    $quotient = $revenuImposable / $nbParts;
    // calcul de l'impôt
    $limites = $this->taxAdminData->getLimites();
    $coeffR = $this->taxAdminData->getCoeffR();
    $coeffN = $this->taxAdminData->getCoeffN();
    // est mis à la fin du tableau limites pour arrêter la boucle qui suit
    $limites[count($limites) - 1] = $quotient;
    // recherche du taux d'imposition
    $i = 0;
    while ($quotient > $limites[$i]) {
      $i++;
    }
    // du fait qu'on a placé $quotient à la fin du tableau $limites, la boucle précédente
    // ne peut déborder du tableau $limites
    // maintenant on peut calculer l'impôt
    $impôt = floor($revenuImposable * $coeffR[$i] - $nbParts * $coeffN[$i]);
    // résultat
    return ["impôt" => $impôt, "surcôte" => $surcôte, "taux" => $coeffR[$i]];
  }

  // revenuImposable=salaireAnnuel-abattement
  // l'abattement a un min et un max
  private function getRevenuImposable(float $salaire): float {
    // abattement de 10% du salaire
    $abattement = 0.1 * $salaire;
    // cet abattement ne peut dépasser $this->taxAdminData->getAbattementDixPourCentMax()
    if ($abattement > $this->taxAdminData->getAbattementDixPourCentMax()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMax();
    }
    // l'abattement ne peut être inférieur à $this->taxAdminData->getAbattementDixPourcentMin()
    if ($abattement < $this->taxAdminData->getAbattementDixPourcentMin()) {
      $abattement = $this->taxAdminData->getAbattementDixPourcentMin();
    }
    // revenu imposable
    $revenuImposable = $salaire - $abattement;
    // résultat
    return floor($revenuImposable);
  }

// calcule une décôte éventuelle
  private function getDecôte(string $marié, float $salaire, float $impots): float {
    // au départ, une décôte nulle
    $décôte = 0;
    // montant maximal d'impôt pour avoir la décôte
    $plafondImpôtPourDécôte = $marié === "oui" ?
      $this->taxAdminData->getPlafondImpotCouplePourDecote() :
      $this->taxAdminData->getPlafondImpotCelibatairePourDecote();
    if ($impots < $plafondImpôtPourDécôte) {
      // montant maximal de la décôte
      $plafondDécôte = $marié === "oui" ?
        $this->taxAdminData->getPlafondDecoteCouple() :
        $this->taxAdminData->getPlafondDecoteCelibataire();
      // décôte théorique
      $décôte = $plafondDécôte - 0.75 * $impots;
      // la décôte ne peut dépasser le montant de l'impôt
      if ($décôte > $impots) {
        $décôte = $impots;
      }
      // pas de décôte <0
      if ($décôte < 0) {
        $décôte = 0;
      }
    }
    // résultat
    return ceil($décôte);
  }

// calcule une réduction éventuelle
  private function getRéduction(string $marié, float $salaire, int $enfants, float $impots): float {
    // le plafond des revenus pour avoir droit à la réduction de 20%
    $plafondRevenuPourRéduction = $marié === "oui" ?
      $this->taxAdminData->getPlafondRevenusCouplePourReduction() :
      $this->taxAdminData->getPlafondRevenusCelibatairePourReduction();
    $plafondRevenuPourRéduction += $enfants * $this->taxAdminData->getValeurReducDemiPart();
    if ($enfants > 2) {
      $plafondRevenuPourRéduction += ($enfants - 2) * $this->taxAdminData->getValeurReducDemiPart();
    }
    // revenu imposable
    $revenuImposable = $this->getRevenuImposable($salaire);
    // réduction
    $réduction = 0;
    if ($revenuImposable < $plafondRevenuPourRéduction) {
      // réduction de 20%
      $réduction = 0.2 * $impots;
    }
    // résultat
    return ceil($réduction);
  }

  // calcul des impôts en mode batch
  public function executeBatchImpots(string $taxPayersFileName, string $resultsFileName, string $errorsFileName): void {
    // on laisse remonter les exceptions qui proviennent de la couche [dao]
    // on récupère les données contribuables
    $taxPayersData = $this->dao->getTaxPayersData($taxPayersFileName, $errorsFileName);
    // tableau des résultats
    $results = [];
    // on les exploite
    foreach ($taxPayersData as $taxPayerData) {
      // on calcule l'impôt
      $result = $this->calculerImpot(
        $taxPayerData->getMarié(),
        $taxPayerData->getEnfants(),
        $taxPayerData->getSalaire());
      // on complète [$taxPayerData]
      $taxPayerData->setMontant($result["impôt"]);
      $taxPayerData->setDécôte($result["décôte"]);
      $taxPayerData->setSurCôte($result["surcôte"]);
      $taxPayerData->setTaux($result["taux"]);
      $taxPayerData->setRéduction($result["réduction"]);
      // on met le résultat dans le tableau des résultats
      $results [] = $taxPayerData;
    }
    // enregistrement des résultats
    $this->dao->saveResults($resultsFileName, $results);
  }

}
  • lignes 19-26 : le constructeur de la classe PHP. Parce que nous avons dit qu’on construisait une couche [métier] indépendante de la couche [dao], nous ferons en Javascript deux modifications à ce constructeur :
    • il ne recevra pas une instance de la couche [dao] (il n’en a plus besoin) ;
    • il ne demandera pas les données fiscales de l’administration [taxAdminData] à la couche [dao] : c’est le code appelant qui transmettra cette donnée au constructeur ;
  • lignes 197-122 : nous n’implémenterons pas la méthode [executeBatchImpots] dont le but final était d’enregistrer des résultats de simulations dans un fichier texte. Nous voulons un code qui fonctionne à la fois sous [node.js] et dans un navigateur. Or sauvegarder des données sur le système de fichiers de la machine exécutant le navigateur client n’est pas possible ;

Avec ces restrictions, le code de la classe Javascript [Métier] 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
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
178
179
180
181
182
183
184
185
186
187
'use strict';

// classe Métier
class Métier {

  // constructeur
  constructor(taxAdmindata) {
    // this.taxAdminData : données de l'administration fiscale
    this.taxAdminData = taxAdmindata;
  }

  // calcul de l'impôt
  // --------------------------------------------------------------------------
  calculerImpot(marié, enfants, salaire) {
    // marié : oui, non
    // enfants : nombre d'enfants
    // salaire : salaire annuel
    // this.taxAdminData : données de l'administration fiscale
    //
    // calcul de l'impôt avec enfants
    const result1 = this.calculerImpot2(marié, enfants, salaire);
    const impot1 = result1["impôt"];
    // calcul de l'impôt sans les enfants
    let result2, impot2, plafondDemiPart;
    if (enfants !== 0) {
      result2 = this.calculerImpot2(marié, 0, salaire);
      impot2 = result2["impôt"];
      // application du plafonnement du quotient familial
      plafondDemiPart = this.taxAdminData.plafondQfDemiPart;
      if (enfants < 3) {
        // PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
        impot2 = impot2 - enfants * plafondDemiPart;
      } else {
        // PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
        impot2 = impot2 - 2 * plafondDemiPart - (enfants - 2) * 2 * plafondDemiPart;
      }
    } else {
      // pas de reclacul de l'impôt
      impot2 = impot1;
      result2 = result1;
    }
    // on prend l'impôt le plus fortdans [impot1, impot2]
    let impot, taux, surcôte;
    if (impot1 > impot2) {
      impot = impot1;
      taux = result1["taux"];
      surcôte = result1["surcôte"];
    } else {
      surcôte = impot2 - impot1 + result2["surcôte"];
      impot = impot2;
      taux = result2["taux"];
    }
    // calcul d'une éventuelle décôte
    const décôte = this.getDecôte(marié, impot);
    impot -= décôte;
    // calcul d'une éventuelle réduction d'impôts
    const réduction = this.getRéduction(marié, salaire, enfants, impot);
    impot -= réduction;
    // résultat
    return {
      "impôt": Math.floor(impot), "surcôte": surcôte, "décôte": décôte, "réduction": réduction,
      "taux": taux
    };
  }

  // --------------------------------------------------------------------------
  calculerImpot2(marié, enfants, salaire) {
    // marié : oui, non
    // enfants : nombre d'enfants
    // salaire : salaire annuel
    // this->taxAdminData : données de l'administration fiscale
    //
    // nombre de parts
    marié = marié.toLowerCase();
    let nbParts;
    if (marié === "oui") {
      nbParts = enfants / 2 + 2;
    } else {
      nbParts = enfants / 2 + 1;
    }
    // 1 part par enfant à partir du 3ième
    if (enfants >= 3) {
      // une demi-part de + pour chaque enfant à partir du 3ième
      nbParts += 0.5 * (enfants - 2);
    }
    // revenu imposable
    const revenuImposable = this.getRevenuImposable(salaire);
    // surcôte
    let surcôte = Math.floor(revenuImposable - 0.9 * salaire);
    // pour des pbs d'arrondi
    if (surcôte < 0) {
      surcôte = 0;
    }
    // quotient familial
    const quotient = revenuImposable / nbParts;
    // calcul de l'impôt
    const limites = this.taxAdminData.limites;
    const coeffR = this.taxAdminData.coeffR;
    const coeffN = this.taxAdminData.coeffN;
    // est mis à la fin du tableau limites pour arrêter la boucle qui suit
    limites[limites.length - 1] = quotient;
    // recherche du taux d'imposition
    let i = 0;
    while (quotient > limites[i]) {
      i++;
    }
    // du fait qu'on a placé quotient à la fin du tableau limites, la boucle précédente
    // ne peut déborder du tableau limites
    // maintenant on peut calculer l'impôt
    const impôt = Math.floor(revenuImposable * coeffR[i] - nbParts * coeffN[i]);
    // résultat
    return { "impôt": impôt, "surcôte": surcôte, "taux": coeffR[i] };
  }

  // revenuImposable=salaireAnnuel-abattement
  // l'abattement a un min et un max
  getRevenuImposable(salaire) {
    // abattement de 10% du salaire
    let abattement = 0.1 * salaire;
    // cet abattement ne peut dépasser taxAdminData.getAbattementDixPourCentMax()
    if (abattement > this.taxAdminData.abattementDixPourCentMax) {
      abattement = this.taxAdminData.abattementDixPourcentMax;
    }
    // l'abattement ne peut être inférieur à taxAdminData.getAbattementDixPourcentMin()
    if (abattement < this.taxAdminData.abattementDixPourcentMin) {
      abattement = this.taxAdminData.abattementDixPourcentMin;
    }
    // revenu imposable
    const revenuImposable = salaire - abattement;
    // résultat
    return Math.floor(revenuImposable);
  }

  // calcule une décôte éventuelle
  getDecôte(marié, impots) {
    // au départ, une décôte nulle
    let décôte = 0;
    // montant maximal d'impôt pour avoir la décôte
    let plafondImpôtPourDécôte = marié === "oui" ?
      this.taxAdminData.plafondImpotCouplePourDecote :
      this.taxAdminData.plafondImpotCelibatairePourDecote;
    let plafondDécôte;
    if (impots < plafondImpôtPourDécôte) {
      // montant maximal de la décôte
      plafondDécôte = marié === "oui" ?
        this.taxAdminData.plafondDecoteCouple :
        this.taxAdminData.plafondDecoteCelibataire;
      // décôte théorique
      décôte = plafondDécôte - 0.75 * impots;
      // la décôte ne peut dépasser le montant de l'impôt
      if (décôte > impots) {
        décôte = impots;
      }
      // pas de décôte <0
      if (décôte < 0) {
        décôte = 0;
      }
    }
    // résultat
    return Math.ceil(décôte);
  }

  // calcule une réduction éventuelle
  getRéduction(marié, salaire, enfants, impots) {
    // le plafond des revenus pour avoir droit à la réduction de 20%
    let plafondRevenuPourRéduction = marié === "oui" ?
      this.taxAdminData.plafondRevenusCouplePourReduction :
      this.taxAdminData.plafondRevenusCelibatairePourReduction;
    plafondRevenuPourRéduction += enfants * this.taxAdminData.valeurReducDemiPart;
    if (enfants > 2) {
      plafondRevenuPourRéduction += (enfants - 2) * this.taxAdminData.valeurReducDemiPart;
    }
    // revenu imposable
    const revenuImposable = this.getRevenuImposable(salaire);
    // réduction
    let réduction = 0;
    if (revenuImposable < plafondRevenuPourRéduction) {
      // réduction de 20%
      réduction = 0.2 * impots;
    }
    // résultat
    return Math.ceil(réduction);
  }
}

// export de la classe
export default Métier;
  • le code Javascript suit scrupuleusement le code PHP ;
  • la classe [Métier] est exportée, ligne 187 ;

La classe Javascript [Dao2]

image7

La classe [Dao2] implémente la couche [dao] du client Javascript ci-dessus 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
 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 Dao2 {

  // 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 Dao2;

Commentaires

  • la classe [Dao2] n’implémente que trois des requêtes possibles vers le serveur de calcul d’impôt :
    • [init-session] (lignes 17-29) : pour initialiser la session jSON ;
    • [authentifier-utilisateur] (lignes 31-50) : pour s’authentifier ;
    • [get-admindata] (lignes 52-65) : pour avoir les données de l’administration fiscale qui vont permettre de faire les calculs de l’impôt, côté client ;
  • lignes 52-65 : nous introduisons une nouvelle action [get-admindata] vers le serveur. Cette action n’était pas jusqu’alors implémentée. Nous le faisons maintenant.

Modification du serveur de calcul de l’impôt

Le serveur de calcul de l’impôt doit implémenter une nouvelle action. Nous allons le faire sur la version 14 du serveur. L’action à implémenter a les caractéristiques suivantes :

  • elle est demandée par une opération [get /main.php?action=get-admindata] ;
  • elle rend la chaîne jSON d’un objet encapsulant les données de l’administration fiscale ;

Nous allons revoir comment ajouter une action à notre serveur.

La modification se fera sous Netbeans :

image8

En [2], nous modifions le fichier [config.json] pour ajouter la nouvelle action :

 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
{
    "databaseFilename": "Config/database.json",
    "corsAllowed": true,
    "rootDirectory": "C:/myprograms/laragon-lite/www/php7/scripts-web/impots/version-14",
    "relativeDependencies": [

        "/Entities/BaseEntity.php",
        "/Entities/Simulation.php",
        "/Entities/Database.php",
        "/Entities/TaxAdminData.php",
        "/Entities/ExceptionImpots.php",

        "/Utilities/Logger.php",
        "/Utilities/SendAdminMail.php",

        "/Model/InterfaceServerDao.php",
        "/Model/ServerDao.php",
        "/Model/ServerDaoWithSession.php",
        "/Model/InterfaceServerMetier.php",
        "/Model/ServerMetier.php",

        "/Responses/InterfaceResponse.php",
        "/Responses/ParentResponse.php",
        "/Responses/JsonResponse.php",
        "/Responses/XmlResponse.php",
        "/Responses/HtmlResponse.php",

        "/Controllers/InterfaceController.php",
        "/Controllers/InitSessionController.php",
        "/Controllers/ListerSimulationsController.php",
        "/Controllers/AuthentifierUtilisateurController.php",
        "/Controllers/CalculerImpotController.php",
        "/Controllers/SupprimerSimulationController.php",
        "/Controllers/FinSessionController.php",
        "/Controllers/AfficherCalculImpotController.php",
        "/Controllers/AdminDataController.php"
    ],
    "absoluteDependencies": [
        "C:/myprograms/laragon-lite/www/vendor/autoload.php",
        "C:/myprograms/laragon-lite/www/vendor/predis/predis/autoload.php"
    ],
    "users": [
        {
            "login": "admin",
            "passwd": "admin"
        }
    ],
    "adminMail": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "plantage du serveur de calcul d'impôts",
        "tls": "FALSE",
        "attachments": []
    },
    "logsFilename": "Logs/logs.txt",
    "actions":
            {
                "init-session": "\\InitSessionController",
                "authentifier-utilisateur": "\\AuthentifierUtilisateurController",
                "calculer-impot": "\\CalculerImpotController",
                "lister-simulations": "\\ListerSimulationsController",
                "supprimer-simulation": "\\SupprimerSimulationController",
                "fin-session": "\\FinSessionController",
                "afficher-calcul-impot": "\\AfficherCalculImpotController",
                "get-admindata": "\\AdminDataController"
            },
    "types": {
        "json": "\\JsonResponse",
        "html": "\\HtmlResponse",
        "xml": "\\XmlResponse"
    },
    "vues": {
        "vue-authentification.php": [700, 221, 400],
        "vue-calcul-impot.php": [200, 300, 341, 350, 800],
        "vue-liste-simulations.php": [500, 600]
    },
    "vue-erreurs": "vue-erreurs.php"
}

La modification consiste :

  • ligne 67 : ajouter l’action [get-admindata] et l’associer à un contrôleur ;
  • ligne 36 : déclarer ce contrôleur dans la liste des classes à charger par l’application PHP ;

La phase suivante est d’implémenter le contrôleur [AdminDataController] [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
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
<?php

namespace Application;

// dépendances Symfony
use \Symfony\Component\HttpFoundation\Response;
use \Symfony\Component\HttpFoundation\Request;
use \Symfony\Component\HttpFoundation\Session\Session;
// alias de la couche [dao]
use \Application\ServerDaoWithSession as ServerDaoWithRedis;

class AdminDataController implements InterfaceController {

  // $config est la configuration de l'application
  // traitement d'une requête Request
  // utile la session Session et peut la modifier
  // $infos sont des informations supplémentaires propres à chaque contrôleur
  // rend un tableau [$statusCode, $état, $content, $headers]
  public function execute(
    array $config,
    Request $request,
    Session $session,
    array $infos = NULL): array {

    // on doit avoir un unique paramètre GET
    $method = strtolower($request->getMethod());
    $erreur = $method !== "get" || $request->query->count() != 1;
    if ($erreur) {
      // on note l'erreur
      $message = "il faut utiliser la méthode [get] avec l'unique paramètre [action] dans l'URL";
      $état = 1001;
      // retour résultat au contrôleur principal
      return [Response::HTTP_BAD_REQUEST, $état, ["réponse" => $message], []];
    }

    // on peut travailler
    // Redis
    \Predis\Autoloader::register();
    try {
      // client [predis]
      $redis = new \Predis\Client();
      // on se connecte au serveur pour voir s'il est là
      $redis->connect();
    } catch (\Predis\Connection\ConnectionException $ex) {
      // ça s'est mal passé
      // retour résultat avec erreur au contrôleur principal
      $état = 1050;
      return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
        ["réponse" => "[redis], " . utf8_encode($ex->getMessage())], []];
    }

    // récupération des données de l'administration fiscale
    // on cherche d'abord dans le cache [redis]
    if (!$redis->get("taxAdminData")) {
      try {
        // on va chercher les données fiscales en base de données
        $dao = new ServerDaoWithRedis($config["databaseFilename"], NULL);
        // taxAdminData
        $taxAdminData = $dao->getTaxAdminData();
        // on met dans redis les données récupérées
        $redis->set("taxAdminData", $taxAdminData);
      } catch (\RuntimeException $ex) {
        // ça s'est mal passé
        // retour résultat avec erreur au contrôleur principal
        $état = 1041;
        return [Response::HTTP_INTERNAL_SERVER_ERROR, $état,
          ["réponse" => utf8_encode($ex->getMessage())], []];
      }
    } else {
      // les données fiscales sont prises dans la mémoire [redis] de portée [application]
      $arrayOfAttributes = \json_decode($redis->get("taxAdminData"), true);
      // on instancie un objet [TaxAdminData] à partir du tableau d'attributs précédent
      $taxAdminData = (new TaxAdminData())->setFromArrayOfAttributes($arrayOfAttributes);
    }

    // retour résultat au contrôleur principal
    $état = 1000;
    return [Response::HTTP_OK, $état, ["réponse" => $taxAdminData], []];
  }

}

Commentaires

  • ligne 12 : comme les autres contrôleurs du serveur, [AdminDataController] implémente l’interface [InterfaceController] constituée par la méthode [execute] des lignes 19-79 ;
  • ligne 78 : comme pour les autres contrôleurs du serveur, la méthode [AdminDataController.execute] rend un tableau [$status, $état, [‘réponse’=>$response]] avec :
    • [$status] : le code de statut de la réponse HTTP ;
    • [$état] : un code interne à l’application représentant l’état dans lequel se trouve le serveur après exécution de la requête du client ;
    • [$response] : un tableau encapsulant la réponse à envoyer au client. Ici, ce tableau sera ultérieurement transformé en chaîne jSON ;
  • lignes 25-34 : on vérifie que l’action [get-admindata] du client est syntaxiquement correcte ;
  • lignes 37-74 : on récupère un objet [TaxAdminData] trouvé soit :
    • lignes 56-59 : dans la base de données si on ne l’a pas trouvé dans le cache [redis] ;
    • lignes 70-73 : dans le cache [redis] ;

Ce code reprend celui du contrôleur [CalculerImpotController] expliqué dans l’article lien. En effet, ce contrôleur devait lui aussi récupérer l’objet [TaxAdminData] encapsulant les données de l’administration fiscale.

Lors des tests du client Javascript, la forme jSON de [TaxAdminData] a posé problème lorsque cet objet était trouvé dans le cache [redis]. Pour le comprendre, examinons sous quelle forme cet objet est stocké dans [redis] :

image9

image10

  • en [5-7], on voit que des valeurs numériques ont été stockées sous forme de chaînes de caractères. PHP s’en est accommodé car l’opérateur + dans les calculs entre nombres et chaînes provoque implicitement un changement de type de la chaîne vers un nombre. Mais Javascript fait le contraire : l’opérateur + dans les calculs entre nombres et chaînes provoque implicitement un changement de type du nombre vers une chaîne de caractères. Les calculs de la classe Javascript [Métier] sont alors erronés ;

Pour remédier à ce problème, nous modifions la méthode [TaxAdminData.setFromArrayOfAttributes] utilisée ligne 71 du contrôleur pour instancier un objet [TaxAdminData] (cf. article) à partir de la chaîne jSON trouvée dans le cache [redis] :

  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
<?php

namespace Application;

class TaxAdminData extends BaseEntity {
  // tranches d'impôt
  protected $limites;
  protected $coeffR;
  protected $coeffN;
  // constantes de calcul de l'impôt
  protected $plafondQfDemiPart;
  protected $plafondRevenusCelibatairePourReduction;
  protected $plafondRevenusCouplePourReduction;
  protected $valeurReducDemiPart;
  protected $plafondDecoteCelibataire;
  protected $plafondDecoteCouple;
  protected $plafondImpotCouplePourDecote;
  protected $plafondImpotCelibatairePourDecote;
  protected $abattementDixPourcentMax;
  protected $abattementDixPourcentMin;

  // initialisation
  public function setFromJsonFile(string $taxAdminDataFilename) {
    // parent
    parent::setFromJsonFile($taxAdminDataFilename);
    // on vérifie les valeurs des attributs
    $this->checkAttributes();
    // on rend l'objet
    return $this;
  }

  protected function check($value): \stdClass {
    // $value est un tableau d'éléments de type string ou un unique élément
    if (!\is_array($value)) {
      $tableau = [$value];
    } else {
      $tableau = $value;
    }
    // on transforme le tableau de strings en tableau de réels
    $newTableau = [];
    $result = new \stdClass();
    // les éléments du tableau doivent être des nombres décimaux positifs ou nuls
    $modèle = '/^\s*([+]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$/';
    for ($i = 0; $i < count($tableau); $i ++) {
      if (preg_match($modèle, $tableau[$i])) {
        // on met le float dans newTableau
        $newTableau[] = (float) $tableau[$i];
      } else {
        // on note l'erreur
        $result->erreur = TRUE;
        // on quitte
        return $result;
      }
    }
    // on rend le résultat
    $result->erreur = FALSE;
    if (!\is_array($value)) {
      // une seule valeur
      $result->value = $newTableau[0];
    } else {
      // une liste de valeurs
      $result->value = $newTableau;
    }
    return $result;
  }

  // initialisation par un tableau d’attributs
  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // parent
    parent::setFromArrayOfAttributes($arrayOfAttributes);
    // on vérifie les valeurs des attributs
    $this->checkAttributes();
    // on rend l'objet
    return $this;
  }

  // vérification des valeurs des attributs
  protected function checkAttributes() {
    // on vérifie que les valeurs des attributs sont des réels >=0
    foreach ($this as $key => $value) {
      if (is_string($value)) {
        // $value doit être un nbre réel >=0 ou un tableau de réels >=0
        $result = $this->check($value);
        // erreur ?
        if ($result->erreur) {
          // on lance une exception
          throw new ExceptionImpots("La valeur de l'attribut [$key] est invalide");
        } else {
          // on note la valeur
          $this->$key = $result->value;
        }
      }
    }

    // on rend l'objet
    return $this;
  }

  // getters et setters
  ...

}

Commentaires

  • ligne 5 : la classe [TaxAdminData] étend la classe [BaseEntity] qui a déjà la méthode [setFromArrayOfAttributes]. Celle-ci ne convenant pas, nous la redéfinissons aux lignes 67-75 ;
  • ligne 70 : la méthode [setFromArrayOfAttributes] de la classe parent est d’abord utilisée pour initialiser les attributs de la classe ;
  • ligne 72 : la méthode [checkAttributes] vérifie que les valeurs associées sont bien des nombres. Si ce sont des chaînes, celles-ci sont converties en nombres ;
  • ligne 74 : l’objet [$this] rendu est alors un objet avec des attributs à valeurs numériques ;
  • lignes 78-93 : la méthode [checkAttributes] vérifie que les valeurs associées aux attributs de l’objet sont bien numériques ;
  • ligne 80 : on parcourt la liste des attributs ;
  • ligne 81 : si la valeur d’un attribut est de type [string] ;
  • ligne 83 : alors on vérifie que cette chaîne représente un nombre ;
  • ligne 90 : si c’est le cas, la chaîne est transformée en nombre et affectée à l’attribut testé ;
  • lignes 85-86 : si ce n’est pas le cas, une exception est lancée ;
  • lignes 32-65 : la fonction [check] fait un peu plus que nécessaire. Elle traite aussi bien des tableaux que des valeurs uniques. Or ici, elle n’est appelée que pour vérifier une valeur de type [string]. Elle rend un objet avec les propriétés [erreur, value] où :
    • [erreur] est un booléen signalant une erreur ou non ;
    • [value] est le paramètre [value] de la ligne 32, transformée en nombre ou tableau de nombres selon les cas ;

La classe [BaseEntity] qui pouvait avoir un attribut nommé [arrayOfAttributes] est modifiée pour ne plus avoir celui-ci : il pollue en effet la chaîne jSON de [TaxAdminData]. La classe est réécrite 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
68
69
70
<?php

namespace Application;

class BaseEntity {

  // initialisation à partir d'un fichier JSON
  public function setFromJsonFile(string $jsonFilename) {
    // on récupère le contenu du fichier des données fiscales
    $fileContents = \file_get_contents($jsonFilename);
    $erreur = FALSE;
    // erreur ?
    if (!$fileContents) {
      // on note l'erreur
      $erreur = TRUE;
      $message = "Le fichier des données [$jsonFilename] n'existe pas";
    }
    if (!$erreur) {
      // on récupère le code JSON du fichier de configuration dans un tableau associatif
      $arrayOfAttributes = \json_decode($fileContents, true);
      // erreur ?
      if ($arrayOfAttributes === FALSE) {
        // on note l'erreur
        $erreur = TRUE;
        $message = "Le fichier de données JSON [$jsonFilename] n'a pu être exploité correctement";
      }
    }
    // erreur ?
    if ($erreur) {
      // on lance une exception
      throw new ExceptionImpots($message);
    }
    // initialisation des attributs de la classe
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // on vérifie la présence de tous les attributs
    $this->checkForAllAttributes($arrayOfAttributes);
    // on rend l'objet
    return $this;
  }

  public function checkForAllAttributes($arrayOfAttributes) {
    // on vérifie que toutes les clés ont été initialisées
    foreach (\array_keys($arrayOfAttributes) as $key) {
      if (!isset($this->$key)) {
        throw new ExceptionImpots("L'attribut [$key] de la classe "
          . get_class($this) . " n'a pas été initialisé");
      }
    }
  }

  public function setFromArrayOfAttributes(array $arrayOfAttributes) {
    // on initialise certains attributs de la classe (pas forcément tous)
    foreach ($arrayOfAttributes as $key => $value) {
      $this->$key = $value;
    }
    // on retourne l'objet
    return $this;
  }

  // toString
  public function __toString() {
    // attributs de l'objet
    $arrayOfAttributes = \get_object_vars($this);
    // chaîne jSON de l'objet
    return \json_encode($arrayOfAttributes, JSON_UNESCAPED_UNICODE);
  }

}

Commentaires

  • ligne 20 : l’attribut [$this→arrayOfAttributes] a été transformée en variable qui doit être désormais passée à la méthode [checkForAllAttributes], ligne 38 qui auparavant opérait sur l’attribut [$this→arrayOfAttributes] ;

A cause de ce changement sur [BaseEntity], la classe [Database] doit être également légèrement 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
<?php

namespace Application;

class Database extends BaseEntity {
  // attributs
  protected $dsn;
  protected $id;
  protected $pwd;
  protected $tableTranches;
  protected $colLimites;
  protected $colCoeffR;
  protected $colCoeffN;
  protected $tableConstantes;
  protected $colPlafondQfDemiPart;
  protected $colPlafondRevenusCelibatairePourReduction;
  protected $colPlafondRevenusCouplePourReduction;
  protected $colValeurReducDemiPart;
  protected $colPlafondDecoteCelibataire;
  protected $colPlafondDecoteCouple;
  protected $colPlafondImpotCelibatairePourDecote;
  protected $colPlafondImpotCouplePourDecote;
  protected $colAbattementDixPourcentMax;
  protected $colAbattementDixPourcentMin;

  // setter
  // initialisation
  public function setFromJsonFile(string $jsonFilename) {
    // parent
    parent::setFromJsonFile($jsonFilename);
    // on retourne l'objet
    return $this;
  }

  // getters et setters
  ...
}

Commentaires

  • dans le code original, après la ligne 30, on appelait la méthode [parent::checkForAllAttributes]. Cela n’a plus à être fait puisque c’est désormais pris automatiquement en charge par la méthode [parent::setFromJsonFile($jsonFilename)] ;

Tests [Postman] du serveur

[Postman] a été présenté dans l’article lien.

Nous utilisons les tests Postman suivants :

image11

image12

image13

Le résultat jSON de cette dernière requête est la suivante :

image14

  • en [5-8], on peut remarquer que les attributs de la chaîne jSON ont bien des valeurs numériques (et non chaînes de caractères). Ce résultat va permettre à la classe Javascript [Métier] de s’exécuter normalement ;

Le script principal [main]

image15

Le script principal [main] du client Javascript 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
// imports
import axios from 'axios';

// imports
import Dao from './Dao2';
import Métier from './Métier';

// fonction asynchrone [main]
async function main() {
  // configuration axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instanciation couche [dao]
  const dao = new Dao(axios);
  // requêtes HTTP
  let taxAdminData;
  try {
    // init session
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    // authentification
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    // données fiscales
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    taxAdminData = response.réponse;
  } catch (error) {
    // on logue l'erreur
    console.log("erreur=", error.message);
    // fin
    return;
  }

  // instanciation couche [métier]
  const métier = new Métier(taxAdminData);

  // calculs d'impôt
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // liste des simulations
  log("-----------liste-des-simulations");
  log(simulations);
  // suppression d'une simulation
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// exécution
main();

Commentaires

  • lignes 5-6 : imports des classes [Dao] et [Métier] ;
  • ligne 9 : la fonction asynchrone [main] qui va organiser le dialogue avec le serveur grâce à la classe [Dao] et demander à la classe [Métier] de faire les calculs d’impôt ;
  • lignes 10-36 : le script appelle successivement et de façon bloquante, les méthodes [initSession, authentifierUtilisateur, getAdminData] de la couche [dao] ;
  • ligne 38 : on n’a plus besoin de la couche [dao]. On a tous les éléments pour faire travailler la couche [métier] du client Javascript ;
  • lignes 41-46 : on fait trois calculs d’impôt dont on cumule les résultats dans un tableau [simulations] ;
  • ligne 49 : on affiche le tableau des simulations ;
  • ligne 52 : on supprime l’une d’elles ;

Les résultats de l’exécution du script principal sont 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
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
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\client impôts\client http 2\main2.js"
"-----------init-session"
{
  "action": "init-session",
  "état": 700,
  "réponse": "session démarrée avec type [json]"
}
"-----------authentifier-utilisateur"
{
  "action": "authentifier-utilisateur",
  "état": 200,
  "réponse": "Authentification réussie [admin, admin]"
}
"-----------get-admindata"
{
  "action": "get-admindata",
  "état": 1000,
  "réponse": {
    "limites": [
      9964,
      27519,
      73779,
      156244,
      0
    ],
    "coeffR": [
      0,
      0.14,
      0.3,
      0.41,
      0.45
    ],
    "coeffN": [
      0,
      1394.96,
      5798,
      13913.69,
      20163.45
    ],
    "plafondQfDemiPart": 1551,
    "plafondRevenusCelibatairePourReduction": 21037,
    "plafondRevenusCouplePourReduction": 42074,
    "valeurReducDemiPart": 3797,
    "plafondDecoteCelibataire": 1196,
    "plafondDecoteCouple": 1970,
    "plafondImpotCouplePourDecote": 2627,
    "plafondImpotCelibatairePourDecote": 1595,
    "abattementDixPourcentMax": 12502,
    "abattementDixPourcentMin": 437
  }
}
"-----------calculer-impot x 3"
"-----------liste-des-simulations"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 3250,
    "surcôte": 370,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.3
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]
"-----------suppression simulation n° 1"
[
  {
    "impôt": 502,
    "surcôte": 0,
    "décôte": 857,
    "réduction": 126,
    "taux": 0.14
  },
  {
    "impôt": 1687,
    "surcôte": 0,
    "décôte": 0,
    "réduction": 0,
    "taux": 0.14
  }
]

[Done] exited with code=0 in 0.583 seconds

Client HTTP 3

image16

Dans cette section, nous portons l’application [Client HTTP 2] dans un navigateur selon l’architecture suivane :

image17

Le portage n’est pas immédiat. Si [node.js] sait exécuter du Javascript ES6, ce n’est pas le cas en général des navigateurs. Il faut alors utiliser des outils qui traduisent le code ES6 en code ES5 compris par les navigateurs récents. Heureusement ces outils sont à la fois puissants et plutôt simples d’utilisation.

Nous avons ici suivi l’article [How to write ES6 code that’s safe to run in the browser - Web Developer’s Journal].

Dans le dossier [client HTTP 3/src], on a mis les éléments [main.js, Métier.js, Dao2.js] de l’application [Client Http 2] que nous venons de développer.

Initialisation du projet

Nous allons travailler dans le dossier [client http 3]. Nous ouvrons un terminal dans [VSCode] et nous nous positionnons sur ce dossier :

image18

Nous initialisons ce projet avec la commande [npm init] et nous acceptons pour les questions posées les réponses proposées par défaut :

image19

  • en [4-5], le fichier de configuration du projet [package.json] généré à partir des différentes réponses données ;

Installation des dépendances du projet

Nous allons installer les dépendances suivantes :

  • [@babel/core] : le coeur de l’outil [Babel] [https://babeljs.io] qui transforme du code ES 2015+ en code exécutable sur les navigateurs récents et plus anciens ;
  • [@babel/preset-env] : fait partie de l’outillage Babel. Intervient avant la transpilation ES6 → ES5 ;
  • [babel-loader] : cette dépendance permet à l’outil [webpack] de faire appel à l’outil [Babel] ;
  • [webpack] : chef d’orchestre. C’est [webpack] qui fait appel à Babel pour faire la transpilation des codes ES6 → ES5 puis lui qui assemble la totalité des fichiers résultants dans un unique fichier ;
  • [webpack-cli] : nécessaire à [webpack] ;
  • [@webpack-cli/init] : utilisé pour configurer [webpack] ;
  • [webpack-dev-server] : fournit un serveur web de développement opérant par défaut sur le port 8080. Lorsque les fichiers sources sont modifiés, recharge automatiquement l’application web ;

Les dépendances du projet sont installées de la façon suivante dans un terminal de [VSCode] :

npm –save-dev install @babel/core @babel/preset-env babel-loader webpack webpack-cli webpack-dev-server @webpack-cli/init

image20

Après l’installation des dépendances, le fichier [package.json] a évolué 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
{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "client jS du serveur de calcul de l'impôt",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "cross-env": "^6.0.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  }
}
  • lignes 12-19 : les dépendances du projet sont des [devDependencies] : on en a besoin pendant la phase de développement mais plus dans la phase de production. En effet, en production, c’est le fichier [dist/main.js] qui est utilisé. Il est codé en ES5 et n’a plus besoin des outils de transpilation de code ES6 vers du code ES5 ;

Il nous faut ajouter deux dépendances au projet :

  • [core-js] : contient des « polyfills » pour ECMAScript 2019. Un polyfill permet d’exécuter un code récent, comme ECMAScript 2019 (sept 2019), sur des navigateurs anciens ;
  • [regenerator-runtime] : selon le site de la bibliothèque –> [Source transformer enabling ECMAScript 6 generator functions in JavaScript-of-today] ;

Ces deux dépendances remplacent, à partir de Babel 7, la dépendance [@babel/polyfill] qui jouait auparavant ce rôle et qui est maintenant (sept 2019) dépréciée. Elles sont installées de la façon suivante :

image21

Le fichier [package.json] évolue alors 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
{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}

L’utilisation des dépendances [core-js, regenerator-runtime] impose de mettre les [imports] suivants (lignes 3-4) dans le script principal [src/main.js] :

1
2
3
4
5
6
7
8
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// imports
import Dao from './Dao2';
import Métier from './Métier';

Configuration de [webpack]

[webpack] est l’outil qui va piloter :

  • la transpilation ES6 → ES5 de tous les fichiers Javascript du projet ;
  • l’assemblage des fichiers générés dans un unique fichier ;

Cet outil est piloté par un fichier de configuration [webpack.config.js] qui peut être généré grâce à une dépendance nommée [@webpack-cli/init] (sept 2019). Celle-ci a été installée avec les autres au paragraphe lien.

Nous exécutons la commande [npx webpack-cli init] dans un terminal [VSCode] :

image22

Après avoir répondu aux différentes questions (dont on peut accepter la plupart des réponses proposées par défaut), un fichier [webpack.config.js] est généré à la racine du projet [4] :

Le fichier [webpack.config.js] ressemble à ceci :

 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
/* eslint-disable */

const path = require('path');
const webpack = require('webpack');

/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
 * https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */

const HtmlWebpackPlugin = require('html-webpack-plugin');

/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
 * https://github.com/jantimon/html-webpack-plugin
 *
 */

module.exports = {
     mode: 'development',
     entry: './src/index.js',

     output: {
             filename: '[name].[chunkhash].js',
             path: path.resolve(__dirname, 'dist')
     },

     plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],

     module: {
             rules: [
                     {
                             test: /.(js|jsx)$/,
                             include: [path.resolve(__dirname, 'src')],
                             loader: 'babel-loader',

                             options: {
                                     plugins: ['syntax-dynamic-import'],

                                     presets: [
                                             [
                                                     '@babel/preset-env',
                                                     {
                                                             modules: false
                                                     }
                                             ]
                                     ]
                             }
                     }
             ]
     },

     optimization: {
             splitChunks: {
                     cacheGroups: {
                             vendors: {
                                     priority: -10,
                                     test: /[\\/]node_modules[\\/]/
                             }
                     },

                     chunks: 'async',
                     minChunks: 1,
                     minSize: 30000,
                     name: true
             }
     },

     devServer: {
             open: true
     }
};

Je ne comprends pas tous les détails de ce fichier mais on peut remarquer quelques points :

  • ligne 1 : le fichier ne contient pas du code ES6. [Eslint] déclare alors des erreurs qui remontent jusqu’à la racine du projet [javascript]. C’est gênant. Pour éviter qu’Eslint n’analyse un fichier, il suffit de mettre le commentaire de la ligne 1 ;
  • ligne 31 : on travaille en mode [développement] ;
  • ligne 32 : le script d’entrée est ici [src/index.js]. Nous serons amenés à changer cela ;
  • ligne 36 : le dossier où seront tockées les produits de [webpack] sera le dossier [dist] ;
  • ligne 46 : on voit que [webpack] utilise [babel-loader], une des dépendances que nous avons installées ;
  • ligne 54 : on voit que [webpack] utilise [@babel-preset/env], une des dépendances que nous avons installées ;

L’initialisation de [webpack] a modifié le fichier [package.json] (il demande l’autorisation) :

 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
{
  "name": "client-http-3",
  "version": "1.0.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "serge.tahe@gmail.com",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "@webpack-cli/init": "^0.2.2",
    "babel-loader": "^8.0.6",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "html-webpack-plugin": "^3.2.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "core-js": "^3.2.1",
    "regenerator-runtime": "^0.13.3"
  }
}
  • ligne 4 : elle a été modifiée ;
  • lignes 8-9, 18-19 : elles ont été ajoutées ;
  • ligne 8 : la tâche [npm] qui permet de compiler le projet ;
  • ligne 9 : la tâche [npm] qui permet de l’exécuter ;
  • ligne 18 : ?
  • ligne 19 : permet la génération d’un fichier [dist/index.html] embarquant automatiquement le script [dist/main.js] généré par [webpack] et c’est celui-ci qui est exploité lorsque le projet est exécuté ;

Enfin la configuration de [webpack] a généré un fichier [src/index.js] :

image23

Le contenu de [index.js] est le suivant (sept 2019) :

  1. console.log(« Hello World from your main file! »);

Compilation et exécution du projet

Le fichier [package.json] a trois tâches [npm] :

1
2
3
4
5
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server"
},

Ces tâches sont comprises par [VSCode] qui les propose à l’exécution :

image24

  • en [1-3], on compile le projet ;
  • en [4] : le projet est compilé dans [dist/main.hash.js] et une page [dist/index.html] est créée ;

La page [index.html] générée est la suivante :

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="main.87afc226fd6d648e7dea.js"></script></body>
</html>

Cette page se contente donc d’encapsuler le fichier [main.hash.js] généré par [webpack].

Le projet est exécuté par la tâche [start] :

image25

La page [dist/index.html] est alors chargée sur un serveur, appartenant à la suite [webpack], opérant sur le port 8080 de la machine locale et affichée par le navigateur par défaut de la machine :

image26

  • en [2], le port de service du serveur web de [webpack] ;
  • en [3], le corps de la page [dist/index.html] est vide ;
  • en [4], l’onglet [console] des outils de développement du navigateur, ici Firefox (F12) ;
  • en [5], le résultat de l’exécution du fichier [src/index.js]. On rappelle que le contenu de celui-ci était le suivant :
  1. console.log(« Hello World from your main file! »);

Maintenant, changeons ce contenu en la ligne suivante :

  1. console.log(« Bonjour le monde »);

Automatiquement (sans recompiler), de nouveaux fichiers [main.js, index.html] sont générés et le nouveau fichier [index.html] chargé dans le navigateur :

image27

Il n’est pas nécessaire d’exécuter la tâche [build] avant la tâche [start] : cette dernière fait d’abord la compilation du projet. Elle ne stocke pas les produits de cette compilation dans le dossier [dist]. Pour s’en apercevoir, il suffit de supprimer ce dossier. On verra alors que la tâche [start] compile et exécute le projet sans créer le dossier [dist]. Elle semble stocker ses produits [index.html, main.hash.js] dans un dossier propre à [webpackdev-server]. Ce comportement est suffisant pour nos tests.

Lorsque le serveur de développement est lancé, toute modification sauvegardée d’un des fichiers du projet provoque une recompilation. Pour cette raison, nous inhibons le mode [Auto Save] de [VSCode]. En effet, nous ne voulons pas de recompilation dès qu’on tape des caractères dans un des fichiers du projet. Nous ne voulons de recompilation qu’au moment des sauvegardes des modifications :

image28

  • en [2], l’option [Auto Save] ne doit pas être cochée ;

Tests du client Javascript du serveur de calcul de l’impôt

Pour tester le client Javascript du serveur de calcul de l’impôt, il faut désigner [main.js] [1] comme le point d’entrée du projet dans le fichier [webpack.config.js] [2-3] :

image29

N’oublions pas que le script [main.js] doit inclure deux imports supplémentaires par rapport à sa version dans [Client http 2] :

image30

Par ailleurs, nous avons légèrement modifié le code pour gérer les erreurs que peut envoyer 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
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// imports
import Dao from './Dao2';
import Métier from './Métier';

// fonction asynchrone [main]
async function main() {
  // configuration axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  // instanciation couche [dao]
  const dao = new Dao(axios);
  // requêtes HTTP
  let taxAdminData;
  try {
    // init session
    log("-----------init-session");
    let response = await dao.initSession();
    log(response);
    if (response.état != 700) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // authentification
    log("-----------authentifier-utilisateur");
    response = await dao.authentifierUtilisateur("admin", "admin");
    log(response);
    if (response.état != 200) {
      throw new Error(JSON.stringify(response.réponse));
    }
    // données fiscales
    log("-----------get-admindata");
    response = await dao.getAdminData();
    log(response);
    if (response.état != 1000) {
      throw new Error(JSON.stringify(response.réponse));
    }
    taxAdminData = response.réponse;
  } catch (error) {
    // on logue l'erreur
    console.log("erreur=", error.message);
    // fin
    return;
  }

  // instanciation couche [métier]
  const métier = new Métier(taxAdminData);

  // calculs d'impôt
  log("-----------calculer-impot x 3");
  const simulations = [];
  simulations.push(métier.calculerImpot("oui", 2, 45000));
  simulations.push(métier.calculerImpot("non", 2, 45000));
  simulations.push(métier.calculerImpot("non", 1, 30000));
  // liste des simulations
  log("-----------liste-des-simulations");
  log(simulations);
  // suppression d'une simulation
  log("-----------suppression simulation n° 1");
  simulations.splice(1, 1);
  log(simulations);
}

// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// exécution
main();

Commentaires

  • aux lignes [24-26], [31-33], [38-40], on teste le code [response.état] envoyé dans la réponse jSON du serveur. Si ce code dénote une erreur, une exception est lancée avec pour message d’erreur la chaîne jSON de la réponse du serveur [response.réponse] ;

Ceci fait, nous exécutons le projet [5-6].

La page [index.html] est alors générée et chargée dans le navigateur :

image31

  • en [7], on voit que l’action [init-session] n’a pu aller à son terme à cause d’un problème [CORS] (Cross-Origin Resource Sharing) ;

Le problème CORS vient de la relation client / serveur :

  • notre client Javascript a été téléchargée sur la machine [http://localhost:8080];
  • le serveur de calcul d’impôt s’exécute sur la machine [http://localhost:80];
  • le client et le serveur ne sont pas alors dans les mêmes domaines (même machine mais pas même port);
  • le navigateur qui exécute le client Javascript chargé à partir de la machine [http://localhost:8080] bloque toute requête qui n’a pas pour cible [http://localhost:80]. C’est une mesure de sécurité. Aussi bloque-t-il la requête du client vers le serveur qui opère sur la machine [http://localhost:80];

En fait, le navigateur ne bloque pas totalement la requête. Il attend en fait que le serveur lui ‘dise’ qu’il accepte les requêtes inter-domaines. S’il obtient cette autorisation, le navigateur transmettra alors la requête inter-domaines.

Le serveur donne son autorisation en envoyant des entêtes HTTP particuliers :

1
2
3
4
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Headers: Accept, Content-Type
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Credentials: true
  • ligne 1 : le client Javascript opère sur le domaine [http://localhost:8080]. Le serveur doit explicitement répondre qu’il accepte ce domaine ;
  • ligne 2 : le client Javascript va utiliser dans ses requêtes les entêtes HTTP [Accept, Content-Type] :
    • [Accept] : cet entête est envoyé dans toute requête ;
    • [Content-Type] : cet entête est utilisé dans les opérations POST pour indiquer le type des paramètres du POST ;
Le serveur doit explicitement accepter ces deux entêtes HTTP ;
  • ligne 3 : le client Javascript va utiliser des requêtes GET et POST. Le serveur doit explicitement accepter ces deux types de requêtes ;
  • ligne 4 : le client Javascript va envoyer des cookies de session. Le serveur les accepte avec l’entête de la ligne 4 ;

Il nous faut donc modifier le serveur. Nous faisons cela dans [Netbeans]. Le problème des CORS est un problème rencontré uniquement en mode développement. En production, le client et le serveur travailleront dans le même domaine [http://localhost:80] et il n’y aura pas de problème CORS. Il nous faut donc un moyen d’autoriser ou pas les requêtes CORS par configuration du serveur.

image32

Les modifications du serveur se font à trois endroits :

  • [1, 4] : dans le fichier de configuration [config.json] pour y mettre un booléen qui contrôlera l’acceptation ou non des requêtes inter-domaines ;
  • [2] : dans la classe [ParentResponse] qui envoie la réponse au client Javascript. C’est elle qui enverra les entêtes CORS attendus par le navigateur client ;
  • [3] : dans les classes [HtmlResponse, JsonResponse, XmlResponse] qui génèrent les réponses pour respectivement les sessions [html, json, xml]. Ces classes doivent passer à leur classe parente [2], le booléen [corsAllowed] trouvé en [4]. Cela se fait en [5], en passant le tableau image du fichier jSON [2] ;

La classe [ParentResponse] [2] é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
<?php

namespace Application;

// dépendances Symfony
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ParentResponse {

  // int $statusCode : le code HTTP de statut de la réponse
  // string $content : le corps de la réponse à envoyer
  // selon les cas, c'est une chaîne JSON, XML, HTML
  // array $headers : les entêtes HTTP à ajouter à la réponse

  public function sendResponse(
    Request $request,
    int $statusCode,
    string $content,
    array $headers,
    array $config): void {

    // préparation de la réponse texte du serveur
    $response = new Response();
    $response->setCharset("utf-8");
    // code de statut
    $response->setStatusCode($statusCode);
    // headers pour les requêtes inter-domaines
    if ($config['corsAllowed']) {
      $origin = $request->headers->get("origin");
      if (strpos($origin, "http://localhost") === 0) {
        $headers = array_merge($headers,
          ["Access-Control-Allow-Origin" => $origin,
            "Access-Control-Allow-Headers" => "Accept, Content-Type",
            "Access-Control-Allow-Methods" => "GET, POST",
            "Access-Control-Allow-Credentials" => "true"
        ]);
      }
    }
    foreach ($headers as $text => $value) {
      $response->headers->set($text, $value);
    }
    // cas particulier de la méthode [OPTIONS]
    // seuls les entêtes sont importants dans ce cas
    $method = strtolower($request->getMethod());
    if ($method === "options") {
      $content = "";
      $response->setStatusCode(Response::HTTP_OK);
    }
    // on envoie la réponse
    $response->setContent($content);
    $response->send();
  }

}
  • ligne 29 : on regarde si on doit gérer les requêtes inter-domaines. Si oui, on va générer les entêtes HTTP CORS (lignes 33-37) même si la requête courante n’est pas une requête inter-domaines. Dans ce dernier cas, les entêtes CORS seront inutiles et resteront inexploités par le client ;
  • ligne 30 : dans une requête inter-domaines, le navigateur client qui interroge le serveur envoie un entête HTTP [Origin: http://localhost:8080] (dans le cas précis de notre client Javascript). Ligne 30, on récupère cet entête HTTP dans la requête [$request] ;
  • ligne 31 : on n’acceptera des requêtes inter-domaines provenant uniquement de la machine [http://localhost]. On rappelle que ces requêtes n’ont lieu qu’en mode développement du projet ;
  • lignes 32-36 : on ajoute les entêtes CORS aux entêtes déjà présents dans le tableau [$headers] ;
  • lignes 45-49 : la façon dont le navigateur client demande les autorisations CORS peut différer selon le client exécuté. Il arrive parfois que le navigateur client demande ces autorisations avec une commande HTTP [OPTIONS]. C’est une nouveauté pour notre serveur qui a été construit pour servir uniquement les commandes [GET, POST]. Dans le cas d’une commande [OPTIONS], le serveur génère actuellement une réponse d’erreur. Lignes 46-49, nous corrigeons cela au dernier moment : si ligne 46, nous constatons que la commande courante est une commande [OPTIONS], alors on génère pour le client :
    • lignes 47, 51 : une réponse [$content] vide ;
    • ligne 48 : un code de statut de 200 indiquant que la commande est réussie. La seule chose importante pour cette commande est l’envoi des entêtes CORS des lignes 33-36. C’est ce qu’attend le navigateur client ;

Une fois le serveur ainsi corrigé, le client Javascript s’exécute mieux mais fait apparaître une nouvelle erreur :

image33

  • en [1], la session jSON est correctement initialisée ;
  • en [2], l’action [authentifier-utilisateur] échoue : le serveur indique qu’il n’y a pas de session en cours. Cela signifie que le client Javascript ne lui pas renvoyé correctement le cookie de session qu’il a envoyé lors de l’action [init-session] ;

Examinons les échanges réseau qui ont eu lieu :

image34

  • en [4], la requête [init-session]. Elle s’est bien déroulée avec un code 200 pour le statut de la réponse ;
  • en [5], la requête [authentifier-utilisateur]. Celle-ci échoue avec un code 400 (Bad Request) [6] pour le statut de la réponse ;

Si on examine les entêtes HTTP [7] de la requête [5], on peut voir que le client Javascript n’a pas envoyé l’entête HTTP [Cookie] qui lui aurait permis de renvoyer le cookie de session envoyé initialement par le serveur. C’est la raison pour laquelle celui-ci déclare qu’il n’y a pas de session.

Pour que le client envoie le cookie de session, il faut ajouter une configuration à l’objet [axios] :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// imports
import axios from 'axios';
import "core-js/stable";
import "regenerator-runtime/runtime";

// imports
import Dao from './Dao2';
import Métier from './Métier';

// fonction asynchrone [main]
async function main() {
  // configuration axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL = 'http://localhost/php7/scripts-web/impots/version-14';
  axios.defaults.withCredentials = true;
  // instanciation couche [dao]
  const dao = new Dao(axios);
  // requêtes HTTP
  let taxAdminData;
...

La ligne 15 demande à ce que les cookies soient inclus dans les entêtes HTTP de la requête [axios]. Remarquons que cela n’avait pas été nécessaire dans l’environnement [node.js]. Il y a donc des différences de code entre les deux environnements.

Une fois cette erreur corrigée, le client Javascript se déroule normalement :

image35

image36

Amélioration du client HTTP 3

Lorsque la classe [Dao2] précédente s’exécute au sein d’un navigateur, la gestion du cookie de session est inutile. En effet, c’est le navigateur qui héberge la couche [dao] qui gère le cookie de session : il renvoie automatiquement tout cookie que le serveur lui envoie. Du coup la classe [Dao2] peut être réécrite en la classe [Dao3] 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
80
81
82
83
84
85
86
"use strict";

// imports
import qs from "qs";

class Dao3 {
  // 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 Dao3;

Tout ce qui avait trait à la gestion du cookie de gestion a disparu.

Nous modifions le projet précédent de la façon suivante :

image37

Dans le dossier [src], nous avons ajouté deux fichiers :

  • la classe [Dao3] que nous venons de présenter ;
  • le fichier [main3] chargée de lancer la nouvelle version ;

Le fichier [main3] reste identique au fichier [main] de la version précédente mais il utilise désormais la classe [Dao3] :

 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
// imports
import axios from "axios";
import "core-js/stable";
import "regenerator-runtime/runtime";

// imports
import Dao from "./Dao3";
import Métier from "./Métier";

// fonction asynchrone [main]
async function main() {
  // configuration axios
  axios.defaults.timeout = 2000;
  axios.defaults.baseURL =
    "http://localhost/php7/scripts-web/impots/version-14";
  axios.defaults.withCredentials = true;
  // instanciation couche [dao]
  const dao = new Dao(axios);
  // requêtes HTTP
  ...
}

// log jSON
function log(object) {
  console.log(JSON.stringify(object, null, 2));
}

// exécution
main();

Le fichier [webpack.config] est modifié pour exécuter maintenant, le script [main3] :

 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
/* eslint-disable */

const path = require("path");
const webpack = require("webpack");

/*
 * SplitChunksPlugin is enabled by default and replaced
 * deprecated CommonsChunkPlugin. It automatically identifies modules which
 * should be splitted of chunk by heuristics using module duplication count and
 * module category (i. e. node_modules). And splits the chunks…
 *
 * It is safe to remove "splitChunks" from the generated configuration
 * and was added as an educational example.
 *
 * https://webpack.js.org/plugins/split-chunks-plugin/
 *
 */

const HtmlWebpackPlugin = require("html-webpack-plugin");

/*
 * We've enabled HtmlWebpackPlugin for you! This generates a html
 * page for you when you compile webpack, which will make you start
 * developing and prototyping faster.
 *
 * https://github.com/jantimon/html-webpack-plugin
 *
 */

module.exports = {
  mode: "development",
  //entry: "./src/mainjs",
  entry: "./src/main3.js",
  output: {
    filename: "[name].[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },

  plugins: [new webpack.ProgressPlugin(), new HtmlWebpackPlugin()],
...
};

Ceci fait, on exécute le projet après avoir lancé le serveur de calcul de l’impôt :

image38

Les résultats obtenus dans la console du navigateur sont identiques à ceux de la version précédente.

Conclusion

Nous avons désormais désormais tous les outils pour développer le code Javascript d’une application web. Nous pouvons :

  • utiliser le code ECMAScript le plus récent ;
  • tester des éléments isolés de ce code dans un environnement [node.js] plus simple pour le débogage et les tests ;
  • porter ensuite ce code dans un navigateur grâce aux outils [babel] et [webpack] ;