PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Service, factory et provider dans AngularJS

mardi 29 avril 2014 à 11:07

AngularJS est un framework difficile à prendre en main. Pas parce qu’il est particulièrement compliqué, mais parce que ses concepts sont vraiment différents de ceux qu’on a l’habitude de rencontrer dans les frameworks habituels. Le pire, c’est quand on vient de jQuery, car Angular est un peu l’anti-jQuery et il faut littéralement désapprendre ses habitudes.

Généralement, les gens s’en sortent avec les contrôleurs. Ils ne mettent pas le bon code dedans, ils ne savent pas comment rendre les bouts de code indépendants et réutilisables, mais ils arrivent à en faire quelque chose. Les directives, ils n’y touchent pas, mais ils peuvent s’en passer pendant un certain temps et juste réutiliser du code trouvé sur Github.

Par contre le côté service/factory/provider, ça c’est un gros problème. On ne peut pas faire sans, mais peu de gens savent faire avec. En codant All that counts, j’ai réalisé que c’était un bon tuto à faire. Donc en avant.

Article long = musique, évidement.

On m’a très justement demandé les prérequis pour suivre cet article : il faut avoir fait des tutos de base sur Angular et notamment comprendre l’injection de dépendance et le data binding.

Que font ces trucs là ?

Techniquement, un service, une factory ou un provider, dans AngularJS, ça sert à la même chose. Un service est juste une manière plus facile d’écrire une factory, qui est juste une manière plus simple d’écrire un provider.

Les 3 servent à créer un objet Javascript ordinaire, c’est tout.

Oui, oui, c’est vraiment tout. C’est le seul et unique but de cela. Vous allez me dire, mais alors pourquoi se faire chier à les utiliser alors qu’on peut écrire un objet à la main ? Tout simplement parce qu’ils permettent d’encapsuler cette création, l’isoler du reste du code, un problème toujours difficile en Javascript.

Ils sont là pour éviter de pourrir le namespace global, tout en n’utilisant pas la technique bien connu de “je met un gros conteneur avec tout dedans et tout le monde y accède”, qu’on a tendance à voir dans la plupart des projets JS. Le code est ainsi plus maintenable et testable.

Chaque service/factory/provider permet d’avoir un groupe de fonctionnalités séparé du reste. On peut utiliser celui-ci facilement partout ailleurs en utilisant l’injection de dépendance. Si vous ne savez pas ce que c’est, arrêtez-vous tout de suite, c’est le principe de fonctionnement de base d’Angular, il faut quitter cet article et prendre un tuto pour débutant. Revenez ensuite.

Dans un service/factory/provider, on va donc mettre du code métier et/ou des données de l’app, mais groupés par thème.

Par exemple, sur une app video, on peut en faire un qui va contenir le code pour gérer le profile utilisateur et un pour gérer la playlist.

On aura donc deux services/factories/providers, qui vont créer au final deux objets. Chaque objet va contenir des méthodes et des données. Pour le profile, on peut imaginer qu’il va contenir le nom d’utilisateur, son ID, et une méthode de login et de logout :

{
    "name": "Hodor",
    "Id": 7898,
    "login": function(){
        // faire un truc pour se loger
    },
    "logout": function(){
        // faire un truc pour se deco
    }
}

Pour la playlist, on peut imaginer que ça va contenir la liste des videos, la video en cours de lecture et de quoi passer à la suivante et précédente :

{
    "name" : "Super playlist"
    "videos": [
        {"title": "video 1", "url": "http://..."},
        {"title": "video 2", "url": "http://..."}
    ],
    "currentVideo": 1,
    "next": function(){
        // passer à la vid suivante
    },
    "previous": function(){
        // passer à la vid précédente
    }
 
}

Et c’est tout. Ce n’est ni plus ni moins que ça, des objets normaux dans lesquels ont va mettre nos données et nos méthodes. Ce qui est “special”, c’est comment ils sont créés et utilisés. Et c’est ça qu’on va voir tout de suite.

Créer un objet via un service/factory/provider

Voyons le plus compliqué d’abord : le provider. Les deux autres ne sont que des raccourcis pour écrire un provider, de toute façon.

Le rôle du provider, c’est de créer et mettre à disposition un objet. C’est tout. Il n’y a rien de plus. Je répète, ça ne sert qu’à ça. “provider”, ça veut dire “fournisseur” en anglais. Et c’est ce que ça fait : ça fournit un objet. Supposons que vous avez déjà créé une app “monApp”. Écrire un provider pour créer l’objet “profile” ressemble à ça :

// On appelle notre objet qui sera créé "profile". Ce qui signifie que l'on
// pourra ensuite utiliser le nom "profile" pour annoncer quand
// on souhaite utiliser l'objet issu de ce provider.
monApp.provider("profile", function(){
 
    // Un provider, au final, c'est juste une fonction anonyme qu'on relie
    // à un nom :)
 
    // Cette fonction DOIT avoir une méthode nommé "$get" attachée à son "this"
    this.$get = function() {
 
        // Et la méthode "$get" DOIT retourner l'objet qu'on veut créer.
        return {
            "name": "Anonymous",
            "Id": null,
            "login": function(){
                // faire un truc pour se loger
            },
            "logout": function(){
                // faire un truc pour se deco
            }
        }
    }
});

