MVC Design Pattern

AngularJS repose en grande partie sur le patron de conception (en anglais, Design Pattern) Modèle-Vue-Contrôleur. Ce design pattern — parce que ça sonne quand même mieux en anglais :) — se base, comme son nom l'indique, sur la subdivision conceptuelle de chaque page en un Modèle, une Vue et un Contrôleur.

J'ai précisé en partie parce que l'on verra dans les parties suivantes du tutoriel qu'AngularJS ajoute au Contrôleur d'autres outils comme les services et les directives. Mais le plus important de tous est bien le Contrôleur puisque c'est celui qui contient la logique de la page.

Si vous avez déjà développé des applications en PHP, avec ou sans framework (tel que Symfony2 ou Zend Framework 2), vous avez sûrement dû rencontré le modèle MVC. Même si le design pattern a le même nom, des différences notables sont à remarquer entre la version PHP, qui est plus classique, et celle proposée par AngularJS, surtout au niveau du modèle ! Lisez donc bien ce paragraphe en entier, vous y gagnerez en clarté.

Pour illustrer les explications quelque peu abstraite, je vais utiliser l'exemple basique de la Todo List (= liste de choses à faire). Pour l'instant, je ne donne les sources que pour éclairer mes propos, ne cherchez pas à tout comprendre dès maintenant. Les chapitres suivants sont là pour çà ;) .
Vous pouvez d'ores-et-déjà voir le rendu final grâce à cette Live Demo.

La Vue

C'est ce que voit l'utilisateur final. Elle est générée à partir du template. Le template est le fichier HTML enrichi de certains attributs et balises propres à AngularJS que l'on découvrira au fur et à mesure.

Mème si par abus de langage La Vue désigne parfois le fichier HTML, celui-ci n'est que le template. La vue n'est pas un fichier mais bien la projection de ce que voit le visiteur sur son navigateur. Et elle diffère du fichier HTML puisque des éléments apparaissent ou disparaissent grâce au Javascript, alors que le template lui ne change pas pendant toute la durée de vie de la page.

C'est la partie la plus facile à lire et à écrire. C'est d'ailleurs là que toute la puissance d'AngularJS saute aux yeux. Vous verrez que ce fichier sera incroyablement lisible même après de nombreux ajouts de fonctionnalités, contrairement par exemple aux fichiers de front-end n'utilisant que PHP.

Ci-dessous le template (à peine simplifié) de la mini-application Todo List :

<!DOCTYPE html>
<html lang="fr" ng-app="demoApp">
    <head>
        <title>Todo List</title>        
        <link rel="stylesheet" href="web/css/style.css">
        <script src="js/vendor/bower_components/angular/angular.js"></script>
        <script src="js/todoList.js"></script>
    </head>
    <body>
        <header>
            <h1>Todo List</h1>
        </header>
        <section id="todo-list" ng-controller="TodoCtrl">
            <form id="todo-form" ng-submit="addTodo()">
                <input id="new-todo" placeholder="Que devez-vous faire ?" ng-model="newTodo" />
            </form>
            <article ng-show="todos.length" ng-cloak>
                <ul id="todo-list">
                    <li ng-repeat="todo in todos" ng-class="{completed: todo.completed}">
                        <div class="view">
                            <input class="mark" type="checkbox" ng-model="todo.completed" />
                            <span>{{todo.title}}</span>
                            <i class="fa fa-times" ng-click="removeTodo(todo)"></i>
                        </div>
                    </li>
                </ul>
                <input id="mark-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)" />
                <label class="btn btn-info" for="mark-all">Tout cocher</label>
                <button class="btn btn-danger" ng-click="clearCompletedTodos()">Supprimer les tâches cochées</button>
            </article>
        </section>
    </body>
</html>

Moteur de template

AngularJS, intègre nativement un moteur de template (EN). Ceci permet d'utiliser des raccourcis syntaxiques pour afficher des variables ou même des entités logiques telles que les boucles et bien d'autres choses encore.

Il existe beaucoup de moteurs de template pour le web, en particulier pour PHP. Twig (celui intégré à Symfony2), par exemple, a une syntaxe très proche d'AngularJS — notamment avec les doubles accolades —, donc si vous l'avez déjà utilisé, vous ne serez pas dépaysé.

Sans chercher à tout apprendre dès maintenant sur la syntaxe d'AngularJS, on peut remarquer quelques points à partir de l'exemple ci-dessus.

