Sortie de Knotter

29/04/2013

Et bien on y est. Je me suis lancé un défis il y a quelques semaines et il est réalisé en quelques temps. Knotter, mon microframework NodeJS passe en version 0.1.1. Et comme je pense que personne ne le connais, je vais vous le présenter avec des exemples à l’appui. Knotter, c’est simple un framework NodeJS basé sur un principe prototypé simple, rapide et malléable.

image

Knotter est né lors d’une étude comparative que je devais faire entre une solution Python WSGI (webapp2) et NodeJS. Comme je devais réaliser à peu de chose près la même structure de code, j’ai créé un tout petit middleware en quelques lignes pour simuler le même fonctionnement. J’aurais put utiliser Connect mais il ne collait pas exactement à mon attention. En quelques minutes j’avais la base, et j’ai réalisé mon étude. Et puis… l’idée à pris son chemin…

Les jours passaient, et dans ma tête se peaufinais l’idée. Oui j’adore Connect. Mais j’aime le pattern composite des closures pour créer des pseudo classes. Le mode Objet de Javascript n’est pas réputé, mais il faut dire que la plupart des développeurs oubli que Javascript n’a pas de classes, ni même de fonctions… Il a des “closures”. Du moment où vous acceptez cette idée, que vous la comprenez, alors le langage devient limpide.

Je ferai un billet de blog à ce sujet, passons au coeur de l’idée.

Mon blog tombait, je ne le maintenait plus et après plusieurs années de bons et loyaux services, Copix 3 sous PHP 5.2 n’y arrivait plus. Depuis des mois je n’ai pas touché à PHP, je code en Python et JS à longueur de journée… et voilà alors le projet qui nait. Je me décide, je fais mon framework sur mesure pour gérer mon blog.

Plus de MySQL, on passe à MongoDB et je me lance dans le code.

Knotter commence à naitre, au départ avec des erreurs monumentales de conception: les “handlers” sont des objets non prototypés, les cookies sont mal gérés, et surtout pas moyen de faire passer des sessions d’un worker à l’autre. Mais outre cela, le modèle était pas mauvais, il fallait juste une refonte, repenser le core.

La version 0.1.1 devait faire sauter le bouchon, et voici comment vous pouvez créer un petit projet avec Knotter, Petit car, mis à part mon site, je ne sais pas à quel point cela est viable. Mais mes benchmarks sont assez encourageant.

Installation

On va commencer par créer notre dossier de travail. Nous allons le créer dans “~/Dev/knotter-example”. Au final, vous aurez cette structure de répertoire:

~/Dev/knotter-example/
                     |_ index.js
                     |_ templates/
                     |      |_example.html
                     |
                     |_ handlers/
                     |      |_page.js
                     |
                     |_ statics/
                               |_ css/
                               |     |_base.css
                               |_ images/
                                     |_test.png

Ce n’est pas compliqué, tapez dans un terminal:

mkdir -p ~/Dev/knotter-example/{handlers,statics,statics/css,static/images,templates}
cd ~/Dev/knotter-example

Installez knotter via “npm”:

npm install knotter

Inutile de l’installer globalement, knotter s’installe localement. Du coup, vous allez avoir un répertoire “node_modules” qui va apparaitre, avec à l’intérieur une installation toute prête de knotter.

Puisqu’on va faire les choses bien, on va utiliser un moteur de template. Knotter est agnostique au moteur de templates car il utilise le génial module “consolidate”. Mon moteur de templates préféré étant “swig”, on passe par la commande:

npm install swig

Swig a l’avantage de ressembler fortement à Twig ou Jinja et me permet de ne pas me mélanger les pinceaux. Bref, passons au code.

Premier handler

Un handler est une closure prototypée (oui, une classe allez…) qui hérite (se compose en réalité) de knotter.Handler. Afin d’assurer l’héritage, il faut respecter une syntaxe simple:

// fichier ~/Dev/knotter-example/handlers/page.js

var knotter = require('knotter'),
    util    = require('util');

//on crée notre classe PageHandler, le nom est arbitraire
PageHandler(){
    // on appelle le constructeur parent
    knotter.Handler.call(this);
}
//on fait hériter la classe
util.inherits(PageHandler, knotter.Handler)

