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 avancée

NodeAtlas offre également tout un système de fonctionnalités de développement et de packaging à travers son sytème de configuration. Voyons cela.

Page 404

Écouter toutes les URL, même les adresses du dossier assetsRelativePath

Pour afficher une page personnalisée quand une ressource n'est pas trouvée il faut :

  1. Préparer une page 404.
  2. Remplir le paramètre pageNotFound avec comme value la key de la page 404 préparée.

Voyez l'exemple ci-dessous :

{
   "pageNotFound": "/pages-inexistantes/",
   "routes": {
      "/liste-des-membres/": {
         "view": "members.htm"
      },
      "/": {
         "view": "index.htm"
      },
      "/pages-inexistantes/": {
         "view": "error.htm",
         "statusCode": 404
      }
   }
}

vous pourrez accéder à :

Page d'erreur localisée

Il vous suffit de créer une nouvelle route finissant par * dans la langue souhaitée.

Voyez l'exemple ci-dessous :

{
   "pageNotFound": "/pages-inexistantes/",
   "languageCode": "fr-fr",
   "routes": {
      "/liste-des-membres/": {
         "view": "members.htm",
         "variation": "members.json"
      },
      "/": {
         "view": "index.htm",
         "variation": "index.json"
      },
      "/pages-inexistantes/": {
         "view": "error.htm",
         "variation": "error.json",
         "statusCode": 404
      },
      "/english/list-of-members/": {
         "view": "members.htm",
         "languageCode": "en-gb",
         "variation": "members.json"
      },
      "/english/": {
         "view": "index.htm",
         "languageCode": "en-gb",
         "variation": "index.json"
      },
      "/english/*": {
         "view": "error.htm",
         "languageCode": "en-gb",
         "variation": "error.json",
         "statusCode": 404
      }
   }
}

Routage dynamique

Bien que vous puissiez paramétrer des URL statiques, vous pouvez également paramétrer une écoute d'URL dynamiques !

Paramètres

Il est possible de récupérer des paramètres de l'URL pour afficher un contenu différent en fonctions de ces paramètres.

Avec la configuration suivante :

{
    "routes": {
        "/liste-des-membres/:member/:action/": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/liste-des-membres/:member/:action": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/liste-des-membres/:member/": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/liste-des-membres/:member": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/liste-des-membres/": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/liste-des-membres": {
            "view": "members.htm",
            "controller": "members.js"
        },
        "/": {
            "view": "index.htm"
        }
    }
}

vous pourrez accéder à :

et récupérer les valeurs de :member, :action, example et test dans le changeVariations (commun et specifique).

exports.changeVariations = function (next, locals, request, response) {

    console.log("param request:", request.params.member);
    // $ undefined, 'toto', 'bob-eponge99', 'node-atlas' or 'etc'.
    console.log("param locals:", locals.params.member);
    // $ undefined, 'toto', 'bob-eponge99', 'node-atlas' or 'etc'.

    console.log("param request", request.params.action);
    // $ undefined, 'show' or 'lolol'.
    console.log("param locals", locals.params.action);
    // $ undefined, 'show' or 'lolol'.

    console.log("query request", request.query.example);
    // $ undefined or 'test'
    console.log("query locals", locals.query.example);
    // $ undefined or 'test'

    console.log("body request", request.body.test);
    // $ undefined or 'This is a test'.
    console.log("body locals", locals.body.test);
    // $ undefined or 'This is a test'.

    next();
};

Paramètres avancés

Nous voyons que nous utilisons une configuration identique pour trois routes dans l'exemple précédent. Vous pouvez également vous aider d'expressions régulières pour définir ce qui peut varier pour accéder à votre URL ou préciser quels sont les paramètres valides dans l'URL. Ce sytème est simplifié étant donné que beaucoup de caractères ne se trouvent pas dans l'URL. Il n'est donc pas nécéssaire par exemple d'échaper les / par exemple.

Avec la configuration suivante :

{
    "routes": {
        "/liste-des-membres/?(:member([-a-zA-Z0-9]+)/?(:action(afficher|editer)/?)?)?": {
            "view": "members.htm"
        },
        "/": {
            "view": "index.htm"
        }
    }
}

