HTML5 persistence

Michel Buffa
Juin 201 6

buffa@unice.fr

HTML5 propose plusieurs outils pour la persistence côté client:

  • La cache API pour les applications offlines,
  • Les Service Workers (implémentations en cours, testable sur Chrome pour le moment)
  • La "Web Storage API: sortes de super cookies, mais côté client !
  • La File API, accompagnée de deux autres APIs additionnelles: FileSystem et FileWriter APIs,
  • Une base de données NoSQL transactionnelle : IndexedDB
  • Une BD relationnelle, transactionelle : WebSQL. OBSOLETE!

Tester si on est online / offline ...

Propriétés et évènements online/offline

function online(event) {
  statusElem.className = navigator.onLine ? 'online' : 'offline';
  statusElem.innerHTML = navigator.onLine ? 'online' : 'offline';
  state.innerHTML += '<li>New event: ' + event.type + '</li>';
}

window.addEventListener('online', online);
window.addEventListener('offline', online);
                

L'API HTML5 de gestion du cache

Conçue pour mieux gérer les applications offline

Principle: inclure un fichier "manifeste" dans chaque page HTML qui doit être cachée.

<html manifest="myCache.appcache"/>
                

Nouveau type MIME :

AddType text/cache-manifest .appcache
               

Simple exemple de fichier manifeste

CACHE MANIFEST
clock.html
clock.css
clock.js
                

Tiré de cet exemple de la doc W3C sur la cache API.

La première ligne (CACHE MANIFEST) est obligatoire,

Chaque fichier qui doit être caché doit être inclu individuellement (pas de wildcards),

La page HTML elle-même est inclue par défaut...

Mise à jour du cache?

Quand un navigateur trouve un fichier manifeste, il ajoute chaque fichier listé dans le cache, alors, il prendra dorénavant ces fichiers dans le cache...

Sauf si le fichier manifeste côté serveur est mis à jour (date changée).

Bonne pratique : ajouter un timestamp / commentaire dans le manifeste à chaque update.

Il est stupide de vouloir TOUT cacher...

Imaginez un formulaire login / password , ne doit pas être caché ou affiché en mode offline.

Le manifeste a des directives pour celà :

  • CACHE: liste des fichiers devant être cachés,
  • NETWORK: l'inverse ! fichiers à ne pas cacher,
  • FALLBACK: fichiers à afficher quand on essaie en mode offline d'accéder à une resource non disponible.

Note: wildcards autorisées dans NETWORK et FALLBACK, pas avec la directive CACHE .

Exemple de fichier manifeste plus compliqué...

CACHE MANIFEST

CACHE:

#images
/images/image1.png
/images/image2.png

#pages
/pages/page1.html
/pages/page2.html

#CSS
/style/style.css

#scripts
/js/script.js

# / = any resource that is not available in the cache
FALLBACK:
/ /offline.html

NETWORK:
login.html
                

Autre exemple avec des "wildcards"

...
NETWORK:
*

FALLBACK:
/login.html  /offline.html

Cliquer l'image suivante pour un exemple réel...

Limitations sur la taille du cache

Habituellement : 5 megas pour toute persistence client-side, infinie si l'application vient du Chrome AppStore, du Windows Store,

Parfois configurable (Opera), une "quota API" est en route,

Ce pirate (par Andrés Lucero) surveille vos données !

Cache JavaScript API

Il est possible de mettre à jour le cache par programme :

 var appCache = window.applicationCache;

appCache.update(); // Attempt to update the user's cache.

if (appCache.status == window.applicationCache.UPDATEREADY) {
  appCache.swapCache();  // The fetch was successful, swap in the new cache.
}
            

L'API est très complète et propose d'autres événements : onerror, onupdate, ondownloading, onprogress, etc.

Prévenir l'utilisateur que le cache a été mis à jour...

// Check if a new cache is available on page load.
window.addEventListener('load', function(e) {

  window.applicationCache.addEventListener('updateready', function(e) {

    if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
      // Browser downloaded a new app cache.
      // Swap it in and reload the page to get the new hotness.
      window.applicationCache.swapCache();

      if (confirm('Une nouvelle version du site est disponible : mise à jour du cache.')) {
        window.location.reload();
      }
    } else {
      // Manifest didn't changed. Nothing new to server.
    }
  }, false);

}, false);
                

Informations complémentaires

  • Il est difficile de déterminer la liste des fichiers à cacher: des outils existent !
  • On peut cacher des ressources externes (i.e. jQuery, etc.), sauf si accessibles par https:// !
  • Ce dernier point est très discuté (Chrome par exemple ne suit pas ce point de la spécification).

Comment tester l'espace disponible ? La Quota API!