Le préfixe ng-
Marque de fabrique d'AngularJS, vous remarquez qu'on le trouve à toutes les balises où une action définie par le Javascript sera exécutée. Ce sont des directives AngularJS. On les découvrira peu-à-peu, mais si vous êtes curieux, vous avez la liste complète de celles qui existent et leur mode d'emploi dans la documentation (EN).
La directive
ng-app
À la ligne 2, cette directive initialise l'application demoApp : AngularJS sait quel module il va devoir lire pour la page courante. Il génère par la même occasion le $rootScope de la page. Plus d'informations sur le scope au paragraphe Le Modèle
La directive
ng-controller
À la ligne 13, cette directive permet de dire à AngularJS que le contrôleur de cette section est TodoCtrl. Les variables qui seront à l'intérieur de cette balise seront accesibles et modifiables depuis TodoCtrl.
Les
doubles accolades
On remarque qu'à la ligne 22, on affiche le titre du todo avec une double accolade : {{todo.title}}. Grâce au moteur de template d'AngularJS, cette séquence sera automatiquement — c'est-à-dire sans que le développeur n'ait à paramétrer quoi que ce soit ! — repérée et remplacée par la valeur de la variable todo.title. C'est magique !
La boucle
ng-repeat
À la ligne 19, on remarque la directive ng-repeat. Elle permet de dire à AngularJS qu'il doit répéter l'élément en question (et tous ses éléments enfants) tant qu'il y a des entrées dans le tableau todos... mais pas seulement ! À l'image d'une boucle foreach ($tableau as $entree) de PHP, on dispose de la variable temporaire todo à l'intérieur de la boucle, avec toutes ses propriétés. Ce qui est très pratique pour afficher son titre par exemple.

Bref, vous l'aurez compris, ce moteur de template va grandement vous faire gagner en clarté et rapidité. Pour ceux qui ne sont pas convaincus (même si je doute qu'il y en ait), voici la version PHP du template de la Todo List (bien entendu les actions Javascript ne sont pas implémentées, il faudrait un script réagissant en fonction des identifiants et classes) :

<!DOCTYPE html>
<html lang="fr">
    <head>
        <title>Todo List</title>        
        <link rel="stylesheet" href="web/css/style.css">
        <script src="js/todoList.js"></script>
    </head>
    <body>
        <header>
            <h1>Todo List</h1>
        </header>
        <section id="todo-list">
            <form id="todo-form">
                <input id="new-todo" placeholder="Que devez-vous faire ?" />
            </form>
            <?php if(sizeof($todos) > 0) ?>
            <article>
                <ul id="todo-list">
                <?php foreach ($todos as $todo) ?>
                    <li <?php if($todo["completed"]) echo 'class="completed"'; ?> >
                        <div class="view">
                            <input class="mark" type="checkbox" <?php if($todo["completed"]) echo 'checked'; ?> />
                            <span><?php echo $todo["title"]; ?></span>
                            <i class="fa fa-times"></i>
                        </div>
                    </li>
                </ul>
                <input id="mark-all" type="checkbox" <?php if($allChecked) echo 'checked'; ?> />
                <label class="btn btn-info" for="mark-all">Tout cocher</label>
                <button id="clearCompletedTodos" class="btn btn-danger">Supprimer les tâches cochées</button>
            </article>
        </section>
    </body>
</html>


Le Contrôleur

C'est le cœur du code correspondant à la page. Comme je le dis plus haut, on met dans le contrôleur toute sa logique, c'est-à-dire la description des actions que la page doit être capable de réaliser... et rien d'autre ! :) On verra que tout ce qui ne correspond pas à ces actions aura sa place dans les services ou directives. En pratique, ce n'est qu'une grande fonction un peu particulière. Puisqu'un exemple est plus parlant que de longs discours, voici le contrôleur de la Todo List :

// js/todoList.js
'use strict';


/**
 * Déclaration de l'application demoApp
 */
var demoApp = angular.module('demoApp', [
    // Dépendances du "module"
    'todoList'
]);

/**
 * Déclaration du module todoList
 */
var todoList = angular.module('todoList',[]);


/**
 * Contrôleur de l'application "Todo List" décrite dans le chapitre "La logique d'AngularJS".
 */
