Vue + NodeAtlas : de l'art du SSR ou Rendu Côte Serveur avec JavaScript

Nous allons voir dans cet article comment faire du rendu côté serveur ou SSR (Server-Side Render) avec le framework JavaScript client MVVM (Modèle-Vue-Vue Modèle) Vue couplé au framework JavaScript serveur MVC(2) (Model View Controller) NodeAtlas !

Alors ce titre parle peut-être aux habitués des architectures clientes MVVM qui ont des difficultés avec le référencement et semble peut-être barbare pour d'autres. Lançons-nous dans une petite explication histoire de rendre cet article intéressant également pour les néophytes : comprenons le problème et trouvons la solution à travers cette page.

Quel est le problème traité dans cet article ?

Le problème avec les frameworks MVVM client est qu'ils construisent le site à partir de rien. Fouillez la source du code de la réponse HTTP de la page courante, celle lue par les indexeurs de contenus ou les navigateurs sans JavaScript ; il n'y a rien. Aussi, si je crée une liste d'actions futures pour la roadmap de ma super App, et que je souhaite pouvoir manipuler aisément (ajout et retrait) ses éléments grâce à un coupleur de données vue-modèle ; le revers de la médaille sera que les informations utilisées pour cette construction proviendront de fichiers JavaScript ou morceaux de HTML qui ne veulent rien dire pour les indexeurs où même les validateurs de page. Vos sites sont donc souvent « SEO merdiques » et « W3C bancales ».

Quelle solution ?

Le SSR ou Rendu Côté Serveur. Voyons ça au travers de cette page exemple avec Vue et NodeAtlas !

Vue et NodeAtlas

Tout d'abord Vue et NodeAtlas sont tous les deux écrits en JavaScript et tournent avec un moteur JavaScript. Vue tourne grâce au moteur JS embarqué dans les navigateurs, NodeAtlas tourne grâce au moteur serveur JS installé avec Node.js. Oui, on parle bien ici d'un développement intégral avec HTML, CSS et JavaScript seulement.

Nous avons ici deux frameworks aux rôles complémentaires :

Vue

  • Vue (vue.js) est un data-binder simple (équivalent à Angular ou Riot mais bien plus performant) dont la versatilité et la suite d'outils lui permettent de devenir un puissant système MVVM (équivalent à Angular2 ou React mais plus performant bis). Attention, il ne remplace pas jQuery (ou Vanilla JS) qui servent avant tout à manipuler le DOM. Vue lie les données en provenance de fichiers JavaScript au HTML de sorte qu'une modification des données se reflète directement dans le HTML sans aucune manipulation de votre part. Là où faire ce travail avec jQuery demanderait de trouver une liste, de récupérer un élément de liste, d'insérer la nouvelle donnée dans l'élément de liste, d’insérer l'élément de liste à la fin de la liste, de trouver le compteur qui compte les lignes, de l'incrémenter de 1, etc. ; il suffit avec Vue de simplement ajouter une donnée dans le tableau JavaScript lié et tout est « re-calculé ».

