Présentation de Node.JS et du framework Express

Je me rends compte que dans ce blog je vous parle de Node.JS et d’Express, sans vous avoir jamais présenté ces technologies. Allez, revenons vite fait aux bases :)

Installation

Pour l’installation, je vous renvoie au site officiel, ou à un article en français par Atinux, mais globalement l’installation se résume à ces 3 lignes:

git clone https://github.com/ry/node.git
cd node
./configure && make && sudo make install

Présentation de Node.JS

Node.JS est un projet open-source se basant sur le moteur « V8 » de Chrome (il existe un fork « SpiderNode » par Mozilla, basé sur leur moteur SpiderMonkey).

Il s’agit donc finalement d’un simple interpréteur Javascript, exécutable, et enrichissant le langage avec sa propre API (accès au système de fichier, à la couche réseau, etc.). Ça permet, en résumé, d’exécuter des fichiers « .js » comme des scripts PHP, Python, Ruby, etc…

La spécificité n’est pas vraiment là (après tout, exécuter du JS en ligne de commande, ça se fait déjà), mais évidemment sur son API, toute entière orientée vers le non bloquant, c’est-à-dire que la plupart des commandes, notamment d’accès au système de fichier, rendront la main directement sans attendre la réponse du système, ceci étant largement simplifié par l’orientation évènementielle de Javascript. C’est ce choix de conception, ainsi que le choix du moteur V8 qui offre d’excellentes performances, qui permet d’écrire des applications avec d’excellents temps de réponse, et notamment des serveurs (web ou autres). Son API d’accès à la couche réseau (le protocole HTTP y est implémenté) couplé à cette capacité de gestion des accès concurrents en font évidemment un excellent candidat pour écrire des applications web.


Tout ça tombe bien, les gens du Web a priori savent déjà un peu utiliser Javascript 😉 Il est même possible de réutiliser des librairies client côté serveur, comme jQuery (pour parser du HTML côté serveur par exemple), Backbone.js (pour partager la couche modèle, ou au moins sa validation), et quelques librairies « utilitaires » comme underscore (set d’outils pour augmenter l’API JS).

Il y a un exemple de serveur http sur la page d’accueil, donc pour changer je vous mets un exemple de requête vers une base de données utilisant node-mysql illustrant une série d’appels asynchrones:

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
var Client = require('mysql').Client,
    client = new Client();
 
// Connexion à la BDD
client.connect(onConnect);
 
// Callback appelé dès que la connexion est faite
function onConnect(err) {
  if (err) throw err; // En cas d'erreur
  // Exécution de la requêtes "USE ..."
  client.useDatabase('mabase', onReady);
}
 
// Callback appelé dès que le "USE ..." est terminé
function onReady(err) {
  if (err) throw err; // En cas d'erreur
  // Exécution d'une requête SELECT, avec passage d'un callback anonyme
  client.query('SELECT username FROM users WHERE status = ?', ['active'],
    function onActiveUsers(err, results) {
      // Parcours des résultats
      results.forEach(function(result) {
        console.log(result.username);
      });
    }
  );
}

Cette utilisation intensive des callbacks ou des évènements pour gérer les accès concurrents est le plus gros avantage de Node.JS, mais aussi un des plus gros pièges quand vous débuterez sur cette plateforme, la tentation d’imbriquer les callbacks anonymes est forte, et on la paie bien avant d’attendre les 25 niveaux d’indentation 😉 Le module « async.js » permet de simplifier ce type de code.

Les modules

Node.JS implémente une partie des spécifications de CommonJS pour les modules.

Basiquement, un module est un fichier JS qui va présenter à l’extérieur son objet « exports »:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// monmodule.js
console.log(module); // Mon module courant, il s'agit d'un objet avec quelques propriétés,
                     // la plus importante étant pour nous "exports"
console.log(module.exports); // Un objet vide, qui sera l'API publique de mon module
// Note: "exports" est aussi défini, et est un synonyme de module.exports
 
// Une fonction privée, que seul mon module peut utiliser
function log(texte) {
  console.log(texte); 
}
 
// Une fonction publique, ou "exportée"
exports.coucouGamin = function coucouGamin() {
  log("coucou gamin");
}

On utilise des modules par l’intermédiaire de la fonction « require() »:

1
2
var monmodule = require('./monmodule.js');
monmodule.coucouGamin(); // affiche "coucou gamin"

Tout ça est assez trivial, et il en découle des méthodes pour partager un module entre le client et le serveur.

