Activité : JavaScript toolchain

Situation

Vous êtes au début d’un projet de développement d’une application web. Vous envisagez d’utiliser JavaScript pour le développement de la partie client et de la partie serveur. Vous êtes désormais familiarisé avec le langage de programmation et la programmation asynchrone.

Toutefois, vous réalisez que tous les programmes que vous avez faits jusque là étaient extrêmement simples. Votre expérience en développement d’application vous suffit pour savoir qu’il vous manque quelques éléments essentiels.

La première chose que vous remarquez est que lors de la réalisation de vos tests technologique, le code du fichier main.js ne contient aucune indication du fait qu’il dépend du module onTwoThreeLib.js. En Java, par exemple, une telle dépendance serait indiquée par une clause import. Il semble qu’en JavaScript, du moins dans le navigateur, les différents fichiers .js doivent être chargés dans le bon ordre avec des éléments script dans le code HTML. Vous vous dites qu’une gestion manuelle des dépendances n’est pas optimale et qu’il doit exister une meilleure solution. Vous n’êtes sans doute pas la première personne à y penser.

En effectuant une recherche sur le web avec le mot clé « module » et « javascript », vous trouvez une multitude d’informations, vous remarquez à plusieurs reprises des instructions dont la forme vous dit quelque chose. Vous vous souvenez en avoir rencontré une similaire dans l’un de vos tests technologiques.

1
const express = require('express')

Mais vous réalisez que ce test était pour la plateforme Node.js, pas pour le navigateur. Vous cherchez alors comment utiliser la fonction require dans le navigateur, mais les informations que vous trouvez semblent aller dans tous les sens. On vous parle de la bibliothèque require.js, de format AMD, de format CommonJS, de l’outil browserify, de l’outil webpack, et même de npm qui fait partie de la plateforme Node.js.

Confus, vous en parlez autour de vous. Un collègue vous explique que le JavaScript est défini par les normes ECMAScript et que dans ces normes, la notion de module n’apparaît qu’en 2015. Pire encore, lance-t-il, le support des modules par les navigateurs n’apparaît que fin 2017; avec la complexification des applications web et l’absence d’alternative viable au JavaScript pour l’exécution de code dans le navigateur, tu penses bien que la communauté des développeurs n’a pas attendu sur l’ECMA pour utiliser des modules en JavaScript.

Votre collègue vous explique alors qu’il y a deux approches :

  • Charger le code des bibliothèques à l’exécution.
  • Utiliser un outil pour combiner le code des modules des bibliothèques et du programme dans un seul fichier (bundle) et charger ce bundle à l’aide d’un élément <script> dans le document HTML.

La première approche est celle utilisée par la bibliothèque require.js. Cette bibliothèque est liée au format de module AMD (Asynchronous Module definition). Bien qu’elle soit encore très répandue, il n’est pas recommandé de l’utiliser pour un nouveau projet. Cette approche est également celle adoptée par le standard ECMAScript avec le mot clé import (synchrone) et la fonction import() (asynchrone). À l’exception d’IE, tous les navigateurs supportent depuis peu les modules ES2015 (ES6) dans des éléments script de type module (<scritp type="module">).

La seconde approche est celle mise en œuvre par les static module bundler ou bundler, tels que browserify et webpack. Les développeurs de browserify l’ont déclaré obsolète. Bien qu’il existe d’autres options, la documentation des principaux frameworks clients (Angular, React et Vue) recommande l’utilisation de webpack. Le CLI d’Angular l’utilise implicitement. Le principal avantage de l’approche statique est qu’elle permet d’utiliser npm et la plateforme Node.js pour la gestion de dépendance.

Après vous avoir dit tout cela, votre collègue ajoute encore que l’approche statique comporte un autre avantage. Webpack prend en charge non seulement le bundling du code JavaScript, mais aussi celui du CSS et du HTML. Il permet également, dans une large mesure, de remplacer un task runner comme gulp ou grunt pour lancer des tâches telles que l’exécution de tests, la compilation de ES.next vers ES.now avec Babel ou la compilation de TypeScript en ES.

Ces quelques explications piquent votre curiosité et vous décidez d’étudier cela d’un peu plus près.

Consigne

