Résolvez vos problèmes de Hashbangs grâce au PushState en HTML5

Jean-Philippe BUNAZ

Cet article a été publié avec l'aimable autorisation de Ippon technologies et de Jean-Philippe BUNAZ.

Commentez

- Titre : Résolvez vos problèmes de Hashbangs grâce au PushState en HTML5

- Auteur : Jean-Philippe BUNAZ

- Parution : 2 octobre 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Jean-Philippe BUNAZ. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Compatibilité

- III. Navigation via du hash

- IV. Navigation via pushstate

- V. Pushstate dans les frameworks MVC

- V-A. Backbone

- V-B. AngularJS

- V-C. GWT

- VI. Pourquoi le pushstate c'est génial ?

- VI-A. Site Web

- VI-B. Application Web

- Remerciements

- Lectures associées

I - Introduction

Au début du Web, nous n'avions que des pages statiques, nous entrions une adresse et le serveur nous retournait une page HTML. Puis nous avons voulu dynamiser un peu nos sites avec du JavaScript et de l'AJAX, jusqu'à avoir des applications complètes dans nos navigateurs. Ce que nous appelons maintenant des « Single-Page-Application ».

Une des problématiques lors du développement d'application de type « single-page » est que nous chargeons une page, puis nous modifions le contenu de celle-ci au gré des clics de l'utilisateur sans modifier visiblement l'URL de la page. Ceci est logique, car nous restons toujours sur la même page, mais ne suit pas la philosophie de Web où un contenu est identifié de manière unique.

Pour résoudre ce problème et avoir un identifiant unique pour chaque ressource, il existait jusqu'à maintenant une solution : ajouter des ancres (hash) pour avoir des adresses comme ceci http://monsite/#article/0/spring_c_est_bien.

L'ancre apporte une solution partielle, car si cette technique permet d'avoir un identifiant unique pour une ressource, celle-ci est connue uniquement du navigateur, mais pas du serveur. Alors oui, nous pouvons mettre l'adresse en favori et lorsque l'utilisateur rechargera cette page, il aura bien la page dans le bon état, mais il reste des problèmes.

Tout d'abord, l'affichage de la page n'est pas optimale, car la remise dans le bon état se fait forcément côté client via JavaScript. Le serveur n'ayant pas l'intégralité du nom de la ressource, il ne peut pas générer la page dans son état final. Pour l'utilisateur ça veut dire attendre que tous les scripts soient chargés, puis que le javaScript s'exécute. Sur une machine peu puissante ou si vous utilisez 90 % votre CPU pour lire une vidéo avec Flash, un temps plus ou moins long est nécessaire pour que la page s'affiche, l'expérience utilisateur est donc dégradée.

L'autre problème d'avoir le nom de la ressource uniquement sur le client est que si le serveur doit faire une redirection, il la fera uniquement vers le début de l'adresse. Par exemple si vous devez vous authentifier, vous serez redirigé vers la page d'accueil et non vers la page que vous avez appelé initialement.

Mais ça c'était avant, maintenant avec HTML5, il est possible d'avoir des vraies adresses comme ceci http://monsite/article/0/spring_c_est_bien tout en continuant à utiliser AJAX. Pour réaliser ce miracle, nous utilisons la nouvelle fonction pushstate de l'objet History. Alors quand je dis nous utilisons, c'est vrai : pushstate a été mis en place sur la prochaine version de Tatami. Nous ne sommes pas les seuls : Twitter utilise cette technique pour accélérer son affichage, comme indiqué ici.

II - Compatibilité

IE

Firefox

Safari

Chrome

Opera

iPhone

Android

-

4.0+

5.0+

8.0+

11.50+

4.2.1+

4.2+ *

Android a annoncé le support pour des versions plus anciennes, mais l'implémentation est trop boguée pour être réellement utilisable.

III - Navigation via du hash

Nous allons créer les liens de la manière suivante :