Les dépôts de modules: NPM

Bien sûr, on évite de réinventer la roue, et il faut donc packager ses modules, les versionner, et gérer les dépendances. Pour cela on va utiliser des gestionnaires de packages, comme NPM (pour « Node Package Manager », sobrement).

Un petit coup de « npm install monmodule », il va chercher dans les dépôts de npmjs.org, télécharger le module, et le placer dans « ./node_modules/monmodule ». Il se base sur un fichier « package.json » qui va déclarer le nom du script principal, ainsi que les dépendances à d’autres modules. Comme ce packager est le principal utilisé avec NodeJS, le système de résolution du chemin vers les modules s’est adapté au packager :) du coup tout ça marche ensemble de manière totalement transparente.

Pour illustrer le succès de Node: on a aujourd’hui plus de 2200 modules sur npmjs.org, et au moins 10 nouveaux modules par jour.

Exemple d’un package.json minimal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "author": "Nicolas Chambrier", // Nom de l'auteur
  "name": "SharedWhiteboard", // Nom de l'application
  "version": "0.0.0",
  "repository": {
    "url": "" // Pas encore versionné :)
  },
  "engines": {
    "node": "0.4.x" // Versions supportées de Node pour cette application
  },
  "dependencies": { // Liste des modules requis
    "socket.io": "*", // Socket.IO, n'importe quelle version
    "express": ">= 2.3.5", // Express, au moins la version 2.3.5
    "cluster": "*", // Cluster (serveur multi-core)
    "ejs": "*" // EJS (moteur de template)
  },
  "devDependencies": { // Dépendances en environnement de développement
    "vows": "*" // Tests unitaires à la Behat
  }
}

Pour aller plus loin: « Package.json dependencies done right » sera une bonne lecture.

Node.JS pour le web: présentation du framework Express

On l’a vu, Node.JS est très polyvalent, et permet d’écrire n’importe quel type de service TCP, mais quand-même son protocole star reste le HTTP, et il est donc fait avant tout pour le Web.

Supposons qu’on veuille faire un site avec deux pages: « / » qui affiche un lien vers « /hello » qui affiche « coucou gamin ». Ouais. Gros site.

Sans framework on pourra travailler comme ça:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var http = require('http');
http.createServer(function (req, res) {
  if (req.url == '/') {
    // Home
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('<a href="/hello">Say hello</a>');
  } else if (req.url == '/hello') {
    // Coucou gamin
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('Coucou gamin.');
  } else {
    // Autres: 404
    res.writeHead(404);
    res.end();
  }
}).listen(8080, undefined);

À partir de là on va se dire que c’est dommage de coller tout cet HTML en dur et on va donc sortir ça dans des fichiers, puis utiliser un moteur de template pour le rendu. On va se créer un template générique qu’on appellera « layout », et des « sous-templates » pour le rendus spécifiques. Les codes 200 seront le retour par défaut, et on commencera à y voir plus clair.

L’étape d’après serait de matcher les URLs avec des expressions régulières pour faire un joli routing. Bon, on s’arrête là, on est en train de réinventer Express (lui-même basé sur Connect qui s’occupe de la partie réseau et routing, Express ajoutant quelques raccourcis simplificateurs ainsi que le support des templates). Voici le même serveur, écrit avec ce framework:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express');
var app = module.exports = express.createServer();
// Configuration
app.configure(function configure() {
  this.set('view engine', 'ejs'); // On utilise le moteur de template "EJS"
  this.set('view options', {"title": 'Coucou gamin'}); // Dans tous nos templates, la variable
                                                       // "title" vaudra "Coucou gamin"
});
// Route "/" avec la méthode GET
app.get('/', function home(req, res, next) {
  res.render('home'); // HTML généré à partir de "views/home.ejs", inclus dans "views/layout.ejs"
});
// Route "/hello" ou "/hello/nom-de-la-personne-a-qui-on-dit-coucou", avec la méthode GET
app.get('/hello/:name?', function hello(req, res, next) {
  res.render('hello', { // HTML généré à partir de "views/hello.ejs" et "views/layout.ejs
    "name": req.params.name // Dans le template, la variable "name" sera disponible, et vaudra
                            // la portion correspondante de l'URL, ou undefined
  });
});
// Démarrage du serveur
app.listen(8080);

Pas forcément plus court, mais le routing est déjà là, le support des templates aussi, on peut structurer notre application et ajouter de nouvelles URLs, potentiellement complexes, de manière très simples.

