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

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é à partir de ceux-ci 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, couplé à des fonctionnalités natives de Node.js ou de npm, en fonction :

  • d'informations dans des fichiers,
  • d'informations dans des bases de données,
  • d'informations dans des sessions utilisateurs actives,
  • d'informations fournit par échanges WebSockets et
  • de faire bien plus encore !

Cycle de vie

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]
        ┊
        ∞

Point d'ancrage 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 argument la fonction de rappel next().
  • En deuxième argument 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 argument l'objet request pour cette page.
  • En quatrième argument l'objet response 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": "exemple",
    "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/exemple/?title=MachinisteWeb 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 moteur de template.
// Ce code sera exécuté pour toute requête HTTP, toutes pages confondues.
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); // `"/exemple"`
    console.log("urlBasePath", locals.urlBasePath); // `"http://localhost/exemple"`
    console.log("urlFilePath", locals.urlFilePath); // `"/"`
    console.log("urlQueryPath", locals.urlQueryPath); // `"?title=MachinisteWeb"`
    console.log("urlPath", locals.urlPath); // `"http://localhost/example/?title=MachinisteWeb"`

    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 MachinisteWeb"`
    console.log(locals.specific.content); // `"Ceci est un test"`

    // On passe à la suite.
    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, request, response) {

    // Ici on modifie les variables de `locals`.

    console.log(locals.common.titleWebsite); // `"Titre du site"`
    console.log(locals.specific.titlePage); // `"Bienvenue MachinisteWeb"`
    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 MachinisteWeb"`
    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 MachinisteWeb</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 MachinisteWeb</h1>
            Ceci est un test
        </div>
    </body>
</html>

Point d'ancrage 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 argument la fonction de retour next([dom]) acceptant optionellement en premier argument un objet' dom utilisée pour manipuler le DOM virtuel.
  • En deuxième argument l'objet locals contenant entre autre la chaine de caractères locals.dom contenant la réponse ou la fonction locals.virtualDom() générant un Dom virtuel manipulable.
  • En troisième argument l'objet request pour cette page.
  • En quatrième argument l'objet response 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é pour toute requête HTTP, toutes pages confondues.
exports.changeDom = function (next, locals, request, response) {
    // Transformer la chaîne HTML en DOM virtuel.
    var dom = locals.virtualDom();

    // Après tous les h1 de la sortie HTML de `dom`...
    Array.prototype.forEach.call(dom.window.document.getElementsByTagName("h1"), function (h1) {

        // ...on crée une div,
        var div = dom.window.document.createElement('div');

        // ... on injecte le contenu du h1 dans la div...
        div.innerHTML = h1.innerHTML;
        h1.parentNode.insertBefore(div, h1.nextElementSibling);

        // ...et supprime le h1.
        h1.parentNode.removeChild(h1);
    });

    // On retourne les modifications pour qu'elles redeviennent une chaîne de caractères HTML.
    next(dom);
};

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,
        jsdom = NA.modules.jsdom, // Récupération de `jsdom` pour parcourir le DOM virtuel.
        dom = new jsdom.JSDOM(locals.dom); // On charge les données pour les manipuler comme un DOM.

    // On modifie le contenu du nœud avec la classe `.title`.
    dom.window.document.getElementsByClassName("title")[0].textContent = "Modification de Contenu";

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

    // 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>

Point d'ancrage 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 le commun à plusieurs pages, puis celui pour 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"))(); // affiche `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(); // affiche `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 onglets, constater que tout est bien mis à jour chez tout le monde. Chaque nouvel onglet 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.

Point d'ancrage 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 d'un module</title>
    </head>
    <body>
        <div class="title">Test d'un module</div>
        <div>
            <h1>Test d'un 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 l'instance 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 d'un module</title>
    </head>
    <body>
        <div class="title">Test d'un module</div>
        <div>
            <h1>Test d'un module</h1>
            <p>J'utilise <strong>markdown</strong>.</p>
        </div>
    </body>
</html>

Point d'ancrage setConfigurations

Pour configurer le serveur web de NodeAtlas (Express) vous pouvez utiliser le contrôleur commun pour tout le site afin de 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 argument la fonction de rappel next().

Voici un exemple utilisant un middleware pour Express :

{
    "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 passe à la suite.
    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": { /* ... */ }
}

Point d'ancrage 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 gestion 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();
};

Point d'ancrage 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 argument la fonction de rappel 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();
};

Échanges 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>Mettre à jour</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 possibles 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 `server-render`...
        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 « Mettre à jour », 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.

Middlewares

NodeAtlas repose en partie sur le module npm Express. 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
    })];
};

ou sans :

middlewares/redirect.js

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

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

    next();
};