CentraleSupélecDépartement informatique
Gâteau du Glouton
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
TD 6 — Node.js

L'objectif des exercices de ce TD est de vous familiariser avec la création de programmes JavaScript côté serveur avec Node.js. Voir le cours en question.

Code de Livraison: H83W

1 - Mise en route

Sur les machines du bâtiment Bréguet

utilisez les postes sous Ubuntu pour ce TD.

  • Si nécessaire, redémarrer l'ordinateur pour passer sous Ubuntu.
  • Dans le répertoire "Documents" de votre compte, créer un répertoire spécifique dans lequel le code des différents exercices sera mis, comme par exemple "ExercicesNode".

A la fin du TD, n'oubliez pas de récupérer les fichiers que vous aurez créés, sinon ils seront effacés du disque automatiquement...

Sur votre machine perso

  • Installer Node.js : https://nodejs.org
  • Vous aurez besoin de la ligne de commande:
    • cmd.exe sous Windows
    • Le "Terminal Mac" sous MacOS
    • Quelques commandes:
      • cd <nom-de-répertoire> : pour changer de répertoire
      • ls (sous MacOS/Linux) et dir (sous Windows) : pour voir les fichiers du répertoire courant
      • La touche [TAB] vous permet de completer automatiquement un nom de répertoire ou de fichier
    • Il faudra vous déplacer avec la ligne de commande dans le répertoire où se trouve vos fichiers

Dans tous les cas

Assurez-vous de faire fonctionner les exemples du tuto NodeJS.

2 - Un Web Service avec Express

L'objectif de cet exercice est de transformer les quelques pages que nous avons créées en une application web sérieuse.

Notre application a besoin de faire deux choses:

  1. Fournir les pages html, css et javascript lors de la connection d'un client
  2. Garder sur le serveur les paramètres de configuration de notre application, et les délivrer sous la forme du fichier JSON que nous avions utiliser au dernier TD.

Le développement d'une application web avec Node peut être très fastidieuse, c'est pourquoi nous utiliserons la bibliothèque Express.js qui fournit un ensemble de fonctions facilitant le développement de ce type d'applications.

Si vous allez assez loin dans le TD, les données de configurations seront stockées dans une base de données SQLite3.

2.1 - Configuration et installation des bibliothèques Express

  • Créez un nouveau répertoire MeteoNodeJs.
  • Nous allons maintenant demander à NPM d'installer Express pour notre application.
  • Si sur les machines du bâtiment Bréguet : configurez le proxy. En ligne de commande, tappez la commande
npm config set https-proxy http://proxy.supelec.fr:8080
  • En ligne de commande, placez-vous dans le répertoire puis tapez la commande:
npm install express
  • Observez le contenu de votre répertoire. Que contient-il maintenant ?

2.2 - Création de l'application Express

  • Dans le répertoire "MeteoNodeJs", créez un fichier "server.js".

Créer une application Express qui écoute les requêtes HTTP est très simple. Il suffit d'instancier une application Express puis de définir les requêtes HTTP qu'elle accepte (et auxquelles elle répond), c'est-à-dire les "routes".

Dans notre cas, on voudra à terme deux "routes" : Une pour le contenu des page, et une pour le "service web" qui donnera la configuration sous forme JSON.

Import d'Express :

Dans le fichier "server.js", copier les lignes suivantes :

var express = require('express'); //import de la bibliothèque Express
var app = express(); //instanciation d'une application Express

//ici on définira ensuite les "routes" = les requêtes HTTP acceptées par l'application

// ....
// ....

// Finalement, on "lance" le serveur.

app.listen(8000); //commence à accepter les requêtes
console.log("App listening on port 8000...");

A ce stade vous pouvez essayer de démarrer votre application, mais puisqu'aucune route n'est encore définie celle-ci n'accepte encore aucune requête HTTP et vous ne pourrez donc pas la tester.

Définition d'une route :

Nous allons maintenant ajouter une "route", c'est-à-dire une requête HTTP que votre application va accepter et traiter. Une route est constituée de deux éléments : un verbe HTTP (par exemple : GET, POST, PUT…) et un chemin (par exemple : /voici/un/chemin, /chemin, /un/autre/long/chemin…).

  • Ajouter au code précédent la route suivante, qui définit que l'application accepte une requête GET sur le chemin /meteo (l'application répond donc sur l'url http://localhost:8000/meteo) :
app.get('/meteo, function(req, res) {
    //ici construire la réponse HTTP
});
  • Dans le corps de la fonction de callback de cette route, ajouter le code nécessaire pour que l'application réponde un texte de votre choix: par exemple, créez un fichier test.txt dans le répertoire MeteoNodeJs et utiliser res.sendFile(__dirname+"/test.txt")