Ce « micro-framework » (dans la même veine que Sinatra en Ruby, Silex en PHP, web.py en Python, etc…), est aussi puissant que ses cousins des autres langages, et n’ajoute qu’une couche assez fine entre lui et le module « http » natif de Node, n’aggravant ainsi pas trop les performances. Sa gestion du cache lui permet d’être très efficace malgré la gestion des templates (probablement plus que ce qu’on réaliserait en le faisant soi-même dans un premier temps).

Le support de « middleware » est un gros atout, que je tiens à présenter ici car je considèrerai cette notion acquise dans un prochain article qui décrira les bonnes pratiques pour réaliser des applications basées sur Express bien packagées et facilement réutilisables.

Les middlewares

Dans Connect, un middleware est un processus qui va se placer entre la requête et l’envoi de la réponses. Du coup, tous les traitements qui consistent à récupérer la requête, la manipuler, et renvoyer une réponse, sont des middlewares. Le routing n’est d’ailleurs qu’un middleware parmi d’autres :)

Par exemple, pour créer un middleware qui va ajoute une ligne de log à chaque requête:

1
2
3
4
5
6
app.use(function(req, res, next) {
  console.log('...');
  next(); // L'appel à next() indique qu'on souhaite continuer la chaîne des middlewares
  // Si on interrompt ici cette chaîne, sans avoir renvoyé de réponse au client, il n'y aura
  // pas d'autres traitements, et le client verra simplement une page mouliner dans le vide...
});

De plus, un middleware peut être limité à un prefixe d’URL (une route):

1
2
3
4
app.use('/hello', function(req, res, next) {
  // Filtre exécuté sur toutes les URLs commençant par "/hello"
  next();
});

On peut également affecter un middleware à une route en particulier:

1
2
3
4
5
6
7
8
9
10
11
var middleware1 = function(req, res, next) {
  ...
  next();
};
var middleware2 = function (req, res, next) {
  ...
  next();
};
app.get('/hello/:name', [middleware1, middleware2], function (req, res) {
  res.render('hello'...);
});

Il existe un grand nombre de middlewares inclus par défaut dans Connect pour réaliser des tâches standard: décoder les requêtes POST, gérer les cookies, mettre en place un système de session, gérer le favicon, compiler les assets (css & js), faire du logging, ou même gérer les vhosts… Ce qu’il est surtout intéressant de noter c’est que toutes ces fonctionnalités ne sont pas actives par défaut: si on n’a pas besoin d’une de ces fonctionnalités, on ne l’active pas voilà tout :)

Il y a du très bon, et des choix de conception un peu malheureux qui rendent compliquées certaines opérations. Je reviendrai plus en détail sur les middlewares dans un prochain article.

Prochainement…

Je crois très fort en Node.JS, mais je suis conscient de ses lacunes. Une des plus grosses lacunes concerne ses modules: c’est encore globalement du travail d’amateur, de passionné d’open-source, qui sont en général des gens très compétents, mais infoutus de pondre une documentation lisible. On n’y coupe pas, et beaucoup de modules très importants manquent de ressources: la doc se résume généralement au README sur github, ou à une doc d’API auto-générée à partir des commentaires du code. C’est déjà ça, mais on est loin des cookbooks de Symfony 😉

Au revanche, tant au niveau de la qualité du code, des tests unitaires, et des performances bruts du résultat, on a des choses très satisfaisantes: avec des ressources comme Express (qui simplifie grandement l’écriture d’applications même complexes), Cluster (qui tire parti du multi-core sur un serveur de production), Connect et ses middlewares (qui permettent de filtrer ou d’enrichir les requêtes de manière transparente), et la floppée de connecteurs à des services externes (bases de données, services web, etc…), on peut vraiment dire qu’on a tous les outils pour faire du bon boulot, et du boulot qui tiendra bien la montée en charge s’il-vous-plait !

Je vais me concentrer sur ces sujets pendant un petit moment: vous présenter Cluster, aller plus en profondeur dans le fonctionnement d’Express, et vous parler des projets que j’ai en cours. Il y a encore un bout de chemin à faire 😉

7 réflexions au sujet de « Présentation de Node.JS et du framework Express »

  1. Ping : Bonnes pratiques pour gérer son code asynchrone en Javascript « naholyr

  2. Ping : Benchmark Node.JS: méthodes synchrones ou asynchrones ? « naholyr

  3. Ping : Comment bien débuter avec Node.js 30 minutes par jour

Laisser un commentaire