A voir : Les GROS tutos Play!> 1 & 2 !

Express.js, le Play!>Framework du Javascript ?

Pas tout seul, mais en lui ajoutant 2 ou 3 petites choses, on s'en approche.

Introduction

Pour démontrer ce que j'ai écrit dans mon titre, je vais "réaliser" une application à l'aide de :

  • Node.js
  • Express.js
  • Backbone.js
  • ... et d'autres, mais vous verrez ça plus loin

Cette application, permettra (pour cette fois) :

  • de saisir des messages en markdown avec la possibilité de coloriser les portions de codes présentes dans les messages
  • de garder ces messages en mémoire (je vais simuler un système de persistance en mémoire, mais je ré-initialise tous les 5 messages)

Donc, nous allons :

  • créer des routes, des vues, des modèles, …
  • faire des requêtes de types REST, avec des POST, PUT, GET, DELETE

Si vous arrivez au bout, vous aurez de quoi vous amuser. Et je tenterais d'aller plus loin pour les prochains articles (cf. fin de cet article).

Par avance, désolé, je ne fait aucune gestion d'erreur, j'utilise les id dans mon code html, etc. …

Bon, on s'y colle. J'ai appelé mon application stykkekode qui veut dire "bout de code" en norvégien.

Installation côté serveur

Pré-requis

Tout d'abord, vous devez installer Nodejs : http://nodejs.org/#download, l'installeur en profite pour installer npm (node package manager), ce qui nous permettra d'installer le reste des composants.