Nous vous recommandons de réaliser votre projet avec Angular. Bien que les trois principales options que sont Angular, React et Vue se valent du point de vue des fonctionnalités, il semble que la documentation et les outils proposés par Angular soient supérieurs aux autres. Vous restez toutefois libre de choisir n’importe laquelle de ces trois options.

Si vous choisissez de réaliser l’application avec React ou Vue, nous vous recommandons d’utiliser webpack comme bundler et Babel comme compilateur ESnext vers ESnow.

Si vous choisissez de réaliser l’application avec Angular, vous devez utiliser le CLI d’Angular (ng). Vous devez également utiliser le langage TypeScript. Pour faire cours, il s’agit de JavaScript avec des types. Tout ce que vous avez appris avec JavaScript durant vos tests technologiques reste valable.

Quelle que soit l’option que vous choisissez, vous avez besoin de gérer des dépendances avec npm et de savoir comment réaliser des modules en JavaScript. Nous vous suggérons donc de commencer par réaliser le lab8 décrit dans ce document.

Nous vous recommandons ensuite de suivre les tutoriels disponibles pour l’option que vous avez choisie de manière à mettre en place la base de votre projet. Faites attention à vos sources. Assurez-vous que les tutoriels concernent bien la version actuelle du framework et donnez la priorité au tutoriels venant de la documentation du framework.

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître l’utilité d’un gestionnaire de paquet (packet manager) pour l’installation de programmes et de bibliothèques.
  2. Connaître l’utilité d’un gestionnaire de dépendance (dependency manager) pour l’installation d’outils et de bibliothèque nécessaires à un projet.
  3. Connaître l’utilité d’un bundler pour la mise en œuvre de modules JavaScript dans le navigateur.
  4. Connaître l’utilité d’un task runner et de (trans)compilateur pour compiler du code JavaScript compatible avec tous les navigateurs.
  5. Connaître l’utilité d’un serveur de développement (development server) pour la réalisation de la partie client d’une application.

Résultat attendu

  • Un squelette du projet pour la partie cliente de l’application.

Ressources

Logiciel :

  • Un éditeur de texte (VS Code).
  • Le gestionnaire de dépôt git.
  • La plateforme Node.js (node, npm, npx, etc.)
  • Un shell (bash, powershell, cmd).
  • Un navigateur web (Chrome, Firefox, IE).

Réaliser un projet de développement en JavaScript (lab8)

Gérer des dépendances avec npm

Rendez-vous dans votre répertoire de travail, créez un répertoire lab8 et faites-en le répertoire courant. Lancez ensuite les commandes ci-dessous pour initialiser un fichier package.json pour la gestion des dépendances et ouvrir l’éditeur VSCode dans le répertoire courant.

1
2
npm init -y
code .

Dans l’explorateur de l’éditeur, vous devriez voir le fichier package.json et il devrait contenir le code JSON ci-après. Il s’agit de la configuration par défaut d’un package npm.

L’attribut “name” a été initialisé avec le nom du répertoire dans lequel vous avez lancé la commande. Vous pouvez utiliser l’attribut “description” pour décrire le projet et l’attribut “author” pour indiquer votre nom. L’attribut “license” indique sous quels termes vous souhaitez publier votre code. La valeur par défaut, “ISC”, est une licence de logiciel libre compatible avec la licence GPL. Nous reviendrons sur les attributs “main” et “script”.

1
2
3
4
5
6
7
8
9
10
11
12
{
  "name": "lab8",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Lancez maintenant les commandes ci-dessous pour installer les packages webpack et webpack-cli. Le second package, webpack-cli est l’interface en ligne de commande de webpack. Comme nous n’aurons besoin de webpack que pour le développement, nous utilisons l’option --save-dev ou -D pour indiquer qu’il s’agit d’une dépendance de développement.

1
2
npm install --save-dev webpack
npm install --save-dev webpack-cli

Lancez ensuite la commande ci-après pour installer le package qui permet d’installer la bibliothèque lodash.js que nous utiliserons un peu plus tard. Remarquez que cette fois, nous utilisons l’option --save ou -P pour indiquer qu’il s’agit d’une dépendance de production.

1
npm install --save lodash

Lorsque l’opération est terminée, votre fichier package.json devrait avoir deux entrées sous “devDependencies” et une entrée sous “dependencies”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "name": "lab8",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 0"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.29.6",
    "webpack-cli": "^3.2.3"
  },
  "dependencies": {
    "lodash": "^4.17.11"
  }
}