Test :

  • Démarrer votre application en ligne de commande : dans le répertoire "MeteoNodeJs", taper node server.js.
  • Avec un navigateur, aller à l'url http://localhost:8000/meteo et observer le résultat.
  • Notez que le nom du fichier est à priori complètement indépendant de l'url.

Note

Pour arrêter une version précédente de votre application qui tourne encore, taper ctrl+c dans le shell.

Pour que Node prenne en compte automatiquement les modifications faites sur votre application et que vous n'ayez pas besoin de la redémarrer, utilisez la commande nodemon au lieu de node pour lancer votre application : nodemon server.js.

2.3 - Ajout de contenu

Dans le cas qui nous intéresse, on souhaite que l'application délivre non pas un fichier texte idiot, mais par exemple la page principale meteo.html de notre site. Une solution serait de remplacer "test.txt" par "meteo.html". Le problème en faisant cela serait que les autres resources: fichiers CSS, autres pages HTML, fichiers javascripts, ne seraient pas délivrés par l'application. Pour voir ce qui se passe:

Premiers pas

  • Placez tous vos fichiers HTML, CSS et JS dans le repertoire "MeteoNodeJS".
    • Si vous voulez, vous pouvez utiliser les miens.
    • Dans la suite, je vais utiliser pour les exemples ces fichiers. Vous pouvez bien sûr utiliser les votres...
  • Ouvrez le fichier meteo.html directement avec votre navigateur: admirez le magnifique CSS que j'ai intégré à la page.
  • Remplacez le contenu de votre fichier server.js par:
var express = require('express'); //import de la bibliothèque Express
var app = express(); //instanciation d'une application Express

app.get('/meteo', function(req, res) {
    console.log(req.url);
    res.sendfile(__dirname + "/meteo.html");
});


app.listen(8000); //commence à accepter les requêtes
console.log("App listening on port 8000...");
  • Lorsque l'on se connecte au serveur à l'adresse http://127.0.0.1:8000/meteo, la page html est retournée. Par contre, rien d'autre n'est accèdé. Le CSS n'est en particulier pas chargé
    • Dans la console est affiché le contenu de la variable req.url: celle-ci comporte le chemin d'accès à la resource demandée par chaque connection. Vous devriez voir une liste de demandes dans le terminal (ici, on parle de la console de node.js, pas de la console du navigateur)
    • Celles-ci ne sont pas honorées car ne faisant pas partie de la route. C'est en particulier pour cela que le fichier CSS n'est pas chargé.

Répondre à toutes les requêtes.

On va plutôt faire en sorte que la fonction de callback analyse l'url demandée, en récupérant les noms des resources demandées : Il faut donc

  1. modifier la route pour accepter tout ce qui serait sous la forme 127.0.0.1:8000/meteo/un/chemin/d/acces
  2. utiliser req.url pour récupérer /un/chemin/d/acces.

