Introduction
IndexedDB est un moyen efficace de stocker des données côté client. Si vous ne l'avez pas encore fait, nous vous encourageons à lire les tutoriels MDN utiles sur le sujet. Cet article suppose que vous disposez de connaissances de base sur les API et les fonctionnalités. Même si vous n'avez jamais vu IndexedDB auparavant, nous espérons que la démonstration de cet article vous donnera une idée de ce qu'il est possible de faire avec.
Notre démonstration est une simple démonstration de faisabilité d'une application intranet pour une entreprise. L'application permettra aux employés de rechercher d'autres employés. Pour offrir une expérience plus rapide et plus réactive, la base de données des employés est copiée sur l'ordinateur du client et stockée à l'aide d'IndexedDB. La démonstration fournit simplement une recherche de type saisie semi-automatique et l'affichage d'un seul enregistrement d'employé. Toutefois, une fois que ces données sont disponibles sur le client, nous pouvons les utiliser de différentes manières. Voici un aperçu de ce que notre application doit faire.
- Nous devons configurer et initialiser une instance d'IndexedDB. La plupart du temps, c'est simple, mais faire en sorte que cela fonctionne à la fois dans Chrome et Firefox s'avère un peu délicat.
- Nous devons vérifier si nous disposons de données et, si ce n'est pas le cas, les télécharger. En général, cela se fait via des appels AJAX. Pour notre démonstration, nous avons créé une classe utilitaire simple pour générer rapidement des données fictives. L'application devra reconnaître quand elle crée ces données et empêcher l'utilisateur de les utiliser d'ici là. Il s'agit d'une opération unique. La prochaine fois que l'utilisateur exécutera l'application, il n'aura pas à suivre ce processus. Une démonstration plus avancée gérerait les opérations de synchronisation entre le client et le serveur, mais cette démonstration est davantage axée sur les aspects de l'interface utilisateur.
- Lorsque l'application est prête, nous pouvons utiliser le contrôle Autocomplete de jQuery UI pour la synchroniser avec IndexedDB. Bien que la commande de saisie semi-automatique permette d'utiliser des listes et des tableaux de données de base, elle dispose d'une API permettant d'utiliser n'importe quelle source de données. Nous allons vous montrer comment l'utiliser pour vous connecter à vos données IndexedDB.
Premiers pas
Cette démonstration comporte plusieurs parties. Pour commencer simplement, examinons la partie HTML.
<form>
<p>
<label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
</p>
</form>
<div id="displayEmployee"></div>
Pas grand-chose, n'est-ce pas ? Cette UI comporte trois aspects principaux qui nous intéressent. Le premier est le champ "name", qui sera utilisé pour la saisie semi-automatique. Il se charge à l'état désactivé et sera activé ultérieurement via JavaScript. La balise span à côté est utilisée lors de l'amorçage initial pour fournir des mises à jour à l'utilisateur. Enfin, la div avec l'ID "displayEmployee" sera utilisée lorsque vous sélectionnerez un employé dans la suggestion automatique.
Examinons maintenant le code JavaScript. Cela fait beaucoup d'informations à assimiler, alors nous allons procéder étape par étape. Le code complet sera disponible à la fin pour que vous puissiez le voir dans son intégralité.
Tout d'abord, il existe des problèmes de préfixe dont nous devons nous préoccuper parmi les navigateurs compatibles avec IndexedDB. Voici un code issu de la documentation Mozilla, modifié pour fournir des alias simples pour les composants IndexedDB de base dont notre application a besoin.
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
Ensuite, quelques variables globales que nous utiliserons tout au long de la démonstration :
var db;
var template;
Nous allons maintenant commencer par le bloc jQuery "document ready" :
$(document).ready(function() {
console.log("Startup...");
...
});
Notre démonstration utilise Handlebars.js pour afficher les informations sur les employés. Il ne sera utilisé que plus tard, mais nous pouvons compiler notre modèle dès maintenant pour ne plus avoir à nous en soucier. Nous avons configuré un bloc de script en tant que type reconnu par Handlebars. Il n'est pas très sophistiqué, mais il facilite l'affichage du code HTML dynamique.
<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>
Il est ensuite recompilé dans notre code JavaScript comme suit :
//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);
Commençons maintenant à travailler avec notre IndexedDB. Tout d'abord, nous l'ouvrons.
var openRequest = indexedDB.open("employees", 1);
L'ouverture d'une connexion à IndexedDB nous permet de lire et d'écrire des données, mais avant cela, nous devons nous assurer que nous disposons d'un objectStore. Un objectStore est semblable à une table de base de données. Une base de données IndexedDB peut contenir plusieurs objectStores, chacun contenant une collection d'objets associés. Notre démonstration est simple et ne nécessite qu'un seul objectStore, que nous appelons "employee". Lorsque l'indexedDB est ouverte pour la première fois ou lorsque vous modifiez la version dans le code, un événement onupgradeneeded est exécuté. Nous pouvons l'utiliser pour configurer notre objectStore.
// Handle setup.
openRequest.onupgradeneeded = function(e) {
console.log("running onupgradeneeded");
var thisDb = e.target.result;
// Create Employee
if(!thisDb.objectStoreNames.contains("employee")) {
console.log("I need to make the employee objectstore");
var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
objectStore.createIndex("searchkey", "searchkey", {unique: false});
}
};
openRequest.onsuccess = function(e) {
db = e.target.result;
db.onerror = function(e) {
alert("Sorry, an unforseen error was thrown.");
console.log("***ERROR***");
console.dir(e.target);
};
handleSeed();
};
Dans le bloc du gestionnaire d'événements onupgradeneeded, nous vérifions si objectStoreNames, un tableau de stores d'objets, contient "employee". Si ce n'est pas le cas, nous l'obligeons simplement à le faire. L'appel createIndex est important. Nous devons indiquer à IndexedDB les méthodes, en dehors des clés, que nous utiliserons pour récupérer les données. Nous allons utiliser une clé appelée "searchkey". Nous y reviendrons plus tard.
L'événement onungradeneeded s'exécutera automatiquement la première fois que nous exécuterons le script. Une fois qu'elle est exécutée ou ignorée lors des exécutions futures, le gestionnaire onsuccess est exécuté. Nous avons défini un gestionnaire d'erreurs simple (et moche), puis nous appelons handleSeed.
Avant de continuer, récapitulons rapidement ce qui se passe ici. Nous ouvrons la base de données. Nous vérifions si notre store d'objets existe. Si ce n'est pas le cas, nous la créons. Enfin, nous appelons une fonction nommée "handleSeed". Passons maintenant à la partie de notre démonstration consacrée à l'amorçage des données.
Donnez-moi des données !
Comme indiqué dans l'introduction de cet article, cette démonstration recrée une application de type intranet qui doit stocker une copie de tous les employés connus. Normalement, cela implique de créer une API basée sur un serveur qui peut renvoyer le nombre d'employés et nous permettre de récupérer des lots d'enregistrements. Imaginez un service simple qui accepte un nombre de départ et renvoie 100 personnes à la fois. Cela peut s'exécuter de manière asynchrone en arrière-plan pendant que l'utilisateur fait autre chose.
Pour notre démonstration, nous allons faire quelque chose de simple. Nous voyons combien d'objets, le cas échéant, nous avons dans notre IndexedDB. Si le nombre est inférieur à un certain seuil, nous créerons simplement de faux utilisateurs. Sinon, nous considérons que la partie "seed" est terminée et nous pouvons activer la partie "autocomplete" de la démo. Examinons handleSeed.
function handleSeed() {
// This is how we handle the initial data seed. Normally this would be via AJAX.
db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
var count = e.target.result;
if (count == 0) {
console.log("Need to generate fake data - stand by please...");
$("#status").text("Please stand by, loading in our initial data.");
var done = 0;
var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
// Generate 1k people
for (var i = 0; i < 1000; i++) {
var person = generateFakePerson();
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
resp = employees.add(person);
resp.onsuccess = function(e) {
done++;
if (done == 1000) {
$("#name").removeAttr("disabled");
$("#status").text("");
setupAutoComplete();
} else if (done % 100 == 0) {
$("#status").text("Approximately "+Math.floor(done/10) +"% done.");
}
}
}
} else {
$("#name").removeAttr("disabled");
setupAutoComplete();
}
};
}
La toute première ligne est un peu complexe, car plusieurs opérations sont enchaînées les unes aux autres. Examinons-la de plus près :
db.transaction(["employee"], "readonly");
Cette opération crée une transaction en lecture seule. Toutes les opérations de données avec IndexedDB nécessitent une transaction d'une manière ou d'une autre.
objectStore("employee");
Obtenez le store d'objets "employee".
count()
Exécutez l'API count qui, comme vous pouvez le deviner, effectue un décompte.
onsuccess = function(e) {
Une fois l'opération terminée, exécutez ce rappel. Dans le rappel, nous pouvons obtenir la valeur du résultat, qui correspond au nombre d'objets. Si le nombre est égal à zéro, nous commençons alors notre processus d'amorçage.
Nous utilisons la div d'état mentionnée précédemment pour indiquer à l'utilisateur que nous allons commencer à récupérer des données. En raison de la nature asynchrone d'IndexedDB, nous avons configuré une variable simple, "done", qui suivra les ajouts. Nous effectuons une boucle et insérons les fausses personnes. La source de cette fonction est disponible dans le téléchargement, mais elle renvoie un objet qui ressemble à ceci :
{
firstname: "Random Name",
lastname: "Some Random Last Name",
department: "One of 8 random departments",
email: "first letter of firstname+lastname@fakecorp.com"
}
À lui seul, cela suffit à définir une personne. Mais nous avons une exigence particulière pour pouvoir effectuer des recherches dans nos données. IndexedDB ne permet pas de rechercher des éléments de manière non sensible à la casse. Par conséquent, nous copions le champ "lastname" dans une nouvelle propriété, "searchkey". Pour rappel, il s'agit de la clé que nous avons dit devoir créer en tant qu'index pour nos données.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
Comme il s'agit d'une modification spécifique au client, elle est effectuée ici plutôt que sur le serveur de backend (ou, dans notre cas, le serveur de backend imaginaire).
Pour effectuer les ajouts à la base de données de manière efficace, vous devez réutiliser la transaction pour toutes les écritures par lot. Si vous créez une transaction pour chaque écriture, le navigateur peut entraîner une écriture sur le disque pour chaque transaction, ce qui dégradera considérablement les performances lors de l'ajout de nombreux éléments (par exemple, une minute pour écrire 1 000 objets).
Une fois l'amorçage terminé, la partie suivante de notre application est déclenchée : setupAutoComplete.
Créer la saisie semi-automatique
Passons maintenant à la partie la plus intéressante : l'association au plug-in Autocomplete de l'UI jQuery. Comme pour la plupart des éléments de l'UI jQuery, nous commençons par un élément HTML de base et l'améliorons en appelant une méthode de constructeur sur celui-ci. Nous avons résumé l'ensemble du processus dans une fonction appelée setupAutoComplete. Examinons maintenant ce code.
function setupAutoComplete() {
//Create the autocomplete
$("#name").autocomplete({
source: function(request, response) {
console.log("Going to look for "+request.term);
$("#displayEmployee").hide();
var transaction = db.transaction(["employee"], "readonly");
var result = [];
transaction.oncomplete = function(event) {
response(result);
};
// TODO: Handle the error and return to it jQuery UI
var objectStore = transaction.objectStore("employee");
// Credit: http://stackoverflow.com/a/8961462/52160
var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
var index = objectStore.index("searchkey");
index.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
result.push({
value: cursor.value.lastname + ", " + cursor.value.firstname,
person: cursor.value
});
cursor.continue();
}
};
},
minLength: 2,
select: function(event, ui) {
$("#displayEmployee").show().html(template(ui.item.person));
}
});
}
La partie la plus complexe de ce code est la création de la propriété source. La commande Autocomplete de l'interface utilisateur jQuery vous permet de définir une propriété source qui peut être personnalisée pour répondre à tous les besoins, même pour nos données IndexedDB. L'API vous fournit la requête (en gros, ce qui a été saisi dans le champ du formulaire) et un rappel de réponse. Vous devez renvoyer un tableau de résultats à ce rappel.
La première chose que nous faisons est de masquer la div displayEmployee. Elle est utilisée pour afficher un employé individuel et, si un employé est déjà chargé, pour l'effacer. Nous pouvons maintenant lancer la recherche.
Nous commençons par créer une transaction en lecture seule, un tableau appelé "result" et un gestionnaire "oncomplete" qui transmet simplement le résultat au contrôle de saisie semi-automatique.
Pour trouver les éléments qui correspondent à notre entrée, nous allons utiliser une astuce de l'utilisateur Fong-Wan Chau de StackOverflow : nous utilisons une plage d'index basée sur l'entrée comme limite inférieure et l'entrée plus la lettre z comme limite supérieure. Notez également que nous mettons le terme en minuscules pour qu'il corresponde aux données en minuscules que nous avons saisies.
Une fois cette opération effectuée, nous pouvons ouvrir un curseur (pensez-y comme à l'exécution d'une requête de base de données) et itérer sur les résultats. Le contrôle de saisie semi-automatique de jQuery UI vous permet de renvoyer n'importe quel type de données, mais nécessite au minimum une clé de valeur. Nous définissons la valeur sur une version correctement formatée du nom. Nous renvoyons également l'intégralité de la personne. Vous allez bientôt voir pourquoi. Voici d'abord une capture d'écran de la saisie semi-automatique en action. Nous utilisons le thème Vader pour l'UI jQuery.
Cela suffit pour renvoyer les résultats de nos correspondances IndexedDB à la saisie semi-automatique. Mais nous voulons également pouvoir afficher une vue détaillée du match lorsqu'un match est sélectionné. Nous avons spécifié un gestionnaire de sélection lors de la création de la saisie semi-automatique qui utilise le modèle Handlebars précédent.