vous pourrez accéder à :

et récupérer les valeurs de :member, :action, example et test dans une vue.

<!DOCTYPE html>
<html lang="fr-fr">
  <head>
    <meta charset="utf-8">
    <title>URL Rewriting Test</title>
  </head>
  <body>
    Member: <strong><?- params.member ?></strong><br>
    Action: <strong><?- params.action ?></strong><br>
    Example: <strong><?- query.example ?></strong><br>
    Test: <strong><?- body.test ?></strong>
  </body>
</html>

vous ne pourrez pas accéder à :

Expressions régulières

Vous pouvez également activer les expressions régulières pour un chemin précis avec regExp. Si celui-ci vaut true, le précédent mode ne fonctionne plus et vous passez en mode expression régulière. Si regExp est une chaine de caractères, celle-ci fait office de flag (g, i, m ou y).

Voyez la configuration suivante :

{
    "routes": {
        "/liste-des-membres/([-a-z0-9]+)/?": {
            "view": "members.htm",
            "regExp": "i"
        },
        "/liste-des-membres/?": {
            "view": "members.htm",
            "regExp": true
        },
        "/": {
            "view": "index.htm"
        }
    }
}

vous pourrez accéder à :

et récupérer les valeurs de ([-a-z0-9]+) dans le changeVariations (commun et spécifique).

exports.changeVariation = function (next, locals) {

    if (locals.params && locals.params[0]) { locals.params.member = locals.params[0]; }
    // locals.params[1] pour le deuxième match, etc...

    console.log(locals.params.member);
    // $ 'toto', 'bob-eponge99', 'node-atlas' ou 'etc'.

    next();
}

Les règles de création d'URL dynamiques avec regExp sont celles des RegExp JavaScript.

Routage programmatique

Nous avons pu voir qu'avec setRoutes il était possible d'injecter dynamiquement des routes. Cependant, l'injection de route ne se fait qu'à la fin car NA.webconfig.routes est un objet. Il n'y a donc pas de moyen d'ordonner les routes, ce qui est génant car les routes sont résolue dans l'ordre dans lesquels elles ont été injectées.

Nous allons résoudre ça en changeant la manière de créer les routes de routes: { <key>: { ... } } à routes: [{ "key": <key>, ... }].

Voici l'ensemble de fichier suivant :

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

Avec le webconfig.json initialement comme ceci avec routes: <Object> :

{
    "controller": "common.js",
    "routes": {
        "/doc/index.html": {
            "view": "index.htm"
        },
        "/doc/*": {
            "view": "error.htm",
            "statusCode": 404
        }
    }
}

se transformant en cela avec routes: <Array> :

{
    "controller": "common.js",
    "routes": [{
        "url": "/doc/index.html",
        "view": "index.htm"
    }, {
        "url": "/doc/*",
        "view": "error.htm",
        "statusCode": 404
    }]
}

Avec le fichier common.js nous pouvons maintenant injecter les routes à des positions précises. Nous allons les ajouter au début.

// 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` au débuts de nos routes.
    route.unshift({
        "url": "/doc/content.html",
        "view": "content.htm"
    });

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

De cette manière l'adresse http://localhost/doc/content.html renverra la vue content.htm et non la vue error.htm en 404.

Redirections

Pour aller à une autre adresse (redirection 301 ou 302) quand vous arrivez à une url il faut utiliser le paramètre redirect.

Note : si vous ne précisez pas un statusCode, la redirection ne se fera pas. Le statusCode est obligatoire.

En statique

Voyez l'exemple ci-dessous :

{
    "routes": {
        "/liste-des-membres/": {
            "view": "members.htm"
        },
        "/liste-des-membres": {
            "redirect": "/liste-des-membres/",
            "statusCode": 301
        },
        "/aller-sur-node-atlas/": {
            "redirect": "https://node-atlas.js.org/",
            "statusCode": 302
        },
        "/": {
            "view": "index.htm"
        }
    }
}