Si vous listez le contenu de votre répertoire lab8, vous y trouvez également un fichier package-lock.json et un répertoire node_modules.

Le répertoire node_modules contient tous les packages que vous avez installés ainsi que toutes leurs dépendances. Cela représente plusieurs centaines de packages, plusieurs milliers de fichiers et environ 40 MO de données.

Comme ce répertoire peut être reconstruit à n’importe quel moment, il ne faut pas inclure le répertoire node_modules dans vos sauvegardes, vos archives, ou lorsque vous copiez votre projet dans un répertoire réseau ou dans le cloud.

Supprimez le répertoire node_modules et lancez la commande npm install sans argument dans à l’intérieur du répertoire lab8.

1
npm install

Lorsque npm install est lancé sans argument, il cherche un fichier package.json et installe toutes les dépendances de développement et de production qui s’y trouvent. Si un fichier package-lock.json est également présent, il l’utilise pour connaître la version exacte des packages à installer.

Utilser un bundler (webpack)

Dans le répertoire lab8, créez la structure de répertoires suivante :

1
2
3
4
5
6
lab8
├── dist
|  └── index.html
└── src
   └── js
      └── index.js

Copiez le code ci-après dans le fichier index.html.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>M120 - Lab 8</title>
</head>

<body>
    <h1>M120 – Lab 8</h1>
    <p>
        Si vous utilisez un serveur de développement avec une fonctionnalité «live reload», la page est automatiquement
        réactualisée lorsque vous faites une modification dans votre code.
    </p>
    <p>
        Si ce n’est pas le cas, actualisez la page pour exécuter le code après un changement dans vos scripts.
    </p>

    <!-- En principe, les éléments script sont regroupés à la fin de l’élément body. -->
    <script src="./js/main.js"></script>
</body>

</html>

Copiez le code ci-après dans le fichier index.js.

1
console.log('hello world!')

Rendez-vous dans le répertoire lab8 et lancez la commande ci-après pour compiler votre programme. La commande npx permet d’exécuter un package qui se trouve dans le répertoire node_module. La commande npx webpack-cli à pour effet d’exécuter webpack-cli.

1
npx webpack-cli --entry "./src/js/index.js" --output-filename "./js/main.js"

Si la commande a bien fonctionné (vous pouvez ignorer les warnings en jaune), vous devriez voir le fichier main.js dans le répertoire dist/js.

Pour voir le résultat, nous avons besoin d’un serveur HTTP de développement. Nous pourrions installer le package live-server comme nous l’avons fait pour les labos précédents, mais webpack dispose également d’un serveur de développement.

Installez le package webpack-dev-server comme une dépendance de développement. Lorsque c’est fait, assurez-vous que dans le fichier package.json, l’entrée webpack-dev-server se trouve bien sous “devDependencies”. Si ce n’est pas le cas, désinstallez le package et recommencez.

Lorsque le serveur de développement est correctement installé, lancez la commande suivante pour exécuter votre projet.

1
npx webpack-dev-server --entry "./src/js/index.js" --output-filename "./js/main.js" --content-base "./dist" --open

En plus des dernière option --content-base et --open, nous avons utiliser les même option que pour la commande webpack-cli. Cela permet au serveur de développement de recompiler le projet avant de recharger la page si le code source est modifié.

Ouvrez les outils de développement du navigateur et vérifiez que le texte “Hello world !” apparaît dans la console. Ouvrez maintenant le fichier index.js dans votre éditeur, modifiez le texte et enregistrez la modification. Le texte de la console devrait avoir changé.

Utiliser un fichier de configuration pour webpack

Il est possible de tout faire avec les options du CLI mais cela manque de souplesse. Il est préférable de spécifier ces options à l’aide d’un fichier de configuration. Lorsqu’on lance le CLI ou le serveur de développement sans arguments, webpack cherche un fichier webpack.config.js. S’il existe, webpack utilise les paramètres qu’il contient.

Créez un fichier webpack.config.js dans le répertoire lab8 et copiez le code ci-après. Il n’est pas nécessaire de comprendre tout le code pour l’instant, mais vous pouvez déjà constater que le fichier contient les mêmes informations que celles que nous avions passées à l’aide des options.

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path');

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: './js/main.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    open: true
  }
}

