cogimator.net

Une ligne à la fois...

Mise en cache intelligente des templates AngularJS

Avant de rentrer dans le vif du sujet, petit rappel sur le fonctionnement du cache du navigateur.

Lorsque le navigateur demande une ressource (index.html par exemple) au  serveur, celui ci lui renvoie plusieurs éléments :

  • un statut HTTP (200 SUCCESS ici)
  • le contenu du fichier index.html
  • un ensemble d’en-têtes http

Parmi ces en-têtes, on retrouvera (en fonction de la configuration du serveur) :

  • un ETAG, qui est un checksum calculé selon le contenu renvoyé au navigateur
  • une date d’expiration, qui indique au navigateur la durée de vie de la ressource demandée

Il est à noter que les valeurs dans l’en-tête sont purement prescriptives, charge au navigateur de les respecter.

Lorsque le navigateur refait une requête au serveur pour le fichier index.html, il adjoindra également des en-têtes à sa requête. L’en-tête ETAG permettra de répondre au navigateur par un statut 304 NOT MODIFIED, dans le cas ou le fichier n’aurait pas changé.

L’en-tête date d’expiration permet au navigateur d’éviter d’envoyer des requêtes au serveur : tant que la date d’expiration n’est pas atteinte, le navigateur utilise la copie en cache.

Les templates AngularJS sont chargés au travers du service $http, qui se base sur xmlHttpRequest, qui lui même bénéficie du cache du navigateur.

Imaginons le scénario suivant :

  • le matin a 8h, le navigateur du client A demande le fichier index.html
  • le serveur lui renvoie, avec un ETAG “ABCD”, et une date d’expiration fixée au lendemain 8h
  • lors de l’utilisation de l’application, le navigateur va se baser sur la version en cache du fichier, tant que la date d’expiration est atteinte
  • a 12h, une nouvelle version de l’application est mise en ligne

Jusqu’au lendemain 8h, le client A utilisera une version potentiellement obsolète du fichier index.html.

Pour eviter cela, je vais mettre en place un mécanisme permettant d’assurer le bon fonctionnement du cache, tout en utilisant les derniers fichiers disponibles de l’application.

Dans un premier temps, il faut fournir un numéro identifiant l’application déployée. J’ai choisi d’utiliser la date de création d’un assembly du projet Web. Pour cela, j’ai intégré une simple balise script déclarant une constante AngularJs :

<script type="text/javascript">
    angular.module('SampleApplication.Config', [])
        .constant('SampleApplicationVersion', '<%: Version %>');
</script>

La suite de l’implémentation est aisée, grâce au système de HttpInterceptors fourni par AngularJs. Ces HttpInterceptors permettent de modifier les requêtes envoyées et reçues par le service $http.

.factory('SmartCacheInterceptor', ['$q', 'SampleApplicationVersion', function ($q, SampleApplicationVersion) {
    return {
        request: function (config) {
            if (config.url.indexOf(".htm") > -1) {
                var separator = config.url.indexOf("?") === -1 ? "?" : "&";
                config.url = config.url + separator + "v=" + SampleApplicationVersion;
            }
            return config || $q.when(config);
        }
    };
}]);

Voici les résultats dans fiddler. Les requêtes sur l’url “/” correspondent a un rafraichissement de la page du navigateur. Le serveur web utilisé est IIS Express, sans paramétrage spécifique.

image

Le code source est disponible sur github.

Indicateur de chargement AngularJs

Depuis la version 1.2.0 de AngularJs, le service $resource retourne des promise lors des appels aux méthodes get, query, save... Ceci ouvre des possibilités intéressantes, notamment la mise en place rapide d'indication de chargement.

Pour ce faire, j'ai choisi d'implémenter une directive, afin de pouvoir déclarer mon Loader ainsi :

<div loader="data">
    {{data | json}}
</div>

Ceci va donc orienter la déclaration de la directive, pour utiliser la transclusion et un scope isolé :

.directive('loader', ['$q', function ($q) {
    return {
        transclude: true,
        templateUrl: 'app/loader/loader.html',
        scope: {
            source: '=loader'
        },
        link: function (scope, elem, attrs) {
            
        }
    }
}])

Ensuite il faut écrire la fonction link pour réagir aux évènements du promise :

.directive('loader', ['$q', function ($q) {
    return {
        transclude: true,
        templateUrl: 'app/loader/loader.html',
        scope: {
            source: '=loader'
        },
        link: function (scope, elem, attrs) {
            scope.$watch("source", function (val) {
                scope.status = 0;
                val.$promise.then(function (success) {
                    scope.status = 200;
                }, function (err) {
                    scope.status = err.status;
                });
            });
        }
    }
}])

Le $watch permet de réagir à une assignation de la valeur en chargement, notamment lors de l'appel à une fonction de rechargement de données. Pour obtenir une référence sur l’objet promise renvoyé par $resource, il faut passer par la propriété $promise de celui-ci

Enfin, pour afficher tout ça, il nous faut un template :

<div>
    <div ng-hide="status==200"  ng-switch="status">
        <div ng-switch-when="0">
            <span><i class="fa fa-2x fa-spin fa-spinner"></i> Loading</span>
        </div>
        <div ng-switch-default>
            <span>Error from server : {{status}}</span>
        </div>
    </div>
    <div ng-show="status==200" ng-transclude></div>
</div>

Le code est disponible sur github .