<ul id="links">
<li><a href="#article/0_spring_c_est_bien" class="article-link">Spring c'est bien</a></li>
<li><a href="#article/1_scala_forever" class="article-link">Scala forever</a></li>
<li><a href="#article/2_html5_pour_les_nuls" class="article-link">HTML5 pour les nuls</a></li>
</ul>

Lorsqu'un utilisateur clique sur un lien, le serveur n'est pas appelé puisque nous restons sur la même page. Pour modifier le contenu de la page, il ne reste plus qu'à ajouter un événement sur le changement du hash.

window.onhashchange = function(){
changeRoute(window.location.hash);
}
function changeRoute(route){
var patt= new RegExp("/([0-9])_");
if(!patt.test(route)){
window.location.hash = "#article/0_spring_c_est_bien";
} else {
getArticle(patt.exec(route)[1]);
}
}
changeRoute(window.location.hash);

Attention, l'événement n'est pas lancé au chargement de la page, il faut appeler la fonction de modification du contenu au chargement de la page.

IV - Navigation via pushstate

Pour ce type de navigation, nous utilisons les liens suivants avec de vraies URL :

<ul id="links">
<li><a href="/article/0/spring_c_est_bien" class="article-link">Spring c'est bien</a></li>
<li><a href="/article/1/scala_forever" class="article-link">Scala forever</a></li>
<li><a href="/article/2/html5_pour_les_nuls" class="article-link">HTML5 pour les nuls</a></li>
</ul>

Lorsque l'utilisateur clique sur un lien, il est nécessaire de supprimer le comportement par défaut du navigateur, sinon le serveur sera appelé. Nous allons ensuite changer l'URL et appeler la fonction modifiant le contenu de la page :

function changeRoute(route){
var patt= new RegExp("/([0-9])/");
if(!patt.test(route)){
history.pushState(null, null, "/article/0/spring_c_est_bien");
changeRoute("/article/0/spring_c_est_bien");
} else {
getArticle(patt.exec(route)[1]);
}
}
var links = $(".article-link");
links.click(function (event) {
//Suppression du comportement par défaut
event.preventDefault();
// Modification de l'url avec la valeur du lien
var target = event.target;
var route = target.pathname;
history.pushState(null, null, route);
// Modification du contenu de la page
changeRoute(route);
});

Il est nécessaire d'ajouter l'événement suivant pour que le contenu de la page soit modifié lors de la navigation via les boutons « précédent » et « suivant » :

window.onpopstate = function(){
changeRoute(window.location.hash);
}

Attention, le comportement par défaut de cet événement est différent entre les navigateurs Webkit et Firefox. Sous Webkit l'événement est appelé lors du chargement de la page, alors qu'il ne l'est pas sous Firefox.

V - Pushstate dans les frameworks MVC

Il est possible dès maintenant d'avoir le support du pushstate dans certains frameworks MVC. L'utilisation de ces frameworks permet de détecter le support de la fonctionnalité par le navigateur et d'utiliser la méthode de l'ancre pour les vieux navigateurs.

V-A - Backbone

Pour l'utiliser, il suffit de l'activer au moment du lancement de la gestion des routes :

var Route = Backbone.Router.extend({
routes: {
"article/:id/*path" : "getArticle",
"*path" : "defaultRoute"
},
getArticle: function(id){
$('#article').html(articles[id].content);
},
defaultRoute: function(){
$('#article').html(articles[0].content);
}
});
var route = new Route();
Backbone.history.start({pushState: true});

Il est également possible de passer en paramètre de start la propriété silent: true, qui permet de ne pas déclencher la route lors du chargement initial de la page.

V-B - AngularJS

Il suffit de le déclarer dans la configuration de l'application :

angular.module('article', []).
config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/article/:id/[a-zA-z0-9\.]*', {templateUrl: '/partials/article.html', controller: ArticleCtrl}).
otherwise({redirectTo: '/article/0/spring_c_est_bien'});
}]).config(function($routeProvider, $locationProvider) {
$locationProvider.html5Mode(true);
});