todoList.controller('todoCtrl', ['$scope',
    function ($scope) {

        // Pour manipuler plus simplement les todos au sein du contrôleur
        // On initialise les todos avec un tableau vide : []
        var todos = $scope.todos = [];

        // Ajouter un todo
        $scope.addTodo = function () {
            // .trim() permet de supprimer les espaces inutiles
            // en début et fin d'une chaîne de caractères
            var newTodo = $scope.newTodo.trim();
            if (!newTodo.length) {
                // éviter les todos vides
                return;
            }
            todos.push({
                // on ajoute le todo au tableau des todos
                title: newTodo,
                completed: false
            });
            // Réinitialisation de la variable newTodo
            $scope.newTodo = '';
        };

        // Enlever un todo
        $scope.removeTodo = function (todo) {
            todos.splice(todos.indexOf(todo), 1);
        };

        // Cocher / Décocher tous les todos
        $scope.markAll = function (completed) {
            todos.forEach(function (todo) {
                todo.completed = !completed;
            });
        };

        // Enlever tous les todos cochés
        $scope.clearCompletedTodos = function () {
            $scope.todos = todos = todos.filter(function (todo) {
                return !todo.completed;
            });
        };
    }
]);

Comme pour le template, on peut remarquer un certain nombre de choses propres à AngularJS sans pour autant s'acharner à tout comprendre tout de suite :

La déclaration
du contrôleur
À la ligne 22, on déclare le contrôleur avec la fonction controller(). On verra plus tard ce que représentent les variables demoApp et todoList, mais ce n'est pas très important pour l'instant.
On donne donc à ce contrôleur :
  1. Un nom (ici TodoCtrl) qui permettra de l'appeler quand on en aura besoin.
  2. La liste de ses dépendances. Celles-ci peuvent être des services ou des modules.
    AngularJS met à notre disposition de nombreux outils qui ont chacun un rôle précis, les services. Chacun commence par un $. Vous pouvez voir la liste complète des services natifs dans la documentation (EN). Un chapitre de la partie 2 est dédié aux services. Ici, nous n'avons besoin que du service $scope. Plus d'informations sur le scope au paragraphe Le Modèle.
    Les modules peuvent être développés par n'importe qui et pour faire n'importe quoi :) . C'est ce qui permet la modularité d'Angularjs.
  3. La fonction en elle-même de la ligne 22 à la ligne 65.
Pas de manipulation
du DOM
Si l'on est un habitué à jQuery, ou aux librairies qui imposent de toujours sélectionner un élément (par sa classe, son identifiant, etc...) pour ensuite effectuer des changements tant sur le fond (les nœuds textuels) que sur la forme (les balises HTML et leur style), on sera surpris de remarquer qu'ici l'application ajoute, supprime et modifie la page HTML sans aucun appel au DOM. Vous n'avez plus à vous embêter à calculer le parent, les différents enfants de tel ou tel nœud (parfois il y a des incompréhensions si l'on a fait un retour à la ligne dans le fichier HTML entre 2 balises par exemple...), AngularJS fait tout pour vous et sait directement de quel élément vous parlez !
Finie l'utilisation des sélecteurs $(".className").each(...);. Vous allez enfin réellement coder du Javascript avec des variables dignes de ce nom ! ;)

Dependency Injection

On a vu dans la déclaration du contrôleur qu'on signalait ses dépendances. Ce système très puissant d'ajout de fonctionnalités à un contrôleur (ou d'autres composants comme on le verra par la suite) via des services ou modules est rendu possible grâce à la Dependency Injection (EN).

Un système d'Injection de Dépendances a pour rôle de créer un composant, de résoudre ses dépendances et de l'injecter lorsqu'il est nécessaire à un autre composant. Ainsi, chaque application, ou page de l'application dit au DI ce dont elle a besoin et l'on n'est pas obligé d'intégrer manuellement tous les morceaux de code que l'on va utiliser. Par exemple, pour la Todo List, on a juste à dire que le contrôleur utilise le service $scope et celui-ci sera utilisable à l'intérieur de TodoCtrl, avec toutes ses variables et fonctions disponibles. Mais en plus, le DI nous assure qu'il n'y aura pas de conflits entre les différentes parties du code, même si elles utilisent les mêmes services... encore une fois, si c'est pas beau çà ! :)

Selon le logiciel auquel il est intégré, le DI impose une certaine syntaxe pour qu'il puisse savoir ce qu'est un composant, la façon de l'identifier, etc...
Voir la documentation sur la DI d'AngularJS (EN)

Nous verrons, lors du chapitre dédié aux contrôleurs, les différentes possibilités pour déclarer correctement un contrôleur. Mais sachez dès maintenant que celle que j'ai utilisée ici est la plus fiable et la plus complète.


Le Modèle

C'est la partie la plus abstraite du design pattern, mais elle n'est pas plus dure à comprendre que les autres.

Le modèle selon AngularJS, c'est l'ensemble des scopes de la page. Un scope, c'est un domaine dans lequel des variables (ou fonctions, on ne fera plus la différence maintenant) peuvent exister. C'est en quelque sorte une grande famille au sein de laquelle les variables peuvent intéragir sans risquer de modifier les autres familles de variables. Ce principe nous permet d'éviter les collusions et effets de bord. Bien sûr, ces familles ne sont pas complètement indépendantes les unes envers les autres : elles sont liées entre elles par des liens de parenté.

On peut se représenter ces familles de scopes sous la forme d'un arbre, comme un arbre généalogique par exemple. La racine de tous les scopes est le $rootScope qui est instancié lorsqu'AngularJS voit la directive ng-app (à la balise <html> pour notre template). Vient ensuite le scope du contrôleur instancié par la directive ng-controller, logique :) . Enfin, certaines directives créent leur propre scope, comme ici ng-repeat. Une petite illustration de cette arborescence :