NodeAtlas

  • NodeAtlas (node-atlas.js) est un serveur HTTP simple dans sa forme la plus basique (équivalent à Express) par référencement de route dont le point commun avec Vue est l'évolutivité et la versatilité. Cela signifie que l'on peut faire tourner des sites multilingues performants avec un nombre conséquent de pages uniquement avec une partie route et vue active (parfait pour débuter en Node.js). Les parties modèle et contrôleur sont à activer au besoin (parfait pour les experts). Il suit une architecture MVC dans sa pleine utilisation (équivalent d'un Sails.js) avec des contrôleurs dédiés ou une architecture MVC2 avec un contrôleur commun (ou les deux, ou aucun) et permet de créer des sites orientés composants si souhaité et des architectures orientées service (Site Front simple + Collection d'API distantes + Serveur d'authentification + ...).

NodeAtlas - Sans MVVM, les bons vieux sites habituels

Lançons nous dans une petite page HTML sans prétention que vous auriez faite dans les règles de l'art avec tout ce qui va bien. Ici nous allons rester minimalistes, le but de l'article étant de comprendre et de résoudre le problème de référencement.

Avec NodeAtlas, créons-nous une page qui liste des actions futures à entreprendre. Nous allons faire cela en utilisant une vue dans le dossier views et en utilisant une variation dans le dossier variations comme source de données.

Ce qu'il faut retenir c'est que l'injection des données dans le HTML va se faire « côté serveur ». La réponse HTTP contient donc les données pour l'indexeur de contenus. C'est typiquement le cas avec n'importe quelle techno serveur (PHP, Ruby, C#, etc.).

Nous avons donc l'architecture NodeAtlas suivante :

├─ variations/
│  └─ data.json
├─ views/
│  └─ show.htm
└─ webconfig.json

avec le contenu des fichiers suivants :

webconfig.json

Nous créons une page a-faire composée du HTML de show.htm et des données de data.json.

{
    "routes": {
        "/a-faire": {
            "view": "show.htm",
            "variation": "data.json"
        }
    }
}

variations/data.json

Nous ajoutons trois entrées dans la variation spécifique derrière la propriété todos.

{
    "todos": [{
        "title": "v1.0",
        "description": "Il va falloir faire la v1.0 !"
    }, {
        "title": "v2.0",
        "description": "Puis faudra faire la v2.0, parce que la v1.0 on la sent déjà pas."
    }, {
        "title": "v3.0",
        "description": "Il faudra faire la v3.0 parce que une fois la v2.0 finie, on voudra encore changer ce qui va pas !"
    }]
}

views/show.htm

Ici on alimente notre HTML avec les données en provenance du fichier de variation en utilisant le moteur de template de NodeAtlas.

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <div class="todo-list">
            <h1>Dans le futur</h1>
            <? if (specific.todos.length) { ?>
            <ul>
                <? for (var i = 0; i < specific.todos.length; i++) { ?>
                <li><strong><?- specific.todos[i].title ?></strong> : <?- specific.todos[i].description ?></li>
                <? } ?>
            </ul>
            <? } ?>
        </div>
    </body>
</html>

Maintenant, lançons notre instance serveur NodeAtlas que l'on va afficher en français avec la commande suivante :

\> node-atlas --browse a-faire --lang fr-fr

Notre navigateur par défaut s'ouvre à l'adresse http://localhost/a-faire et le code source, celui mangé par les indexeurs, ressemblera à ceci :

http://localhost/a-faire

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <div class="todo-list">
            <h1>Dans le futur</h1>
            <ul>
                <li><strong>v1.0</strong> : Il va falloir faire la v1.0 !</li>
                <li><strong>v2.0</strong> : Puis faudra faire la v2.0, parce que la v1.0 on la sent déjà pas.</li>
                <li><strong>v3.0</strong> : Il faudra faire la v3.0 parce que une fois la v2.0 finie, on voudra encore changer ce qui va pas !</li>
            </ul>
        </div>
    </body>
</html>

Jusque-là tout va bien, tout est indexable. C'est assez simple étant donné que nous n'avons pas besoin de manipuler les données depuis le navigateur, nous n'avons donc pas besoin d'un système MVVM.

Vous pouvez retrouver l'intégralité des fichiers de cet exemple dans le dépôt VueAtlas partie step/step1.

Vue - Avec MVVM, l’interaction facile à mettre en place !

Nous allons maintenant utiliser Vue ! Cela signifie que nous allons injecter les données dans le HTML côté client pour permettre à Vue de savoir quelles données sont liées à quelles parties du HTML : cela va nous permettre d'ajouter ou de retirer des entrées simplement ! Pour réaliser cela, nous allons ajouter un fichier assets/javascripts/todo-list.js accessible côté client et créer une nouvelle page basée sur views/update.htm.

├─ assets/
│  └─ javascripts/
│     └─ todo-list.js
├─ variations/
│  └─ data.json
├─ views/
│  ├─ update.htm
│  └─ show.htm
└─ webconfig.json

En ce qui concerne variations/data.json, rien ne va bouger, il va servir de source de données aussi bien pour les pages views/show.htm que views/update.htm.

webconfig.json

Ajoutons notre nouvelle page :

{
    "routes": {
        "/a-faire": {
            "view": "show.htm",
            "variation": "data.json"
        },
        "/gerer-a-faire": {
            "view": "update.htm",
            "variation": "data.json"
        }
    }
}

views/update.htm

Nous allons maintenant :

  • Changer l'implémentation en remplaçant les tags NodeAtlas par les tags Vue. Il ne seront donc pas touché lors de l'analyse du rendu côté serveur et le code sera envoyé côté client tel quel.
  • Glisser les données en provenance de variations/data.json dans un attribut data-model sur la balise todo-list afin de pouvoir alimenter notre vue quand elle s'initialisera dans le navigateur côté client.
  • Nous allons ajouter une partie destinée à ajouter ou supprimer des entrées.
<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <div class="todo-list" data-model="<?= JSON.stringify(specific.todos) ?>">
            <div class="todo-list--content">
                <h1>Dans le futur</h1>
                <ul v-if="todos.length">
                    <li v-for="todo in todos">
                        <strong>{{ todo.title }}</strong> : {{ todo.description }} <span v-on:click="removeTodo(index)">[X]</span>
                    </li>
                </ul>
            </div>
            <div class="todo-list--form">
                Nouveau:
                <input v-model="newTitle" placeholder="Titre">
                <input v-model="newDescription" placeholder="Description">
                <button v-on:click="addTodo">Ajouter</button>
            </div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.min.js"></script>
        <script src="javascripts/todo-list.js"></script>
    </body>
</html>

assets/javascripts/todo-list.js

Nous créons donc la partie modèle de Vue. Nous allons l'associer à l'élément <div class="todo-list">. Pour cela nous allons le chercher dans le DOM avec la variable todosSource. Ensuite nous allons alimenter notre new Vue() avec l'élément en question pour el, avec les données en provenance de data-model pour data.todos. Nous créons ensuite tout ce qu'il faut pour ajouter ou supprimer des entrées.

var todosSource = document.getElementsByClassName("todo-list")[0];

new Vue({
    el: todosSource,
    data: {
        "todos": JSON.parse(todosSource.getAttribute("data-model")),
        "newTitle": "",
        "newDescription": ""
    },
    methods: {
        addTodo: function () {
            this.todos.push({
                "title": this.newTitle,
                "description": this.newDescription
            });
            this.newTitle = "";
            this.newDescription = "";
        },
        removeTodo: function (index) {
            this.todos.splice(index, 1);
        }
    }
});

En coupant l'instance précédente (Ctrl + c) et en lançant notre instance serveur NodeAtlas avec la commande suivante (NodeAtlas est maintenant déjà en français) :

\> node-atlas --browse gerer-a-faire

L'adresse http://localhost/gerer-a-faire s'ouvre de nouveau dans le navigateur et le code source ressemblera à ceci :

http://localhost/gerer-a-faire

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <div class="todo-list" data-model="[{&#34;title&#34;:&#34;v1.0&#34;,&#34;description&#34;:&#34;Il va falloir faire la v1.0 !&#34;},{&#34;title&#34;:&#34;v2.0&#34;,&#34;description&#34;:&#34;Puis faudra faire la v2.0, parce que la v1.0 on la sent déjà pas.&#34;},{&#34;title&#34;:&#34;v3.0&#34;,&#34;description&#34;:&#34;Il faudra faire la v3.0 parce que une fois la v2.0 finie, on voudra encore changer ce qui va pas !&#34;}]">
            <h1>Dans le futur</h1>
            <div class="todo-list--content">
                <ul v-if="todos.length">
                    <li v-for="(todo, index) in todos">
                        <strong>{{ todo.title }}</strong> : {{ todo.description }} <span v-on:click="removeTodo(index)">[X]</span>
                    </li>
                </ul>
            </div>
            <div class="todo-list--form">
                Nouveau:
                <input v-model="newTitle" placeholder="Titre">
                <input v-model="newDescription" placeholder="Description">
                <button v-on:click="addTodo">Ajouter</button>
            </div>
        </div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.min.js"></script>
        <script src="javascripts/todo-list.js"></script>
    </body>
</html>

C'est là que les bactéries attaquent ! Le code source de notre page est (presque) équivalent à ce que nous avons pu voir côté serveur. Car le code ici écrit est fait pour être rendu par le moteur de Vue côté client, et non par le moteur de NodeAtlas côté serveur. Cela ne ressemble donc à rien pour les indexeurs de contenus. Il y a bien l'attribut HTML « data-model » dont nous nous servons pour alimenter Vue qui est dans la source, mais rien d'exploitable…

Une solution coûteuse en temps est donc de délivrer le contenu statique sur une page pour les indexeurs (ex :a-faire), et de permettre aux utilisateurs d'ajouter des éléments depuis une autre page (ex :gerer-a-faire). Une solution coûteuse en temps donc puisque l'implémentation de la page est à écrire deux fois.

Vous pouvez retrouver l'intégralité des fichiers de cet exemple dans le dépôt VueAtlas partie step/step2.

Vue + NodeAtlas - Avec SSR, les avantages des deux mondes !

Nous allons maintenant résoudre le problème en permettant aux fichiers Vue d'être exécutés côté serveur. Pour permettre cela à Vue, il va falloir dans un premier temps rendre accessible Vue côté serveur et utiliser en plus Vue Server Renderer en utilisant npm :

Installons cela avec les commandes suivantes dans la console de notre OS.

\> npm install vue

et

\> npm install vue-server-renderer

Nous allons également mettre nos données sources dans un dossier data car les variations de NodeAtlas ne sont normalement pas faites pour cela.

data/todo-list.json

[{
    "title": "v1.0",
    "description": "Il va falloir faire la v1.0 !"
}, {
    "title": "v2.0",
    "description": "Puis faudra faire la v2.0, parce que la v1.0 on la sent déjà pas."
}, {
    "title": "v3.0",
    "description": "Il faudra faire la v3.0 parce que une fois la v2.0 finie, on voudra encore changer ce qui va pas !"
}]

Ce qui nous donne, avec les fichiers rémanents des deux exemples précédents l'arborescence suivante :

├─ node_modules/
│  ├─ vue/
│  └─ vue-server-renderer/
├─ assets/
│  └─ javascripts/
│     └─ todo-list.js
├─ data/
│  └─ todo-list.json
├─ variations/
│  └─ data.json
├─ views/
│  ├─ update.htm
│  └─ show.htm
└─ webconfig.json

Pour que Vue puisse s'exécuter des deux côtés, nous allons utiliser le contrôleur all.js en plus du côté client avec la vue NodeAtlas all.htm. Il faut également que le vue-modèle de Vue soit disponible sur le serveur et le client. Nous allons le faire en abonnant les fichiers nécessaires à la liste des fichiers statiques de NodeAtlas et en ajoutant une nouvelle route /.

webconfig.json

{
    "statics": {
        "/view-model": "views/partials",
        "/data": "/data"
    },
    "routes": {
        "/": {
            "view": "all.htm",
            "controller": "all.js"
        },
        "/a-faire": {
            "view": "show.htm",
            "variation": "data.json"
        },
        "/gerer-a-faire": {
            "view": "update.htm",
            "variation": "data.json"
        }
    }
}

Maintenant, depuis le navigateur, nous aurons accès aux fichiers :

  • données : http://localhost/data/todo-list.json (accessible sur le serveur via data/todo-list.json),
  • modèle : http://localhost/view-model/todo-list.js (accessible sur le serveur via views/partials/todo-list.js) et
  • vue : http://localhost/view-model/todo-list.htm (accessible sur le serveur via views/partials/todo-list.htm).

Les contenus de views/partials/todo-list.js et views/partials/todo-list.htm sont les suivants :

views/partials/todo-list.htm

Avec v-if="client" permettant de piloter ce qui ne doit apparaître que lors du rendu client (et donc ne pas être dans la source de la réponse HTTP).

<div class="todo-list">
    <h1>Dans le futur</h1>
    <div class="todo-list--content">
        <ul v-if="todos.length">
            <li v-for="(todo, index) in todos">
                <strong>{{ todo.title }}</strong> : {{ todo.description }} <span v-if="client" v-on:click="removeTodo(index)">[X]</span>
            </li>
        </ul>
    </div>
    <div v-if="client" class="todo-list--form">
        Nouveau:
        <input v-model="newTitle" placeholder="Titre">
        <input v-model="newDescription" placeholder="Description">
        <button v-on:click="addTodo">Ajouter</button>
    </div>
</div>

views/partials/todo-list.js

Avec un code encapsulant la fonctionnalité pour lui permettre d'être exécutable côté navigateur et côté Node.js.

(function () {
    var setTodos = function (view, model, client) {
        return new Vue({
            template: view,
            data: {
                "todos": model,
                "newTitle": "",
                "newDescription": "",
                "client": client
            },
            methods: {
                addTodo: function () {
                    this.todos.push({
                        "title": this.newTitle,
                        "description": this.newDescription
                    });
                    this.newTitle = "";
                    this.newDescription = "";
                },
                removeTodo: function (index) {
                    this.todos.splice(index, 1);
                }
            }
        });
    };
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = setTodos;
    } else {
        this.setTodos = setTodos;
    }
}).call(this);

Cette vue-modèle est ensuite appelée côté client depuis / grâce à la vue NodeAtlas suivante :

views/all.htm

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <section class="todo-list"></section>
        <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.min.js"></script>
        <script src="view-model/todo-list.js"></script>
        <script>
        $.ajax({
          url: "data/todo-list.json"
        }).done(function (model) {
            $.ajax({
              url: "view-model/todo-list.htm"
            }).done(function (view) {
                var todos = setTodos(view, model, true);
                todos.$mount(".todo-list");
            });
        });
        </script>
    </body>
</html>

et le contrôleur NodeAtlas suivant pour utiliser Vue côté serveur :

controllers/all.js

exports.changeDom = function (next, locals) {
    var NA = this,
        Vue = require("vue"),
        ServerRenderer = require("vue-server-renderer"),
        renderer = ServerRenderer.createRenderer(),
        path = NA.modules.path,
        fs = NA.modules.fs,
        view = path.join(NA.serverPath, NA.webconfig.viewsRelativePath, "partials/todo-list.htm"),
        model = path.join(NA.serverPath, NA.webconfig.viewsRelativePath, "partials/todo-list.js"),
        data = path.join(NA.serverPath, "data/todo-list.json");

    global.Vue = Vue;

    fs.readFile(view, "utf-8", function (error, view) {
        fs.readFile(data, "utf-8", function (error, data) {
            renderer.renderToString(require(model)(view, JSON.parse(data)), function (error, html) {
                locals.dom = locals.dom.replace("<section class="todo-list"></section>", html.replace('server-rendered="true"', ""));
            });
        });
    });
};

ce qui nous donne l'arborescence complète suivante :

├─ node_modules/
│  ├─ vue/
│  └─ vue-server-renderer/
├─ assets/
│  └─ javascripts/
│     └─ todo-list.js
├─ controllers/
│  └─ all.js
├─ data/
│  └─ todo-list.json
├─ variations/
│  └─ data.json
├─ views/
│  ├─ partials/
│  │  ├─ todo-list.htm
│  │  └─ todo-list.js
│  ├─ all.htm
│  ├─ update.htm
│  └─ show.htm
└─ webconfig.json

En lançant notre instance serveur NodeAtlas avec la commande suivante :

\> node-atlas --browse

Notre navigateur par défaut s'ouvre à l'adresse http://localhost/ et le code source (celui mangé par les indexeurs), ressemblera à ceci :

http://localhost/

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <div class="todo-list">
            <h1>Dans le futur</h1>
            <div class="todo-list--content">
                <ul>
                    <li><strong>v1.0</strong> : Il va falloir faire la v1.0 ! <!----></li>
                    <li><strong>v2.0</strong> : Puis faudra faire la v2.0, parce que la v1.0 on la sent déjà pas. <!----></li>
                    <li><strong>v3.0</strong> : Il faudra faire la v3.0 parce que une fois la v2.0 finie, on voudra encore changer ce qui va pas ! <!----></li>
                </ul>
            </div> <!---->
        </div>
        <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.min.js"></script>
        <script src="view-model/todo-list.js"></script>
        <script>
        $.ajax({
          url: "data/todo-list.json"
        }).done(function (model) {
            $.ajax({
              url: "view-model/todo-list.htm"
            }).done(function (view) {
                var todos = setTodos(view, model, true);
                todos.$mount(".todo-list");
            });
        });
        </script>
    </body>
</html>

Bingo !

  • Vue est fonctionnel côté serveur :
    • votre document est valide W3C,
    • votre document est complètement SEO indexable (avec les formulaires retirés de la source).
  • Vue est fonctionnel côté client (avec les formulaires inclus dans le DOM de ce côté).
  • Vous n'avez écrit qu'une seule fois le code pour le rendu client et serveur.

Vous pouvez retrouver l'intégralité des fichiers de cet exemple dans le dépôt VueAtlas partie step/step3.

Bonus - Enregistrement côté serveur et mise à jour temps réel

Cet article à atteint son objectif puisque nous avons une architecture qui démontre comment faire du Rendu côté serveur avec Vue et NodeAtlas. Afin de finir cet article correctement, nous allons :

  • épurer le code pour ne garder que l'exemple final,
  • enregistrer les modifications faites côté serveur pour que l'ouverture d'une page affiche toutes les entrées et
  • utiliser les Websockets de NodeAtlas pour mettre à jour la liste en temps réel dans toutes les pages déjà ouvertes !

Finalement notre arborescence va ressembler à cela :

├─ node_modules/
│  ├─ vue/
│  └─ vue-server-renderer/
├─ assets/
│  └─ javascripts/
│     └─ index.js
├─ controllers/
│  └─ index.js
├─ data/
│  └─ todo-list.json
├─ variations/
│  ├─ edit.json
│  └─ show.json
├─ views/
│  ├─ partials/
│  │  ├─ todo-list.htm
│  │  └─ todo-list.js
│  └─ index.htm
└─ webconfig.json

Pour information, nous avons procédé, par rapport aux trois exemples précédents, aux modifications suivantes :

  • controllers/all.js qui devient controllers/index.js,
  • views/all.htm qui devient views/index.htm,
  • views/show.htm et views/update.htm qui sont supprimés,
  • variations/data.json qui est supprimé,
  • variations/show.json et variations/edit.json qui sont ajoutés,
  • assets/javascripts/todo-list.js qui est supprimé.
  • assets/javascripts/index.js qui est ajouté.

Voici le contenu de chaque fichier restant après modification.

webconfig.json

Une page en lecture seule http://localhost/ et une page de modification http://localhost/editer.

{
    "statics": {
        "/view-model": "views/partials",
        "/data": "/data"
    },
    "routes": {
        "/": {
            "view": "index.htm",
            "variation": "show.json",
            "controller": "index.js"
        },
        "/editer": {
            "view": "index.htm",
            "variation": "edit.json",
            "controller": "index.js"
        }
    }
}

Avec la vue NodeAtlas unique suivante :

views/index.htm

Vous remarquerez que nous avons ajouté les fichiers socket.io/socket.io.js et node-atlas/socket.io.js fournis par NodeAtlas pour faire fonctionner les échanges Websockets temps réel plus loin.

<!DOCTYPE html>
<html lang="fr">
    <head>
        <meta charset="utf-8">
        <title>SSR</title>
    </head>
    <body>
        <section class="todo-list"></section>
        <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.3/vue.min.js"></script>
        <script src="socket.io/socket.io.js"></script>
        <script src="node-atlas/socket.io.js"></script>
        <script src="view-model/todo-list.js"></script>
        <script src="javascripts/index.js"></script>
        <script>todoList(<?- specific.editable ?>);</script>
    </body>
</html>

Qui utilise les vue-modèle Vue suivantes :

views/partials/todo-list.js

(function () {
    var setTodos = function (view, model, editable, callback) {
        return new Vue({
            template: view,
            data: {
                "todos": model,
                "newTitle": "",
                "newDescription": "",
                "editable": editable
            },
            methods: {
                addTodo: function () {
                    this.todos.push({
                        "title": this.newTitle,
                        "description": this.newDescription
                    });
                    this.newTitle = "";
                    this.newDescription = "";
                    if (callback) {
                        callback(this.todos);
                    }
                },
                removeTodo: function (index) {
                    this.todos.splice(index, 1);
                    if (callback) {
                        callback(this.todos);
                    }
                }
            }
        });
    };
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = setTodos;
    } else {
        this.setTodos = setTodos;
    }
}).call(this);

