Créer un framework avec Node.js – 2nd partie

L’objectif de cette série d’articles est de présenter une à une chacune des briques nécessaires à la réalisation d’un framework. Dans le première partie, nous nous sommes intéressés à la structure de base de notre framework. Arborescence, virtual hosts, HTTPS, définition des chemins, paramètres d’url, routes, templates, contexte d’exécution, logs et envoi de fichiers statiques. Dans cette seconde partie, nous nous intéresserons entre autres mais surtout à tout ce qui touche à la gestion des données.

Notre but ici n’est pas de réaliser un micro framework comme Express (qui forme avec Connect un outil réellement puissant), mais un framework full stack MVC basé sur le principe de convention over configuration. L’idée étant de fournir des rails sur lesquels le développeur peut s’appuyer. Express propose un jeu de brique, du ciment et une truelle, nous faisons le choix de proposer des fondations un peu plus avancés tout en détaillant les mécanismes internes du framework. Bonne lecture.

Avertissement

Suite à un souci, j’ai malheureusement perdu tout le contenu de cet article. Aussi, la version que vous avez sous les yeux est volontairement un peu plus courte, donc au choix, moins complète, ou plus synthétique. Si des points ne vous semblent pas assez détaillés, n’hésitez pas à me le signaler en commentaire ou par mail, j’ajouterais des compléments à l’article.

Gestion des données

A partir de la structure de base que nous avons défini dans l’article précédent, créer des modèles pour l’accès aux données n’est pas très compliqué. Nous allons voir ici comment faire avec MongoDB, mais l’approche serait similaire avec n’importe quel autre SGBD ou API. Voyons cela.

Pour commencer, nous allons aller sur le site de MongoDB pour télécharger la base que nous décompresserons dans system/mongodb, nous ajouterons également un dossier datas à ce répertoire. Puis, nous allons créer les deux fichiers batch suivant, le premier à la racine et le second dans system.

database.bat

"system/mongodb/bin/mongod.exe" --dbpath system/mongodb/datas
pause

dbclient.bat

"mongodb/bin/mongo.exe"

Nous lançons le serveur, et nous pouvons vérifier avec le client que tout fonctionne.

Nous nous rendons ensuite dans system, pour faire un npm install mongodb afin d’installer le driver MongoDB de Node.js.

Nous allons maintenant essayer d’insérer une donnée à partir du controller index.js pour vérifier que tout fonctionne en reprenant le code d’exemple de la documentation du module :

var MongoClient = require('mongodb').MongoClient
    , format = require('util').format;    

  MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) {
    if(err) throw err;

    var collection = db.collection('test_insert');
    collection.insert({a:2}, function(err, docs) {

      collection.count(function(err, count) {
        console.log(format("count = %s", count));
      });

      // Locate all the entries using find
      collection.find().toArray(function(err, results) {
        console.dir(results);
        // Let's close the db
        db.close();
      });      
    });
  })

Pour factoriser cela, nous pourrions ici créer un module qui nous reverrai la connexion demandé, dont les informations seraient stockés dans un fichier de configuration. Nous créerions ensuite un modèle par collection dans lesquels nous placerions nos méthodes d’accès aux données (ces fichiers seraient bien sur dans le dossier models). Nous finirions par ajouter une méthode model dans app.js que nous pourrions appeler depuis nos contrôleurs pour charger le modèle demandé. Ici, nous allons faire un petit peu plus compliqué, en utilisant l’ODM Mongoose. Pour autant, vous pouvez choisir de vous en passer. Comme le souligne MongoDB dans la documentation :

Because MongoDB is so easy to use, the basic Node.js driver can be the best solution for many applications. However, if you need validations, associations, and other high-level data modeling functions, then an Object Document Mapper may be helpful.

Traduction
Comme MongoDB est facile à utiliser, le driver de base est la meilleure solution pour beaucoup d’applications. Toutefois, si vous avez besoin de validations, d’associations et d’autres fonctions haut niveau de modélisation de données, un ODM pourrait vous être utile.