Arbre des scopes de l'application Todo List
Arbre des scopes de l'application Todo List

D'autre part, généralement, vous n'avez pas à déclarer ces scopes. L'initialisation de chaque scope de notre exemple a été faite par AngularJS lui-même, et il en sera ainsi dans la très grande majorité des cas d'utilisation. Voir la documentation d'AngularJS sur les scopes

Le service $scope

Étant donné que le contrôleur a pour but de manipuler les variables du modèle, il doit y avoir accès. Pour ce faire, il passe par le service $scope. Celui-ci permet la constante et transparente synchronisation des variables entre le modèle et la vue. Ainsi, toute modification d'une valeur de variable de la vue est immédiatement répercutée dans le modèle et inversement. C'est ce service qui est donc au centre du Two-Way data Binding.

À partir de la version 1.2, AngularJS propose une nouvelle syntaxe utilisant ControllerAs. Ceci permet d'éviter d'insérer obligatoirement le service $scope à tous les contrôleurs en passant par this. Cela allège la syntaxe du contrôleur mais il est important de savoir que this et $scope ne sont pas strictement équivalents.
Présentation vidéo de la syntaxe avec ControllerAs (EN)
Différences entre this et $scope dans les contrôleurs AngularJS (EN)

Par habitude, je continue de déclarer le service $scope dans les dépendances de tous mes contrôleurs, mais libre à vous de faire comme moi ou pas ;)

Two-Way Data Binding

Si vous avez déjà un peu lu autour d'AngularJS, mais aussi KnockoutJS ou EmberJS, vous n'avez sûrement pas échappé à ce terme : le Two-Way Data Binding. En français, on pourrait le traduire par Lien bilatéral des données. Autrement dit, les données de deux domaines distincts sont liées entre elles et dans les deux sens.

Les deux domaines sont ici le modèle et la vue : lors de la génération de la vue, au chargement de la page, AngularJS crée le modèle comme on l'a vu plus haut, et initialise les variables des scopes selon ce qui est déclaré dans le contrôleur et le template. D'ailleurs, en cas de conflit entre les deux, le contrôleur prendra toujours le dessus sur le template. Puis à partir de ce modèle, il génère enfin la vue.

Même si ce procédé peut sembler lourd et trop lent, il est en réalité très performant. Les navigateurs modernes permettent au visiteur de ne pas tellement sentir de ralentissement lors de l'affichage de la page et une fois cette étape passée, vous êtes absolument certain de manipuler les bonnes variables dans votre contrôleur. ;)

Ce concept est au centre du framework, et pour cause, il simplifie grandement la tâche du développeur. En effet, il assure via de nombreux services (dont notre cher $scope), que les variables de la vue et du modèle soient constamment synchronisées.
Documentation officielle : Le Two-Way Data Binding (EN)

Enfin, c'est le Two-Way Data Binding qui permet, dans beaucoup de situations, d'éviter la manipulation du DOM, comme on l'a remarqué au paragraphe sur le contrôleur.

Finalement, voici le schéma que propose la documentation d'AngularJS pour illustrer toutes les notions centrales que nous avons abordées dans ce chapitre. Il est adapé à l'application PhoneCat utilisée en exemple dans leur tutoriel mais correspond tout à fait à notre Todo List :

Schéma récapitulatif de la documentation
Schéma récapitulatif de docs.angularjs.org (adapté à leur exemple)
Chapitre 2 du tutoriel officiel (EN)

AngularJS et le back-end

Après avoir lu ce chapitre, une question vient inévitablement à la tête de ceux qui ont déjà développé des sites web en suivant un design pattern MVC en PHP :

Le design pattern MVC d'AngularJS et celui de PHP sont-ils compatibles pour un même site web ? Autrement dit, est-il possible / utile / pertinent d'avoir un back-end en PHP suivant le design pattern MVC et un front-end sous AngularJS ?

