Comme d'habitude, pour être sûr que nous soyons tous d'accord sur les notions que l'on va aborder, je tiens à éclaircir quelques points essentiels pour le routage.
Différence URI / URL
Ces deux termes sont largement employés dans le monde du développement web et entraînent (inutilement) une certaine confusion.
Un URI (...) est une courte chaîne de caractères identifiant une ressource sur un réseau (par exemple une ressource Web) physique ou abstraite, et dont la syntaxe respecte une norme d'Internet mise en place pour le World Wide Web.
Wikipédia, Uniform Ressource Identifier
Ainsi, les URI identifient 2 choses : une ressource et sa place au sein d'un réseau. Pour donner ces 2 informations, il y a 2 spécifications... logique :
Quoi ?
Spécification URN : Rarement citée en ce qui concerne internet, cette spécification décrit une ressource. Elle peut être un document sur internet, un livre (norme ISBN) ou autre. On ne s'en souciera pas plus ici. Wikipédia : Uniform Ressource Name
Où ?
Spécification URL : Communément appelée adresse web, ce surnom est révélateur. Cette spécification décrit un emplacement sur un réseau (sans assurer la présence ou l'intégrité d'une ressource correspondant à cet emplacement). Par habitude, on a donc ensuite associé dans notre utilisation courante emplacement et ressource. Cependant lorsque cette ressource est supprimée ou déplacée, notre URL reste valide mais ne désigne plus rien : c'est un lien mort. Wikipédia : Uniform Ressource Locator
Une URL est donc une URI, mais pas l'inverse !
Bref, ce que vous mettez dans la barre d'adresses de votre navigateur (ex : http://www.exemple.com/chemin/page.php?id=5&p=4) tout comme les liens relatifs qu'on utilise dans les pages HTML (ex : ../contact) sont des URL et donc des URI. Autrement dit, inutile de s'embêter plus que ça, on dira à présent URL pour les désigner puisque c'est le terme le plus répandu (en dehors des spécifications techniques des organismes normatifs).
Qu'est-ce que le routage ?
Un système de routage permet de faire correspondre une URL donnée à une page précise. Or par page précise, je pense à un ensemble de 3 éléments qui vous permettent de la (re)construire sans ambiguïté :
Le template
Le contrôleur
Les variables (de session ou données par l'URL)
Autrement dit, grâce à notre système de routage, on sera toujours capable d'envoyer notre visiteur là où il le veut de façon automatisée. De plus, vous pourrez établir une table de routage dans laquelle vous spécifierez ces correspondances ce qui vous permettra de complètement définir vos URL. Vous aurez enfin des URL propres et explicites.
Certains pourraient penser que tout ceci n'est qu'un travail superflu et inutile.
Je m'en sors très bien avec mes pages, j'en ai pas plus de 15. Et l'esthétique des URL, franchement, qu'est-ce qu'on en a à f*** !
Bien sûr, quand on a un petit site, on peut facilement gérer ses pages. Mais qu'en sera-t-il quand il grandira ?
Vous serez quasiment le seul à pouvoir modifier votre code source... et encore si vous y arrivez. :)
Il faudra TOUT relire et vous replonger longuement dedans à chaque fois que vous voudrez le modifier sans faire de bêtise puisque rien ne sera aisément compréhensible dans votre code. Bref, vous perdrez du temps.
Si vous voulez exporter un module de votre site vers un autre et que celui-ci comprend un ensemble cohérent de pages, vous allez faire face à une difficulté telle que je suis prêt à parier que vous recommencerez ou vous abandonnerez — c'est du vécu —
L'esthétique des URL n'est pas une fin en soi. Il est vrai qu'on s'en fiche pas mal d'avoir de jolies URL si personne ne les touche jamais. Mais il y a au moins 2 raisons qui rendent ces URL explicites utiles voire nécessaires :
Il est toujours plus agréable de pouvoir deviner ce que l'on aura en suivant un lien que l'on reçoît ou sur lequel on tombe. Et si le visiteur peut faire cette déduction il sera plus enclin à suivre ce lien et donc à venir sur votre site. ;)
Les moteurs de recherches détestent les URL à rallonge. Il y a un certain temps, Google avait du mal à référencer une URL avec 3 paramètres ou plus et ne référençait pas les URL avec un paramètre id ! Même si les robots des moteurs de recherche se sont beaucoup amélioré, ces consignes sont toujours à respecter si vous ne voulez pas vous mettre à dos tous les outils modernes de référencement.
Le module ngRoute
Présentation
Introduction
Initialement, AngularJS est un framework pour développer des applications One-Page. Et donc par définition, il n'y a pas besoin de changer de page (puisqu'il n'y en a qu'une :p) et le routage n'est pas utile. Cependant, le framework grandissant et ses utilisations se diversifiant, il a très vite été question d'intégrer un système de routage.
Dans les versions antérieures à la 1.2, qui étaient des versions bêta, les développeurs avaient intégré nativement ce système de routage au framework par souci de simplicité (ils avaient alors bien d'autres priorités). Cependant, dès la version 1.2 qui est stable et qui a fait le renom d'AngularJS, il a fallu le recentrer sur ces objectifs premiers : ces fameuses applications One-Page. Or le projet avançant, beaucoup de fonctionnalités ont vu le jour et l'équipe a donc décidé de séparer celles n'étant pas essentielles.
Ainsi, comme d'autres fonctionnalités (qu'on découvrira dans le tutoriel ou via des billets), le système de routage a maintenant son propre module : ngRoute.
Ses composants
Le module ngRoute regroupe plusieurs éléments permettant la mise en place de votre routage :
2 services
$route est le service central du module. Il lit l'URL demandée par le navigateur et la parse pour pouvoir la traiter ensuite en fonction des informations données au provider. Et paradoxalement, on ne l'appellera quasiment jamais, excepté pour redémarrer cette analyse dans le cas d'un rafraîchissement de la page ou d'un changement de route... logique ;) Documentation officielle sur le service $route (EN)
$routeParams gére les arguments / paramètres passés via l'URL. Voir paragraphe dédié
1 provider
$routeProvider est le fournisseur du module. Autrement dit, il initialise le routage grâce aux rêgles qu'on lui donne lors de la configuration de notre application. Voir le paragraphe dédié.
1 directive
ngView sert à insérer le template de votre page à l'intérieur de votre layout général. Voir le paragraphe dédié
Attends, c'est allé un peu vite là, j'ai pas tout suivi... :/
Effectivement, pour être un peu plus clair, je dois préciser quelques termes que je viens d'utiliser et qui ne sont pas forcément évidents :)
parser
Du verbe anglais to parse, parser signifie analyser et s'applique le plus souvent à une chaîne de caractères structurée selon un modèle connu ou pas. Autrement dit, lorsque l'on parse la chaîne exemple::de_chaine/structuree, on extrait l'information qu'elle contient avant les ::, puis après le / et entre les deux. Dans le cas d'une URL, on peut ainsi obtenir la page demandée, les paramètres donnés, etc...
Pour rappel, voici la structure d'une URL absolue :
provider
Rarement présentés lorsque l'on parle d'AngularJS, les providers (fournisseurs en français) sont la pierre d'achoppement du framework. Ils indiquent à AngularJS la recette à suivre pour utiliser un module. Ils sont donc utilisés dans les modules les plus courants et les plus complets pour pouvoir initialiser et lier entre eux tous leurs éléments. Leur utilisation n'étant pas aisée, et puisqu'on ne sera pas amené à en coder tout de suite, je ne m'étendrai pas plus à leur sujet. Documentation officielle sur les providers (EN)
layout
Le layout n'est autre qu'un template. Simplement, on parle d'un template pour une page et d'un layout pour un site. Le layout comprend donc les différents éléments que l'on retrouve dans toutes les pages du site (le menu, le header, le footer, etc...) alors que le template est dédié à une page précise. Bien entendu, AngularJS nous permet d'inclure ce template au sein du layout pour construire la vue finale qu'utilise le visiteur.
Voilà pour une brève présentation des composants du module ngRoute. Si vous voulez approfondir certains points je vous conseille de lire la documentation qui est très claire (mais en anglais bien entendu). Documentation officielle sur le module ngRoute (EN)
Son fonctionnement
Pour comprendre comment fonctionne ngRoute, il faut revenir au principe de base d'AngularJS : le framework permet de déplacer toute la logique de présentation (pour reprendre les termes de la première partie) côté client. Or en déportant aussi le routage, le navigateur du client doit pouvoir être indépendant du serveur pour naviguer d'une page à l'autre (à l'exception bien sûr du template qu'il peut télécharger au moment approprié). Le serveur ne guide absolument pas la visite, c'est le client qui fait des demandes asynchrones des objets dont il a besoin. (Re)voir le paragraphe AngularJS et le back-end
Ainsi, à chaque page visitée, le navigateur ne formule plus une requête directement au serveur, mais à AngularJS. Celui-ci sait alors s'il faut télécharger de nouveaux documents, et comment composer la page demandée.
Mais comment fait AngularJS pour intercepter ces requêtes ?
En fait, il utilise une ressource des URL justement réservée aux navigateurs et qui n'est pas envoyée dans les requêtes : les ancres.
En effet, les ancres ne sont traitées que par le navigateur, après réception de la réponse du serveur à sa demande. Et, alors que changer d'ancre au sein d'un document modifie bien l'URL et l'état de la page, cela ne renouvelle en aucun cas une requête auprès du serveur.
Finalement, AngularJS a sa solution toute trouvée c'est la raison pour laquelle tous les liens de votre application seront de la forme http://www.exemple.com/#/accueil ou http://www.exemple.com/#/contact, le # étant le délémiteur du début de l'ancre.
Une dernière remarque : le fichier index.html doit évidemment être dans le dossier pointé par l'adresse http://www.exemple.com/ puisque c'est lui qui lance toute l'application.
Installation
Nous allons construire une application de démonstration très simple que l'on va appeler routeApp. Elle ne contient que deux pages (accueil et contact) pour illustrer le système de routage. Vous pouvez en voir une Live Demo. Comme pour la Todo List, je vous donne le fichier style.css que j'ai utilisé, mais vous pouvez très bien en utiliser un autre. ;)
Comme ngRoute est un module extérieur au cœur d'AngularJS, on doit l'installer pour pouvoir s'en servir. Tout d'abord, il faut évidemment inclure le fichier JavaScript du module, angular-route.js, en même temps qu'AngularJS dans le fichier index.html :
Ensuite, lors de sa déclaration, on dit à AngularJS que notre application routeApp dépend du module ngRoute :
// js/script.js
'use strict';
/**
* Déclaration de l'application routeApp
*/
var routeApp = angular.module('routeApp', [
// Dépendances du "module"
'ngRoute'
]);
Et c'est tout, notre application est prête à utiliser ngRoute.
Mise en place du routage
Organisation
Avant de créer tout plein de templates et de contrôleurs, voyons comment organiser nos fichiers. Le but est de pouvoir facilement s'y retrouver tout en évitant la duplication de code. Je vous propose donc cette arborescence très simple mais bien adaptée pour les petits projets comme le nôtre (je vous rappelle qu'ici on fait une application qui n'a que 2 vues) :
Je ne pense pas qu'il y ait besoin de beaucoup d'explications. Le fichier index.html sera notre layout et le dossier partials/ comprendra tous les templates des pages.
Ici, on mettra tout le code Javascript dans le fichier script.js mais on verra que si l'application doit grandir, il faudra évidemment le scinder. On en reparlera plus en détails dans la partie 3.
La directive ngView
Comme on l'a vu, on va donc faire un layout qui sera le même pour toutes nos (2) pages, le fichier index.html. Celui-ci sera chargé d'inclure les templates des pages de la page courante. Pour ce faire, il faut d'abord dire à AngularJS où placer ces templates au sein du layout. Rien de plus simple avec la directive ngView :
Grâce au provider du module ngRoute, on peut configurer notre routeur. On dit qu'on définit notre table de routage. On va faire correspondre à chaque URL souhaitée une entrée dans le $routeProvider avec la méthode .when() (quand en français):
/**
* Configuration du module principal : routeApp
*/
routeApp.config(['$routeProvider',
function($routeProvider) {
// Système de routage
$routeProvider
.when('/home', {
templateUrl: 'partials/home.html',
controller: 'homeCtrl'
})
.when('/contact', {
templateUrl: 'partials/contact.html',
controller: 'contactCtrl'
});
}
]);
Cette méthode prend 2 arguments :
Le path
En français le chemin, c'est la partie de l'URL après le # qui identifie votre page. Cet argument est donc une simple chaîne de caractères, rien de bien sorcier. ;)
La route
Une route est un objet qui peut avoir beaucoup de propriétés. Ici, nous lui en donneront 2 :
templateUrl : Peut être une chaîne de caractères ou une fonction renvoyant une chaîne de caractères qui corresponde à un fichier HTML qui sera inclus dans le layout au niveau de la directive ngView.
controller : Définit la fonction qui sera utilisée comme contrôleur pour cette page. D'où l'intérêt de donner des noms explicites à vos différents éléments.
Enfin, pour pouvoir gérer les URL erronées d'un visiteur qui se serait perdu par exemple, vous disposez de la méthode .otherwise() (sinon en français):
/**
* Configuration du module principal : routeApp
*/
routeApp.config(['$routeProvider',
function($routeProvider) {
// Système de routage
$routeProvider
.when('/home', {
templateUrl: 'partials/home.html',
controller: 'homeCtrl'
})
.when('/contact', {
templateUrl: 'partials/contact.html',
controller: 'contactCtrl'
})
.otherwise({
redirectTo: '/home'
});
}
]);
Cette méthode ne prend qu'un seul argument : la définition d'une route à suivre. Elle est utilisée lorsqu'aucune des autres entrées données via les méthodes .when() ne correspond à l'URL demandée. Ici, on choisit tout simplement de rediriger le visiteur vers la page d'accueil.
Le $routeProvider n'a que les 2 méthodes que nous venons de voir, mais si vous voulez approfondir un point ou connaître toutes les propriétés que peut avoir une route, n'hésitez pas à voir l'API de référence. Documentation officielle sur le $routeProvider (EN)
Écrire nos pages
Maintenant que notre routeur est en place, il ne nous reste plus qu'à rédiger nos 2 templates et leurs contrôleurs respectifs.
Avant tout, il faut déclarer le module routeAppControllers auquel on attachera les contrôleurs et l'ajouter dans les dépendances du module principal routeApp :
/**
* Déclaration de l'application routeApp
*/
var routeApp = angular.module('routeApp', [
// Dépendances du "module"
'ngRoute',
'routeAppControllers'
]);
...configuration du routeur...
/**
* Définition des contrôleurs
*/
var routeAppControllers = angular.module('routeAppControllers', []);
// Contrôleur de la page d'accueil
routeAppControllers.controller('homeCtrl', ['$scope',
function($scope){
$scope.message = "Bienvenue sur la page d'accueil";
}
]);
La page de contact
Le template est quasiment aussi simple que pour la page d'accueil. On ajoute juste un champ pour pouvoir écrire un message et on le duplique dans une citation :
Pour le contrôleur, on précise aussi la valeur de départ de notre variable msg avec une phrase :
// Contrôleur de la page de contact
routeAppControllers.controller('contactCtrl', ['$scope',
function($scope){
$scope.message = "Laissez-nous un message sur la page de contact !";
$scope.msg = "Bonne chance pour cette nouvelle appli !";
}
]);
Et voilà, nos 2 vues sont prêtes ! Pas très dur finalement ce routage avec AngularJS. :)
Passer des paramètres
Comme on l'a vu au début du chapitre, il peut être intéressant d'avoir des URL propres tout en transmettant des variables. AngularJS est un framework moderne et cette problématique est au centre de son système de routage.
Implicitement, je vous montrais plus haut qu'une URL dite propre sépare les différentes informations qu'elle donne avec des slashs : /. Et les développeurs d'AngularJS pensent la même chose, tant mieux. :p
Un exemple parlant plus que de longues descriptions, considérons qe nous voulons passer en paramètre à notre page de contact le message par défaut. Voici comment on s'y prendrait :
Modification du routeur
/**
* Configuration du module principal : routeApp
*/
routeApp.config(['$routeProvider',
function($routeProvider) {
// Système de routage
$routeProvider
.when('/home', {
templateUrl: 'partials/home.html',
controller: 'homeCtrl'
})
.when('/contact/:msg', {
templateUrl: 'partials/contact.html',
controller: 'contactCtrl'
})
.otherwise({
redirectTo: '/home'
});
}
]);
Modifiaction du contrôleur contactCtrl
// Contrôleur de la page de contact
routeAppControllers.controller('contactCtrl', ['$scope','$routeParams',
function($scope, $routeParams){
$scope.message = "Laissez-nous un message sur la page de contact !";
$scope.msg = $routeParams.msg;
}
]);
Comme vous le voyez, c'est un jeu d'enfant ! Il suffit de préciser lors de la définition de la route qu'elle contiendra un paramètre grâce au :. Et pour le récupérer, on utilise le service $routeParams qui contient tous les paramètres récupérés dans l'URL.
Vous pouvez bien entendu mettre plusieurs paramètres dans votre URL et tous les récupérer dans votre contrôleur. Et si vous voulez transmettre des paramètres en plus de ceux prévus par votre route, vous pouvez le faire en ajoutant un ? à la fin de l'URL :
// on considére l'URL donnée : http://www.exemle.com/#/immeuble/57/appartement/10?nom=dupont
// Votre routeur sera de la forme :
exempleApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider
.when('/immeuble/:imble/appartement/:appart', {
templateUrl: 'partials/appartement.html',
controller: 'appartementCtrl'
});
}
]);
// Le contrôleur appartementCtrl
exempleAppControllers.controler('appartementCtrl', ['$scope', '$routeParams',
function($scope, $routeParams){
// Pour afficher les informations
$scope.immeuble = $routeParams.imble; // ici 57
$scope.appartement = $routeParams.appart; // ici 10
$scope.nom = $routeParams.nom; // ici "dupont"
}
]);
Paramètres optionnels
Si vous essayez notre application routeApp telle qu'on vient de la modifier, vous remarquerez deux choses :
Quand on entre l'URL #/contact/Bonjour%20tout%20le%20monde%20!, tout fonctionne comme prévu et on a bien la page de contact avec le message pré-entré Bonjour tout le monde !.
Par contre, si on clique sur le bouton ou qu'on entre directement l'URL #/contact (ou #/contact/, ça ne change rien), on est redirigé sur la page d'accueil. En fait, cette URL ne correspond à aucune route enregistrée et c'est la dernière close .otherwise()qui nous renvoie à la page #/home.
Seulement, on aimerait que cette URL soit fonctionnelle même si on ne lui passe pas de paramètre, sans avoir à redéfinir une route pour autant bien sûr. Pour ce faire, il suffit d'ajouter un ? dans la définition de notre route pour dire à ngRoute que ce paramètre est optionnel.
Modification du routeur
/**
* Configuration du module principal : routeApp
*/
routeApp.config(['$routeProvider',
function($routeProvider) {
// Système de routage
$routeProvider
.when('/home', {
templateUrl: 'partials/home.html',
controller: 'homeCtrl'
})
.when('/contact/:msg?', {
templateUrl: 'partials/contact.html',
controller: 'contactCtrl'
})
.otherwise({
redirectTo: '/home'
});
}
]);
Modifiaction du contrôleur contactCtrl
// Contrôleur de la page de contact
routeAppControllers.controller('contactCtrl', ['$scope','$routeParams',
function($scope, $routeParams){
$scope.message = "Laissez-nous un message sur la page de contact !";
// Si aucun paramètre n'est passé, on met notre phrase initiale
$scope.msg = $routeParams.msg || "Bonne chance pour cette nouvelle appli !";
}
]);
Ainsi, la route sera bien suivie que l'on mette une URL avecousans paramètre.
Il faut bien faire attention à ne pas confonfre les ? que l'on met dans une URL et ceux que l'on met dans une route. Le premier permet d'ajouter un paramètre que la route n'avait pas prévu alors que le second dit à ngRoute qu'un paramètre n'est pas nécessaire pour suivre une route.
On a vu toutes les notions à maîtriser pour utiliser le service $routeParams, mais si vous avez encore des interrogations, n'hésitez pas à consulter l'API d'AngularJS. Documentation officielle sur le $routeParams (EN)
Après tous ces changements, je vous propose ici la version finale de notre fichier script.js pour que tout soit clair :
// js/script.js
'use strict';
/**
* Déclaration de l'application routeApp
*/
var routeApp = angular.module('routeApp', [
// Dépendances du "module"
'ngRoute',
'routeAppControllers'
]);
/**
* Configuration du module principal : routeApp
*/
routeApp.config(['$routeProvider',
function($routeProvider) {
// Système de routage
$routeProvider
.when('/home', {
templateUrl: 'partials/home.html',
controller: 'homeCtrl'
})
.when('/contact/:msg?', {
templateUrl: 'partials/contact.html',
controller: 'contactCtrl'
})
.otherwise({
redirectTo: '/home'
});
}
]);
/**
* Définition des contrôleurs
*/
var routeAppControllers = angular.module('routeAppControllers', []);
// Contrôleur de la page d'accueil
routeAppControllers.controller('homeCtrl', ['$scope',
function($scope){
$scope.message = "Bienvenue sur la page d'accueil";
}
]);
// Contrôleur de la page de contact
routeAppControllers.controller('contactCtrl', ['$scope','$routeParams',
function($scope, $routeParams){
$scope.message = "Laissez-nous un message sur la page de contact !";
// Si aucun paramètre n'est passé, on met notre phrase initiale
$scope.msg = $routeParams.msg || "Bonne chance pour cette nouvelle appli !";
}
]);
Le module UI-Router
Les développeurs d'AngularJS ont fait le choix de restreindre son noyau à l'extrême nécessaire afin de l'alléger au maximum. Mais ce n'est pas le seul but. Cela permet à chacun de ses utilisateurs de choisir la solution qui lui convient le mieux pour mettre en place les fonctionnalités qu'il souhaite. C'est encore une fois le principe de modularité qui fait la force de ce framework.
En effet, rien n'empêche un développeur d'utiliser un module indépendant tout en sachant qu'il n'y a pas de redondance entre les différents outils auxquels il fait appel. Car si plusieurs modules ont en charge la même tâche, l'excédent de code allourdit le script et donc ralentit son téléchargement et son exécution, et peut amener à des incohérences du fait qu'ils se gènent mutuellement.
Or l'un des avantages (et non des moindres) d'AngularJS est sa grande communauté d'utilisateurs mais aussi de développeurs avec Google en tête de cortège. Cela permet à beaucoup de modules très intéressants de voir le jour de plus en plus vite. De nombreux sites répertorient les nouveautés développées pour AngularJS. Angular Modules (EN) angular-js.in — site spécialisé pour les directives (EN)
L'un des projets indépendants les plus avancés menés autour autour d'AngularJS est certainement AngularUI. Il a très vite cherché à faciliter son utilisation en convertissant et adaptant des outils largement utilisés dans le développement web comme Google Maps, TinyMCE ou encore Bootstrap.
Ils ont aussi développé un routeur très performant alternatif à celui proposé par l'équipe d'AngularJS : UI-Router. Il réalise toutes les tâches que ngRoute accomplit et bien plus. Il est plus puissant en ce sens qu'il permet plus de souplesse. Il ne se concentre pas uniquement sur l'URL au sein de l'application mais sur l'état de l'application.
Un état peut être une URL mais peut aussi être un moment ou une étape lors de la visite d'une page par exemple. Ainsi, une même URL peut donner lieu à plusieurs états, et donc plusieurs vues.
C'est pourquoi UI-Router a un autre atout par rapport à ngRoute : les templates et vuesemboîtées. En effet, lorsqu'on navigue sur une page d'une application digne de ce nom, on est souvent amené à modifier la vue en fonction de nos actions. Contrairement à ngRoute qui utilise la directive ngView ne permettant d'inclure qu'un seul template à la fois dans le layout, UI-Router utilise uiView. Cette directive peut être appelée plusieurs fois dans une page, rendant le développement beaucoup plus intuitif et logique.
Bref, UI-Router a beaucoup d'avantages par rapport à ngRoute et est très complet. Ce chapitre avait pour but de présenter le routage avec AngularJS. ngRoute nous a permis de l'atteindre et il faudra un autre article pour détailler les nombreuses possibilités qu'offre UI-Router. Je les expliquerai dès que possible dans un billet dédié.
Vos commentaires