Vous serez redirigé :

  • sur http://localhost/liste-des-membres/ quand vous accéderez à http://localhost/liste-des-membres avec une entête redirection permanente.
  • sur https://node-atlas.js.org/ quand vous accéderez à http://localhost/aller-sur-node-atlas/ avec une entête redirection temporaire.

En dynamique

Voyez l'exemple ci-dessous :

{
    "routes": {
        "/liste-des-membres/:member/": {
            "view": "members.htm"
        },
        "/liste-des-membres/:member": {
            "redirect": "/liste-des-membres/:member/",
            "statusCode": 301
        },
        "/": {
            "view": "index.htm"
        }
    }
}

Vous serez redirigé sur http://localhost/liste-des-membres/machinisteweb/ quand vous accéderez à http://localhost/liste-des-membres/machinisteweb avec une entête redirection permanente.

Avec expressions régulières

Voyez l'exemple ci-dessous :

{
    "routes": {
        "/membres/([-a-z0-9]+)/": {
            "view": "members.htm",
            "regExp": true
        },
        "/liste-des-membres/([-a-z0-9]+)/": {
            "redirect": "/membres/$0/",
            "statusCode": 301,
            "regExp": true
        },
        "/liste-des-membres/": {
            "view": "members.htm"
        },
        "/": {
            "view": "index.htm"
        }
    }
}

Vous serez redirigé sur http://localhost/membres/machinisteweb/ quand vous accéderez à http://localhost/liste-des-membres/machinisteweb/ avec une entête redirection permanente.

Pour le second match utilisez $1, pour le troisième $2, etc.

Entêtes HTTP

Par défaut, les entêtes HTTP envoyées par NodeAtlas sont les suivantes : Content-Type:text/html; charset=utf-8 avec un statusCode à 200.

Il est tout à fait possible de modifier ces valeurs pour une entrée de route (pour des APIs local au site).

{
    "mimeType": "application/json",
    "charset": "utf-16",
    "routes": {
        "/": {
            "view": "index.htm",
            "mimeType": "text/html"
        },
        "/api/articles": {
            "view": "display-json.htm",
            "controller": "blog/list-of-articles.js",
            "charset": "utf-8",
            "statusCode": 203
        }
    }
}