Après avoir enregistré les modifications, lancez le serveur de développement avec la commande suivante :

1
npx webpack-dev-server

Il est également possible d’utiliser un fichier de configuration dont le nom est différent de webpack.config.js, mais dans ce cas, il faut indiquer ce nom au webpack-cli ou au webpack-dev-server à l’aide de l’option --config "<nom du fichier>"

Arrêtez le serveur de développement et renommez le fichier webpack.config.js en webpack.dev.js, par exemple. Lancez ensuite une nouvelle fois le serveur de développement avec la commande :

1
npx webpack-dev-server --config "./webpack.dev.js"

Programmer en ES.next ou en TypeScript

Ces dernières années JavaScript a subi une évolution rapide, avec des normes éditées selon un rythme annuel depuis 2015. Toutefois, les navigateurs ne parviennent pas à suivre cette évolution et les avancées ne se font pas au même rythme sur chacun d’eux. Alors que nous sommes à la version 2018 du standard ECMAScript (ES), les navigateurs ne sont que depuis peu compatibles avec la version 2015.

Toutefois, rien ne nous oblige à programmer dans le langage de la plateforme d’exécution. Nous pouvons programmer dans n’importe quel langage pour peu qu’il existe un compilateur pour traduire le programme. Dans notre cas, « n’importe quel langage » inclue la dernière version du standard ES. Il reste à se demander quels sont les langages qui disposent d’un compilateur vers ES6. Les plus notables sont :

  • ES.next (dernière évolution du standard)
  • TypeScript
  • Dart
  • CoffeeScript

ES.next et TypeScript sont les langages les plus utilisés, principalement parce qu’ils permettent d’utiliser directement toutes les bibliothèques écrites en JavaScript. En effet, TypeScript a la même syntaxe que JavaScript. Il dispose de quelques constructions syntaxiques supplémentaires, mais il permet surtout de spécifier des types pour les variables et les paramètres des fonctions.

Puisque webpack opère déjà une transformation du code pour effectuer le bundling, il est facile d’insérer dans ce processus une phase de compilation. Pour illustrer cela, configurons webpack pour qu’il compile les fichiers .js et .mjs avec le compilateur Babel.

Lancez la commande suivante pour installer les packages nécessaires. Cette commande installe les packages babel-loader, @babel/core et @babel/preset-env. Comme vous pouvez le constater, avec npm il est également possible de spécifier plusieurs packages à la fois.

1
npm install -D babel-loader @babel/core @babel/preset-env

Remplacez le contenu du fichier de configuration de webpack avec 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
const path = require('path');

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: './js/main.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    open: true
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}

Assurez-vous que le fichier de configuration est bien nommé webpack.config.js et lancez la commande npx webpack pour vérifier que tout fonctionne correctement (vous pouvez ignorer les warnings).

Vous pouvez maintenant coder en JavaScript sans avoir à vous demander si la fonctionnalité que vous utilisez est supportée ou non par les navigateurs.

Utiliser des modules JavaScript

En JavaScript dans la plateforme Node.js ou avec un bundler comme webpack, on peut importer un module de deux manières. Soit avec la fonction require soit en utilisant le mot clé import du standard ES2015 (ES6). Pour illustrer cela, nous allons utiliser la bibliothèque lodash d’abord en utilisant require puis en utilisant import.

Copier le code suivant dans le fichier index.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Importe le module lodash. Ce module exporte un
// objet qui contient un grand nombre de fonctions 
// utilitaires. Après l'exécution de l'instruction
// la variable _ contient une référence à cet objet.
// (On utilise par convention le nom de variable _ 
// pour la la bibliothèque lodash et $ pour la 
// bibliothèque jQuery.)
const _ = require('lodash')

const words = [
  'sky', 'wood', 'forest', 'falcon',
  'pear', 'ocean', 'universe'];

// Utilise la bibliothèque lodash pour obtenir
// le premier et le dernier mot du tableau.
const fel = _.first(words);
const lel = _.last(words);

// Les accents graves (back-ticks) indiquent qu'il 
// s'agit d'une chaîne dans laquelle peuvent se trouver 
// des variables (template string). 
console.log(`First element: ${fel}\nLast element: ${lel}`);

