Créer un framework Web avec Node.js - Partie 2

Deuxième partie : la gestion des données

Auteurs

| Julien Alric | Didier Mouronval |

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, hôtes virtuels, 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 à 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 briques, du ciment et une truelle, nous faisons le choix de proposer des fondations un peu plus avancées tout en détaillant les mécanismes internes du framework. Bonne lecture.

Cet article a été publié avec l'aimable autorisation de Julien Alric. L'article original (Créer un framework avec Node.js – 2nd partie) peut être vu sur le blog de Julien Alric.

Commentez

- Titre : Créer un framework Web avec Node.js - Partie 2

- Auteurs : Julien Alric, Didier Mouronval

- Parution : 25 octobre 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Julien Alric. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Gestion des données

- II. Intégrer Mongoose

- III. Création d'un schéma

- IV. Création d'un modèle

- V. Accès aux modèles depuis les contrôleurs

- VI. Ajout de librairies externes au niveau global

- VII. Conclusion

- Remerciements

I - Gestion des données

À partir de la structure de base que nous avons définie 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 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 contrôleur 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 reverrait la connexion demandée, dont les informations seraient stockées 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 sûr 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.

II - Intégrer Mongoose

Pour installer Mongoose, rien de très compliqué. Il suffit là 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émas de données. Les définir au-dessus poserait 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.

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ées 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;
}
}
}
}

III - 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 renvoie vers la documentation officielle de Mongoose.

IV - 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();
});
}

V - 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.

VI - Ajout de librairies externes au niveau global

Vous vous rappelez peut-être que dans la première partie, nous avions créé une variable globale $ contenant par la suite plusieurs fonctions que nous avons ajoutées dans framework.js. Dans le paragraphe précédant, vous avez vu apparaître une variable globale S utilisée pour manipuler les chaînes 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 contexte global peut être écrasé. On évitera cela en utilisant Object.defineProperties.

À ce stade, j'ai intégré trois librairies :

- $, le framework ;

- _, Lo-Dash, 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')
}
});

Et nous plaçons notre fonction define dans framework.js

exports.define = function(property, value, scope) {
Object.defineProperty(scope, property, {
value: value,
enumerable: true
});
}

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

VII - 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ées (compression, gestion des eTags…), tests et debogage, ou autre chose. Nous verrons bien, suite dans la prochaine partie.

Remerciements

Cet article a été publié avec l'aimable autorisation de Julien Alric. L'article original (Créer un framework avec Node.js – 2nd partie) peut être vu sur le blog de Julien Alric.

Nous tenons à remercier XXX pour sa relecture attentive de cet article.

N'hésitez pas à commenter cet article sur le forum. Commentez