Il est également possible de modifier complètement les entêtes, ce qui écrase toutes les autres valeurs de headers (à l'exception du statusCode donc). Mettre une valeur à false retire le Headers précédemment mis en place.

{
    "headers": {
        "Content-Type": "application/json; charset=utf-8",
        "Access-Control-Allow-Origin": "*"
    },
    "routes": {
        "/api/articles": {
            "view": "display-json.htm",
            "controller": "blog/list-of-articles.js",
            "statusCode": 203,
            "headers": {
                "Access-Control-Allow-Origin": false
            }
        }
    }
}

Configuration dynamique

Plutôt que d'utiliser plusieurs configurations statiques .json, il est tout a fait possible d'utiliser des configurations dynamiques .js. Dans ce cas, ce que votre fichier .js devra retourner avec module.exports sera un objet JSON valide.

Nous pouvons ainsi aisément remplacer les six fichiers suivants :

webconfig.json

{
    "languageCode": "fr-fr",
    "statics": "statics.fr-fr.json",
    "routes": {
        "/": "index.htm"
    }
}

webconfig.prod.json

{
    "cache": true,
    "languageCode": "fr-fr",
    "statics": "statics.fr-fr.json",
    "routes": {
        "/": "index.htm"
    }
}

webconfig.en-us.json

{
    "languageCode": "fr-fr",
    "statics": "statics.fr-fr.json",
    "routes": {
        "/": "index.htm"
    }
}

webconfig.en-us.prod.json

{
    "cache": true,
    "languageCode": "en-us",
    "statics": "statics.en-us.json",
    "routes": {
        "/": "index.htm"
    }
}

statics.fr-fr.json

{
    "/variations/": "variations/fr-fr/",
}

statics.en-us.json

{
    "/variations/": "variations/en-us/",
}

par les deux fichiers suivants :

webconfig.js

module.export = (function () {
    var webconfig = {
        "cache": false,
        "languageCode": "fr-fr",
        "statics": "statics.json"
        "routes": {
            "/": "index.htm"
        }
    };

    if (process.env.NODE_ENV === 'production') {
        webconfig["cache"] = true;
    }

    if (process.env.LANG) {
        webconfig["languageCode"] = process.env.LANG;
    }

    return webconfig;
}());

statics.js

module.export = (function () {
    var NA = this.NA,
        languageCode = NA.webconfig.languageCode

    return {
        "/variations/": "variations/" + languageCode + "/",
    };
}());

en supposant les variables d'environnements suivantes pour les quatre environnements suivants :

Local FR

NODE_ENV=DEVELOPMENT
LANG=fr-fr

Local EN

NODE_ENV=DEVELOPMENT
LANG=en-us

Prod FR

NODE_ENV=PRODUCTION
LANG=fr-fr

Prod EN

NODE_ENV=PRODUCTION
LANG=en-us

HTTPs

Il est très simple de faire tourner une instance de NodeAtlas avec le protocol HTTPs. Pour cela il suffit de créer, par exemple un dossier security dans lequel vous allez placer vos fichiers server.key et server.crt afin d'alimenter le protocol.

Il ne vous reste plus qu'à utiliser la configuration suivante :

{
    "httpSecure": true,
    "httpSecureKeyRelativePath": "security/server.key",
    "httpSecureCertificateRelativePath": "security/server.crt",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

Vous pouvez également, si —comme c'est le cas ici— vos deux fichiers .key et .crt portent le même nom, utiliser cette configuration :

{
    "httpSecure": "security/server",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

Pour finir, il est également possible de seulement laisser la valeur de httpSecure à true pour obtenir un https dans vos chemins comme urlBasePath ou urlBase. Cependant le serveur ce lancera en HTTP, il vous faudra un proxy qui gère pour vous la lecture du certificat.

{
    "httpSecure": true,
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

Note : en production, si vous redirigez un proxy vers votre instance de NodeAtlas, n'oubliez pas qu'en HTTPs ce n'est pas urlPort: 80 mais urlPort: 443

GET / POST

Vous pouvez également manager la manière dont le serveur va répondre aux demandes GET/POST pour une page donnée. Par exemple, nous allons autoriser l'accès aux pages uniquement en GET pour tout le site et autoriser un POST pour une page seulement (et même lui interdire le GET).

{
    "get": true,
    "post": false,
    "routes": {
        "/": {
            "view": "index.htm"
        },
        "/liste-des-membres/": {
            "view": "members.htm"
        },
        "/rediger-commentaire/": {
            "view": "write-com.htm"
        },
        "/commentaire-sauvegarde/": {
            "view": "save-com.htm",
            "get": false,
            "post": true
        }
    }
}

Note : si rien n'est précisé, get et post sont à true au niveau global et par page.

PUT / DELETE

Fonctionnant exactement de la même manière que get et post, les deux actions HTTP PUT et DELETE qui part défaut ne sont pas activé peuvent être activé avec put et delete.

{
    "get": false,
    "post": false,
    "put": true,
    "routes": {
        "/read-all-entry/": {
            "view": "display-json.htm",
            "variation": "all-entry.json",
            "get": true,
            "put": false
        },
        "/read-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "get": true,
            "put": false
        },
        "/create-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "post": true,
            "put": false
        },
        "/update-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json"
        },
        "/delete-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "delete": true,
            "put": false
        }
    }
}

Avec la configuration ci-dessus, seulement une action HTTP n'est possible par entrée, cela permet de faire des APIs REST facilement avec NodeAtlas.

CORS et OPTIONS

Par défaut, les requêtes pré-vérifiées ("preflighted requests") ne sont pas activées. Vous allez en avoir besoin pour, par exemple, effectuer des requêtes Cross-Domain ou CORS. Les requêtes pré-vérifiées se font avec la méthode HTTP OPTIONS.

Pour activer OPTIONS sur une route, utilisez la propriété options sur une route dans le webconfig. Pour activer OPTIONS sur toutes les routes, utilisez alors la propriété options du webconfig sur la configuration global.