views/partials/todo-list.htm

<div class="todo-list">
    <h1>Todo list</h1>
    <div class="todo-list--content">
        <ul v-if="todos.length">
            <li v-for="(todo, index) in todos">
                <strong>{{ todo.title }}</strong> : {{ todo.description }} <span v-if="editable" v-on:click="removeTodo(index)">[X]</span>
            </li>
        </ul>
    </div>
    <div v-if="editable" class="todo-list--form">
        Nouveau:
        <input v-model="newTitle" placeholder="Titre">
        <input v-model="newDescription" placeholder="Description">
        <button v-on:click="addTodo">Ajouter</button>
    </div>
</div>

Qui ne varie que de la variable specific.editable qui sert à indiquer si la page est en lecture seule ou éditable :

variations/show.json

{
    "editable": "false"
}

variations/edit.json

{
    "editable": "true"
}

Avec toujours le même jeu de données pour data/todo-list.json.

Attaquons-nous à présent aux deux fichiers qui vont permettre :

  • les échanges client-serveur temps réel côté client et serveur respectivement grâce au fichier node-atlas/socket.io.js et au point d'ancrage setSockets() ainsi que,
  • l'enregistrement côté serveur grâce à writeFile.

controllers/index.js

exports.changeDom = function (next, locals) {
    var NA = this,
        Vue = require("vue"),
        renderers = require("vue-server-renderer"),
        renderer = renderers.createRenderer(),
        path = NA.modules.path,
        fs = NA.modules.fs,
        view = path.join(NA.serverPath, NA.webconfig.viewsRelativePath, "partials/todo-list.htm"),
        model = path.join(NA.serverPath, NA.webconfig.viewsRelativePath, "partials/todo-list.js"),
        data = path.join(NA.serverPath, "data/todo-list.json");

    global.Vue = Vue;

    fs.readFile(view, "utf-8", function (error, view) {
        fs.readFile(data, "utf-8", function (error, data) {
            renderer.renderToString(require(model)(view, JSON.parse(data)), function (error, html) {
                locals.dom = locals.dom.replace('<section class="todo-list"></section>', html.replace('server-rendered="true"', ""));
                next();
            });
        });
    });
};