Et c’est tout… enfin presque. Là nous avons un handler, mais il doit répondre à une url et surtout à une méthode HTTP précise, continuez:

// l'expression régulière pour répondre
// la route *doit* faire parti du prototype
PageHandler.prototype.route = "^/page/example";

// le handler "get" 
PageHandler.prototype.get = function () {
    this.end("Coucou")
};

// on exporte le handler
module.exports = PageHandler;

Voilà pour le handler, on passe au serveur

Le serveur

Comme la plupart des nouvelles technologies de service Web, c’est l’outil qui sert les réponses depuis sont propre mécanisme de serveur. Ainsi, votre application Web répond elle-même aux clients. On aime passer par un proxy frontal type nginx pour simplifier une partie du travail, mais dans l’ensemble le serveur HTTP intégré de Node s’en sort bien.

Mais knotter fourni une classe qui va permettre de définir la configuration, savoir quels handlers utiliser, etc…

// fichier ~/Dev/knotter-example/index.js

var knotter = require('knotter'),
    PageHandler = require('./handlers/page.js');

var server = new knotter.Server({
    handlers : [ PageHandler ]
})
// on lance le serveur
server.start();

Voilà, rien de bien compliqué ici. Pour le moment notez simplement que nous n’avons qu’un handler dans la liste (n’oubliez pas que “handlers” est un tableau). Il suffit alors de lancer:

node index.js

Et de se rendre sur http://127.0.0.1:8000/page/example

Un fabuleux “Coucou” apparait, c’est gagné :)

Gérer les arguments et utiliser un moteur de template

Nous avons installé un moteur de template, alors utilison le ! Mais pour rendre l’exemple plus sexy, on va utiliser les arguments passés à la route.

Reprenons handler/page.js et modifions la route et le handler “get”.

// la regexp qui prend deux arguments
// le "?" permet de ne pas capturer de "/" dans la première
// capture.
PageHandler.prototype.route = "^/page/example/(.*?)/(.*)";

// on va utiliser un template
PageHandler.prototype.get = function () {

    var firstname = this.params.args[1];
    var lastname = this.params.args[2];


    // on appelle le template example.html à la racine de "templates"
    // La racine sera déclaré dans la configuration serveur, lisez la suite...
    // Le second argument est le contexte envoyé au template, les 
    // propriétés seront des variables de template
    this.render('/example.html', {
        lastname: lastname,
        firstname : firstname
    });

};

Dans le fichier “templates/example.html” on va juste faire simple:

Bonjour {{firstname}} {{lastname}}

On passe à “index.js”, on y configure l’utilisation de swig

var server = new knotter.Server({
    handlers : [ PageHandler ],
    engine   : "swig",
    templates : __dirname + "/templates",
    engineOptions: {
        root: __dirname +"/templates",
        allowErrors : true,
        autoescape : false,
        cache : false
    }
});

Il faut malheureusement bel et bien répéter les deux options de “templates” et “root”. Je n’ai pas encore trouvé de palliatif…

Bref, coupez le serveur si ce n’est pas déjà fait, et relancez le. Puis allez vois les pages:

Les arguments passés à l’url apparaissent bien, c’est encore gagné !

Sessions

Les sessions fonctionnent correctement depuis la version 0.0.5 mais la version 0.1.1 permet désormais de les partager entre clusters (voir section suivante).

Pour utiliser des variables de sessions, il suffit, dans un handler, d’utiliser l’objet “this.sessions”.

this.sessions.get('key'); // retourne la valeur de session utilisateur pour la clef "key"
this.sessions.set('key', 'une valeur'); // assigne la valeur à une clef

Les valeurs sont sérialisée via JSON, ce qui induit que vous ne pouvez pas garder une classe complexe en session, mais un objet simple sera correctement récupéré.

Les sessions restent en mémoire, elles ont un TTL de 15 minutes d’inactivité. Je vais régler d’ici la version 0.1.2 le fait de pouvoir changer ce TTL.

Cluster, et sessions

L’un des mes soucis majeurs était de réussir à faire tourner plusieurs “workers” en parallèle tout en pouvant gérer des variables de sessions. Car vous le savez certainement, Node est “monoprocess”. Cela signifie que pour pouvoir répondre en même temps à plusieurs clients, il faut jongler avec le module “cluster”.