C’est tout. Je vous jure que c’est tout. C’est juste une boîte qu’Angular va prendre, et appeler la méthode $get pour obtenir un objet. En plus, un seul objet, car la méthode ne sera appelée qu’une fois : l’objet sera un singleton.

Plus tard dans le code, quand vous ferez un controller :

mymonAppApp.controller('UnControlleurCommeUnAutre', function($scope, profile) {
    $scope.profile = profile;
});

Vous utiliserez profile en paramètre, il va donc être injecté automatiquement. C’est tout le principe d’angular, des tas de boîtes qui déclarent quelles autres boîtes elles utilisent via l’injection de dépendance.

Ici, l’objet va être créé (si ce n’est pas déjà fait ailleurs), et c’est tout. Le boulot d’un controller, c’est essentiellement de mettre des objets issus des providers dans le scope pour que le HTML puisse les utiliser. Je vous promet, c’est 90% des use cases.

Votre objet sera le conteneur que l’on va manipuler pour changer le nom du profile, se logger, etc. C’est juste une boîte qui rend un service. D’ailleurs on appelle souvent les objets issus des providers des “services”.

Utiliser des providers évite de créer tout une logique de classes, de gérer des namespaces, des instanciations : on est obligé de faire de l’encapsulation et de la composition. Ca rend le code (javascript) plus propre et plus testable, bien décomposé et découplé. Avoir un code propre est le but principal des providers, ils ne fournissent rien comme fonctionnalités qu’on ne puisse faire autrement.

Maintenant voyons ce qu’est une factory. En fait, c’est juste un raccourci pour écrire un provider :

monApp.factory('profile', function() {
    return {
        "name": "Anonymous",
        "Id": null,
        "login": function(){
            // faire un truc pour se loger
        },
        "logout": function(){
            // faire un truc pour se deco
        }
    }
});

Ça fait exactement la même chose, et on l’utilise exactement pareil. C’est juste plus court. Pas de méthode $get à écrire, on retourne l’objet cash pistache.

Pourquoi on utilise un provider alors ? Parce que c’est plus pratique à configurer, comme on le verra plus bas. Mais la factory est suffisante la plupart du temps. Ce qui est marrant, c’est qu’en terme informatique, un provider est un design pattern “factory”, ce qui m’a vachement rendu confus au début…

Et le service alors ? C’est une syntaxe alternative :

monApp.service('profile', function() {
    this.name = "Anonymous";
    this.id = null;
    this.login = function(){
        // faire un truc pour se loger
    }
    this.logout = function(){
        // faire un truc pour se deco
    }
});

La fonction va être utilisée directement (avec new) pour créer l’objet, donc pas besoin de return : on attache tout à this. Mais le résultat est strictement le même. Le choix entre une factory et un service est vraiment une question de goût. Personnellement je vous conseille d’utiliser des factories car ça vous évite le casse-tête de la portée de this, grande cause d’erreurs en JS.

Dépendances et injections

Souvenez-vous qu’on peut faire ça :

mymonAppApp.controller('UnControlleurCommeUnAutre', function($scope, profile) {
    $scope.profile = profile;
});

Pour dire “mon controller dépend de profile, donc s’il te plait Angular, crée l’objet et passe le moi”.

Et bien ça marche aussi entre les providers/factories/services. Par exemple, si je fais un provider pour ma playlist, mais que la playlist dépend du profile :

monApp.provider("playlist", function(){
 
    // En utilisant le nom de l'autre provider ici, je demande à ce qu'il
    // soit injecté ici, et donc disponible ici. Je dis "ce provider
    // est dépendant de l'objet fournit par cet autre provider."
    this.$get = function(profile) {
 
        return {
            // Et du coup je peux accéder aux attributs de l'objet.
            "name": "Playlist de " + profile.name
            "videos": [],
            "currentVideo": 1,
            "next": function(){
                // passer à la vid suivante
            },
            "previous": function(){
                // passer à la vid précédente
            }
        }
    }
});

Si je le faisais avec une factory, se serait pareil, mais en plus simple :

// l'injection de dépendance se fait au niveau de la fonction anonyme
// car il n'y a pas de méthode "$get"
monApp.factory("playlist", function(profile){
    return {
        "name": "Playlist de " + profile.name
        "videos": [],
        "currentVideo": 1,
        "next": function(){
            // passer à la vid suivante
        },
        "previous": function(){
            // passer à la vid précédente
        }
    }
});

L’interêt des providers

L’interêt principal d’utiliser un provider plutôt qu’une factory, c’est qu’on peut le rendre configurable.

Par exemple, je veux que ma playlist ne soit jamais vide, alors si elle l’en, on met deux vidéos par défaut dedans :

