Node Atlas NodeAtlas

Le Framework JavaScript Serveur Évolutif

  • Mise en place Simple

    Au point avec HTML & CSS ?
    Débutant en JavaScript ?

    Réalisez rapidement des sites vitrines multilingues sans effort avec l'utilisation de routes, vues ou variations.

  • Site vivant et Évolutif

    Expert en JavaScript client ?
    Prêt à embrasser Node.js ?

    Améliorer progressivement votre base à mesure de vos besoins en utilisant des contrôleurs, modèles ou modules.

  • Partie Cliente Agnostique

    Déjà vos habitudes Front-end ?
    Habitué(e) du Data Binding ?

    Du léger Vanilla au simple jQuery en passant par Vue, Angular ou React : utiliser vos bibliothèques clientes favorites !

Partie contrôleur et modèle

NodeAtlas ne se contente pas uniquement de faciliter la génération de page web en fonction de variables dans les fichiers de variation. NodeAtlas vous permet également d'intéragir avec le contenu des fichiers de variations ou avec le DOM généré en fonction :

  • des paramètres dans la partie query de l'URL (GET),
  • des paramètres dans le corps de la requête (POST)

mais également :

  • de vous connecter à des bases de données,
  • de maintenir des sessions,
  • de faire des échanges websockets et
  • de faire bien plus encore !

Cycle de vie et points d'ancrage

Le cycle de vie de NodeAtlas est le suivant. D'abord, les ressources se chargent, le serveur démarre, les routes s'initialisent et tout est opérationnel. Puis, à chaque requête HTTP entrante, une réponse est générée. Vous pouvez intervenir grâce à différents points d'ancrage pendant le démarrage, et pendant la création d'une page.

Voici à quoi peut ressembler un webconfig.json permettant d'atteindre tous les points d'ancrage du cycle de vie d'une page.

{
    "controllersRelativePath": "controllers",
    "controller": "common.js",
    "routes": {
        "/": {
            "view": "index.htm",
            "controller": "index.json"
        }
    }
}

Note : Si controllersRelativePath n'est pas présent dans « webconfig.json », par défaut le dossier des contrôleurs est bien controllers. controllersRelativePath est donc utile seulement pour changer le nom/chemin du répertoire.

et voici le détail des endroits ou vous pouvez intervenir pendant :

Le lancement du serveur

