Stockage côté client. Il s'agit d'un terme général qui englobe plusieurs API distinctes mais liées : stockage Web, base de données Web SQL, base de données indexée et accès aux fichiers. Chaque technologie offre une manière unique de stocker les données sur le disque dur de l'utilisateur, plutôt que sur le serveur où les données sont généralement stockées. Cela se fait principalement pour les deux raisons suivantes : (a) rendre l'application Web disponible hors ligne (b) améliorer les performances ; Pour une explication détaillée de l'utilisation du stockage côté client, consultez l'article HTML5Rocks « Hors ligne » : qu'est-ce que cela signifie ? Pourquoi devrais-je m'en soucier ? »
Jetons un coup d'œil à ce qu'ils ont en commun :
Fonctionnalités communes
Stockage basé sur le client
En fait, « stockage du temps client » signifie Oui, les données sont transmises à l'API de stockage du navigateur, qui stocke les données dans une zone de l'appareil local. C'est également dans cette zone qu'elles stockent d'autres informations spécifiques à l'utilisateur telles que les préférences personnelles et le cache. En plus de stocker des données, ces API peuvent être utilisées pour récupérer des données et, dans certains cas, effectuer des recherches et des opérations par lots.
Sandbox
Les quatre API de stockage lient les données à une seule « origine ».
Limites d'espace (quotas)
Vous pouvez imaginer le chaos si un site Web était autorisé à remplir des gigaoctets de données sur un disque dur sans méfiance. Les navigateurs imposent donc des limites à la capacité de stockage. Si votre application tente de dépasser la limite, le navigateur affichera généralement une boîte de dialogue demandant à l'utilisateur de confirmer l'augmentation. Vous pourriez penser que les navigateurs imposent la même limite individuelle sur tout le stockage pouvant être utilisé par une seule origine, mais la plupart des mécanismes de stockage sont limités individuellement. Cela peut changer si l'API Quota est adoptée. Mais pour l’instant, considérons le navigateur comme une matrice bidimensionnelle dont les dimensions sont l’origine et le stockage. abc.example peut autoriser jusqu'à 5 Mo de stockage Web et 25 Mo de base de données Web SQL, mais il est interdit d'utiliser la base de données indexée car l'accès est refusé à l'utilisateur. L'API Quota rassemble le problème et vous permet d'interroger la quantité d'espace disponible et la quantité utilisée.
Dans certains cas, les utilisateurs peuvent également voir à l'avance la quantité de stockage qui sera utilisée. Par exemple, lorsque les utilisateurs installent une application dans le Chrome App Store, ils seront invités à pré-accepter ses autorisations, qui incluent le stockage. limites. Il peut y avoir une valeur dans le manifeste (pour l'application) qui est « unlimited_storage ».
Traitement de la base de données (Transactions)
Deux formats de stockage « base de données » prennent en charge le traitement des données. La finalité est la même que celle du traitement normal des données d’une base de données relationnelle : garantir l’intégrité de la base de données. Les transactions de base de données empêchent les « conditions de concurrence », c'est-à-dire les situations dans lesquelles deux séquences d'opérations sont appliquées à la base de données en même temps, ce qui entraîne des résultats imprévisibles et une exactitude douteuse de la base de données.
Modes synchrones et asynchrones
La plupart des formats de stockage prennent en charge les modes synchrones et asynchrones. Le mode synchrone est bloquant, ce qui signifie que l'opération de stockage sera complètement exécutée avant l'exécution de la ligne suivante de code js. Le mode asynchrone entraînera l'exécution du code js suivant avant la fin de l'opération de base de données. L'opération de stockage sera effectuée en arrière-plan. Une fois l'opération terminée, l'application recevra une notification sous la forme d'une fonction de rappel appelée. Cette fonction doit être spécifiée lors de l'appel.
Vous devriez essayer d'éviter d'utiliser le mode synchrone. Bien que cela semble relativement simple, cela bloquera le rendu de la page une fois l'opération terminée, et dans certains cas bloquera même l'intégralité du navigateur. Vous avez peut-être remarqué que cela se produit sur des sites Web ou même sur des applications. Lorsque vous cliquez sur un bouton, tout devient inutilisable. Lorsque vous vous demandez encore, est-ce qu'il s'est écrasé ? En conséquence, tout est soudainement revenu à la normale.
Certaines API n'ont pas de mode asynchrone, comme "localStorage". Lorsque vous utilisez ces API, vous devez surveiller attentivement les performances et être prêt à passer à une API asynchrone si cela pose des problèmes.
Présentation et comparaison de l'API
Web Storage
Web Storage est un objet persistant appelé localStorage. Vous pouvez utiliser localStorage.foo = "bar" pour enregistrer la valeur, puis utiliser localStorage.foo pour l'obtenir, même après la fermeture et la réouverture du navigateur. Vous pouvez également utiliser un objet appelé sessionStorage, qui fonctionne de la même manière, sauf qu'il sera effacé à la fermeture de la fenêtre.
Le stockage Web est un type de magasin de valeurs-clés NoSQL
Avantages du stockage Web
Depuis plusieurs années, il est utilisé par tous les navigateurs modernes. Oui, c'est le cas. également pris en charge sous les systèmes iOS et Android (IE le prend en charge à partir d'IE8).
Signature API simple.
API synchrone, facile à appeler.
Les événements sémantiques maintiennent les autres onglets et fenêtres synchronisés.
Faiblesses du stockage Web
Mauvaises performances lors du stockage de données volumineuses ou complexes à l'aide de l'API synchrone (qui est le mode le plus largement pris en charge).
L'absence d'index entraîne de mauvaises performances lors de la récupération de données volumineuses ou complexes. (Les opérations de recherche nécessitent une itération manuelle de tous les éléments.)
Les performances sont médiocres lors du stockage ou de la lecture de structures de données volumineuses ou complexes, car elles doivent être sérialisées manuellement en chaînes ou désérialisées à partir de chaînes. Les principales implémentations de navigateurs ne prennent en charge que les chaînes (bien que la spécification ne le dise pas).
La continuité et l'intégrité des données doivent être assurées car les données sont effectivement non structurées.
Base de données Web SQL
Web SQL Database est une base de données structurée avec toutes les fonctionnalités et la complexité d'une base de données relationnelle SQL typique. La base de données indexée se situe quelque part entre les deux. Web SQL Database possède des paires clé-valeur de forme libre, un peu comme Web Storage, mais a également la possibilité d'indexer les champs à partir de ces valeurs, ce qui rend les recherches beaucoup plus rapides.
Avantages de la base de données Web SQL
Supporté par les principaux navigateurs mobiles (navigateur Android, Mobile Safari, Opera Mobile) ainsi que certains navigateurs PC (Chrome, Safari, Opera).
En tant qu'API asynchrone, les performances globales sont très bonnes. L'interaction avec la base de données ne verrouille pas l'interface utilisateur. (L'API synchrone est également disponible pour les WebWorkers.)
Bonnes performances de recherche car les données peuvent être indexées en fonction des clés de recherche.
Puissant car il prend en charge le modèle de base de données transactionnelle.
Les structures de données rigides facilitent le maintien de l'intégrité des données.
Faiblesses de la base de données Web SQL
Obsolète, ne sera pas prise en charge par IE ou Firefox et peut être progressivement supprimée des autres navigateurs à un moment donné.
La courbe d'apprentissage est abrupte et nécessite une connaissance des bases de données relationnelles et de SQL.
Inadéquation d'impédance objet-relationnelle.
Agilité réduite car le schéma de la base de données doit être prédéfini et tous les enregistrements de la table doivent correspondre à la même structure.
Base de données indexée (IndexedDB)
Jusqu'à présent, nous avons vu que le stockage Web et la base de données Web SQL présentent diverses forces et faiblesses. La base de données indexée est née de l'expérience de ces deux premières API et peut être considérée comme une tentative de combiner les avantages des deux sans en subir les inconvénients.
La base de données indexée est une collection de « magasins d'objets » dans lesquels des objets peuvent être placés directement. Ce stockage s'apparente un peu à une table SQL, mais dans ce cas il n'y a aucune contrainte sur la structure des objets, donc rien n'a besoin d'être défini au préalable. C'est donc un peu comme le stockage Web, avec plusieurs bases de données et chaque base de données ayant plusieurs magasins. Mais contrairement au Web Storage, il présente également des avantages importants en termes de performances : une interface asynchrone qui peut créer des index sur le stockage pour augmenter la vitesse de recherche.
Avantages d'IndexedDB
Dans l'ensemble, fonctionne bien en tant qu'API asynchrone. L'interaction avec la base de données ne verrouille pas l'interface utilisateur. (L'API Sync est également disponible pour les WebWorkers.)
Bonnes performances de recherche car les données peuvent être indexées en fonction des clés de recherche.
Prend en charge le contrôle de version.
Puissant car il prend en charge le modèle de base de données transactionnelle.
Parce que le modèle de données est simple, la courbe d'apprentissage est également assez simple.
Bon support des navigateurs : Chrome, Firefox, mobile FF, IE10.
Faiblesses d'IndexedDB
API très complexe, entraînant un grand nombre de rappels imbriqués.
FileSystem
Les API ci-dessus conviennent au texte et aux données structurées, mais lorsqu'il s'agit de fichiers volumineux et de contenu binaire, nous avons besoin d'autre chose. Heureusement, nous disposons désormais du standard API FileSystem. Il donne à chaque domaine un système de fichiers hiérarchique complet, et au moins sous Chrome, ce sont de vrais fichiers sur le disque dur de l'utilisateur. En termes de lecture et d'écriture de fichiers individuels, l'API s'appuie sur l'API de fichiers existante.
L'API FileSystem (File System) présente des avantages
qui permettent de stocker une grande quantité de contenu et de fichiers binaires, adaptés aux images, audio, vidéo, PDF, etc.
En tant qu'API asynchrone, elle offre de bonnes performances.
Faiblesses de l'API FileSystem
Standard très ancien, uniquement pris en charge par Chrome et Opera.
Aucune prise en charge des transactions.
Pas de support de recherche/indexation intégré.
Regardez le code
Cette section compare la manière dont différentes API résolvent le même problème. Cet exemple est un système d'enregistrement « géo-humeur », dans lequel vous pouvez enregistrer votre humeur dans le temps et dans l'espace. Les interfaces vous permettent de basculer entre les types de bases de données. Bien sûr, dans une situation réelle, cela peut sembler un peu artificiel, le type de base de données aurait certainement plus de sens que l'autre, et l'API du système de fichiers ne serait tout simplement pas adaptée à cette application ! Mais à des fins de démonstration, il est utile de voir différentes manières d’obtenir le même résultat. Notez également que certains extraits de code ont été refactorisés pour préserver la lisibilité.
Vous pouvez désormais essayer notre application « géo-humeur ».
Pour rendre la démo plus intéressante, nous séparons le stockage des données et utilisons des techniques de conception orientées objet standard. La logique de l'interface utilisateur sait uniquement qu'il existe un magasin ; elle n'a pas besoin de savoir comment le magasin est implémenté car les méthodes sont les mêmes pour chaque magasin. Par conséquent, le code de la couche UI peut être appelé store.setup(), store.count() et ainsi de suite. En fait, notre magasin dispose de quatre implémentations, une pour chaque type de stockage. Au démarrage de l'application, l'URL est vérifiée et le magasin correspondant est instancié.
Pour maintenir la cohérence de l'API, toutes les méthodes sont asynchrones, c'est-à-dire qu'elles renvoient les résultats à l'appelant. L'implémentation de Web Storage est même comme ça, l'implémentation sous-jacente est native.
Dans la démo suivante, nous ignorerons l'interface utilisateur et la logique de positionnement et nous concentrerons sur la technologie de stockage.
Créer un magasin
Pour localStorage, nous effectuons une simple vérification pour voir si le stockage existe. S'il n'existe pas, créez un nouveau tableau et stockez-le dans localStorage sous la clé d'enregistrement. Tout d’abord, nous utilisons un objet JSON pour sérialiser la structure en chaîne, car la plupart des navigateurs ne prennent en charge que le stockage de chaînes.
if (!localStorage.checkins) localStorage.checkins = JSON.stringify([]);
Pour Web SQL Database, si la structure de la base de données n'existe pas, nous devons d'abord la créer. Heureusement, la méthode openDatabase créera automatiquement la base de données si elle n'existe pas ; de la même manière, l'utilisation de l'instruction SQL "if not exist" garantit que la nouvelle table checksins ne sera pas écrasée si elle existe déjà. Nous devons définir à l'avance la structure des données, c'est-à-dire le nom et le type de chaque colonne de la table checksins. Chaque ligne de données représente un enregistrement.
this.db = openDatabase('geomood', '1.0', 'Geo-Mood Checkins', 8192);this.db.transaction(function(tx) { tx.executeSql( "create table if not exists " + "checkins(id integer primary key asc, time integer, latitude float," + "longitude float, mood string)", [], function() { console.log("siucc"); } ); });
La base de données indexée nécessite un certain travail pour démarrer, car elle nécessite l'activation d'un système de gestion des versions de base de données. Lorsque nous nous connectons à la base de données, nous devons indiquer clairement de quelle version nous avons besoin. Si la base de données actuelle utilise une version précédente ou n'a pas encore été créée, l'événement onupgradeneeded sera déclenché. Une fois la mise à niveau terminée, l'événement onsuccess sera déclenché. être déclenché. S'il n'est pas nécessaire de procéder à une mise à niveau, l'événement onsuccess sera déclenché immédiatement.
Une autre chose à faire est de créer un index « d'humeur » afin que les émotions correspondantes puissent être interrogées rapidement plus tard.
var db;var version = 1; window.indexedStore = {}; window.indexedStore.setup = function(handler) { // attempt to open the database var request = indexedDB.open("geomood", version); // upgrade/create the database if needed request.onupgradeneeded = function(event) { var db = request.result; if (event.oldVersion < 1) { // Version 1 is the first version of the database. var checkinsStore = db.createObjectStore("checkins", { keyPath: "time" }); checkinsStore.createIndex("moodIndex", "mood", { unique: false }); } if (event.oldVersion < 2) { // In future versions we'd upgrade our database here. // This will never run here, because we're version 1. } db = request.result; }; request.onsuccess = function(ev) { // assign the database for access outside db = request.result; handler(); db.onerror = function(ev) { console.log("db error", arguments); }; }; };
Enfin, démarrez FileSystem. Nous encoderons chaque JSON d'enregistrement dans un fichier séparé, et ils se trouveront dans le répertoire "checkins/". Encore une fois, ce n'est pas l'utilisation la plus appropriée de l'API FileSystem, mais elle convient à des fins de démonstration.
Commencez à contrôler l'ensemble du système de fichiers pour vérifier le répertoire "checkins/". Si le répertoire n'existe pas, utilisez getDirectory pour le créer.
setup: function(handler) { requestFileSystem( window.PERSISTENT, 1024*1024, function(fs) { fs.root.getDirectory("checkins", {}, // no "create" option, so this is a read op function(dir) { checkinsDir = dir; handler(); }, function() { fs.root.getDirectory( "checkins", {create: true }, function(dir) { checkinsDir = dir; handler(); }, onError ); } ); }, function(e) { console.log("error "+e.code+"initialising - see http:php.cn"); } ); }
Enregistrer un enregistrement (Check-in)
En utilisant localStorage, il suffit de retirer le tableau d'enregistrement, d'en ajouter un à la fin, puis de l'enregistrer encore. Nous devons également utiliser la méthode objet JSON pour l'enregistrer sous forme de chaîne.
var checkins = JSON.parse(localStorage["checkins"]); checkins.push(checkin); localStorage["checkins"] = JSON.stringify(checkins);
Avec Web SQL Database, tout se passe dans une transaction. Nous voulons créer une nouvelle ligne dans la table checksins. Il s'agit d'un simple appel SQL. Nous utilisons la syntaxe "?" au lieu de mettre toutes les données d'enregistrement dans la commande "insert". Les données réelles - les quatre valeurs que nous souhaitons enregistrer - sont placées sur la deuxième ligne. Les éléments "?" seront remplacés par ces valeurs (checkin.time, checkin.latitude, etc.). Les deux paramètres suivants sont des fonctions appelées une fois l'opération terminée, appelées respectivement après le succès et l'échec. Dans cette application, nous utilisons le même gestionnaire d'erreurs commun pour toutes les opérations. De cette façon, la fonction de rappel de réussite est le handle que nous transmettons à la fonction de recherche - assurez-vous que le handle est appelé en cas de succès afin que l'interface utilisateur puisse être avertie lorsque l'opération est terminée (par exemple, pour mettre à jour le nombre d'enregistrements). jusqu'à présent).
store.db.transaction(function(tx) { tx.executeSql("insert into checkins " + "(time, latitude, longitude, mood) values (?,?,?,?);", [checkin.time, checkin.latitude, checkin.longitude, checkin.mood], handler, store.onError ); });
Une fois le stockage configuré, le stocker dans IndexedDB est à peu près aussi simple que Web Storage, avec l'avantage de travailler de manière asynchrone.
var transaction = db.transaction("checkins", 'readwrite'); transaction.objectStore("checkins").put(checkin); transaction.oncomplete = handler;
Utilisez l'API FileSystem pour créer un nouveau fichier et obtenir le handle correspondant, qui peut être rempli avec l'API FileWriter.
fs.root.getFile( "checkins/" + checkin.time, { create: true, exclusive: true }, function(file) { file.createWriter(function(writer) { writer.onerror = fileStore.onError; var bb = new WebKitBlobBuilder; bb.append(JSON.stringify(checkin)); writer.write(bb.getBlob("text/plain")); handler(); }, fileStore.onError); }, fileStore.onError );
Rechercher des correspondances
La fonction suivante trouve tous les enregistrements qui correspondent à une émotion spécifique, par exemple, l'utilisateur peut voir quand et où il a passé un bon moment récemment. En utilisant localStorage, nous devons parcourir manuellement chaque enregistrement et le comparer au sentiment de la recherche, en créant une liste de correspondances. La meilleure pratique consiste à renvoyer un clone des données stockées, plutôt que l'objet réel, car la recherche doit être une opération en lecture seule ; nous transmettons donc chaque objet d'enregistrement correspondant à la méthode générique clone() pour l'opération.
var allCheckins = JSON.parse(localStorage["checkins"]);var matchingCheckins = []; allCheckins.forEach(function(checkin) { if (checkin.mood == moodQuery) { matchingCheckins.push(clone(checkin)); } }); handler(matchingCheckins);
当然,在 IndexedDB 解决方案使用索引,我们先前在 “mood” 表中创建的索引,称为“moodindex”。我们用一个指针遍历每次签到以匹配查询。注意这个指针模式也可以用于整个存储;因此,使用索引就像我们在商店里的一个窗口前,只能看到匹配的对象(类似于在传统数据库中的“视图”)。
var store = db.transaction("checkins", 'readonly').objectStore("checkins");var request = moodQuery ? store.index("moodIndex").openCursor(new IDBKeyRange.only(moodQuery)) : store.openCursor(); request.onsuccess = function(ev) { var cursor = request.result; if (cursor) { handler(cursor.value); cursor["continue"](); } };
与许多传统的文件系统一样,FileSystem API 没有索引,所以搜索算法(如 Unix中的 “grep” 命令)必须遍历每个文件。我们从 “checkins/” 目录中拿到 Reader API ,通过 readentries() 。对于每个文件,再使用一个 reader,使用 readastext() 方法检查其内容。这些操作都是异步的,我们需要使用 readnext() 将调用连在一起。
checkinsDir.createReader().readEntries(function(files) { var reader, fileCount = 0, checkins = []; var readNextFile = function() { reader = new FileReader(); if (fileCount == files.length) return; reader.onload = function(e) { var checkin = JSON.parse(this.result); if (moodQuery == checkin.mood || !moodQuery) handler(checkin); readNextFile(); }; files[fileCount++].file(function(file) { reader.readAsText(file); }); }; readNextFile(); });
匹配计数
最后,我们需要给所有签到计数。
对localStorage,我们简单的反序列化签到数组,读取其长度。
handler(JSON.parse(localStorage["checkins"]).length);
对 Web SQL Database,可以检索数据库中的每一行(select * from checkins),看结果集的长度。但如果我们知道我们在 SQL 中,有更容易和更快的方式 —— 我们可以执行一个特殊的 select 语句来检索计数。它将返回一行,其中一列包含计数。
store.db.transaction(function(tx) { tx.executeSql("select count(*) from checkins;", [], function(tx, results) { handler(results.rows.item(0)["count(*)"]); }, store.onError); });
不幸的是, IndexedDB 不提供任何计算方法,所以我们只能自己遍历。
var count = 0;var request = db.transaction(["checkins"], 'readonly').objectStore("checkins").openCursor(); request.onsuccess = function(ev) { var cursor = request.result; cursor ? ++count && cursor["continue"]() : handler(count); };
对于文件系统, directory reader 的 readentries() 方法提供一个文件列表,所以我们返回该列表的长度就好。
checkinsDir.createReader().readEntries(function(files) { handler(files.length); });
总结
本文从较高层次的角度,讲述了现代客户端存储技术。你也可以看看 《离线应用概述》(overview on offline apps)这篇文章。