Après avoir enregistré les modifications, lancez la construction du bundle avec la commande npx webpack. Lancez le serveur de développement pour vérifier que le code fonctionne.

Sans arrêter le serveur de développement, ouvrez le fichier index.js et modifiez l’appel à la fonction require en une instruction import ES6, comme dans le code ci-après. Enregistrez la modification et vérifiez que cela fonctionne toujours.

1
2
3
4
5
6
7
8
9
10
...

// const _ = require('lodash')
import _ from 'lodash'

const words = [
  'sky', 'wood', 'forest', 'falcon',
  'pear', 'ocean', 'universe'];

...

Dans les outils de développement du navigateur, allez dans l’onglet source et affichez le code de main.js. Cela ne ressemble guère au code ci-dessus. C’est l’un des principaux problèmes auquel on est confronté lorsque l’on utilise un bundler ou un compilateur. Toutefois, ce problème est bien connu et il existe une solution. Il suffit de demander au compilateur d’émettre des métadonnées que le débogueur des outils de développement puisse afficher le code source du programme. On parle d’information de débogage ou de source map.

Arrêtez le serveur de développement (Ctrl-C) et remplacez le code du fichier webpack.config.js par 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
const path = require('path');

module.exports = {
  entry: './src/js/index.js',
  output: {
    filename: './js/main.js',
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    open: true
  },
  devtool: 'source-map', // information de débogage
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}

Lancez la construction du bundle avec la commande npx webpack et lancez le serveur de développement avec la commande npx webpack-dev-server. Dans les outils de développement, allez dans l’onglet source et cherchez dans cette vue le moyen d’afficher index.js.

Créer un module en JavaScript

Il y a plusieurs manières de créer un module en JavaScript. Nous allons créer uniquement un module avec la syntaxe de ES2015 pour l’utiliser dans un programme «bundlé» avec webpack.

Créez un répertoire my-modules dans src/js et copiez le code ci-après dans le fichier my-modules/one-two-three.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
// Affecte un littéral d'objet à la variable anObject.
// La variable anObject n'a pas une portée globale
// elle n'est visible que dans le moudle.
const anObject =  {

  // En JavaScript une méthode est simplement un attribut 
  // auquel on affecte une expression lambda.
  one: function () {
    console.log('one was called!')
  },

  two: function () {
    console.log('two was called!')
  },

  three: function () {
    console.log('three was called!')
  }
}

// Exporte anObject sans spécifier de nom (default export).
// Le mot clé export sert à spécifier l'interface du module.
// Pour commencez n'exportez qu'un seul symbole (un objet)
// sans spécifier de nom comme dans cet exemple.
export default anObject

Remplacez le code de fichier index.js, par le code suivant :

1
2
3
4
5
6
7
8
9
10
11
12
// Importe le symbole par défaut du module on-two-three.js. 
//
// Remarquez qu'on donne le chemin du fichier : 
// './my-modules/...' et non 'my-modules/...'. 
// C'est important, car si le chemin n'est pas spécifié, 
// le bundler cherche le module dans node_module. 
// Le nom du module est le nom du fichier avec ou sans .js.
import oneTwoThree from './my-modules/one-two-three'

oneTwoThree.three();
oneTwoThree.two();
oneTwoThree.one();

Lancez le serveur de développement et utilisez le débogueur des outils de développement pour exécuter le programme pas-à-pas.

Utiliser une convention de codage

Pour s’aider à respecter les conventions de codage et pour uniformiser la présentation du code dans une équipe de développement, il peut être intéressant d’utiliser des outils.

Nous vous recommandons en particulier d’utiliser un fichier .editorconfig et d’installer le plugin EditorConfig pour VSCode ou pour votre éditeur favori. La configuration recommandée pour un projet web est la suivante :

1
2
3
4
5
6
7
8
9
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

Pour la convention de codage, nous vous recommandons d’utiliser JavaScript Standard Style et d’installer le plugin SandardJS pour VSCode ou pour votre éditeur favori. Vous devez en outre installer le package standard comme dépendance de développement dans votre projet (npm install -D standard).

Conclusion

Il s’agissait dans le lab8 de vous donner un aperçu de la mise en œuvre des outils de développement. Nous vous recommandons de commencer à les utiliser sans attendre.