La quota API est déjà disponible (seul Chrome l'implémente complètement pour le moment)

Un exemple de démonstration sur jsbin.com

function showFreeSpace() {
   //the type can be either TEMPORARY or PERSISTENT
   webkitStorageInfo.queryUsageAndQuota(webkitStorageInfo.TEMPORARY, onSuccess,
                                        onError);
}

function onSuccess(usedSpace, remainingSpace) {
   var displaySpace = document.querySelector("#space");
   displaySpace.innerHTML = "Used space = " + usedSpace + ", remaining space = "
   + remainingSpace;
}

function onError(error) {
   console.log('Error', error);
}
                

Web storage: enregistrer des données (strings/JSON objects) côté client...

Ce formulaire sauvegarde son contenu automatiquement. Essayez de recharger cette page !
Remarque : avec certains navigateurs, cette page doit être accédé par http:// pas par file://

Code source.

Web Storage est juste un datastore clé/valeur

  • Les clés et valeurs sont des strings/json,
  • Deux interfaces: localStorage et sessionStorage,
  • localStorage: durée de vie des données infinie, partageables entre tabs/windows du même domaine / même browser,
  • sessionStorage: durée de vie = celle de l'onglet/fenêtre, valeurs non partageables.
// store data
localStorage.firstName = "Michel";

// retrieve data
var firstName = localStorage.firstName;
                

Exemple

 <head>
  <script>
    // store data
    localStorage.lastName = "Buffa";
    localStorage.firstName = "Michel";

    function getData() {
       // retrieve data
       document.querySelector("#lastName").innerHTML = localStorage.lastName;
       document.querySelector("#firstName").innerHTML = localStorage.firstName;
    }
  </script>
</head>
<body onload="getData()">
  <h1>Data retrieved from localStorage</h1>
  <ul>
    <li>Last name : <span id="lastName"></span></li>
    <li>First name : <span id="firstName"></span></li>
  </ul>
            

Exemple (clicker la première image pour tester en ligne)...

With Chrome dev tools:

Exemple complet d'un formulaire (cliquer l'image) :



HTML5 File API

Avant HTML5:

  • Forms avec input <type=file ... />
  • Envoi de fichiers via multipart.
  • Aucun accès aux fichiers client-side, depuis du JavaScript...

Avec HTML5 File API:

  • Possibilité d'obtenir des details sur un fichier (nom, taille, date de modification, type)
  • Possibilité de lire son contenu,
  • Intégration naturelle avec XHR2 (Ajax++),
  • upload/download facilité (barres de progression)...

Obtenir les détails sur un fichier (click image)


Select one or more files: <input type="file" id="input">
...
var selectedFile = document.getElementById('input').files[0];
var name = selectedFile.name;
var size = selectedFile.size;
var type = selectedFile.type;
var date = selectedFile.lastModifiedDate;
...
                

Obtenir les détails sur un fichier, code complet

<!DOCTYPE html>
<html>
<head>
  <script>
    function displayFirstSelectedFile() {
      var selectedFile = document.getElementById('input').files[0];
      document.querySelector("#singleName").innerHTML = selectedFile.name;
      document.querySelector("#singleSize").innerHTML = selectedFile.size + "  bytes";
      document.querySelector("#singleType").innerHTML = selectedFile.type;
    }
  </script>
</head>
<body>
  Select one or more files: <input type="file" id="input">
  <br/>
  <button onclick="displayFirstSelectedFile()">Click me to see details about the
                selected files</button>
  <ul>
    <li>First selected file name is: <span id="singleName"></span></li>
    <li>First selected file name is: <span id="singleSize"></span></li>
    <li>First selected file name is: <span id="singleType"></span></li>
  </ul>
</body>
</html>
            

Lecture du contenu d'un fichier

L'interface FileReader :

FileReader reader = new FileReader();
            

3 methodes:

  1. reader.readAsText(File, opt_encoding) .
  2. reader.readAsDataURL(File)
  3. reader.readAsArrayBuffer(File)

A propos des Data URLs

Un data URL est un URL qui inclut le contenu et le type en même temps. Voici un exemple de carré rouge, sous forme de data URL. Copiez et collez le dans la barre d'adresse d'un navigateur, vous devriez voir le carré rouge.

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==
            
<img width=50 src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA
AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red square" />
            
Red square

Exemple d'utilisation de readFileAsDataURL (click image)

var reader = new FileReader();

reader.onload = function(e) {
    // Render thumbnail. e.target.result is the data URL of the image
    var span = document.createElement('span');
    span.innerHTML = "<img class='thumb' src='" + e.target.result + "'/>";
    document.getElementById('list').insertBefore(span, null);
};

// Read in the image file as a data URL.
reader.readAsDataURL(f);

Un formulaire qui sauvegarde des images en data URLs dans le sessionStorage


Exemple de lecture d'un fichier texte avec readFileAsText (clicker image)


var reader = new FileReader();

//read the file content.
reader.onload = function(e) {
  alert('Text file content :\n\n' + e.target.result);
};

// Read the file as text
reader.readAsText(f);

Example de lecture binaire avec readFileAsArrayBuffer (click image)