Intégrer Mongoose

Pour installer Mongoose, rien de très compliqué. Il suffit la aussi d’utiliser npm en faisant un npm install mongoose.


Ce qui va changer un petit peu, c’est que nous allons modifier notre arborescence pour rajouter un niveau pour chaque projet. Comme vous pouvez le voir sur l’image.

Par exemple, demo contient ici deux sous projets api et site, ainsi qu’un dossier datas, qui contiendra notamment la définition de nos schéma de données. Les définir au dessus poserais problème. En théorie tous nos projets ne manipuleront pas les mêmes données. De la même manière, définir les données à l’intérieur de site, d’api…, c’est devoir les définir plusieurs fois pour des projets qui utiliseront le même pool de données, c’est donc introduire une redondance de code qui va à l’encontre du DRY (Don’t Repeat Yourself).

Le dossier datas/config contiendra les dossiers dev, test et prod (un dossier par environnement), et le fichier de configuration entities.json. Dans dev, test et prod nous placerons un fichier db.js. Voici le contenu de ces fichiers :

db.json

{
   "main":{
      "dbms":"mongo",
      "host":"127.0.0.1",
      "port":"27017",
      "base":"test",
      "options": null
   },
   "example":{
      "dbms":"mysql",
      "host":"127.0.0.1",
      "port":"3306",
      "base":"test",
      "user":"demo",
      "pass":"hU8@mJ32%1",
      "char":"utf8"
   }
}

Ce fichier contiendra la configuration de nos différentes connexions, ici une connexion vers notre base mongo, et une base MySQL.

entities.json

{
   "user":{
      "connection":"main",
      "base": "test"
   },
   "product":{
      "connection":"main"
   }
}

Dans ce fichier nous associons une connexion à chaque entité, avec optionnellement la base de donnée que nous allons interroger.

Nous créons finalement le module db.js dans datas/modules, en voici le contenu :

var env = process.env.NODE_ENV || 'prod';

var openDb = {};

var enConf = require('../config/entities.json');
var dbConf = require('../config/'+env+'/db.json');

exports.dbCon = function(schema) {
    if (typeof enConf[schema] !== 'undefined') {
        var dbInfos = dbConf[enConf[schema]['connection']];
        var base = enConf[schema]['base'] || dbInfos['base'];
           
        if ($.isset(openDb[base])) {
            return openDb[base];
        } else {
            switch(dbInfos['dbms']) {
                case 'mongo':
                    var mongoose = $.require('mongoose');
                       
                    var dbCon = mongoose.createConnection('mongodb://'+dbInfos['host']+':'+dbInfos['port']+'/'+base, dbInfos['options']);
                    openDb[base] = dbCon;
                    return dbCon;
                break;
            }
        }
    }
}

Création d’un schéma

Nous allons maintenant nous rendre dans datas/schemas, et réaliser un schéma, ici par exemple un schéma de données représentant un utilisateur :

var mongoose = $.require('mongoose')
  , Schema = mongoose.Schema;

var db = require('../modules/db.js');
var dbCon = db.dbCon('user');

var userSchema = new Schema({
    _id: String,
    vanity:  String,
    slug: { type: String, unique: true },
    password: String,
    username: {type: String, default: 'Anonymous'},
    locale: String,
    session: String,
    ip: String,
    profile: {
        firstname: String,
        lastname: String,
        about: String,
        avatar: {type: String, default: 'avatar.jpg'},
        gender: String,
        birthdate: String,
        country: String,
        city: String,
        occupation: String,
        website: String,
        biography: String
    },
    creation: {type: Date, default: Date.now}
});

userSchema.methods.findUserFromSlug = function (cb) {
    return this.model('User').find({ slug: this.slug }, cb);
}

userSchema.virtual('profile.fullname').get(function () {
    return this.profile.firstname + ' ' + this.profile.lastname;
});
 
module.exports = dbCon.model('User', userSchema);