{
    "options": true,
    "routes": {
        "/read-all-entry/": {
            "view": "display-json.htm",
            "variation": "all-entry.json",
            "options": false
        },
        "/create-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "post": true
        },
        "/delete-entry/:id/": {
            "view": "display-json.htm",
            "variation": "entry.json",
            "delete": true
        }
    }
}

Demande cross-domain

Si vous souhaitez authoriser une ressource du serveur NodeAtlas au requête en provenance de www.domain-a.com pour une page précise, vous pouvez le faire ainsi :

{
    "routes": {
        "/api/random-quote": {
            "controller": "get-quote.js",
            "headers": {
                "Access-Control-Allow-Origin": "http://www.domain-a.com"
            }
        }
    }
}

Ainsi vous pourrez par exemple accepter la requête suivante qui a pour Origin, http://www.domain-a.com qui est donc une valeur de Access-Control-Allow-Origin :

GET /api/random-quote HTTP/1.1
Host: www.domain-a.com
...
Origin: http://www.domain-a.com
...

Demande cross-domain avec jeton

Si vous souhaitez authoriser des ressources du serveur NodeAtlas aux requêtes en provenance de n'importe quel domaine externe pour la page /api/random-quote et une page qui attend un jeton d'authentification pour la page /api/protected/random-quote, vous pouvez le faire ainsi :

{
    "mimeType": "application/json",
    "headers": {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Authorization"
    },
    "routes": {
        "/api/random-quote": {
            "controller": "get-quote.js"
        },
        "/api/protected/random-quote": {
            "controller": "get-quote.js",
            "middlewares": "is-authenticated.js",
            "options": true
        }
    }
}

Faire lire un jeton à NodeAtlas depuis un domaine externe nécessite de lui passer l'en-tête HTML Authorization. Pour que celle-ci soit acceptée par NodeAtlas il faut le définir avec Access-Control-Allow-Headers acceptant Authorization. L'envoi d'un jeton nécessitant une requête pré-vérifiée, il faut également mettre options à true pour autoriser les requêtes HTTP avec la méthode OPTIONS.

Ainsi vous pourrez par exemple accepter la requête suivante qui passe un jeton d'authentification à notre serveur pour la ressource /api/protected/random-quote :

GET /api/protected/random-quote HTTP/1.1
Host: localhost:1337
...
Origin: http://localhost
Authorization: Bearer CODE_DU_JETON
...

Autre demande cross-domain

Toutes les entêtes prévues pour faire fonctionner CORS sont acceptées via le mécanisme d'ajout d'en-tête de NodeAtlas.

Paramètres de sessions

Clé et secret

NodeAtlas gère lui-même les sessions stockées sur le serveur avec comme paramètres initiaux :

  • Key : nodeatlas.sid
  • Secret : 1234567890bépo

qui permettent à un client de rester connecté à travers les pages à un même ensemble de variables personnelles côtés serveur.

Il est possible de modifier ces paramètres par défaut (et même obligatoire pour des sites en productions) avec les paramètres de webconfig.json suivant :

{
    "sessionKey": "clé personnelle",
    "sessionSecret": "secret personnel"
}

NodeAtlas utilise également un objet de stockage mémoire (MemoryStore) qui stocke les informations dans la RAM du serveur.

Autres paramètres

Il est possible de changer l'intégralité des paramètres des sessions (sauf le MemoryStore) en utilisant la configuration de webconfig.json suivante :

{
    "session": {
        "key": "clé personnelle",
        "secret": "secret personnel",
        "cookie": {
            "path": "/",
            "httpOnly": true,
            "secure": false,
            "maxAge": null
        },
        ...,
        ...,
        ...
    }
}

L'intégralité de la configuration possible se trouve sur la documentation du module express-session.

Stockage de sessions

Par défaut, c'est NodeAtlas qui stocke les sessions serveurs dans la RAM du serveur par application. Cela ne permet pas de partager des sessions utilisateurs à travers plusieurs applications NodeAtlas (ou autre) et efface toutes les sessions en cours pour une application en cas de redémarrage de celle-ci.