monApp.factory("playlist", function(profile){
 
    // je ne retourne pas l'objet tout de suite, je vais le modifier
    var playlist = {
        "name": "Playlist de " + profile.name
        "videos": [],
        "currentVideo": 1,
        "next": function(){
            // passer à la vid suivante
        },
        "previous": function(){
            // passer à la vid précédente
        }
    };
 
    // hop, je m'assure que la playlist n'est jamais vide en modifiant
    // l'objet
    if (playlist.videos.length === 0){
        playlist.videos = [
            {"title": "video 1", "url": "http://..."},
            {"title": "video 2", "url": "http://..."}
        ]
    }
 
    // ne pas oublier de retourner l'objet à la fin quand même :)
    return playlist;
});

Tout ça sera largement suffisant dans la plupart des cas. Mais dans le rare cas où je veux faire une lib réutilisable, je veux que ces vidéos par défaut soient configurables. Comment alors permettre que l’utilisateur de ma lib les choisisse ?

En utilisant un provider :

monApp.provider("playlist", function(){
 
    // On déclare des données attachée sur le provider. PAS sur l'objet que
    // le provider va créer, attention. Sur le provider lui-même.
    // Le provider, je le rappelle, c'est cette fonction anonyme qui retourne
    // un objet via sa méthode "$get" (oui, les fonctions sont des objets
    // en JS, et peuvent avoir des méthodes. Ce langage est très clair.)
    this.defaultVideos = [
            {"title": "video 1", "url": "http://..."},
            {"title": "video 2", "url": "http://..."}
    ]
 
    // ensuite je fais ma méthode "$get" qui va retourner l'objet voulu,
    // comme d'hab
    this.$get = function(profile) {
 
        var playlist = {
            "name": "Playlist de " + profile.name
            "videos": [],
            "currentVideo": 1,
            "next": function(){
                // passer à la vid suivante
            },
            "previous": function(){
                // passer à la vid précédente
            }
        }
 
        // Au moment de la création de l'objet, j'utilise les données
        // attachées au provider pour fournir la valeur par défaut.
        if (playlist.videos.length === 0){
            playlist.videos = this.defaultVideos;
        }
 
        return playlist;
    }
});

Jusqu’ici, vous allez me dire, mais quelle est la différence avec le précédent ? Et bien il se trouve qu’Angular met à votre disposition automatiquement playlist, mais également playlistProvider, une référence sur le provider qui va créer l’objet playlist.

ATTENTION : quand vous déclarez ici “playlist”, “playlist” est le nom de l’objet créé par le provider. Angular, lui, va automagiquement attacher le nom “playlistProvider” au provider qui a créé l’objet. Vous n’avez rien à faire pour cela, il va automatiquement s’appeler “nomDeVotreObjetProvider” et sera mis à votre disposition.

Du coup :

// On peut utiliser "config" pour lancer du code avant la création des objets
// par les providers. Tout le code dans des blocs "config" est toujours lancé en premier,
// avant tous les providers/factories/services de cette app.
monApp.config(function(playlistProvider){
    // ici l'utilisateur de votre lib a accès au provider avant la création de
    // l'objet, et à donc le loisir de changer les valeurs par défaut
    playlistProvider.defaultVideos = [
            {"title": "Autre video", "url": "http://..."},
    ]
});

Ce n’est pas un cas courant, et la plupart du temps, utiliser une factory marchera très bien.

Et tout ça, je m’en sers pour quoi ?

On met dans les providers/services/factories, tout le code qui n’est pas lié à la navigation ou à la manipulation de DOM. Bref, tout le code de la logique de votre app. Le code qui n’est pas lié au Web. Un profile n’a rien à avoir avec le Web. Une playlist n’a rien à voir avec le Web. Mais il faut un code pour les gérer, et des variables vont devoir êtres mises quelque part.

Généralement, ces providers/services/factories sont utilisés :

Mais aussi, directement dans le HTML.

En effet, quand vous allez faire ça :

// le boulot du controller, c'est essentiellement d'attacher les bons
// services au scope, je vous dis !
mymonAppApp.controller('VideoCtrl', function($scope, playlist) {
    $scope.playlist = playlist;
});

Vous allez ensuite pouvoir déclarer votre controller dans votre HTML. Dans un
bloc de HTML couvert par un controller, vous avez accès à toutes les attributs
du scope, donc à votre service “playlist”. Et du coup, paf, à tout son code :

<div ng-controller="VideoCtrl">
    ...
    <p>Playlist :</p>
    <ul>
       <li ng-repeat="vid for playlist.videos">{{ vid.title }}</li>
    </ul>
    <p>
    <button ng-click="playlist.next()">Next</button>
    </p>
</div>

Et c’est ça la beauté d’Angular : tout est bien séparé, et bien rangé. Votre code métier dans les providers, votre code d’interface dans le HTML, votre liaison entre le HTML et les providers à travers les controllers… Enfin du code JS qui ne ressemble pas à des spaghetti trop cuites par un enfant de 6 ans scatoman.

flattr this!