Il n'y a pas d'équivalent au silent: true de Backbone.js, pour ne pas avoir de déclenchement d'une route lors du premier chargement, ce qui peut s'avérer gênant dans certains cas.

V-C - GWT

GWT ne supporte pas cette fonctionnalité pour le moment, mais il existe l'extension https://github.com/jbarop/gwt-pushstate/.

VI - Pourquoi le pushstate c'est génial ?

L'intérêt d'utiliser AJAX sur un site Web ou une application n'est plus à démontrer. Consommation de bande passante réduite, fluidité, possibilités ergonomiques étendues. Néanmoins, cela pose certaines problématiques qu'il est possible de résoudre avec cette nouvelle possibilité.

VI-A - Site Web

Sur un site Web, la principale problématique est la complexité d'indexation des contenus affichés avec AJAX. Pour l'indexation par Google, la méthode est la suivante :

- ajouter avant le hash un ! pour avoir une URL de type http://monsite/#!article/0/spring_c_est_bien ; Google comprend alors qu'il s'agit d'un contenu AJAX et pas d'une navigation d'ancre ;

- sur ce type de contenu, Google va appeler l'URL http://monsite/?_escaped_fragment_=article/0/spring_c_est_bien qui devra retourner le contenu HTML complet.

Avec pushstate, il n'est plus nécessaire de faire ce type de configuration, mais il est nécessaire qu'un appel direct aux adresses générées via pushstate retourne le code HTML complet.

Dans ce cas le mode silent de Backbone est intéressant, le contenu de la page étant généré complètement par le serveur lors d'un appel direct, il n'a pas à être généré par le client. Ce procédé est utilisé par Twitter.

Si nous accédons à un « Trending-topic » directement depuis la barre d'adresse via l'URL https://twitter.com/search?q=%23RolandGarros&src=tren, nous obtenons la page HTML complète :

Mais si nous accédons à la page en cliquant depuis le bloc « tendance ». L'URL est la même, mais entraîne un appel AJAX avec une réponse en JSON :

VI-B - Application Web

Dans une application Web, la problématique lorsque l'on utilise la méthode des ancres est que tout ce qui se trouve après l'ancre n'est pas envoyé au serveur. Lors d'une redirection, cette information n'étant pas présente, celle-ci est faite sur l'adresse jusqu'à l'ancre, donc la page d'accueil de l'application.

Le cas fonctionnel où ce comportement est gênant est lorsque nous mettons l'adresse en favori ou que nous l'envoyons par e-mail. Si, pour accéder à la page, nous devons passer par un processus d'authentification, nous serons dirigé vers la page d'accueil de l'application plutôt que vers le contenu intéressant.

Pour une application Web, qui n'a pas vocation à être indexée par les moteurs de recherche, il n'est pas nécessaire que le serveur retourne le HTML complet de la page. Il suffit qu'il réponde à toutes les ressources, le client se chargeant de mettre à jour le contenu avec AJAX.

Nous avons fait ceci sur Tatami, lorsque nous accédons à « Trending topic » sans être authentifié.

Avant, nous étions redirigé vers l'accueil lorsque nous saisissions l'URL https://tatami.ippon.fr/tatami/#/tags/TatamiV3 :

Maintenant, nous sommes bien redirigé vers le « Trending topic » lorsque nous saisissons l'URL https://tatami.ippon.fr/tatami/new/tags/TatamiV3 :

Remerciements

Cet article a été publié avec l'aimable autorisation de Ippon technologies et de Jean-Philippe BUNAZ. L'article original (Résolvez vos problèmes de Hashbangs grâce au PushState en HTML5) peut être vu sur le blog d'Ippon.

N'hésitez pas à faire part de vos remarques et commentaires à propos de cet article sur le forum ! Commentez

Lectures associées

En complément de cet article, l'équipe de rédaction vous recommande l'article suivant :

- Ajax et le référencement.