Pour résoudre ce souci, il convient de prendre en charge l'enregistrement des sessions via une base No SQL tel que Redis ou MongoBD.

Pour cela il suffit d'utiliser la fonction setSessions dans le fichier de controller commun.

Session gérées avec Redis

Implémentez le code suivant dans le controller commun pour stocker vos sessions dans Redis en local.

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

    NA.modules.RedisStore = require("connect-redis");
};

exports.setSessions = function (njsext) {
    var NA = this,
        session = NA.modules.session,
        RedisStore = NA.modules.RedisStore(session);

    NA.sessionStore = new RedisStore();

    next();
};

Plus d'informations sur connect-redis.

Session gérées avec MongoDB

Implémentez le code suivant dans controllers/common.js pour stocker vos sessions dans la database sessions d'une MongoDB locale.

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

    NA.modules.MongoStore = require("connect-mongo");
};

exports.setSessions = function (next) {
    var NA = this,
        session = NA.modules.session,
        MongoStore = NA.modules.MongoStore(session);

    NA.sessionStore = new MongoStore({
        db: "sessions"
    });

    next();
};

Plus d'informations sur connect-mongo.

URL d'écoute

Il est possible de générer une URL de visite différente des paramètres d'écoutes demandés avec urlHostname et urlPort. Par exemple on écoute la boucle local sur le port 80 car un script fait du Reverse Proxy depuis le port 7777 sur le 80 avec le module « http-proxy » comme ci-dessous :