// User selects file, read it as an ArrayBuffer and pass to the API.
var fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', function(e) {
  var reader = new FileReader();

  reader.onload = function(e) {
    initSound(this.result);
  };
  // THIS IS THE INTERESTING PART!
  reader.readAsArrayBuffer(this.files[0]);
}, false);

XmlHTTPrequest level 2 (XHR2)

Download de fichiers binaires avec le type ArrayBuffer

Ce code aussi avec l'exemple précédent (lecture d'un fichier binaire) :

// Load sound sample  from a URL with XHR2 as an ArrayBuffer.
function loadSoundFile(url) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'arraybuffer'; // THIS IS NEW WITH HTML5!

  xhr.onload = function(e) {
    initSound(this.response); // this.response is an ArrayBuffer
                              // a binary file, native format
  };
  xhr.send();
}
     

Upload de fichier avec File API et XhR2 (clicker image)


Upload de fichier avec File API et XhR2

<input id="file" type="file" />

    <script>
        var fileInput = document.querySelector('#file');

        fileInput.onchange = function() {

            var xhr = new XMLHttpRequest();
            xhr.open('POST', 'upload.html'); // With FormData, POST is mandatory

            xhr.onload = function() {
                alert('Upload complete!');
            };

            var form = new FormData();
            form.append('file', fileInput.files[0]);
            // send the request
            xhr.send(form);
        };
    </script>
                

Propriétés loaded et total de l'évènement passé à upload.onprogress.

<progress id="progress"></progress>
<script>
  var fileInput = document.querySelector('#file'),
  progress = document.querySelector('#progress');

  fileInput.onchange = function() {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'upload.html');

    xhr.upload.onprogress = function(e) {
        progress.value = e.loaded;
        progress.max = e.total;
    };

    xhr.onload = function() {
        alert('Upload complete!');
    };

    var form = new FormData();
    form.append('file', fileInput.files[0]);
    xhr.send(form);
  };
</script>
            

Detecter un drag (exemple complet)


Faites un drag sur un élément !

  1. Apples
  2. Oranges
  3. Pears
 <ol ondragstart="dragStartHandler(event)">
     <li draggable="true" data-value="fruit-apple">Apples</li>
     <li draggable="true" data-value="fruit-orange">Oranges</li>
     <li draggable="true" data-value="fruit-pear">Pears</li>
  </ol>
            

Détecter un drop et faire quelque chose avec les éléments draggés

Trois étapes:

  • Etape 1: dans le dragstart handler, copier une valeur dans le drag'n'drop clipboard, pour la récupérer plus tard,
  • function dragStartHandler(event) {
         console.log('dragstart event, target: ' + event.target);
    
         // Copy in the drag'n'drop clipboard the value of the data*
         // attribute of the target, with a type "Fruit".
         event.dataTransfer.setData("Fruit",
                         event.target.dataset.value);
     }
                    
  • Etape 2: définir une "drop zone",
  • <div ondragover="return false" ondrop="dropHandler(event);">
        Drop your favorite fruits below:
        <ol id="droppedFruits"></ol>
    </div>
                    
  • Etape 3: écrire un drop handler, récupérer le contenu copié, en faire quelque chose.
  • function dropHandler(event) {
        console.log('drop event, target: ' + event.target);
        ...
        // get the data from the drag'n'drop clipboard, with a
        // type="Fruit"
        var data = event.dataTransfer.getData("Fruit");
        ...
    }
                    

Exemple complet (complete source)

drag'n'droppez un élément !

  1. Apples
  2. Oranges
  3. Pears
Droppez ici :

    Styler les différentes actions avec les propriétés dropEffect et effectAllowed de event.dataTransfer.

    Valeurs possibles: copy, move, link, etc. ou changer l'apparence du curseur (image custom par ex.)

    Drag'n'drop de fichiers + upload et barre de progression


    Dragging de fichier du browser vers le bureau

    A Brazilian monster, at last!

    JS Bin

    IndexedDB: une BD NoSQL transactionnelle dans votre browser !

    Pour applications en provenance des "stores" (Chrome Store ou Windows Store), ou accès concurrents aux données par de multiples Workers.

    C'est un object store JS,

    API asynchrone, pas très compliquée mais non triviale.

    La plupart des exemples sur le Web sont obsolètes et ne fonctionnent pas.

    Voyons quelques exemples qui fonctionnent !

    Conclusion sur la persistence HTML5

    Vous avez besoin de transactions, de faire des recherches dans de gros volumes de données, alors utilisez IndexedDB,

    Vous devez simplement gérer des paires clé/valeur, utilisez localStorage/sessionStorage.

    Applications offline, accès rapide : aujourd'hui, utilisez la cache API, demain Service Worker, ou mettez vous à IndexedDB.

    Vous rêvez de faire du SQL , utilisez WebSQL. Oops, non, pardon, c'est mort WebSQL...