Mais le passage de valeurs a été une bataille sans nom… Knotter fourni une classe pour simplifier le travail. Voici comment vous pouvez procéder.

Coupez les serveur (CTRL+C) et modifiez les fichier index.js:

var cluster = require('cluster'),
    numCPUs = require('os').cpus().length,
    knotter = require('knotter');

if (cluster.isMaster) {
    var n = cluster.fork();
    knotter.Cluster.addWorker(n);

} else {
    // dans le worker, on lance le serveur

    var server = new knotter.Server({
        handlers : [ PageHandler ],
        engine   : "swig",
        templates : __dirname + "/templates",
        engineOptions: {
            root: __dirname +"/templates",
            allowErrors : true,
            autoescape : false,
            cache : false
        }
    });

    server.start();

}

Le sous-module “Cluster” de Knotter permet d’envoyer les sessions lors des modifications de valeurs. Tout est fait automatiquement.

A terme, le système de sessions sera paramétrable pour pouvoir utiliser un memcache ou redis… pour le moment c’est amplement suffisant pour mon cas personnel.

Fichiers statiques

Vous allez avoir besoin de servir des fichiers statiques, ne serait-ce que des images ou des feuilles de styles. Ici nous utilisons le répertoire “statics”. Pour décorèler un peu et que vous comprenniez bien comment cela fonctionne, je fais en sorte de créer deux routes spécifiques pour les images et le css. Voici la configuration à donner au serveur:

var server = new knotter.Server({
    handlers : [ PageHandler ],
    engine   : "swig",
    templates : __dirname + "/templates",
    engineOptions: {
        root: __dirname +"/templates",
        allowErrors : true,
        autoescape : false,
        cache : false
    },
    // deux points pour les fichiers statiques
    statics: {
        'styles': __dirname + '/statics/css/',
        'img' : __diranme + '/statics/images/'
    }
});

Ainsi:

Concrètement

Concrètement mon blog tourne sur Knotter. J’ai créé deux ou trois modules pour gérer les tickets, le flux RSS ou les sitemaps. Cela ne m’a pas pris longtemps, mais je maitrise mon outil… Je vais vraiment devoir documenter Knotter sérieusement si vous êtes intéressés par mon projet. Dans tous les cas, celui-ci ne mourra pas. Le simple fait de l’utiliser ici pour mon blog va me pousser à le maintenir. Mais je vous avoues qu’un peu d’aide ou ne serait-ce que quelques personnes me faisant un retour serait un bon carburant.

Pour aller plus loin

Lisez la documentation de swig, notamment en ce qui concerne “extends” dans l’API. Cela va vous permettre de faire des templates “globaux” dans lesquels vous injecterez des blocks.

L’utilisation d’une base de données sera vraiment un plus, MongoDB est en quelques sorte la panacée puisque la syntaxe est aussi en Javascript… mais n’importe quelle base de données fera l’affaire.

Je pense avoir fait un tour assez clair… à moins que vous ayez des questions ?

Ça peut vous intéresser aussi


Reprise du blog avec NodeJS

Vous avez remarqué ? nouveau blog, nouveau design, et surtout ...


Mise à jour du blog en Go

Si vous connaissiez mon blog, vous avez remarqué qu’à ...


Deux modes de vue Blog

Après tant de remarques sur ma “vue en colonne ...


Et si on regardait nodejs

Quand j’entend beaucoup de développeurs (expérimentés qui plus est) cracher ...

Merci de m'aider à financer mes services

Si vous avez apprécié cet article, je vous serai reconnaissant de m'aider à me payer une petite bière :)

Si vous voulez en savoir plus sur l'utilisation de flattr sur mon blog, lisez cette page: Ayez pitié de moi

Commentaires

Ajouter un commentaire

Ajouter un commentaire

(*) Votre e-mail ne sera ni revendu, ni rendu public, ni utilisé pour vous proposer des mails commerciaux. Il n'est utilisé que pour vous contacter en cas de souci avec le contenu du commentaire, ou pour vous prévenir d'un nouveau commentaire si vous avez coché la case prévue à cet effet.