{
    "httpPort": 7777,
    "httpHostname": "127.0.0.1",
    "urlPort": 80,
    "urlHostname": "localhost",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

URL dynamiques

Les chemins relatifs en absolue

Il est possible que les chemins créés à partir de votre URL soient interprétés comme des sous-dossiers qui n'ont en réalité aucunes existences réelles. Cela a pour conséquence de rendre l'adresse media/images/example.jpg initialement accessible depuis un template affiché à http://localhost impossible à récupérer quand le template est affiché à http://localhost/sub-directory/ (puisqu'il faudrait alors que notre chemin soit plutôt ../media/images/example.jpg).

Pour ne plus avoir à se soucier de l'accès aux ressources, peu importe l'URL qui est demandée, il suffit de transformer toutes les URL relatives telles que :

<link rel="stylesheet" type="text/css" href="stylesheets/common.css" />
<!-- ... -->
<img src="media/images/example.jpg" />
<!-- ... -->
<script type="text/javascript" src="javascripts/common.js"></script>

en URL absolues avec la variable urlBasePath comme ci-dessous :

<link rel="stylesheet" type="text/css" href="<?= urlBasePath ?>stylesheets/common.css" />
<!-- ... -->
<img src="<?= urlBasePath ?>media/images/example.jpg" />
<!-- ... -->
<script type="text/javascript" src="<?= urlBasePath ?>javascripts/common.js"></script>

À noter que dans le cas de la configuration suivante :

{
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

urlBasePath retourne http://localhost/ alors que dans celle-ci :

{
    "httpPort": 7777,
    "urlRelativeSubPath": "sub/folder",
    "routes": {
        "/": {
            "view": "index.htm"
        }
    }
}

urlBasePath retourne http://localhost:7777/sub/folder/.

Les chemins des templates

En utilisant le webconfig suivant :

{
    "routes": {
        "/index.html": {
            "view": "index.htm"
        },
        "/contact.html": {
            "view": "contact.htm"
        }
    }
}

ainsi que la view index.htm correspondante

<!-- ... -->
<a href="http://localhost/index.html">Lien vers l'accueil</a>
<a href="http://localhost/contact.html">Lien pour nous contacter</a>
<!-- ... -->

je serais obligé de changer mon lien dans le template si je change le port d'écoute ou si je change le chemin de l'URL. Le changement de configuration suivant :

{
    "httpPort": 7777,
    "routes": {
        "/home.html": {
            "view": "index.htm"
        },
        "/contact-us.html": {
            "view": "contact.htm"
        }
    }
}

me contraindrait à modifier le template précédent comme suit :

<!-- ... -->
<a href="http://localhost:7777/home.html">Lien vers l'accueil</a>
<a href="http://localhost:7777/contact-us.html">Lien pour nous contacter</a>
<!-- ... -->

Il est possible de solutionner ce problème en donnant une clé à un chemin précis et en déportant son chemin dans la propriété url.

Avec le webconfig suivant :

{
    "routes": {
        "index": {
            "url": "/index.html",
            "view": "index.htm"
        },
        "contact": {
            "url": "/contact.html",
            "view": "contact.htm"
        }
    }
}

je peux à présent écrire le lien dans le template de manière dynamique :

  1. comme suit

    <!-- ... -->
    <a href="<?= urlBasePath ?><?= webconfig.routes.home.url.slice(1) ?>">Lien vers l'accueil</a>
    <a href="<?= urlBasePath ?><?= webconfig.routes.contact.url.slice(1) ?>">Lien pour nous contacter</a>
    <!-- ... -->

    Note : .slice(1) permet de supprimer facilement le double / pour une URL fonctionnelle.

  2. ou comme suit

    <!-- ... -->
    <a href="<?= urlBasePath ?>.<?= webconfig.routes.home.url ?>">Lien vers l'accueil</a>
    <a href="<?= urlBasePath ?>.<?= webconfig.routes.contact.url ?>">Lien pour nous contacter</a>
    <!-- ... -->

    Note : Cela donnerait par exemple http://localhost/./home.html, ce qui est une URL fonctionnelle.

  3. ou comme suit

    <!-- ... -->
    <a href="<?= urlBasePathSlice + webconfig.routes.home.url ?>">Lien vers l'accueil</a>
    <a href="<?= urlBasePathSlice + webconfig.routes.contact.url ?>">Lien pour nous contacter</a>
    <!-- ... -->

    Note : urlBasePathSlice renvoyant http://localhost au lieu de http://localhost/ ou encore http://localhost:7777/sub/folder au lieu de http://localhost:7777/sub/folder/.

Utilisation de la clé pour mapper les pages

Il est parfois utile de connaître la clé utilisée pour la page courante afin de trouver une équivalence dans une autre langue par exemple.

Avec le webconfig suivant :

{
    "languageCode": "fr-fr",
    "routes": {
        "index_fr-fr": {
            "url": "/",
            "view": "/index.htm"
        },
        "index_en-us": {
            "url": "/english/",
            "view": "index.htm",
            "languageCode": "en-us"
        },
        "cv_fr-fr": {
            "url": "/cv/",
            "view": "cv.htm"
        },
        "cv_en-us": {
            "url": "/english/resume/",
            "view": "index.htm",
            "languageCode": "en-us"
        }
    }
}

et les fichiers de variation commun suivant en fr :

{
    "language": [{
        "name": "Anglais",
        "code": "en-us"
    }, {
        "name": "Français",
        "code": "fr-fr"
    }]
}

en en :

{
    "language": [{
        "name": "English",
        "code": "en-us"
    }, {
        "name": "French",
        "code": "fr-fr"
    }]
}

on peut alors créer un lien entre chaque page multilingue comme ceci :

<ul>
    <? for (var i = 0; i < common.language.length; i++) { ?>
    <li><a href="<?= urlBasePath + webconfig.routes[routeKey.split('_')[0] + '_' + common.language[i].code].url ?>"><?- common.language[i].name ?></a></li>
    <? } ?>
</ul>

Moteur de template personnalisé

Il est possible de laisser l'implémentation de Express prendre la main sur le moteur de rendu des vues. Pour cela il faut utiliser le paramètre engine. Un exemple en utilisant le moteur Handlebars :

Tout d'abord, ajouter le middleware Express Handlebars à vos modules :

npm install express-handlebars

puis utiliser engine avec la valeur arbitraire hbs

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

puis expliquer au moteur Express de NodeAtlas comment rendre les vues :

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

    NA.modules.exphbs = require("express-handlebars");
};

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

    NA.express.engine("hbs", exphbs());

    next();
};

enfin voyons rapidement ce que le fichier index.hbs pourrait contenir :