Cette interrogation n'est pas anodine et permet de soulever ici un point très important et parfois assez mal compris tout simplement parce que personne ne prend le temps de l'expliquer clairement. Comme on le voyait dans le premier chapitre du tutoriel, AngularJS déporte toute la logique de présentation côté client. Mais, ce faisant, le système de routage (2.1 Le routage) et la logique de chaque page sont eux aussi envoyés au client. Ainsi, le back-end n'a plus en charge QUE les accès à la base de données, qui sont évidemment bien conservés côté serveur.

Ainsi, la logique de l'application, pour reprendre les termes du premier chapitre, est réduite à la gestion des entités enregistrées en BDD, à la vérification de leur cohérence les unes vis-à-vis des autres, etc... Même si ce n'est pas rien, vous vous doutez bien maintenant qu'il est inutile de coder toute un back-end en MVC dans ces conditions.

Mais ne vous en faites pas, toutes vos connaissances en PHP et back-end ne sont pas perdues pour autant ! ;) Simplement, il va falloir repenser le back-end : comme il ne fera que des manipulations de données avec la BDD et qu'il n'enverra strictement aucune vue mais uniquement des données au front-end quand celui-ci les demandera via des requêtes AJAX, on va développer une API : Application Programming Interface !

API en back-end

Une API est un logiciel qui offre des services à d'autres logiciels. Dans le cas du web, une API est donc une espèce de boîte noire à laquelle on envoie des requêtes (HTTP) et qui nous retourne les données demandées. Ici, nous utiliserons pour ces échanges le format JSON. Ce format est nativement géré par AngularJS et PHP.

Donc cette API devra pouvoir effectuer, à la demande du front-end, les 4 actions suivantes sur chaque entité de sa BDD :

  1. Créer une entrée — Create
  2. Récupérer une entrée — Read
  3. Modifier une entrée — Update
  4. Supprimer une entrée — Delete

    ... et c'est tout !

Notre API va donc simplement implémenter un système de CRUD pour chaque objet. On pourrait donc coder notre API from scratch en ne considérant que cette nécessité. Mais pour des raisons de sécurité, de stabilité et d'uniformité, il existe un protocole tout trouvé : REST : Representational State Transfer. On dit que notre API sera RESTful.

Les API RESTful ne sont pas le propos de ce tutoriel mais elles y sont étroitement liées. Donc je vous conseille de vous familiariser avec cette notion dès maintenant si ce n'est pas déjà fait.
L'architecture REST expliquée en 5 règles

Cette réponse à notre première question du paragraphe en entraîne une autre :

Mais je vais donc devoir développer 2 applications au lieu d'une seule ?!

Je ne vais pas vous le cacher, effectivement, développer correctement une application sous AngularJS implique d'avoir 2 applications distinctes : une pour le front-end et l'autre pour le back-end. MAIS ce n'est absolument pas un problème ! En effet, vous verrez que ce n'est pas si dur et surtout, vous gagnerez sur tous les tableaux :

  1. Votre application sera beaucoup plus réactive et rapide.
  2. Vous pourrez faire en sorte que le visiteur ait une véritable expérience d'utilisation d'une application : sans réellement recharger la page, il pourra naviguer d'une page à l'autre. Et comme par exemple SoundCloud, le visiteur pourra lire un fichier audio tout en changeant de page.
  3. Si vous décidez de développer une ou plusieurs application(s) mobile(s) / tablette(s) pour votre site, vous n'aurez qu'à faire son interface côté client. Les requêtes envoyées au serveur seront les mêmes que pour votre front-end classique et votre API y répondra parfaitement.

Enfin, sachez qu'il existe de nombreux frameworks (en PHP mais aussi utilisant Node.js) qui permettent de développer une API RESTful en un minimum d'efforts (vous serez étonné par la simplicité de leur utilisation). En effet, AngularJS étant entièrement et uniquement tourné vers le front-end, vous êtes complètement libre de choisir la solution qui vous convient pour votre back-end. Certains diront que PHP est aujourd'hui dépassé et qu'un serveur sous Node.js est beaucoup plus performant et facile à gérer. Tout ceci est vrai, mais ce n'est pas forcément évident de se mettre d'emblée à utiliser toutes ses nouvelles technologies.

À titre indicatif, pour une API en PHP, j'apprécie particulièrement Slim. Il est très léger, performant, facile à prendre en main et respecte rigoureusement toutes les règles imposées par le protocole REST, contrairement à d'autres frameworks qui se disent RESTful.
Pour une API back-end en Javascript tournant avec Node.js, il existe quelques solutions très performantes.
Building a RESTful API Using Node and Express 4 (EN)

Vos commentaires

comments powered by Disqus