exports.setSockets = function () {
    var NA = this,
        fs = NA.modules.fs,
        io = NA.io;

    io.sockets.on("connection", function (socket) {
        socket.on("update-todo", function (todos) {
            fs.writeFile("data/todo-list.json", JSON.stringify(todos), function () {
                socket.broadcast.emit("update-todo", todos);
            });
        });
    });
};

assets/javascripts/index.js

var todoList = function (editable) {
    $.ajax({
      url: "data/todo-list.json"
    }).done(function (model) {
        $.ajax({
          url: "view-model/todo-list.htm"
        }).done(function (view) {
            var todos = setTodos(view, model, editable, function (value) {
                NA.socket.emit("update-todo", value);
            });
            todos.$mount(".todo-list");
            NA.socket.on("update-todo", function (value) {
                todos.todos = value;
            });
        });
    });
};

Et voilà un exemple simple et fonctionnel !

En lançant la commande suivante :

node-atlas --browse

Vous ouvrirez le site sur la page en lecture seule http://localhost/.

Ouvrez donc plusieurs onglets aux pages http://localhost/ et http://localhost/editer et même dans plusieurs navigateurs ! Ensuite, modifiez la liste via une de vos pages http://localhost/editer et magie, tout est actualisé partout ! En ouvrant une nouvelle page http://localhost/ ou http://localhost/editer à partir d'ici, vous verrez les nouvelles entrées.

Vous pouvez retrouver l'intégralité des fichiers de cet article dans le dépôt VueAtlas sur GitHub.

Bonus 2 - Pour aller plus loin

Quelques nouveautés ont fait leurs apparition côté Vue depuis la première publication de cet article, aussi je trouve que cela peut faire partie de quelques travaux supplémentaires que vous pouvez essayer d'appliquer vous-même !

vue-ssr-outlet

Nous gérons nous même le remplacement dans le DOM de <section class="todo-list"></section> avec la ligne locals.dom = locals.dom.replace("<section class="todo-list"></section>", html.replace('server-rendered="true"', ""));. Cependant Vue Server Renderer est capable de s'en charger lui même avec le commentaire HTML <!-- vue-ssr-outlet -->. Plus d'information sur la documentation officielle de Vue Server Renderer. À vous de jouer !

Bonnes pratiques Vue

Un guide de bonne pratique est sortie listant toutes les bonnes pratiques dont il faut tenir compte pour conserver un code maintenable et propre. Vous trouverrez ce guide ici. Pourquoi ne pas appliquer toutes ces bonnes pratiques sur ce tutoriel !

Lire dans une autre langue