┌─[Chargement des modules Node.js]
┊
├─[Chargement des variables d'initialisation]
┊
├─[Chargement des modules npm]
┊
├─[Prise en compte des commandes et de la langue du CLI]
┊
├─[Prise en compte des options de l'API]
┊
└─[Chargement de la langue du CLI]
  ┊
  ├─[Chargement des variables globales]
  ┊
  ├─[Prise en compte des instructions du webconfig]
  ┊
  └─[Chargement du contrôleur commun]
    ┊  _________________________________________
    ├─{Point d'ancrage : <controller>.setModules}
    ┊  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
    ├─[Initialisation du serveur]
    ┊  __________________________________________
    ├─{Point d'ancrage : <controller>.setSessions}
    ┊  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
    ├─[Initialisation des sessions]
    ┊
    ├─[Initialisation des sockets]
    ┊ ┊  _________________________________________
    ┊ ├─{Point d'ancrage : <controller>.setSockets}_______
    ┊ └─{Point d'ancrage : routes[<controller>].setSockets}
    ┊    ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
    ┊  ________________________________________________
    ├─{Point d'ancrage : <controller>.setConfigurations}
    ┊  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
    └─[Démarrage du serveur]
      ┊
      ├─[Initialisation du moteur de template]
      ┊  ________________________________________
      ├─{Point d'ancrage : <controller>.setRoutes}
      ┊  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
      └─[Initialisation des routes]
        ┊
        ∞

Le traitement des requêtes de chaque route

∞
┊
└─[Traitement d'une requête]
  ┊
  └─[Chargement du contrôleur spécifique]
    ┊  _______________________________________________
    ├─{Point d'ancrage : <controller>.changeVariations}_______
    ├─{Point d'ancrage : routes[<controller>].changeVariations}
    ┊  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
    └─[Compilation du moteur de template]
      ┊  ________________________________________
      ├─{Point d'ancrage : <controller>.changeDom}_______
      ├─{Point d'ancrage : routes[<controller>].changeDom}
      ┊  ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
      └─[Envoi de la réponse]
        ┊
        ∞

changeVariations

Pour intercepter les variations, vous pouvez soit utiliser le contrôleur commun pour tout le site et/ou également le contrôleur par page.

changeVariations(next, locals, request, response) est une fonction a exports et fournissant :

  • L'objet NA en tant que this.
  • En premier paramètre la fonction de retour next().
  • En deuxième paramètre l'objet locals contenant entre autre la variation locals.common pour accéder aux variations communes et la variation locals.specific pour accéder aux variations spécifiques.
  • En troisième paramètre l'objet request qui va être faites.
  • En quatrième paramètre l'objet response faites pour cette page.

Voici un exemple utilisant les deux points d'entrée, d'abord la commune à plusieurs pages, puis celle de chaque page :

{
    "urlRelativeSubPath": "example",
    "controller": "common.js",
    "variation": "common.json",
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    }
}

avec cet ensemble de fichier :

├─ variations/
│  ├─ common.json
│  └─ index.json
├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ views/
│  ├─ partials/
│  │  ├─ head.htm
│  │  └─ foot.htm
│  └─ index.htm
└─ webconfig.json

En demandant la page http://localhost/example/?title=Haeresis en POST avec une variable example=Ceci+est+un+test dans le corps de requête, les fichiers suivants (entre autre) seront utilisés :

variations/common.json

{
    "titleWebsite": "Titre du site"
}

variations/index.json

{
    "titlePage": "Bienvenue",
    "content": "<p>C'est la page d'accueil.</p>"
}

views/index.htm

    <?- include("partials/head.htm") ?>

    <div class="title"><?- common.titleWebsite ?></div>

    <div>
        <h1><?- specific.titlePage ?></h1>
        <?- specific.content ?>
    </div>

    <?- include("partials/foot.htm") ?>

controllers/common.js

// On intervient avant que les variables soient injectées dans le système de template.
// Ce code sera exécuté pour toute request HTTP, toute page confondue.
exports.changeVariations = function (next, locals, request, response) {

    // Ici on modifie les variables de locals.

    console.log(locals.common.titleWebsite); // "Titre du site"
    console.log(locals.specific.titlePage); // "Bienvenue"
    console.log(locals.specific.content); // "C'est la page d'accueil."

    console.log("urlRootPath", locals.urlRootPath); // "http://localhost"
    console.log("urlSubPath", locals.urlSubPath); // "/example"
    console.log("urlBasePath", locals.urlBasePath); // "http://localhost/example"
    console.log("urlFilePath", locals.urlFilePath); // "/"
    console.log("urlQueryPath", locals.urlQueryPath); // "?title=Haeresis"
    console.log("urlPath", locals.urlPath); // "http://localhost/example/?title=Haeresis"

    if (request.query["title"]) {
        locals.specific.titlePage = locals.specific.titlePage + " " + request.query.title;
    }
    if (request.body["example"]) {
        locals.specific.content = request.body.example;
    }

    console.log(locals.common.titleWebsite); // "Titre du site"
    console.log(locals.specific.titlePage); // "Bienvenue Haeresis"
    console.log(locals.specific.content); // "Ceci est un test"

    // On passe à la suite modifications.
    next();
};

controllers/index.js

// On intervient avant que les variables soient injectées dans le système de template.
// Ce code sera exécuté uniquement lors de la demande de la page « / ».
exports.changeVariations = function (next, locals, request, response) {

    // Ici on modifie les variables de locals.

    console.log(locals.common.titleWebsite); // "Titre du site"
    console.log(locals.specific.titlePage); // "Bienvenue Haeresis"
    console.log(locals.specific.content); // "Ceci est un test"

    locals.common.titleWebsite = "C'est l'accueil, c'est tout.";
    locals.specific.content = "C'est l'accueil, c'est tout.";

    console.log(locals.common.titleWebsite); // "C'est l'accueil, c'est tout."
    console.log(locals.specific.titlePage); // "Bienvenue Haeresis"
    console.log(locals.specific.content); // "C'est l'accueil, c'est tout."

    // On passe à la suite.
    next();
};

ce qui produit la sortie suivante :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>C'est l'accueil, c'est tout.</title>
    </head>
    <body>
        <div class="title">C'est l'accueil, c'est tout.</div>
        <div>
            <h1>Bienvenue Haeresis</h1>
            C'est l'accueil, c'est tout.
        </div>
    </body>
</html>

Si vous décidez de désabonner la variation spécifique avec le webconfig suivant :

{
    "controller": "common.js",
    "variation": "common.json",
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json"
        }
    }
}

alors la sortie sera :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>Titre du site</title>
    </head>
    <body>
        <div class="title">Titre du site</div>
        <div>
            <h1>Bienvenue Haeresis</h1>
            Ceci est un test
        </div>
    </body>
</html>

changeDom

Pour intercepter le DOM avant qu'il ne soit renvoyé, vous pouvez soit utiliser le contrôleur commun pour tout le site et/ou également le contrôleur par page.

changeDom(next, locals, request, response) est une fonction a exports et fournissant :

  • L'objet NA en tant que this.
  • En premier paramètre la fonction de retour next([$]) acceptant optionellement en premier paramètre la function $ utilisée pour manipuler le DOM Virtuel.
  • En deuxième paramètre l'objet locals contenant entre autre la string locals.dom contenant la réponse ou la function locals.virtualDom() générant un Dom Virtuel.
  • En troisième paramètre l'objet request qui va être faites.
  • En quatrième paramètre l'objet response faites pour cette page.

Voici un exemple utilisant les deux points d'entrée, d'abord la commune à plusieurs pages, puis celle de chaque page :

{
    "controller": "common.js",
    "variation": "common.json",
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    }
}

avec cet ensemble de fichier :

├─ variations/
│  ├─ common.json
│  └─ index.json
├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ views/
│  └─ index.htm
└─ webconfig.json

En demandant la page http://localhost/ les fichiers suivants (entre autre) seront utilisés :

variations/common.json

{
    "titleWebsite": "Titre du site"
}

variations/index.json

{
    "titlePage": "Bienvenue",
    "content": "<p>C'est la page d'accueil.</p>"
}

views/index.htm

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title><?- common.titleWebsite ?></title>
    </head>
    <body>
        <div class="title"><?- common.titleWebsite ?></div>
        <div>
            <h1><?- specific.titlePage ?></h1>
            <?- specific.content ?>
        </div>
    </body>
</html>

controllers/common.js

// On intervient avant que le DOM ne soit renvoyé au Client.
// Ce code sera exécuté uniquement lors de la demande de la page « / ».
exports.changeDom = function (next, locals, request, response) {
    var $ = locals.virtualDom(); // Transformer la chaîne HTML en DOM Virtuel.

    // Après tous les h1 de la sortie HTML « dom »,
    $("h1").each(function () {
        var $this = $(this);

        // ...on créé une div,
        $this.after(
            // ... on injecte le contenu du h1 dans la div,
            $("<div>").html($this.html())
        );
        // ...et supprime le h1.
        $this.remove();
    });

    // On retourne les modifications pour qu'elles redeviennet une chaîne HTML.
    next($);
};

controllers/index.js

// On intervient avant que le DOM ne soit renvoyé au Client.
// Ce code sera exécuté uniquement lors de la demande de la page « / ».
exports.changeDom = function (next, locals, request, response) {
    var NA = this,
        cheerio = NA.modules.cheerio, // Récupération de jsdom pour parcourir le DOM avec jQuery.
        $ = cheerio.load(locals.dom, { decodeEntities: false }); // On charge les données pour les manipuler comme un DOM.

    // On modifie tous les contenu des noeuds avec la classe `.title`,
    $(".title").text("Modification de Contenu");

    // On recrée une nouvelle sortie HTML avec nos modifications.
    locals.dom = $.html();

    // On passe à la suite.
    next();
};

ce qui produit la sortie suivante :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8">
        <title>Titre du site</title>
    </head>
    <body>
        <div class="title">Modification de Contenu</div>
        <div>
            <div>Bienvenue</div>
            <p>C'est la page d'accueil.</p>
        </div>
    </body>
</html>

setSockets

Pour maintenir une connexion temps réel entre votre partie Cliente et Serveur à travers toutes les pages ouvertes sur tous les navigateurs de tous les ordinateurs sur le web, vous aller pouvoir définir vos websockets ici Plus de détail dans la partie Socket.IO.

setSockets() est une fonction a exports et fournissant :

  • L'objet NA en tant que this.

Voici un exemple utilisant les deux points d'entrée, d'abord la commune à plusieurs pages, puis celle de chaque page :

{
    "socketClientFile": "/node-atlas/socket.io.js",
    "socketServerOptions": { transports: ['polling', 'websocket'] },
    "controller": "common.js",
    "routes": {
        "/": {
            "view": "index.htm",
            "controller": "index.js"
        }
    }
}

avec cet ensemble de fichier :

├─ assets/
│  └─ javascripts/
│     └─ index.js
├─ controllers/
│  ├─ common.js
│  ├─ index.js
│  └─ test.js
├─ views/
│  └─ index.htm
└─ webconfig.json

En demandant la page http://localhost/ les fichiers suivants (entre autre) seront utilisés :

views/index.htm

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>Exemple Websocket</title>
    </head>
    <body>
        <div class="layout">
            <div class="content"></div>
            <div class="field">Tape du texte : <input class="input" type="text"></div>
        </div>
        <script type="text/javascript" src="socket.io/socket.io.js"></script>
        <script type="text/javascript" src="node-atlas/socket.io.js"></script>
        <script type="text/javascript" src="javascripts/test.js"></script>
        <script type="text/javascript" src="javascripts/index.js"></script>
    </body>
</html>

Note : Si socketClientFile et socketServerOptions ne sont pas présent dans webconfig.json, par défaut le fichier client et les options serveurs pour configurer les sockets sont bien /node-atlas/socket.io.js et { transports: ['polling', 'websocket'] }. Il sont donc utiles seulement pour changer le chemin du fichier ou les transports des sockets permis. Si vous mettez socketClientFile à false, le fichier client ne sera pas accessible.

assets/javascripts/test.js

(function (expose, factory) {
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory;
    } else {
        expose.Test = factory;
    }
}(this, function () {
    if (NA.isClient) {
        console.log("Client");
    } else {
        console.log("Serveur");
    }
}));

controllers/common.js

// On référence les actions de réponse et d'envoi globaux côté serveur.
// Ce code sera exécuté pour toute entrée Websocket entrante.
exports.setSockets = function () {
    var NA = this,
        io = NA.io;

    io.on('connection', function (socket) {
        console.log("Un onglet est ouvert.");
        socket.on('disconnect', function () {
            console.log("Un onglet est fermé.");
        });
    });
};

controllers/index.js

// On référence les actions de réponse et d'envoi globaux côté serveur.
// Ce code sera exécuté pour toute entrée Websocket entrante.
exports.setSockets = function () {
    var NA = this,
        path = NA.modules.path,
        io = NA.io;

    require(path.join(NA.serverPath, NA.webconfig.assetsRelativePath, "javascripts/test.js"))(); // display `Serveur`

    // Attendre un lien valide entre client et serveur
    io.sockets.on("connection", function (socket) {

        // Quelqu'un nous informe que le texte à changé.
        socket.on("update-text", function (data) {

            // On informe les autres que le texte à changé.
            io.sockets.emit("update-text", data);
        });
    });
};

assets/javascripts/index.js

var content = document.getElementsByClassName("content")[0],
    input = document.getElementsByClassName("input")[0];

Test(); // display `Client`

// On alerte les autres de nos modifications.
input.addEventListener("keyup", function () {
    content.innerHTML = input.value;
    NA.socket.emit("update-text", {
        text: input.value
    });
});

// On récupère les modifications des autres.
NA.socket.on("update-text", function (data) {
    content.innerHTML = data.text;
    input.value = data.text;
});

Vous pourrez, en ouvrant divers navigateurs, et divers onglet, constaté que tout est bien mis à jour chez tout le monde. Chaque nouvel ongle ouvert affiche sur le serveur le message de connexion, et chaque onglet fermé, le message de deconnexion sur la console serveur.

Note : Vous pouvez changer le fichier node-atlas/socket.io.js par un fichier fournis par vous-même pour changer la variable optionsSocket. Vous pouvez aussi changer la valeur de NA.optionsSocket côté client (avant l'insertion de node-atlas/socket.io.js) avec un objet d'options personnalisées.

setModules

Pour charger d'autres modules qui ne sont pas fournis avec NodeAtlas vous pouvez utiliser le contrôleur commun pour tout le site afin de les charger une seule fois et de les rendres disponible dans tous vos contrôleurs.

setModules() est une fonction a exports et fournissant :

  • L'objet NA en tant que this.

Voici un exemple utilisant un module externe à NodeAtlas :

{
    "controller": "common.js",
    "routes": {
        "/": {
            "view": "index.htm",
            "controller": "index.js"
        }
    }
}

avec cet ensemble de fichier :

├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ views/
│  └─ index.htm
└─ webconfig.json

En demandant la page http://localhost/ les fichiers suivants (entre autre) seront utilisés :

views/index.htm

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>Test Module</title>
    </head>
    <body>
        <div class="title">Test Module</div>
        <div>
            <h1>Test Module</h1>
            <?- example ?>
        </div>
    </body>
</html>

controllers/common.js

// On intervient avant que la phase de chargement des modules ne soit achevée.
// Ce code sera exécuté au lancement de NodeAtlas.
exports.setModules = function () {
    // Récupérer l'instance « NodeAtlas » du moteur.
    var NA = this;

    // Associations de chaque module pour y avoir accès partout.
    NA.modules.marked = require('marked');
};

controllers/index.js

// On intervient avant que les variables soient injectées dans le système de template.
// Ce code sera exécuté uniquement lors de la demande de la page « / ».
exports.changeVariations = function (next, locals) {
    // Utiliser « NodeAtlas » depuis le moteur.
    var NA = this,
        marked = NA.modules.marked;

    locals.example = marked("J'utilise __markdown__.");

    // On passe à la suite.
    next();
};

ce qui produit la sortie suivante :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>Test Module</title>
    </head>
    <body>
        <div class="title">Test Module</div>
        <div>
            <h1>Test Module</h1>
            <p>I am using <strong>markdown</strong>.</p>
        </div>
    </body>
</html>

setConfigurations

Pour configurer le serveur web de NodeAtlas (ExpressJs) vous pouvez utiliser le contrôleur commun pour tout le site afin faire vos modifications avant le démarrage du serveur.

setConfigurations(next) est une fonction a exports et fournissant :

  • L'objet NA en tant que this.
  • En premier paramètre la fonction de retour next().

Voici un exemple utilisant un middleware pour ExpressJs :

{
    "controller": "common.js",
    "routes": {
        "/": {
            "view": "index.htm",
            "controller": "index.js"
        }
    }
}

avec cet ensemble de fichier :

├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ views/
│  └─ index.htm
└─ webconfig.json

En demandant la page http://localhost/ les fichiers suivants (entre autre) seront utilisés :

views/index.htm

<?- content ?>

controllers/common.js

// On intervient au niveau du serveur avant que celui-ci ne soit démarré.
// Ce code sera exécuté au lancement de NodeAtlas.
exports.setConfigurations = function (next) {
    // Récupérer l'instance « NodeAtlas » du moteur.
    var NA = this;

    // Middleware utilisé lors de chaque requête.
    NA.express.use(function (request, response, next) {
        response.setHeader("X-Frame-Options", "ALLOW-FROM https://www.lesieur.name/");
        next();
    });

    // On ré-injecte les modifications.
    next();
};

controllers/index.js

// On intervient avant que les variables soient injectées dans le moteur de template.
// Ce code sera exécuté uniquement lors de la demande de la page « / ».
exports.changeVariations = function (next, locals) {

    // On prépare le fichier pour un affichage JSON.
    locals.routeParameters.headers = {
        "Content-Type": "application/json; charset=utf-8"
    };
    locals.content = JSON.stringify(locals, null, "    ");

    // On passe à la suite.
    next();
};

ce qui produit la sortie suivante :

{
    "urlRootPath": "http://localhost",
    "urlSubPath": "",
    "urlBasePath": "http://localhost",
    "urlFilePath": "/",
    "urlQueryPath": "",
    "urlPath": "http://localhost/",
    "params": {},
    "query": {},
    "body": {},
    "routeParameters": { /* ... */ },
    "route": "/",
    "webconfig": { /* ... */ }
}

setSessions

Pour configurer les sessions client-serveur de NodeAtlas vous pouvez utiliser le contrôleur commun pour tout le site afin de définir vos sessions avant le démarrage du serveur. Voici un exemple de management de Session avec Redis.

setSessions(next) est une fonction a exports et fournissant :

  • L'objet NA en tant que this.
  • En premier paramètre la fonction de retour next().

Voici l'ensemble de fichier suivant :

├─ controllers/
│  └─ common.js
├─ views/
│  └─ index.htm
├─ variations/
│  ├─ common.json
│  └─ index.json
└─ webconfig.json

Avec le webconfig.json :

{
    "controller": "common.js",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

et avec le fichier « common.js » contenant par exemple :

// On intervient avant que la phase de chargement des modules ne soit achevée.
// Ce code sera exécuté au lancement de NodeAtlas.
exports.setModules = function () {
    // Récupérer l'instance « NodeAtlas » du moteur.
    var NA = this;

    // Associations de chaque module pour y avoir accès partout.
    NA.modules.RedisStore = require('connect-redis');
};

// On intervient au niveau du serveur pendant la configuration des sessions.
// Ce code sera exécuté au lancement de NodeAtlas.
exports.setSessions = function (next) {
    var NA = this,
        session = NA.modules.session,
        RedisStore = NA.modules.RedisStore(session);

    // On remplace la session par default.
    NA.sessionStore = new RedisStore();

    // On redonne la main à NodeAtlas pour la suite.
    next();
};

setRoutes

Pour configurer les routes de NodeAtlas dynamiquement vous pouvez utiliser le contrôleur commun pour tout le site afin de les charger une seule fois et de les rendres disponible dans tous vos contrôleurs.

setRoutes(next) est une fonction a exports et fournissant :

  • L'objet NA en tant que this.
  • En premier paramètre la fonction de retour next().

Voici l'ensemble de fichier suivant :

├─ controllers/
│  └─ common.js
├─ views/
│  ├─ content.htm
│  └─ index.htm
├─ variations/
│  └─ common.json
└─ webconfig.json

Avec le webconfig.json :

{
    "controller": "common.js",
    "variation": "common.json",
    "routes": {
        "/index.html": {
            "view": "index.htm"
        }
    }
}

et avec le fichier « common.js » contenant par exemple :

// On intervient au niveau des routes pendant qu'elles sont ajoutées.
// Ce code sera exécuté au lancement de NodeAtlas.
exports.setRoutes = function (next) {

    // On récupère l'instance de NodeAtlas en cours.
    var NA = this,

        // Et nous récupérons les routes en provenance du webconfig...
        route = NA.webconfig.routes;

    // ...pour ajouter la route "/content.html" à la liste de nos routes.
    route["/content.html"] = {
        "view": "content.htm"
    };

    // On redonne la main à NodeAtlas pour la suite.
    next();
};

Échange client-serveur en temps réel avec websockets

Afin de conserver une liaison ouverte entre la partie Cliente et la partie Serveur de vos applications, NodeAtlas utilise Socket.IO dont vous trouverez plus de détail sur le site officiel.

Grâce à cela, vous pourrez changer des informations en temps réel sur votre page, mais également sur toutes les autres pages ouvertes à travers tous les autres navigateurs.

Avec l'ensemble de fichier suivant :

├─ assets/
│  └─ javascripts/
│     └─ index.js
├─ controllers/
│  └─ index.js
├─ variations/
│  ├─ common.json
│  └─ index.json
├─ views/
│  ├─ partials/
│  │  └─ index.htm
│  └─ index.htm
└─ webconfig.json

Contenant le webconfig.json suivant :

{
    "variation": "common.json",
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    }
}

et contenant les fichiers de template suivant :

views/partials/index.htm

        <div class="title"><?- common.titleWebsite ?></div>
        <div>
            <h1><?- specific.titlePage ?></h1>
            <?- specific.content ?>
            <div><?- new Date() ?></div>
        </div>
        <button>Update</button>

Note : Chaque clique sur button raffraichira le contenu de views/partials/index.htm.

views/index.htm

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title><?- common.titleWebsite ?></title>
    </head>
    <body>
        <div class="layout">
            <?- include('partials/index.htm') ?>
        </div>
        <script type="text/javascript" src="socket.io/socket.io.js"></script>
        <script type="text/javascript" src="node-atlas/socket.io.js"></script>
        <script type="text/javascript" src="javascripts/index.js"></script>
    </body>
</html>

Note : On construit ici la page d'accueil /.

ainsi que les fichiers de variations suivant :

variations/common.json

{
    "titleWebsite": "Exemple Socket.IO"
}

variations/index.json

{
    "titlePage": "Date",
    "content": "<p>La date actuelle est :</p>"
}

Jusque là, rien d'inhabituel et tout fonctionnerait sans partie contrôleur. Mais nous allons mettre en place la communication via Socket.IO côté Serveur puis côté Client.

Côté serveur, nous utiliserons le contrôleur commun suivant :

controllers/index.js

// Intégralité des actions Websocket possible avec `setSockets`.
exports.setSockets = function () {
    var NA = this,
        io = NA.io;

    // Dès qu'on a un lien valide entre le client et notre serveur...
    io.sockets.on("connection", function (socket) {

        // ...rester à l'écoute de la demande « create-article-button »...
        socket.on("server-render", function (data) {
            var sessionID = socket.request.sessionID,
                session = socket.request.session,
                locals = {};

            // On récupère les variations spécifiques dans la bonne langue.
            locals = NA.specific("index.json", data.lang, locals);

            // On récupère les variations communes dans la bonne langue.
            locals = NA.common(data.lang, locals);

            // On récupère le fragment HTML depuis le dossier `viewsRelativePath` et on applique les variations.
            data.render = NA.view("partials/index.htm", locals);

            // Et on répond à tous les clients avec un jeu de donnée dans data.
            io.sockets.emit("server-render", data);
        });
    });
};

Quand au côté client, nous utiliserons les fichiers suivant :

assets/javascripts/index.js

var html = document.getElementsByTagName("html")[0],
    layout = document.getElementsByClassName("layout")[0];

// On associe sur le bouton l'action de communiquer avec le serveur en cliquant dessus.
function setServerRender() {
    var button = document.getElementsByTagName("button")[0];
    button.addEventListener("click", function () {
        NA.socket.emit("server-render", {
            lang: html.getAttribute("lang")
        });
    });
}

// On affecte l'action au bouton.
setServerRender();

// Quand le serveur répond après notre demande auprès de lui...
NA.socket.on("server-render", function (data) {

    // ...on met à jour le contenu...
    layout.innerHTML = data.render;

    // ...et ré-affectons l'action au bouton du nouveau contenu.
    setServerRender();
});

Lancer votre projet et rendez-vous à l'adresse http://localhost/ dans deux onglets différent, voir même, dans deux navigateurs différent. Vous constaterez alors qu'à chaque clique sur « Update », la page se remettra à jour (comme le montre la date courante) sur tous les onglets ouvert.

Grâce à NA.specific, NA.common et NA.view, il est possible de générer une nouvelle compilation d'une vue et d'une variation commune et spécifique.

Si data.lang dans notre exemple est de type undefined, alors les fichiers seront cherchés à la racine. Si locals est de type undefined alors un objet contenant uniquement le scope demandé sera renvoyé.

Note : pour permettre à view d'utiliser le moteur Pug au lieu de celui d'EJS, il faut mettre la valeur locals.pug à true avant d'utiliser NA.common et NA.specific.

Utiliser une base de données MySQL (SQL)

Nous allons voir à présent comment utiliser des informations venant d'une base de données. Pour cela nous allons utiliser le module npm mysql. Il va également nous falloir installer un serveur MySQL.

Donc, depuis le dossier du webconfig.json, utilisez :

npm install mysql

Base de données MySQL

Tout d'abord, nous allons alimenter la base de données avec la base demo :

CREATE DATABASE demo;

et la sélectionner :

USE demo

puis créer la table user :

CREATE TABLE user
(
    id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
    lastname VARCHAR(100),
    firstname VARCHAR(100),
    email VARCHAR(255),
    birthdate DATE,
    gender TINYINT(1),
    country VARCHAR(255),
    town VARCHAR(255),
    zipcode VARCHAR(5),
    address VARCHAR(255)
);

et la remplir avec un jeu de données :

INSERT INTO user (
    lastname,
    firstname,
    email,
    birthdate,
    gender,
    country,
    town,
    zipcode,
    address
) VALUES (
    "Elric",
    "Edward",
    "edward.elric@fma.br",
    "2006/01/01",
    true,
    "Amestris",
    "Resembool",
    00000,
    "The Elric's house"
);
INSERT INTO user (
    lastname,
    firstname,
    email,
    birthdate,
    gender,
    country,
    town,
    zipcode,
    address
) VALUES (
    "Elric",
    "Alphonse",
    "alphonse.elric@fma.br",
    "2008/01/01",
    true,
    "Amestris",
    "Resembool",
    00000,
    "The Elric's house"
);

Fichiers NodeAtlas

Voyons à présent l'architecture de site que nous allons arbitrairement créer pour présenter notre exemple :

├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ models/
│  ├─ objects/
│  │  └─ user.js
│  └─ connectors/
│     └─ user.js
├─ views/
│  └─ index.htm
├─ variations/
│  ├─ common.json
│  └─ index.json
└─ webconfig.json

Nous allons utiliser le webconfig.json suivant avec une variable custom _mysqlConfig qui contiendra toutes les informations pour se connecter à la base de données :

{
    "controller": "common.js",
    "variation": "common.json",
    "statics": {
        "/models": "models/objects"
    },
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    },
    "_mysqlConfig": {
        "host": "localhost",
        "user": "root",
        "password": "root",
        "database": "demo"
    }
}

Nous allons ensuite nous connecter à la base de données avec le contrôleur globale controllers/common.js :

exports.setModules = function () {
    var NA = this;

    // Import du module `mysql`.
    NA.modules.mysql = require('mysql');

    // Création de la collection de modèle...
    NA.models = {};
    // ...et récupération du modèle User avec accès à Mysql.
    NA.models.User = require('../models/connectors/user.js');
};

exports.setConfigurations = function (next) {
    var NA = this,
        path = NA.modules.path,
        mysql = NA.modules.mysql;

    // Créer un pool de connexion à MySQL.
    NA.mySql = mysql.createPool(NA.webconfig._mysqlConfig);

    next();
};

Et afficher les résultats via le contrôleur spécifique controllers/index.js :

exports.changeVariations = function (next, locals) {
    var NA = this,
        user = new NA.models.User(),
        user2 = new NA.models.User(),
        user3 = new NA.models.User(),
        user4 = new NA.models.User();

    NA.mySql.getConnection(function(err, connection) {
        if (err) {
            throw err;
        }

        // Exemple de lecture.
        user
        .setConnection(connection)
        .lastname("Elric")
        .read(function (allUsers) {
            locals.user = user;
            locals.users = allUsers;

            // Exemple de création.
            user2
            .setConnection(connection)
            .firstname("Winry")
            .lastname("Rockbell")
            .email("winry.rockbell@fma.br")
            .gender(true)
            .create(function (infos) {
                locals.insertId = infos.insertId;
                locals.user2 = user2;

                // Exemple de modification.
                user3
                .gender(false)
                .birthdate("2008-01-01")
                .country("Amestris")
                .town("Resembool")
                .zipcode("99999")
                .address("The Rockbell's house");

                user2.update(user3, function (infos) {
                    locals.affectedRows = infos.affectedRows;
                    locals.user2 = user2;

                    // Exemple de suppression.
                    user4
                    .setConnection(connection)
                    .gender(false)
                    .delete(function (infos) {
                        locals.deletedRows = infos.affectedRows;
                        next();
                    });
                });
            });
        });
    });
};

en utilisant le modèle user via le fichier de connexion à la base de données models/connectors/user.js :

var user = require('../objects/user.js');

function User(connection) {
    var privates = {},
        publics = this;

    user.call(publics);

    privates.connection = connection;

    publics.setConnection = function (connection) {
        privates.connection = connection;
        return publics;
    };

    publics.read = function (callback) {
        var select = `SELECT
                    id,
                    lastname,
                    firstname,
                    email,
                    birthdate,
                    gender,
                    country,
                    town,
                    zipcode,
                    address
                FROM user`,
            where = "";

        if (publics.id()) { where += ' && `id` = ' + publics.id(); }
        if (publics.lastname()) { where += ' && `lastname` = "' + publics.lastname() + '"'; }
        if (publics.firstname()) { where += ' && `firstname` = "' + publics.firstname() + '"'; }
        if (publics.email()) { where += ' && `email` = "' + publics.email() + '"'; }
        if (publics.birthdate()) { where += ' && `birthdate` = "' + publics.birthdate() + '"'; }
        if (typeof publics.gender() === "boolean") { where += ' && `gender` = ' + (publics.gender() ? 1 : 0); }
        if (publics.country()) { where += ' && `country` = "' + publics.country() + '"'; }
        if (publics.town()) { where += ' && `town` = "' + publics.town() + '"'; }
        if (publics.zipcode()) { where += ' && `zipcode` = "' + publics.zipcode() + '"'; }
        if (publics.address()) { where += ' && `address` = "' + publics.address() + '"'; }

        where = where.replace("&&", "WHERE");

        privates.connection.query(select + where, function (err, rows) {
            var users = [],
                user;

            if (err) {
                throw err;
            }

            if (rows[0]) {
                publics.id(rows[0].id);
                publics.lastname(rows[0].lastname);
                publics.firstname(rows[0].firstname);
                publics.email(rows[0].email);
                publics.birthdate(rows[0].birthdate);
                publics.gender((rows[0].gender) ? true : false);
                publics.country(rows[0].country);
                publics.town(rows[0].town);
                publics.zipcode(rows[0].zipcode);
                publics.address(rows[0].address);
            }

            for (var i = 0; i < rows.length; i++) {
                user = new User();
                user.id(rows[i].id);
                user.lastname(rows[i].lastname);
                user.firstname(rows[i].firstname);
                user.email(rows[i].email);
                user.birthdate(rows[i].birthdate);
                user.gender((rows[i].gender) ? true : false);
                user.country(rows[i].country);
                user.town(rows[i].town);
                user.zipcode(rows[i].zipcode);
                user.address(rows[i].address);
                users.push(user);
            }

            if (callback) {
                callback(users);
            }
        });

        return publics;
    };

    publics.create = function (callback) {
        var insert = "INSERT INTO user (",
            values = ") VALUES (";

        if (publics.id()) {
            insert += "`id`, ";
            values += publics.id() + ', ';
        }
        if (publics.lastname()) {
            insert += "`lastname`, ";
            values += '"' + publics.lastname() + '", ';
        }
        if (publics.firstname()) {
            insert += "`firstname`, ";
            values += '"' + publics.firstname() + '", ';
        }
        if (publics.email()) {
            insert += "`email`, ";
            values += '"' + publics.email() + '", ';
        }
        if (publics.birthdate()) {
            insert += "`birthdate`, ";
            values += '"' + publics.birthdate() + '", ';
        }
        if (typeof publics.gender() === "boolean") {
            insert += "`gender`, ";
            values += (publics.gender() ? 1 : 0) + ', ';
        }
        if (publics.country()) {
            insert += "`country`, ";
            values += '"' + publics.country() + '", ';
        }
        if (publics.town()) {
            insert += "`town`, ";
            values += '"' + publics.town() + '", ';
        }
        if (publics.zipcode()) {
            insert += "`zipcode`, ";
            values += '"' + publics.zipcode() + '", ';
        }
        if (publics.address()) {
            insert += "`address`, ";
            values += '"' + publics.address() + '", ';
        }

        insert = insert.replace(/, $/g, "");
        values = values.replace(/, $/g, ")");

        privates.connection.query(insert + values, function (err, infos) {
            if (err) {
                throw err;
            }

            publics.id(infos.insertId);

            if (callback) {
                callback(infos);
            }
        });

        return publics;
    };

    publics.update = function (user, callback) {
        var update = "UPDATE user SET",
            where = "";

        if (user.id()) { update += '`id` = ' + user.id() + ', '; }
        if (user.lastname()) { update += '`lastname` = "' + user.lastname() + '", '; }
        if (user.firstname()) { update += '`firstname` = "' + user.firstname() + '", '; }
        if (user.email()) { update += '`email` = "' + user.email() + '", '; }
        if (user.birthdate()) { update += '`birthdate` = "' + user.birthdate() + '", '; }
        if (typeof user.gender() === "boolean") { update += '`gender` = ' + (user.gender() ? 1 : 0) + ', '; }
        if (user.country()) { update += '`country` = "' + user.country() + '", '; }
        if (user.town()) { update += '`town` = "' + user.town() + '", '; }
        if (user.zipcode()) { update += '`zipcode` = "' + user.zipcode() + '", '; }
        if (user.address()) { update += '`address` = "' + user.address() + '", '; }

        update = update.replace(/, $/g, "");

        if (publics.id()) { where += ' && `id` = ' + publics.id(); }
        if (publics.lastname()) { where += ' && `lastname` = "' + publics.lastname() + '"'; }
        if (publics.firstname()) { where += ' && `firstname` = "' + publics.firstname() + '"'; }
        if (publics.email()) { where += ' && `email` = "' + publics.email() + '"'; }
        if (publics.birthdate()) { where += ' && `birthdate` = "' + publics.birthdate() + '"'; }
        if (typeof publics.gender() === "boolean") { where += ' && `gender` = ' + (publics.gender() ? 1 : 0); }
        if (publics.country()) { where += ' && `country` = "' + publics.country() + '"'; }
        if (publics.town()) { where += ' && `town` = "' + publics.town() + '"'; }
        if (publics.zipcode()) { where += ' && `zipcode` = "' + publics.zipcode() + '"'; }
        if (publics.address()) { where += ' && `address` = "' + publics.address() + '"'; }

        where = where.replace("&&", "WHERE");

        privates.connection.query(update + where, function (err, infos) {
            if (err) {
                throw err;
            }

            if (user.id()) { publics.id(user.id()); }
            if (user.lastname()) { publics.lastname(user.lastname()); }
            if (user.firstname()) { publics.firstname(user.firstname()); }
            if (user.email()) { publics.email(user.email()); }
            if (user.birthdate()) { publics.birthdate(user.birthdate()); }
            if (typeof publics.gender() === "boolean") { publics.gender(user.gender()); }
            if (user.country()) { publics.country(user.country()); }
            if (user.town()) { publics.town(user.town()); }
            if (user.zipcode()) { publics.zipcode(user.zipcode()); }
            if (user.address()) { publics.address(user.address()); }

            if (callback) {
                callback(infos);
            }
        });

        return publics;
    };

    publics.delete = function (callback) {
        var del = "DELETE FROM user",
            where = "";

        if (publics.id()) { where += ' && `id` = ' + publics.id(); }
        if (publics.lastname()) { where += ' && `lastname` = "' + publics.lastname() + '"'; }
        if (publics.firstname()) { where += ' && `firstname` = "' + publics.firstname() + '"'; }
        if (publics.email()) { where += ' && `email` = "' + publics.email() + '"'; }
        if (publics.birthdate()) { where += ' && `birthdate` = "' + publics.birthdate() + '"'; }
        if (typeof publics.gender() === "boolean") { where += ' && `gender` = ' + (publics.gender() ? 1 : 0); }
        if (publics.country()) { where += ' && `country` = "' + publics.country() + '"'; }
        if (publics.town()) { where += ' && `town` = "' + publics.town() + '"'; }
        if (publics.zipcode()) { where += ' && `zipcode` = "' + publics.zipcode() + '"'; }
        if (publics.address()) { where += ' && `address` = "' + publics.address() + '"'; }

        where = where.replace("&&", "WHERE");

        privates.connection.query(del + where, function (err, infos) {
            if (err) {
                throw err;
            }

            if (publics.id()) { publics.id(undefined); }
            if (publics.lastname()) { publics.lastname(undefined); }
            if (publics.firstname()) { publics.firstname(undefined); }
            if (publics.email()) { publics.email(undefined); }
            if (publics.birthdate()) { publics.birthdate(undefined); }
            if (typeof publics.gender() === "boolean") { publics.gender(undefined); }
            if (publics.country()) { publics.country(undefined); }
            if (publics.town()) { publics.town(undefined); }
            if (publics.zipcode()) { publics.zipcode(undefined); }
            if (publics.address()) { publics.address(undefined); }

            if (callback) {
                callback(infos);
            }
        });

        return publics;
    };
}

User.prototype = Object.create(user.prototype);
User.prototype.constructor = User;

module.exports = User;

basé sur une classe user partagée entre la partie cliente et serveur models/objects/user.js :

(function (expose, factory) {
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory;
    } else {
        expose.User = factory;
    }
}(this, function User() {
    var privates = {},
        publics = this;

    publics.id = function (id) {
        if (typeof id === 'undefined') {
            return privates.id;
        } else {
            privates.id = id;
            return publics;
        }
    };

    publics.lastname = function (lastname) {
        if (typeof lastname === 'undefined') {
            return privates.lastname;
        } else {
            privates.lastname = lastname;
            return publics;
        }
    };

    publics.firstname = function (firstname) {
        if (typeof firstname === 'undefined') {
            return privates.firstname;
        } else {
            privates.firstname = firstname;
            return publics;
        }
    };

    publics.email = function (email) {
        if (typeof email === 'undefined') {
            return privates.email;
        } else {
            privates.email = email;
            return publics;
        }
    };

    publics.birthdate = function (birthdate) {
        if (typeof birthdate === 'undefined') {
            return privates.birthdate;
        } else {
            privates.birthdate = birthdate;
            return publics;
        }
    };

    publics.gender = function (gender) {
        if (typeof gender === 'undefined') {
            return privates.gender;
        } else {
            privates.gender = gender;
            return publics;
        }
    };

    publics.country = function (country) {
        if (typeof country === 'undefined') {
            return privates.country;
        } else {
            privates.country = country;
            return publics;
        }
    };

    publics.town = function (town) {
        if (typeof town === 'undefined') {
            return privates.town;
        } else {
            privates.town = town;
            return publics;
        }
    };

    publics.zipcode = function (zipcode) {
        if (typeof zipcode === 'undefined') {
            return privates.zipcode;
        } else {
            privates.zipcode = zipcode;
            return publics;
        }
    };

    publics.address = function (address) {
        if (typeof address === 'undefined') {
            return privates.address;
        } else {
            privates.address = address;
            return publics;
        }
    };
}));

Avec les fichiers suivant pour afficher la page :

views/index.htm

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title><?- common.titleWebsite ?></title>
    </head>
    <body>
        <div class="title"><?- common.titleWebsite ?></div>
        <div>
            <h1><?- specific.titlePage ?></h1>
            <div class="first">
                <?- specific.content ?>
                <ul>
                    <li>Id: <strong><?- user.id() ?></strong></li>
                    <li>Lastname: <strong><?- user.lastname() ?></strong></li>
                    <li>Firstname: <strong><?- user.firstname() ?></strong></li>
                    <li>Email: <strong><?- user.email() ?></strong></li>
                    <li>Birthdate: <strong><?- user.birthdate() ?></strong></li>
                    <li>Gender: <strong><?- user.gender() ?></strong></li>
                    <li>Country: <strong><?- user.country() ?></strong></li>
                    <li>Town: <strong><?- user.town() ?></strong></li>
                    <li>Zipcode: <strong><?- user.zipcode() ?></strong></li>
                    <li>Address: <strong><?- user.address() ?></strong></li>
                </ul>
            </div>
            <div class="all">
                <?- specific.contents ?>
                <? for (var i = 0; i < users.length; i++) { ?>
                <ul>
                    <li>Id: <strong><?- users[i].id() ?></strong></li>
                    <li>Lastname: <strong><?- users[i].lastname() ?></strong></li>
                    <li>Firstname: <strong><?- users[i].firstname() ?></strong></li>
                    <li>Email: <strong><?- users[i].email() ?></strong></li>
                    <li>Birthdate: <strong><?- users[i].birthdate() ?></strong></li>
                    <li>Gender: <strong><?- users[i].gender() ?></strong></li>
                    <li>Country: <strong><?- users[i].country() ?></strong></li>
                    <li>Town: <strong><?- users[i].town() ?></strong></li>
                    <li>Zipcode: <strong><?- users[i].zipcode() ?></strong></li>
                    <li>Address: <strong><?- users[i].address() ?></strong></li>
                </ul>
                <? } ?>
            </div>
            <div class="last">
                <?- specific.contentInsert ?>
                <p>insertId: <?- insertId ?></p>
                <p>numberUpdate: <?- affectedRows ?></p>
                <ul>
                    <li>Id: <strong><?- user2.id() ?></strong></li>
                    <li>Lastname: <strong><?- user2.lastname() ?></strong></li>
                    <li>Firstname: <strong><?- user2.firstname() ?></strong></li>
                    <li>Email: <strong><?- user2.email() ?></strong></li>
                    <li>Birthdate: <strong><?- user2.birthdate() ?></strong></li>
                    <li>Gender: <strong><?- user2.gender() ?></strong></li>
                    <li>Country: <strong><?- user2.country() ?></strong></li>
                    <li>Town: <strong><?- user2.town() ?></strong></li>
                    <li>Zipcode: <strong><?- user2.zipcode() ?></strong></li>
                    <li>Address: <strong><?- user2.address() ?></strong></li>
                </ul>
                <p>numberDelete: <?- deletedRows ?></p>
            </div>
        </div>
    </body>
</html>

variations/common.json

{
    "titleWebsite": "Exemple MySql",
    "male": "Homme",
    "female": "Femme"
}

variations/index.json

{
    "titlePage": "Table User",
    "content": "<p>Détail de la première entrée.</p>",
    "contents": "<p>Détail de toutes les entrées.</p>",
    "contentInsert": "<p>Détail de l'utilisateur ajouté puis modifié.</p>"
}

Vous obtiendrez la sortie suivante :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>MySql Exemple</title>
    </head>
    <body>
        <div class="title">MySql Exemple</div>
        <div>
            <h1>Table User</h1>
            <div class="first">
                <p>Détail de la première entrée.</p>
                <ul>
                    <li>Id: <strong>1</strong></li>
                    <li>Lastname: <strong>Elric</strong></li>
                    <li>Firstname: <strong>Edward</strong></li>
                    <li>Email: <strong>edward.elric@fma.br</strong></li>
                    <li>Birthdate: <strong>Sun Jan 01 2006 00:00:00 GMT+0100 (Paris, Madrid)</strong></li>
                    <li>Gender: <strong>true</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>0</strong></li>
                    <li>Address: <strong>The Elric's house</strong></li>
                </ul>
            </div>
            <div class="all">
                <p>Détail de toutes les entrées.</p>
                <ul>
                    <li>Id: <strong>1</strong></li>
                    <li>Lastname: <strong>Elric</strong></li>
                    <li>Firstname: <strong>Edward</strong></li>
                    <li>Email: <strong>edward.elric@fma.br</strong></li>
                    <li>Birthdate: <strong>Sun Jan 01 2006 00:00:00 GMT+0100 (Paris, Madrid)</strong></li>
                    <li>Gender: <strong>true</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>0</strong></li>
                    <li>Address: <strong>The Elric's house</strong></li>
                </ul>
                <ul>
                    <li>Id: <strong>2</strong></li>
                    <li>Lastname: <strong>Elric</strong></li>
                    <li>Firstname: <strong>Alphonse</strong></li>
                    <li>Email: <strong>alphonse.elric@fma.br</strong></li>
                    <li>Birthdate: <strong>Tue Jan 01 2008 00:00:00 GMT+0100 (Paris, Madrid)</strong></li>
                    <li>Gender: <strong>true</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>0</strong></li>
                    <li>Address: <strong>The Elric's house</strong></li>
                </ul>
            </div>
            <div class="last">
                <p>Détail de l'utilisateur ajouté puis modifié.</p>
                <p>insertId: 3</p>
                <p>numberUpdate: 1</p>
                <ul>
                    <li>Id: <strong>3</strong></li>
                    <li>Lastname: <strong>Rockbell</strong></li>
                    <li>Firstname: <strong>Winry</strong></li>
                    <li>Email: <strong>winry.rockbell@fma.br</strong></li>
                    <li>Birthdate: <strong>2008-01-01</strong></li>
                    <li>Gender: <strong>false</strong></li>
                    <li>Country: <strong>Amestris</strong></li>
                    <li>Town: <strong>Resembool</strong></li>
                    <li>Zipcode: <strong>99999</strong></li>
                    <li>Address: <strong>The Rockbell's house</strong></li>
                </ul>
                <p>numberDelete: 1</p>
            </div>
        </div>
    </body>
</html>

Utiliser une base de données MongoDB (NoSQL)

Nous allons voir à présent comment utiliser des informations venant d'une base de données non sql. Pour cela nous allons utiliser le module npm mongoose. Il va également nous falloir installer un serveur MongoDB.

Donc, depuis le dossier du webconfig.json, utilisez :

npm install mongoose

Base de données MongoDB

Tout d'abord, nous allons alimenter la base de données avec la base demo et la sélectionner :

use demo

puis créer la collection user :

db.createCollection("user")

et la remplir avec un document :

db.user.insert({
    email: "john.doe@unknown.com",
    identity: {
        lastname: "Doe",
        firstname: "John",
        gender: true,
        birthdate : new Date("1970/01/01")
    },
    location: {
        country: "Unknown",
        town: "Unknown",
        zipcode: "00000",
        address: "42 unknown"
    }
})

Fichiers NodeAtlas

Avec le jeu de fichier suivant :

├─ controllers/
│  ├─ common.js
│  └─ index.js
├─ models/
│  └─ user.js
├─ views/
│  └─ index.htm
├─ variations/
│  ├─ common.json
│  └─ index.json
└─ webconfig.json

Nous allons utiliser le webconfig.json suivant avec une variable custom _mongodbConfig qui contiendra toutes les informations pour se connecter à la base de données :

{
    "controller": "common.js",
    "variation": "common.json",
    "statics": {
        "/models": "models"
    },
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "index.json",
            "controller": "index.js"
        }
    },
    "_mongodbConfig": {
        "host": "localhost",
        "port": "27017",
        "database": "demo"
    }
}

Avec les fichiers suivant pour afficher la page :

views/index.htm

<!DOCTYPE html>
<html lang="<?- languageCode ?>">
    <head>
        <meta charset="utf-8" />
        <title><?- common.titleWebsite ?></title>
    </head>
    <body>
        <div class="title"><?- common.titleWebsite ?></div>
        <div>
            <h1><?- specific.titlePage ?></h1>
            <?- specific.content ?>
            <ul>
                <li>Id: <strong><?- id ?></strong></li>
                <li>Lastname: <strong><?- lastname ?></strong></li>
                <li>Firstname: <strong><?- firstname ?></strong></li>
                <li>Email: <strong><?- email ?></strong></li>
                <li>Birthdate: <strong><?- birthdate ?></strong></li>
                <li>Gender: <strong><?- gender ?></strong></li>
                <li>Country: <strong><?- country ?></strong></li>
                <li>Town: <strong><?- town ?></strong></li>
                <li>Zipcode: <strong><?- zipcode ?></strong></li>
                <li>Address: <strong><?- address ?></strong></li>
            </ul>
        </div>
    </body>
</html>

variations/common.json

{
    "titleWebsite": "MongoDB Exemple",
    "male": "Homme",
    "female": "Femme"
}

variations/index.json

{
    "titlePage": "Collection User",
    "content": "<p>Détail du document `{ \"identity.firstname\": \"John\" }`.</p>"
}

Enfin nous allons nous connecter à la base de données avec le contrôleur globale controllers/common.js :

exports.setModules = function () {
    var NA = this,
        path = NA.modules.path;

    NA.modules.mongoose = require('mongoose');
    NA.models = {};
    NA.models.User = require('../models/user.js');
};

exports.setConfigurations = function (next) {
    var NA = this,
        mongoose = NA.modules.mongoose,
        config = NA.webconfig._mongodbConfig;

    mongoose.Promise = global.Promise;
    mongoose.model("user", NA.models.User, "user");
    mongoose.connect("mongodb://" + config.host + ":" + config.port + "/" + config.database, function (error) {
        next();
    });
};

Et afficher les résultats via le contrôleur spécifique controllers/index.js :

exports.changeVariations = function (next, locals) {
    var NA = this,
        mongoose = NA.modules.mongoose,
        User = mongoose.model('user');

    User
    .findOne({ "identity.firstname": "John" })
    .exec(function (err, user) {

        locals.id = user._id;
        locals.lastname = user.identity.lastname;
        locals.firstname = user.identity.firstname;
        locals.birthdate = user.identity.birthdate;
        locals.email = user.email;
        locals.gender = (user.identity.gender) ? locals.common.male : locals.common.female;
        locals.country = user.location.country;
        locals.town = user.location.town;
        locals.zipcode = user.location.zipcode;
        locals.address = user.location.address;

        next();
    });
};

en utilisant sur une classe user partagée entre la partie cliente et la partie serveur models/user.js :

var mongoose;
if (typeof module !== 'undefined' && module.exports) {
     mongoose = require('mongoose');
}

(function (expose, factory) {
    if (mongoose) {
        module.exports = factory;
    } else {
        expose.User = factory;
    }
}(this, new mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    email: { type : String, match: /^\S+@\S+$/ },
    identity: {
        lastname: String,
        firstname: String,
        gender: Boolean,
        birthdate : { type : Date, default : Date.now }
    },
    location: {
        country: String,
        town: String,
        zipcode: String,
        address: String
    }
})));

Vous obtiendrez la sortie suivante :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8" />
        <title>Exemple MongoDB</title>
    </head>
    <body>
        <div class="title">Exemple MongoDB</div>
        <div>
            <h1>Collection User</h1>
            <p>Détail de l'entrée `{ "identity.firstname": "John" }`.</p>
            <ul>
                <li>Id: <strong>5804d4d530788ee2e52ea1c7</strong></li>
                <li>Lastname: <strong>Doe</strong></li>
                <li>Firstname: <strong>John</strong></li>
                <li>Email: <strong>john.doe@unknown.com</strong></li>
                <li>Birthdate: <strong>Mon Jan 01 1970 00:00:00 GMT+0200 (Paris, Madrid (heure d’été))</strong></li>
                <li>Gender: <strong>Homme</strong></li>
                <li>Country: <strong>Unknown</strong></li>
                <li>Town: <strong>Unknown</strong></li>
                <li>Zipcode: <strong>00000</strong></li>
                <li>Address: <strong>42 unknown</strong></li>
            </ul>
        </div>
    </body>
</html>

Utiliser des middlewares depuis Express

NodeAtlas repose en partie sur le module npm Express.js. Vous pouvez accéder à l'objet Express d'une instance NodeAtlas par l'intermédiaire de NA#express. Cela vous permet d'ajouter des middlewares Express de la même manière que vous l'auriez fait avec Express seul.

En ce qui concerne la pré-configuration d'Express avec un webconfig vide, elle est faites ainsi :

NA.express.set("strict routing", true);
/* ... */
NA.express.set("x-powered-by", false);
/* ... */
/* Activation de gzip, deflate et cie. */
NA.express.use(compress());
/* ... */
/* Parse le type d'encryption "x-www-form-urlencoded". */
NA.express.use(bodyParser.urlencoded({ extended: true }));
/* ... */
/* Parse le type d'encryption "application/json". */
NA.express.use(bodyParser.json());
/* ... */
/* Parse les cookies. */
NA.express.use(cookieParser());
/* ... */
/* Gére les cookies de session. */
NA.express.use(session(optionSession));
/* ... */
/* Gére de dossier `assets/` et son accès depuis le domain root ou un sous-dossier. */
NA.express.use(NA.webconfig.urlRelativeSubPath, express.static(path.join(NA.serverPath, NA.webconfig.assetsRelativePath), staticOptions));

Vous pouvez vous même ajouter des middlewares de plusieurs manière.

Avec setConfigurations

Vous pouvez obtenir l'objet NA#express près à accueillir des middlewares ici dans le point d'ancrage setConfigurations. Cela ajoutera les mécanismes à toutes les routes de votre site.

exports.setConfigurations = function (next) {
    var NA = this;

    // Middleware fait main.
    NA.express.use(function (request, response, next) {
        response.setHeader("X-Frame-Options", "ALLOW-FROM https://www.lesieur.name/");
        next();
    });

    // Middleware ajoutant diverse entête http de sécurisation.
    NA.express.use(require("helmet")());

    next();
};

Avec le paramètre middlewares des Routes

Il est également possible de délivrer ses middlewares uniquement pour une seule route. Dans ce cas vous pouvez utilisez le paramètre middlewares comme suit :

webconfig.json

{
    "middlewaresRelativePath": "middlewares",
    "routes": {
        "/upload-file": {
            "view": "upload.htm",
            "controller": "upload.js",
            "middlewares": "upload.js"
        }
    }
    "_jwt": {
        secret: "AUTH_CLIENT_SECRET",
        audience: "AUTH_CLIENT_ID"
    }
}

et utiliser le fichier suivant pour autoriser l'envoi de donnée POST encrypté en "multipart/data-form" uniquement si vous êtes authentifié par un token JSON :

middlewares/upload.js

var multer  = require("multer"),
    jwt = require("express-jwt");

module.exports = function () {
    var NA = this,
        path = NA.modules.path,
        upload = multer({ dest: path.join(NA.serverPath, "uploads") });

    return [
        jwt({
            secret: NA.webconfig._jwt.secret,
            audience: NA.webconfig._jwt.audience
        }),
        upload.single("avatar"),
    ];
};

Note : Si middlewaresRelativePath n'est pas présent dans « webconfig.json », par défaut le dossier des contrôleurs est bien middlewares. middlewaresRelativePath est donc utile seulement pour changer le nom/chemin du répertoire.

Avec le paramètre middlewares en Global

Il est également possible d'utiliser ce système pour toutes les routes, ainsi le webconfig se présenterait plutôt ainsi :

webconfig.json

{
    "middlewares": "is-authenticated.js"
    "routes": {
        "/upload-file": {
            "view": "upload.htm",
            "controller": "upload.js"
        }
    }
    "_jwt": {
        secret: "AUTH_CLIENT_SECRET",
        audience: "AUTH_CLIENT_ID"
    }
}

Avec le fichier :

middlewares/is-authenticated.js

var jwt = require("express-jwt");

module.exports = function () {
    var NA = this;

    return [
        jwt({
            secret: NA.webconfig._jwt.secret,
            audience: NA.webconfig._jwt.audience
        })
    ];
};

Tableau de middlewares

Vous pouvez également fournir un tableau vers une liste de fichier de middleware Express que ce soit pour chaque route ou en global :

webconfig.json

{
    "routes": {
        "/upload-file": {
            "view": "upload.htm",
            "controller": "upload.js",
            "middlewares": ["is-authenticated.js", "redirect.js"]
        }
    }
    "_jwt": {
        secret: "AUTH_CLIENT_SECRET",
        audience: "AUTH_CLIENT_ID"
    }
}

Avec l'utilisation de l'objet NA :

middlewares/is-authenticated.js

var jwt = require("express-jwt");

module.exports = function () {
    var NA = this;

    return [jwt({
        secret: NA.webconfig._jwt.secret,
        audience: NA.webconfig._jwt.audience
    })];
};

où sans :

middlewares/redirect.js

module.exports = function (request, response, next) {

    response.redirect('https://go.to.visitor.page/');

    next();
};

Créer une application isomorphique

Une application isomorphique est une application dont le code JavaScript est en grande partie le même qu'il soit exécuté côté client ou exécuté côté serveur. NodeAtlas propose un exemple d'application isomorphique dans son template dédié à Vue.js.

Pour tester cela il vous suffit :

de créer un dossier de test

mkdir hello-vue
cd hello-vue

d'y placer les fichier de hello-vue

node-atlas --create hello-vue

d'installer les dépendances

npm install

et de lancer le site en français

node-atlas --browse

ou en version internationale

node-atlas --browse --webconfig webconfig.en-us.json

Vous trouverrez tout ce qu'il faut pour appréhender la partie serveur du constrollers/common.js sur https://ssr.vuejs.org/ et seul la partie cliente du assets/javascripts/common.js sur https://vuejs.org/.