Javascript et NodeJS pour le développement web

NodeJS & npm

NodeJS est un environnement d’exécution Javascript, généralement utilisé côté serveur, basé sur le moteur Javascript V8 de Google.

Différences avec le JS du navigateur : - les variables globales prédéfinies sont différentes, par exemple document pour JS et process.env pour NodeJS - NodeJS peut directement accéder au système de fichiers (package fs), interdit pour JS - les objets Event sont légèrement différents

Installation sur tout le système

Debian

MacOsX

Utiliser l’installer graphique du site officiel

Installation locale par nvm

npm est un package-manager qui simplifie la distribution, le partage et l’installation de NodeJS.

Création d’un projet node

Installation de packages npm

Installation globale d’un package

Installation locale (pour le projet courant) d’un package et sauvegarde de la dépendance dans ./package.json. Les fichiers sont copiés dans le répertoire ./node_modules/<package_name>

Installation de tous les packages référencés dans package.json :

NodeJS est un environnement d’exécution Javascript, généralement utilisé côté serveur, basé sur le moteur Javascript V8 de Google.

JSON

[TODO]

Héritage des objets par prototypes

Un objet est une instance du type de base Object :

$ node
> x = new Object()
{}
> x.a = 1 ; x
{ a: 1 }

Javascript est un langage objet qui utilise un héritage par prototype: lorsque obj.prop est évalué, la clé prop est recherchée d’abord dans obj, puis dans le type père obj.prototype (s’il existe), puis à défaut dans obj.prototype.prototype, etc. jusqu’à Object

Lors d’un appel à new Type(args) :

  1. un nouvel objet est créé par clonage de Type.prototype. Il possèdera donc déjà toutes les propriétés et méthodes de son prototype, l’équivalent d’une classe ‘père’ dont il hérite
  2. La fonction constructrice Type est appelée avec les arguments fournis, this étant lié au nouvel objet créé.
  3. L’objet renvoyé par le constructeur devient le résultat de l’expression qui contient new.

On peut étendre un type d’objet existant avec des propriétés ou des méthodes supplémentaires, en les ajoutant à son prototype :

> Array.prototype.toString = function() {
>  return `tableau, taille ${this.length}`
> }
> [1, 2].toString()
'tableau, taille 2'

Le type Object possède des méthodes statiques très utiles, notamment :

  • Object.keys(obj) : renvoie la liste des clés des propriétés propres de obj (sans ordre garanti)
  • Object.values(obj) : renvoie la liste des valeurs des propriétés propres de obj (sans ordre garanti)

L’expression booléenne x instanceof t permet de déterminer si x est du type t, par exemple myarray instanceof Array.

Tableaux avancés