Une fois Nodejs installé, nous allons installer Express.js (http://expressjs.com/), "petit" framework un peu dans le même esprit que Play!> qui permet de générer des applications web sous Nodejs.

Pour installer Express.js, ouvrez un terminal et tapez la commande suivante :

npm install -g express

Ensuite nous allons générer le squelette de notre application :

express stykkekode

Puis installer les dépendances :

cd stykkekode
npm install -d

Puis installer le moteur de template ejs : (express "arrive" avec le moteur jade, mais ejs à l'avantage de permettre l'utilisation de code html, ce qui permet de ne pas être trop perdu)

npm install ejs

Ensuite, je vous conseille d'installer nodemon (https://github.com/remy/nodemon) qui démarre, arrête et redémarre automatiquement pour vous, votre application à chaque fois qu'il détecte un changement dans le code source (en fait à chaque fois que vous sauvegardez).

Express.js a généré pour vous tout le squelette et le code de départ de votre application (vous irez voir par vous même l'arborescence générée), pour lancer votre application il faudra, dans le répertoire de celle ci, tapez la commande nodemon app.js, où app.js et le script principal généré par Express.js.

WARNING : Dans le reste de l'article, je l'ai renommé server.js (c'est pour une question technique d'hébergement que je suis en train de tester). Donc vous même, n'oubliez pas de renommer app.js en server.js.

Voilà, voilà, nous pouvons commencer.

1ère view & 1ère route … 1er contrôleur

Aller dans le répertoire views, et créer 2 fichiers index.ejs et layout.ejs

Dans layout.ejs saisissez le code suivant :

<html>
    <head>
        <title>styKKeKode</title>
    </head>
    <%- body %>
</html>

Dans index.ejs saisissez le code suivant :

<H1>styKKeKode</H1>

<% if (message) { %>
    <h2><%= message %></h2>
<% } %>

si vous allez dans server.js (ou app.js) vous trouverez la ligne suivante :

app.get('/', routes.index);

Qu'est ce que ça fait ? Dès que vous appeler votre "domaine" dans l'url (la page principale) c'est la méthode index de routes qui est appelée. Et vous trouvez l'implémentation de cette méthode dans /routes/index.js, que nous allons tout de suite modifier. Remplacer le code de /routes/index.js par :

/*
 * GET home page.
 */

exports.index = function(req, res){
  res.render('index.ejs', { message : 'soon …' })
};

On peut considérer que c'est l'équivalent de nos contrôleurs Java.

Donc à chaque appel de http://localhost:3000/ vous serez redirigé vers la view/vue index.ejs. Et on passe la variable message à la vue index.ejs. Vous notez que je précise l'extension de la vue, cela signifie que l'on peut utiliser plusieurs moteurs de template dans la même application.

Pour essayer :

  • ouvrez un terminal, positionnez vous dans le répertoire de votre application et tapez : nodemon server.js
  • appelez http://localhost:3000/ dans votre navigateur

Nous avons donc rapidement vu les aspects, routes, vue et contrôleur, nous reviendrons plus en détail dessus, mais maintenant, allons un peu plus loin dans la construction de notre "stack".

Installer les librairies javascript

… Cela va nous servir pour plus tard

Dans le répertoire public/javascripts copiez les librairies suivantes :

Dans le répertoire public/stylesheets copiez les css suivantes :

Twitter Bootstrap nous permettra de donner une "bonne tête" à notre application, sans effort.

Allons ensuite déclarer les librairies javascript dans views/index.ejs :

<H1>styKKeKode</H1>

<% if (message) { %>
    <h2><%= message %></h2>
<% } %>


<!-- js libs client -->
<script src="javascripts/jquery.js"></script>
<script src="javascripts/underscore.js"></script>
<script src="javascripts/backbone.js"></script>
<script src="javascripts/tempo.js"></script>
<script src="javascripts/showdown.js"></script>

Puis les feuilles de styles dans views/layout.ejs :

<html>
    <head>
        <title>styKKeKode</title>
        <link rel="stylesheet" href="stylesheets/bootstrap.css">
        <link rel="stylesheet" href="stylesheets/bootstrap-responsive.css">
        <link rel="stylesheet" href="stylesheets/default.min.css">
        <style type="text/css">
            body {
                padding-top: 60px;
                padding-bottom: 40px;
            }
            .sidebar-nav {
                padding: 9px 0;
            }
        </style>
    </head>
     <%- body %>
</html>

Les modèles

On ne vas pas s'occuper tout de suite de la persistance, mais nous allons créer des modèles et simuler cette persistance dans un 1er temps.

Créer dans votre répertoire applicatif un répertoire models sans lequel vous allez ajouter un fichier snippet.js qui sera donc notre modèle, avec le code suivant :

/* SNIPPET MODEL */
var snippetsCounter = 1;

var snippet = function(title, code, user) {
    this.id = null;
    this.title = title ? title : "";
    this.code = code ? code : "";
    this.user = user ? user : "";
}

//static
snippet.list = [];

snippet.prototype.save = function(callBack) {
    if(this.id === undefined || this.id === null || this.id === "") {
    //new snippet to save

        //Je ré-initialise tous les 5 snippets
        if(snippetsCounter > 5) {
            snippet.list = [];
            snippetsCounter = 1;
        }

        this.id = snippetsCounter++;

        snippet.list.push(this)
    } else {//snippet exists => to be updated in list
        var
            that = this,
            tmp = snippet.list.filter(function(record){ return record.id === that.id; })[0];
            if(tmp){
                snippet.list.splice(snippet.list.indexOf(this), 1);
                snippet.list.push(this) ;
            }
    }

    callBack(this);
};

snippet.prototype.delete = function(callBack) {
    var
        that = this,
        tmp = snippet.list.filter(function(record){ return record.id === that.id; })[0];
        if(tmp){
            snippet.list.splice(snippet.list.indexOf(this), 1);
        }
    callBack(this);
}

//static
snippet.findAll = function(callBack) {
    callBack(snippet.list);
}

//static
snippet.findById = function(id, callBack) {
    var tmp = snippet.list.filter(function(record){ return record.id === id; })[0];
    console.log("snippet.findById", tmp)
    callBack(tmp);
}


/*=== Bootstrap with data ===*/

var snippet_one = new snippet("essai 1","//FOO","k33g_org");
var snippet_tow = new snippet("essai 2","//Hello World","k33g_org");
var snippet_three = new snippet("essai 3","//Me again !","k33g_org");

snippet_one.save(function(m){console.log(m);});
snippet_tow.save(function(m){console.log(m);});
snippet_three.save(function(m){console.log(m);});

exports.snippet = snippet;

Dans routes/index.js, ajoutez la ligne suivante en tout début de fichier (on fait un include) :

var snippet = require('../models/snippet').snippet;

Sauvegardez. Si votre application tourne encore (sinon relancez) vous pourrez voir dans la console la liste des données "bootstrapées".

Alt "express01.png"

Maintenant allons écrire quelques routes et contrôleur(s)

Routes et Contrôleurs

Routes

Préparons le travail pour Backbone.

Allez dans server.js (ou app.js) et copiez les routes suivantes (juste après app.get('/', routes.index);) :

app.post("/snippet", routes.createSnippet);
app.put("/snippet", routes.updateSnippet);
app.get("/snippet", routes.getSnippet);
app.del("/snippet", routes.deleteSnippet);

app.get("/snippets", routes.allSnippets);

Contrôleurs

Allez dans routes/index.js et modifiez le code comme ceci :

var snippet = require('../models/snippet').snippet;

/*
 * GET home page.
 */

exports.index = function(req, res){
  res.render('index.ejs', { message : 'soon …' });
};


exports.createSnippet = function(req, res) {
    console.log("CREATE SNIPPET");

    var model_from_client = JSON.parse(req.param("model", null));
    console.log(model_from_client);

    var server_model = new snippet(model_from_client.title, model_from_client.code, model_from_client.user);

    server_model.save(function(m){
        console.log(m);
        res.json(m);
    });

};

exports.updateSnippet = function(req, res) {
    console.log("UPDATE SNIPPET");

    var model_from_client = JSON.parse(req.param("model", null));
    console.log(model_from_client);

    var server_model = new snippet(model_from_client.title, model_from_client.code, model_from_client.user);

    server_model.id = model_from_client.id;

    server_model.save(function(m){
        console.log(m);
        res.json(m);
    });


};

exports.getSnippet = function(req, res) {
    console.log("GET SNIPPET");

    var model_from_client = JSON.parse(req.param("model", null));
    console.log(model_from_client);

    var server_model = snippet.findById(model_from_client.id, function(m) {
        console.log(m);
        res.json(m);
    });

};

exports.deleteSnippet = function(req, res) {
    console.log("DELETE SNIPPET");
    var model_from_client = JSON.parse(req.param("model", null));
    console.log(model_from_client);

    var server_model = snippet.findById(model_from_client.id, function(model) {
        console.log(model);

        model.delete(function(m){
            res.json(m);
        });
    });


};

exports.allSnippets = function(req, res) {
    console.log("ALL SNIPPETS");

    snippet.findAll(function(snippets){
        res.json(snippets);
    });
};

Testons :

Allez dans votre navigateur, ouvrez la console, et essayez les commandes suivantes :

createSnippet

$.ajax({
    type: "POST",
    url: "/snippet",
    data: {"model":JSON.stringify({
        title:"Hello World in Kotlin",
        code : "println('Hello world')",
        user : "@BobMorane"
    })},
    dataType: 'json',
    error: function () {
        console.log("oups");
    },
    success: function (dataFromServer) {
        console.log(dataFromServer);
    }
});

Alt "express02.png"

on peut voir que le serveur nous a affecté un id

updateSnippet

$.ajax({
    type: "PUT",
    url: "/snippet",
    data: {"model":JSON.stringify({
        id : 4, /*vérifier que l'id existe*/
        title:"Hello World in Kotlin",
        code : "println('Hello world $name')",
        user : "@BOBMORANE"
    })},
    dataType: 'json',
    error: function () {
        console.log("oups");
    },
    success: function (dataFromServer) {
        console.log(dataFromServer);
    }
});

Alt "express03.png"

Le serveur nous a renvoyé notre modèle modifié

getSnippet

$.ajax({
    type: "GET",
    url: "/snippet",
    data: {"model":JSON.stringify({id:1})},
    dataType: 'json',
    error: function () {
        console.log("oups");
    },
    success: function (dataFromServer) {
        console.log(dataFromServer);
    }
});

Alt "express04.png"

Le serveur nous a renvoyé le modèle ayant l'id 1

deleteSnippet

$.ajax({
    type: "DELETE",
    url: "/snippet",
    data: {"model":JSON.stringify({id:1})},
    dataType: 'json',
    error: function () {
        console.log("oups");
    },
    success: function (dataFromServer) {
        console.log(dataFromServer);
    }
});

Alt "express05.png"

Et maintenant nous allons appeler la liste de l'ensemble de nos "snippets" pour vérifier que nos modifications ont bien été prises en compte.

allSnippets

$.ajax({
    type: "GET",
    url: "/snippets",
    data: null,
    dataType: 'json',
    error: function () {
        console.log("oups");
    },
    success: function (dataFromServer) {
        dataFromServer.forEach(function(model){
            console.log(model)
        });
    }
});

Alt "express06.png"

Mise en musique avec BackBone.js

On re-écrit Backbone.sync

Vous devez donc créer un fichier backbone.sync.js au même endroit que backbone.js :

(function() {
    Backbone.sync = function(method, model, options) {

        // sympa pour comprendre ce qu'il se passe
        console.log(method, model, options);

        var methodMap = {
            'create': 'POST',
            'update': 'PUT',
            'delete': 'DELETE',
            'read':   'GET'
        }, dataForServer = null;

        if(model.models) {//c'est une collection
            dataForServer:null
        } else {//c'est un modèle

            dataForServer = { model : JSON.stringify(model.toJSON()) };

            console.log(dataForServer);
        }

        return $.ajax({
            type: methodMap[method],
            url: model.url,
            data: dataForServer,
            dataType: 'json',
            error: function (dataFromServer) { //vérifier que cela retourne une erreur
                options.error(dataFromServer);
            },
            success: function (dataFromServer) {
                if(!model.models){
                    dataFromServer.id = dataFromServer._id;
                    console.log(dataFromServer);
                } else {
                    //collection
                    dataFromServer.reverse();
                }
                options.success(dataFromServer);
            }
        });
    };
})();

Il faudra penser à ajouter dans 'index.ejs' :

<script src="javascripts/backbone.sync.js"></script>

Nous allons donc modifier la vue index.ejs :

HTML

<!-- ma barre de titre -->
<div class="navbar navbar-fixed-top">
    <div class="navbar-inner">
        <div class="container">

            <a class="brand" href="#">styKKeKode
                <% if (message) { %>
                    <%= message %>
                <% } %>
            </a>

        </div>
    </div>
</div>

<div class="container">

    <!-- mon formulaire de saisie -->
    <div id="snippet-form">
        <h2>Go ...</h2>
       <form action="/" class="well">
            <label>Title : </label>
            <input id="title" type="text" class="span3" placeholder="title"/>
            <label>Code Snippet : (with markdown) </label>
            <textarea id="code" placeholder="code" style="width:100%" rows="5"></textarea>
            <label>User : </label>
            <input id="user" type="text" placeholder="user"/>
            <button type="submit" class="btn">Ajouter un Snippet</button>
            <!--<input type="submit" value="Ajouter un Snippet" />-->
       </form>
    </div>

    <ul id="snippet-list" style="list-style: none;">
       <li data-template>
        <h2> by </h2><br>
        <hr>
       </li>
    </ul>

</div>

<!-- js libs client -->
<script src="javascripts/jquery.js"></script>
<script src="javascripts/underscore.js"></script>
<script src="javascripts/backbone.js"></script>
<script src="javascripts/backbone.sync.js"></script>
<script src="javascripts/tempo.js"></script>
<script src="javascripts/showdown.js"></script>
<script src="javascripts/highlight.min.js"></script>

Javascript (Backbone & co)

Donc à la suite :

<!-- Application BackBone -->

<script type="text/javascript">
    $(document).ready(function() {

        window.Snippet = Backbone.Model.extend({
            url : 'snippet',
            defaults : {
                id: null,
                title : "",
                code : "",
                user : ""
            }
        });

        window.Snippets = Backbone.Collection.extend({
            model : Snippet,
            url : 'snippets'
        });

        /*=== VIEWS ===*/
        window.SnippetsView = Backbone.View.extend({

            initialize : function() {
                this.template = Tempo.prepare('snippet-list');
            },

            render : function() {
                this.template.render(this.collection.toJSON());
                return this;
            }

        });


        window.converter = new Showdown.converter();

        window.SnippetFormView = Backbone.View.extend({
            el : $('#snippet-form'),

            initialize : function() {
                this.form = arguments[0].form;
            },
            events : {
                'submit form' : 'addSnippet'
            },
            addSnippet : function(e) {
                e.preventDefault();
                var that = this;
                var tmpSnippet = new Snippet({
                    title : this.$('#title').val(),
                    code : converter.makeHtml(this.$('#code').val()),
                    user : this.$('#user').val()
                });

                tmpSnippet.save({},{
                    success : function() {
                        that.collection.fetch({
                            success: function() {
                                that.form .render();
                                $('pre code').each(function(index,e) {hljs.highlightBlock(e, '    ')});
                            }
                        })
                    }
                });
                //on vide le form
                this.$('input[type="text"]').val('');
                this.$('textarea').val('');
            },
            error : function(model, error) {
                console.log(model, error);
                return this;
            }

        });

        /*=== ROUTER ===*/

        window.SnippetsRouter = Backbone.Router.extend({

            initialize : function() {
                /* 1- Création d'une collection */
                window.snippets = new Snippets();
                this.collection = snippets;
                var that = this;
                /* 2- Chargement de la collection */
                snippets.fetch({
                    success:function() {
                        /* 3- Création des vues + affichage */
                        window.snippetsView = new SnippetsView({ collection : snippets });
                        window.snippetForm = new SnippetFormView({ collection : snippets, form : snippetsView });
                        snippetsView.render();
                        /*4- un peu de couleur */
                        $('pre code').each(function(index,e) {hljs.highlightBlock(e, '    ')});

                    },
                    error:function(){

                    }
                });

            },
            routes : {}

        });

        /*--- initialisation du router ---*/

        router = new SnippetsRouter();

    });
</script>

Remarques :

  • window.converter = new Showdown.converter(); et converter.makeHtml(this.$('#code').val()) servent à transformer le code markdown saisi en code html
  • $('pre code').each(function(index,e) {hljs.highlightBlock(e, ' ')}); sert à "coloriser" les parties "code source"

Allez on teste :

  • Vérifiez que votre application est lancée
  • Ouvrez l'url localhost:3000/ dans votre navigateur

Alt "express07.png"

On voit que l'on a encore nos données "bootstrapées".

Saisissons des données au format markdown :

Alt "express08.png"

On ajoute et on obtient :

Alt "express09.png"

Vous pouvez allez vérifier dans un autre navigateur que vos données sont bien là.

That's all ...

Pour aujourd'hui, mais la prochaine fois, nous verrons comment "socialiser" notre application : seules les personnes authentifiées avec Twitter, pourront saisir des snippets. Nous mettrons quelques contrôles de validation. Et enfin (pas forcément dans le même article), nous verrons comment ajouter une véritable persistance avec une base NOSQL (je n'ai pas encore fait mon choix, mais j'ai un gros penchant pour CouchDB).

Si vous voulez voir tourner l'application "en vrai", je l'ai hébergée ici : http://stykkekode.cloudno.de/.

@+ & Bon code.


blog comments powered by Disqus