<!DOCTYPE html>
<html lang="fr-fr">
    <head>
        <meta charset="utf-8">
        <title>{{specific.titlePage}}</title>
        <link rel="stylesheet" href="stylesheets/{{common.classCssCommon}}.css" media="all">
        <link rel="stylesheet" href="stylesheets/{{specific.classPage}}.css" media="all">
    </head>
    <body class="{{specific.classPage}}">
        <div>
            <h1>{{specific.titlePage}}</h1>
            {{{specific.content}}}
        </div>
        <script async="true" type="text/javascript" src="javascripts/{{common.classJsCommon}}.js"></script>
    </body>
</html>

Ce que fait engine, c'est abandonner le système de NodeAtlas et passer par celui d'Express. Comme Express a besoin d'un objet response pour rendre une vue, il est impossible d'utiliser ce mécanisme via l'utilisation de la fonction NA.view de l'API NodeAtlas, celle-ci ne supportant que le moteur NodeAtlas, EJS et Pug.

Différence entre engine, templateEngineDelimiter et pug

Il est tout a fait possible de passer par Express pour rendre EJS et Pug. Dans ce cas, puisque node-atlas embarque les modules ejs et pug en tant que dépendance, il n'est pas nécéssaire de passer par le controller commun et l'utilisation de npm pour les mettre en place. Il suffit juste d'utiliser engine: "ejs" ou engine: "pug".

Cependant, faire cela retire les bénéfices apportés par NodeAtlas pour l'utilisation de ces deux moteurs comme par exemple le support des inclusions dynamiques pour Pug dans la view avec #{routeParameters.view}.

Pas de vue

Il est possible de ne pas utiliser de vue et de seulement faire appel au contrôleur. Dans ce cas le point d'ancrage changeVariations est inutile. Il va falloir alimenter vous même locals.dom dans le point d'ancrage changeDom.

webconfig.json

{
    "routes": {
        "/(:member/)?": {
            "controller": "index.js",
            "mimeType": "application/json"
        }
    }
}

controllers/index.js

exports.changeDom = function (next, locals) {
    locals.dom = `{
  "params": ${locals.params.member},
  "query": ${locals.query.member},
  "body": ${locals.body.member}
}`;

    next();
};

Ainsi à l'adresse http://localhost/riri/?query=fifi demandé en POST avec le body member=loulou vous obtiendrez la sortie :

{
  "params": "riri",
  "query": "fifi",
  "body": "loulou"
}

Pas de routes

Pas un seul webconfig présenté dans la documentation ne se passe du paramètre routes. Pourtant il est facultatif au même titre que tous les autres. Aussi avec le webconfig suivant :

webconfig.json

{
    "controller": "common.js"
}

et le contrôleur suivant :

controllers/common.js

exports.setRoutes = function (next) {
    var NA = this,
        route = NA.webconfig.routes = {};

    route["/"] = {
        "mimeType": "text/plain"
    };

    next();
};

exports.changeDom = function (next, locals) {

    locals.dom = "Hello World";

    next();
};

Il est tout à fait possible d'obtenir à l'adresse http://localhost/ le simple message « Hello World ».

Cache

C'est une bonne chose de ne pas reservir des fichiers qui n'ont pas bougé pour la production. Vous pouvez mettre à true la valeur du webconfig cache pour ça:

{
    "cache": true,
    "route": {
        "/": "index.htm"
    }
}

Vous pouvez également lancer node-atlas avec l'option --cache :

node-atlas --cache

ou mettre votre variable d'environnement NODE_ENV à production :

si vous êtes sous Unix / MacOS

export NODE_ENV=production

ou si vous êtes sur Windows

SET NODE_ENV=production

ou vous pouvez démarrer NodeAtlas comme suit

NODE_ENV=production node-atlas

ou vous pouvez la définir dans votre fichier JavaScript :

process.env.NODE_ENV = "production";

Base de données SQL

Nous allons voir à présent comment utiliser des informations venant d'une base de données. Pour cela nous allons utiliser une base MySQL comme exemple. Le module npm mysql va donc nous être utile. 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>

Base de données 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>

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