Un tableau est en fait un objet du type Array, qui possède les constructeurs, propriétés et méthodes suivantes :

  • new Array(n) : créé un tableau avec n emplacements vides, c’est à dire avec la propriété length à n
  • propriété length : quand un élément est ajouté au tableau à la position i, length prend la valeur i+1 si i >= length
  • méthode includes(elt) : renvoie un booleéen indiquant si elt est inclus dans le tableau
  • méthode indexOf() : retourne le premier (plus petit) index d’un élément égal à la valeur passée en paramètre à l’intérieur du tableau, ou -1 si aucun n’a été trouvé
  • méthode join(sep) : concatène tous les éléments d’un tableau en une chaîne de caractères, les éléments étant séparés par sep
  • méthode slice(start, end) : renvoie un objet tableau, contenant une copie superficielle de la portion du tableau original entre l’indice start (inclus) et l’indice end (exclu). Le tableau original n’est pas modifié
  • méthode fill(val) : remplit tous les éléments du tableau avec la valeur val
  • méthode pop() : supprime le dernier élément d’un tableau et retourne cet élément
  • méthode push(val[, val, ...]) : ajoute un ou plusieurs éléments à la fin d’un tableau et retourne la nouvelle longueur du tableau
  • méthode shift() : supprime le premier élément d’un tableau et retourne cet élément
  • méthode sort(func) : trie sur place le tableau à l’aide de la fonction de comparaison func
  • méthode splice(i, n) : supprime sur place n éléments dans le tableau à partir de la position i
  • méthode concat(a1[, a2,...]) : renvoie un nouveau tableau constitué de ce tableau concaténé avec a1, a2, etc.
  • méthodes map, reduce, filter, find, ’forEach,some,every` qui permettent une programmation fonctionnelle (voir plus loin)

Fonctions avancées

Une fonction Javascript est un objet héritant du type prédéfini Function :

$ node
> parseInt
[Function: parseInt]
> parseInt instanceof Object
true
> parseInt instanceof Function
true

Écriture ‘fat arrow’ (depuis ES6) :

Dans le corps d’une arrow function, this est le dictionnaire de son scope environnant lexical

Une fonction déclarée avec le mot-clé function est remontée dans le code (‘hoisting’) : cela permet de l’utiliser avant qu’elle ait été déclarée dans le code. Ce n’est pas le cas des fonctions anonymes affectées à des variables, dont la portée suit les règles habituelles.

Lors d’un appel de fonction :

  • si le nombre d’arguments est supérieur au nombre de paramètres, les arguments supplémentaires sont ignorés
  • si le nombre d’arguments est inférieur au nombre de paramètres, les paramètres manquants prennent la valeur undefined, sauf si une valeur par défaut a été prévue. Exemple :
function f(a, b=2) {
   console.log('a', a, 'b', b)
}
f(3, 4) // --> a 3 b 4
f(3) // --> a 3 b 2

String avancé

La chaine de caractères est un type primitif en javascript. Lorsqu’une instruction accède à des propriétés (telle que .length) et des méthodes (telles que .substring()) sur une chaine, celle-ci est temporairement convertie en un object de type String qui possède ces méthodes et propriétés. Après usage cet objet est candidat au garbage-collecting.

  • length : propriété, longueur de la chaine. Ne pas modifier.
  • charAt() : Renvoie le caractère (ou plus précisement, le point de code UTF-16) à la position spécifiée.
  • concat() : Combine le texte de deux chaînes et renvoie une nouvelle chaîne.
  • includes() : Défini si une chaîne de caractères est contenue dans une autre chaîne de caractères.
  • endsWith() : Défini si une chaîne de caractère se termine par une chaîne de caractères spécifique.
  • indexOf() : Renvoie la position, au sein de l’objet String appelant, de la première occurrence de la valeur spécifiée, ou -1 si celle-ci n’est pas trouvée.
  • lastIndexOf() : Renvoie la position, au sein de l’objet String appelant, de la dernière occurrence de la valeur spécifiée, ou -1 si celle-ci n’est pas trouvée.
  • localeCompare() : Renvoie un nombre indiquant si une chaîne de référence vient avant, après ou est en position identique à la chaîne donnée selon un ordre de tri.
  • match() : Utilisée pour faire correspondre une expression rationnelle avec une chaîne.
  • matchAll() : Renvoie un itérateur listant l’ensemble des correspondances d’une expression rationnelle avec la chaîne.
  • padEnd() : Complète la chaîne courante avec une autre chaîne de caractères, éventuellement répétée, afin d’obtenir une nouvelle chaîne de la longueur indiquée. La chaîne complémentaire est ajoutée à la fin.
  • padStart() : Complète la chaîne courante avec une autre chaîne de caractères, éventuellement répétée, afin d’obtenir une nouvelle chaîne de la longueur indiquée. La chaîne complémentaire est ajoutée au début.
  • repeat() : Renvoie une chaîne dont le contenu est la chaîne courante répétée un certain nombre de fois.
  • replace() : Utilisée pour rechercher une correspondance entre une expression régulière et une chaîne, et pour remplacer la sous-chaîne correspondante par une nouvelle chaîne.
  • search() : Exécute la recherche d’une correspondance entre une expression régulière et une chaîne spécifiée.
  • slice() : Extrait une section d’une chaîne et renvoie une nouvelle chaîne.
  • split() : Sépare un objet String en un tableau de chaînes en séparant la chaîne en plusieurs sous-chaînes.
  • startsWith() : Détermine si une chaîne commence avec les caractères d’une autre chaîne.
  • substr() : Renvoie les caractères d’une chaîne à partir de la position spécifiée et pour la longueur spécifiée.
  • substring() : Renvoie les caractères d’une chaîne entre deux positions dans celle-ci.
  • toLowerCase() : Renvoie la valeur de la chaîne appelante convertie en minuscules.
  • toUpperCase() : Renvoie la valeur de la chaîne appelante convertie en majuscules.
  • trim() : Retire les blancs en début et en fin de chaîne.
  • trimStart() : Retire les blancs situés au début de la chaîne.
  • trimEnd() : Retire les blancs situés à la fin de la chaîne.

Classes (depuis ES6)

Elles ont été introduites dans la syntaxe, mais restent du sucre syntaxique par dessus l’organisation objet basée sur des proptotypes.

Exemple:

class Rectangle {
   constructor(height, width) {
      this.height = height;
      this.width = width;
   }
   get area() {
      return this.calcArea();
   }

   calcArea() {
      return this.width * this.height;
   }
}

class Square extends Rectangle {
   constructor(size) {
      super(size, size)
   }
}

const square = new Square(100)
console.log('area', square.area)

Les déclarations de classes ne sont pas remontées dans le code comme les déclaration de fonctions.

truthy & falsy

$ node
> [0, 1, null, "", [], {}].map(x => console.log(x, x ? "is truthy" : "is falsy"))
0 is falsy
1 is truthy
null is falsy
 is falsy
[] is truthy
{} is truthy

try / catch / finally

Problématique des modules

Les modules permettent de diviser un programme en différentes parties. Un module exporte des fonctions, classes etc. et peut importer les fonctions, classes etc. d’autres modules, appelés dépendances. Il existe différents formats de modules, incompatibles les uns avec les autres, mais c’est le format le plus récent ESM (Ecma Script Module) qui est amené à les remplacer tous.

Modules CJS (CommonJS)

Il s’agit du format de module historique de NodeJS, utilisé notamment dans les packages npm.

On utilise la fonction require pour importer un module.

Module externe npm

Module sous forme d’un fichier

Module sous forme d’un répertoire

Le répertoire doit contenir un fichier index.js

Variables visibles dans le scope d’un module

  • __dirname : chemin d’accès absolu au répertoire du module courant
  • module : référence au module courant
    • module.exports : définit ce qu’un module exporte, accessible par require()

Modules ESM

C’est le format de modules utilisé dans les navigateurs récents ; il devrait à terme remplacer tous les autres. Il est déjà compatible avec NodeJS version 14. Les modules ESM sont généralement stockés dans des fichiers d’extension .mjs.

On utilise le mot-clé import pour importer un module.

Exportations multiples

$ cat main.js
...
import { pi, square } from './math.mjs'
...

Exportation ‘default’

$ cat main.js
...
import math from './math.mjs'
...
let PI = math.pi
...

Promesses

Une promesse est un object renvoyé par une opération asynchrone (ex : requête HTTP) et auquel sont attachés des callback, lors des événements :

  • de complétion de l’opération, avec renvoi d’une valeur : callback .then(rep => ...; return value)
  • d’échec de l’opération : callback .catch(err => ...)
  • de fin de l’opération, qu’elle ait échoué ou pas : callback .finally(() => ...)

.then(), .catch() et .finally() renvoient la promesse elle-même, ce qui permet de les chainer afin d’écrire un code asynchrone de façon linéaire, ressemblant à un code synchrone.

Exemple :

De nombreuses librairies de promesses existent, mais depuis ES6 la classe Promise est directement disponible et incorpore les fonctionnalités essentielles. Notamment :

  • Promise.all(<liste/iterable de promesses>) : renvoie une promesse qui réussi lorsque toutes les promesses de la liste ont réussi
  • Promise.resolve(valeur) : renvoie une promesse qui réussi en renvoyant valeur
  • Promise.reject(err) : renvoie une promesse qui échoue avec l’erreur err

async / await (depuis ES6)

Ces mot-clés sont du sucre syntaxique qui permet l’utilisation de promesses de façon simplifiée. Ils doivent être préférés à l’utilisation directe des promesses, sauf cas particuliers tels que l’usage de Promise.all().

Exemple :

Programmation fonctionnelle avec map, forEach, reduce, filter, find

Les indices sont accessibles optionnellement :

Exemple : opérations ensemblistes

  • intersection : arr1.filter(x => arr2.includes(x))
  • difference : arr1.filter(x => !arr2.includes(x))

Déstructuration / restructuration

listes

dictionnaires

Ce mécanisme permet d’écrire des fonctions avec l’équivalent d’un passage de paramètres par mots-clés :

Activités à réaliser

  • Écrire une fonction qui inverse une chaine
  • Écrire une fonction qui indique qu’une chaine est un palindrome
  • Écrire une fonction qui renvoie un dictionnaire du nombre d’occurence de chaque lettre d’une chaine
  • Écrire une fonction qui renvoie le caractère le plus fréquent dans une chaine (non vide)

Depuis ES7/ECMA2016

Set

$ node
> let ensemble = new Set([1, 2])
undefined
> ensemble.add(3); ensemble.add(2)
Set { 1, 2, 3 }
> ensemble.has(1)
true
> ensemble.has(4)
false

Map

TODO

Generateurs

Fermetures

Une fermeture est la paire formée d’une fonction et des références à son état environnant (l’environnement lexical).

En JavaScript, une fermeture est créée chaque fois qu’une fonction est créée.

function addTo(x) {
   return function(y) {
      return x + y
   }
}

const add5 = addTo(5)
const add10 = addTo(10)

console.log(add5(2))  // 7
console.log(add10(2)) // 12

Les expressions régulières

TODO

Les événements

Le navigateur et NodeJS implémentent un mode de fonctionnement asynchrone basé sur les événements.

Dans le contexte du navigateur, les événements sont des interactions sur la page (souris, clavier etc.), des événements réseau, des déclenchements de timers, alors qu’en NodeJS les événements sont liés aux opérations asynchrones d’entrées/sorties (accès aux fichiers, réseau, etc.)

Javascript est un langage synchrone et mono-thread, mais son inclusion dans une ‘event-loop’ fournit l’impression d’une exécution asynchrone multi-thread :

Browser events

const event = new Event('build')

document.documentElement.addEventListener('build', (e) => {
  console.log('a build event occurred!', e)
}, false)

document.documentElement.dispatchEvent(event)

Liste des événements navigateur : https://developer.mozilla.org/en-US/docs/Web/Events

NodeJS events

const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter()
myEmitter.on('event', (a, b) => {
  console.log('an event occurred!', a, b)
})
myEmitter.emit('event', 11, 22)

Créer une commande shell avec NodeJS

Modifier package.json et ajouter l’entrée bin ; la propriété va devenir le nom de la commande :

helloCmd.js

#!/usr/bin/env node

console.log("Hello, world!")

Installation de la commande (dans un des chemins de $PATH) :

Tendances dans l’écosystème JS

https://stateofjs.com