Pour plus d’information sur la définition des schémas, je vous renvois vers la documentation officielle de Mongoose.

Création d’un modèle

Nous allons ensuite dans models créer un modèle user.js :

var app  = require(module.parent.id);

exports.create = function(datas) {
    var User = app.entity('user');
    new User(datas).save();
}

exports.read = function(conditions, callback) {
    var User = app.entity('user');
    User.findOne(conditions, function (err, user) {
        if(err) throw err;
        callback(user);
    });
}

exports.update = function(conditions, datas, callback) {
    var User = app.entity('user');
    User.findOneAndUpdate(conditions, datas, {'new': true}, function(err, user) {
        if(err) throw err;
        callback(user);
    });
}

exports.delete = function(conditions, callback) {
    var User = app.entity('user');
    User.remove(conditions, function(err) {
        if(err) throw err;
        callback();
    });
}

Accès aux modèles depuis les contrôleurs

Enfin, dans un de nos contrôleurs nous pourrions trouver les codes suivant :

var user = ctx.params.post.user;
var userModel = app.model('user');
       
user = $.merge(user, {
    _id: slug+'@'+app.site.domain,
    slug: S(user.vanity).slugify().s,
    locale: ctx.client.locale,
    session: ctx.client.id,
    ip: ctx.client.ip});
userModel.create(user);
var userModel = app.model('user');

userModel.update({session: ctx.client.id}, ctx.params.post.user, function(user) {
    console.log(user);
    res.end();
});

Vous avez peut-être remarqué ici que nous avons placé la session au niveau de Mongo. C’est un choix de conception relativement discutable, l’idéal étant plutôt ici de gérer la session indépendamment avec Memcache ou Redis.

Ajout de librairies externes au niveau global

Vous vous rappelez peut-être, dans la première partie, nous avions créer une variable globale $ contenant par la suite plusieurs fonctions que nous avons ajoutés dans framework.js. Dans le paragraphe précédant, vous avez vu apparaître une variable globale S utilisée pour manipuler les chaines de caractères. En faisant cela, notre but est de proposer tout un ensemble de méthodes puissantes et utiles accessibles partout depuis toutes nos applications sans avoir besoin de charger spécifiquement un module. Inconvénient, notre scope global peut être écrasé. On évitera cela en utilisant Object.defineProperties).

A ce stade, j’ai intégré 3 librairies :
- $, le framework.
- _, lodash, une réécriture d’underscore, plus rapide et plus complète.
- S, string.js, une librairie de manipulation de chaines de caractères.

L’ajout de ces trois librairies se fait tout simplement ainsi en haut de notre fichier server.js :

Object.defineProperties(global, {
    "_": {
        value: require('lodash')
    },
    "$": {
        value: require('core')
    },
    "S": {
        value: require('string')
    }
});

$.merge que vous avez rencontré au dessus est un adapteur de _.merge de la librairie lodash (pattern adaptor). A ce stade, tout votre code à ainsi accès à ces librairies.

Conclusion

Ayant perdu une version précédente, cette partie est un peu plus courte et moins ambitieuse que prévu. Je ne sais pas encore de quoi traitera la prochaine. Temps réel avec socket.io, packaging du framework et partage sur GitHub, ajout de fonctionnalité HTTP avancés (compression, gestion des eTags…), tests et debug, ou autre chose. Nous verrons bien, suite dans la prochaine partie.


Contact : julien.alric[at]posthub.net.

6 Comments Créer un framework avec Node.js – 2nd partie

    1. Julien

      Code Igniter est un très bon framework PHP à la fois léger, rapide et assez complet, à mi-chemin entre le micro framework et la grosse machine genre Symfony ou Zend. C’est vers ce type de framework que ce « projet » tend.

      Reply
  1. Renate

    I read a lot of interesting articles here. Probably you spend a lot of time writing, i know how to save you
    a lot of time, there is an online tool that creates readable, SEO friendly articles
    in minutes, just search in google – laranitas free content source

    Reply

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>