Donc:

  • Créez un sous-dossier meteo.
  • Placez les fichiers correspondant au site dans ce sous-dossier (donc tout sauf server.js et node_modules)
  • Changez la route pour /meteo/* (l'étoile signifie "n'importe quelle séquence de caractères")
  • Modifiez l'appel à sendfile pour qu'il récupère la bonne resource
  • Relancez le serveur
    • Dans votre navigateur, accèdez à la page 127.0.0.1:8000/meteo/meteo.html
    • En principe, tout est maintenant chargé et le magnifique CSS est bien utilisé.

3 - Gérer les paramètres

Pour l'instant, on charge les préférence à l'aide d'un fichier JSON écrit en dûr. Ce fichier est à l'adresse /meteo/config.json.

On va plutôt transformer la délivrance de ce contenu un "service web", qui sera généré automatiquement par le serveur. On va allouer pour cela une nouvelle route /config

À faire

  • Créez dans server.js une nouvelle variable globale qui contient le contenu du fichier JSON:
var config = {
    "temp": {
        "unite": 1,
        "state": 1
    },
    "pression": {
        "unite": 1,
        "state": 1
    },
    "nuage": {
        "visib": {
            "unite": 0,
            "state": 1
        },
        "status": 1
    },
    "vent": {
        "unite": 0,
        "state": 1
    }
}
  • Créez une nouvelle route /config
  • La fonction de callback de cette nouvelle route va simplement renvoyer le contenu de la variable sous forme json. À la place de res.sendfile, on utilise la fonction res.json(config).
  • Relancez le serveur, et testez avec votre navigateur en allant à l'adresse 127.0.0.1:8000/config
  • modifiez l'appel à la fonction .get dans le fichier javascript meteo.js: à la place de "config.json", utilisez "/config" (n'oubliez pas le "/" qui indique la racine de l'adresse).
  • Effacez votre fichier json et rechargez la page 127.0.0.1:8000/meteo/meteo.html : en principe, l'appel à .get doit s'effectuer et les bonnes choses doivent s'afficher.
    • Si vous utilisez l'exemple, si vous voyez des XXXX à la place des unités, c'est que cela ne marche pas...

4 - Rendons le fichier config.html fonctionnel

Notre application météo possède un fichier que nous n'avons pour le moment pas développé outre mesure : le fichier config.html. Nous allons remédier à cela dans cet exercice.

Il s'agit d'un formulaire qui permet en théorie de modifier les paramètres de configuration de l'application. Donc concrètement, les valeurs du formulaire doivent être envoyer au serveur qui les utilise pour changer sa variable globale config.

4.1 - Remplissage du formulaire au démarrage

Lorsque vous chargez la page, celle-ci comporte un formulaire vide... C'est un peu agaçant: on aimerait que le formulaire reflète les choix contenus dans la variable config du serveur. Pour cela, nous avons essentiellement deux choix:

  1. Utiliser un template au niveau du serveur pour générer une page config.html directement remplie
  2. Avoir une page fixe, mais qui fait un appel AJAX lors de l'évenement ready au service web /config et qui utilise les valeurs récupérées pour cocher les bonnes cases

Nous allons utiliser cette deuxième méthode, mais vous êtes invités à regarder les templates si cela vous intéresse. Il en existe beaucoup, par exemple allez voir ici pour une sélection possible.

À faire

  • Chargez la page et assurez-vous que les valeurs correspondant à la variable config sont bien sélectionnées.

Note

Si vous utilisez les fichiers que je vous ai donné, vous aurez noté que chaque <select> et chaque <input> a un nom. Vous pouvez interragir avec en utilisant jQuery.

  • Dans la console du navigateur, faites
$("[name=windUnit]").val()

et

$("[name=windUnit]").val(1)

et

$("[name=windUnit]").val(0)

pour voir.

  • Essayez aussi
$("[name=temp]").attr("checked", true)

et

$("[name=temp]").attr("checked", false)
  • Vous devriez en inférer comment modifier les valeurs du formulaire après l'appel au serveur

4.2 - Envoi des valeurs au serveur

La balise <form> admet deux attributs intéressant:

  • action : l'adresse à appeler en lui passant les paramètres
  • method : peut être get ou post. Essayez les deux et regarder le résultat: dans le premier cas, les valeurs du formulaire sont sur la ligne d'addresse. Dans l'autre, les valeurs sont "cachés" : elles sont dans le corps de l'appel HTTP.

Au niveau du serveur...

Derrière l'adresse à passer au formulaire se trouve le serveur: il faut qu'il réagisse correctement en mettant à jour la variable config. On va définir une route spécifique pour cela.

  • Définissez une nouvelle route /set, en mode GET, qui récupère le paramètre monparam et affiche sa valeur:
app.get('/set', function(req, res) {
  if("monparam" in req.query) {
    var txt = "Donnee reçue :" + req.query.monparam;
    res.send(txt);
  } else {
    res.send("pas de paramètre monparam");
  }
});
  • Relancez le serveur, et allez à l'adresse http://127.0.0.1:8000/set?monparam=toto avec votre navigateur. Relancez avec valeur autre que "toto".
  • Dans le fichier config.html, faitez que le formulaire appelle "/set" en mode GET.
  • Modifiez la fonction de callback de la route /set pour qu'elle affiche l'un des paramètres.
  • Testez en ouvrant la page config.html et en utilisant le bouton <submit>.
  • Au lieu de renvoyer du texte, faites une fonction de callback qui ressemble à cela:
app.get('/set', function(req, res) {
  // Mise à jour de la variable config
  config.XXX  =  req.query.XXX
  config.YYY  =  req.query.YYY
  ...

  // Recharger une page
  res.sendFile(__dirname + "/meteo/config.html");
});
  • Testez : En principe, si vous modifiez un paramètre dans le formulaire et cliquez sur le bouton <submit>, le formulaire se recharge avec les bonnes valeurs.
  • Notez que cela n'est vrai que grace à l'appel à ready : si vous supprimer cet appel, le formulaire se rechargera vierge (essayez!)
  • Vous avez en principe maintenant une application "complète" : la page configuration est fonctionelle.

4.3 - Discussion

Le choix que nous avons fait dans cette implémentation est très "classique": le formulaire est équipé d'un bouton "submit" qui envoie les infos en mode POST et qui recharge la page. La page est "statique".

Un autre choix, plus moderne et "AJAX" dans l'esprit aurait été de ne pas utiliser de bouton "submit" mais de faire des appels asynchrones au serveur pour mettre à jour la variable globale config à chaque modification d'un champ du formulaire.

Vous pouvez implémenter cette possibilité si vous le souhaitez, il n'y a à priori pas de difficultés particulières.

5 - Pour aller plus loin : Utilisation d'une base de donnée

Pour le moment, la configuration est stockée au niveau du serveur dans une variable. Si le serveur s'arrête, on perd les informations. On va dans cette exercice utiliser une base de donnée SQLite3 pour stocker et lire ces données. Voir le tuto sur SQlite3.

Le choix que nous faisons dans cet exercice est le suivant:

  • Au démarrage du serveur, on va lire la base de donnée et initialiser la variable globale config
  • À chaque appel à la route /set, après mise-à-jour de la variable globale config on va mettre à jour la base de donnée.

Que mettre dans la base de donnée ?

  • Il s'agit d'une base de donnée relationnelle, c'est-à-dire essentiellement un tableau. On pourrait donc mettre une ligne par parametre de configuration (une pour le vent, une pour la pression, etc...).
  • Pour les besoin de la cause, c'est un peu lourd. On va simplement mettre une seule ligne dans la base de donnée, et associer à default une chaine de caractère qui sera le contenu de la variable config.

5.0 - Vérifions que je ne mens pas

Vérifiez que relancer le serveur efface toutes les modifications !

5.1 - Installation de SQlite3

  • Vous trouverez une version executable sur le site officiel. Placez l'executable dans le dossier dans lequel vous travaillerez.
  • En ligne de commande, placez-vous dans le répertoire du serveur puis tapez la commande suivante:
npm install sqlite3

5.2 - Création d'une base de donnée config.db3

En utilisant le tuto, créez une nouvelle base de donnée config.db3 dans le répertoire MeteoNodeJS:

$ sqlite3 config.db3
SQLite version 3.8.7.1 2014-10-29 13:59:56
Enter ".help" for usage hints.
sqlite> create table config(id TEXT, value TEXT);
sqlite> insert into config values("default",'{"temp":{"unite":1,"state":1},"pression":{"unite":1,"state":1},"nuage":{"visib":{"unite":0,"state":1},"status":1},"vent":{"unite":0,"state":1}}');
sqlite> select * from config;
default|{"temp":{"unite":1,"state":1},"pression":{"unite":1,"state":1},"nuage":{"visib":{"unite":0,"state":1},"status":1},"vent":{"unite":0,"state":1}}
sqlite> .quit

5.3 - Connexion de l'application Express à la base de donnée townsdb.db3

  • Dans le code de votre application, importez la bibliothèque SQLite3 pour Node :
var sqlite3 = require('sqlite3');
sqlite3.verbose(); // pour obtenir des informations sur l'exécution
                   // des requêtes SQL (utile pour le débug)
  • Puis créer une connexion sur la base de données contenue dans le fichier "townsdb.db3" :
var db = new sqlite3.Database(__dirname + '/config.db3');
  • Quelle requête SQL permets de récupérer la configuration ? Testez-là avant dans le terminal avec la commande sqlite3.
  • Si vous faites
db.all("COMMANDE SQL", function(err,rows) {
    console.log(rows[0].value);
});

avec votre commande SQL vous devriez voir dans le terminal s'afficher la chaine JSON.

  • Plutôt que de l'afficher, utiliser la fonction JSON.parse(chaine-de-caractere) pour définir la variable config.
  • Maintenant, on est prêt à travailler: en principe, la variable config est chargée au démarrage du serveur par le contenu de la base de donnée.
  • La dernière chose à faire est de mettre à jour la base de donnée lors de l'appel à la route /set.
    • juste après la redéfinition de la variable config dans la fonction de callback, faites un appel à la base de donnée pour écrire avec JSON.stringify(config) le contenu de la variable config dans la base. Utilisez par exemple la commande db.run("COMMANDE SQL") avec la bonne commande.
    • Encore une fois, essayez avant en utilisant la ligne de commande sqlite3 pour valider votre commande.
  • Ouf ! Maintenant tout est complet. Si vous fermez le serveur puis le relancez, en principe la configuration est maintenant préservée !

Conclusion

L'application est très simple :

  • il n'y a pas de notion d'utilisateurs
  • la ville n'est pas configurable
  • ...

On peut la complexifier à loisir. Néanmoins, dans cette série de TDs nous avons vu comment faire une application relativement complexe, avec un back-end qui fait appel à une base de donnée. C'est la base.

Pour aller plus loin:

  • Pour gérer des utilisateurs, on pourrait utiliser des cookies de session, par exemple avec la librairie express-session.
  • On pourrait aussi ajouter la ville en paramètre
  • L'application pourrait aussi donner les prévisions...