Clientseitige Speicherung. Dies ist ein allgemeiner Begriff, der mehrere separate, aber verwandte APIs umfasst: Webspeicher, Web-SQL-Datenbank, indizierte Datenbank und Dateizugriff. Jede Technologie bietet eine einzigartige Möglichkeit, Daten auf der Festplatte des Benutzers zu speichern – und nicht auf dem Server, auf dem die Daten normalerweise gespeichert sind. Dies geschieht hauptsächlich aus den folgenden zwei Gründen: (a) Offline-Verfügbarkeit der Web-App; (b) Verbesserung der Leistung. Eine ausführliche Erläuterung der clientseitigen Speichernutzung finden Sie im HTML5Rocks-Artikel „Offline“: Was bedeutet das? Warum sollte es mich interessieren?
Werfen wir einen Blick auf die Gemeinsamkeiten:
Gemeinsame Funktionen
Clientbasierter Speicher
Eigentlich bedeutet „Client-Zeitspeicher“. Ja, die Daten werden an die Speicher-API des Browsers übergeben, die die Daten in einem Bereich auf dem lokalen Gerät speichert. In diesem Bereich werden auch andere benutzerspezifische Informationen wie persönliche Präferenzen und Cache gespeichert. Zusätzlich zum Speichern von Daten können diese APIs zum Abrufen von Daten und in einigen Fällen zum Durchführen von Suchvorgängen und Stapelvorgängen verwendet werden.
Sandboxed
Alle vier Speicher-APIs verknüpfen Daten mit einem einzigen „Ursprung“.
Speicherplatzbeschränkungen (Kontingente)
Sie können sich das Chaos vorstellen, wenn eine Website Gigabytes an Daten auf eine ahnungslose Festplatte füllen dürfte. Daher setzen Browser der Speicherkapazität Grenzen. Wenn Ihre App versucht, das Limit zu überschreiten, zeigt der Browser normalerweise ein Dialogfeld an, in dem der Benutzer aufgefordert wird, die Erhöhung zu bestätigen. Man könnte meinen, dass Browser die gleiche individuelle Beschränkung für den gesamten Speicher festlegen, der von einem einzelnen Ursprung genutzt werden kann, aber die meisten Speichermechanismen sind individuell eingeschränkt. Dies kann sich ändern, wenn die Quota-API übernommen wird. Aber stellen Sie sich den Browser zunächst als eine zweidimensionale Matrix vor, deren Dimensionen Ursprung und Speicher sind. abc.example kann bis zu 5 MB Webspeicher und 25 MB Web SQL-Datenbank zulassen, die Verwendung von Indexed DataBase ist jedoch verboten, da dem Benutzer der Zugriff verweigert wird. Die Quota-API fasst das Problem zusammen und ermöglicht die Abfrage, wie viel Speicherplatz verfügbar ist und wie viel genutzt wird.
In einigen Fällen können Benutzer auch sehen, wie viel Speicherplatz verwendet wird. Wenn Benutzer beispielsweise eine App im Chrome App Store installieren, werden sie aufgefordert, deren Berechtigungen, einschließlich Speicher, zu akzeptieren Grenzen. Möglicherweise gibt es im Manifest (der Anwendung) einen Wert, der „unlimited_storage“ lautet.
Datenbankverarbeitung (Transaktionen)
Zwei „Datenbank“-Speicherformate unterstützen die Datenverarbeitung. Der Zweck ist derselbe wie bei der normalen relationalen Datenbankdatenverarbeitung: die Integrität der Datenbank sicherzustellen. Datenbanktransaktionen verhindern „Race Conditions“ – Situationen, in denen zwei Operationssequenzen gleichzeitig auf die Datenbank angewendet werden, was zu unvorhersehbaren Ergebnissen und einem zweifelhaften Genauigkeitsstatus der Datenbank führt.
Synchronische und asynchrone Modi
Die meisten Speicherformate unterstützen synchrone und asynchrone Modi. Der synchrone Modus blockiert, was bedeutet, dass der Speichervorgang vollständig ausgeführt wird, bevor die nächste Zeile des JS-Codes ausgeführt wird. Der asynchrone Modus führt dazu, dass der folgende js-Code ausgeführt wird, bevor der Datenbankvorgang abgeschlossen ist. Der Speichervorgang wird im Hintergrund ausgeführt. Wenn der Vorgang abgeschlossen ist, erhält die Anwendung eine Benachrichtigung in Form einer aufgerufenen Rückruffunktion. Diese Funktion muss beim Aufruf angegeben werden.
Sie sollten versuchen, die Verwendung des synchronen Modus zu vermeiden. Obwohl dies relativ einfach erscheint, blockiert er das Rendern der Seite, wenn der Vorgang abgeschlossen ist, und friert in einigen Fällen sogar den gesamten Browser ein. Möglicherweise haben Sie bemerkt, dass dies bei Websites oder sogar Apps passiert. Wenn Sie auf eine Schaltfläche klicken, wird alles unbrauchbar. Wenn Sie sich immer noch fragen, ist es abgestürzt? Dadurch normalisierte sich plötzlich alles wieder.
Einige APIs verfügen nicht über einen asynchronen Modus, z. B. „localStorage“. Wenn Sie diese APIs verwenden, sollten Sie die Leistung sorgfältig überwachen und bereit sein, auf eine asynchrone API umzusteigen, wenn dies Probleme verursacht.
API-Übersicht und -Vergleich
Web Storage
Web Storage ist ein persistentes Objekt namens localStorage. Sie können localStorage.foo = "bar" verwenden, um den Wert zu speichern, und später localStorage.foo verwenden, um ihn abzurufen – auch nachdem der Browser geschlossen und erneut geöffnet wurde. Sie können auch ein Objekt namens sessionStorage verwenden, das auf die gleiche Weise funktioniert, außer dass es gelöscht wird, wenn das Fenster geschlossen wird.
Web Storage ist eine Art NoSQL-Schlüsselwertspeicher
Vorteile von Web Storage
Es wird seit mehreren Jahren von allen modernen Browsern verwendet. Ja, das ist es wird auch unter iOS- und Android-Systemen unterstützt (IE unterstützt es ab IE8).
Einfache API-Signatur.
Synchrone API, einfach aufzurufen.
Semantische Ereignisse halten andere Registerkarten und Fenster synchron.
Schwächen von Web Storage
Schlechte Leistung beim Speichern großer oder komplexer Daten mithilfe der synchronen API (dem am häufigsten unterstützten Modus).
Ein fehlender Index führt zu schlechter Leistung beim Abrufen großer oder komplexer Daten. (Suchvorgänge erfordern eine manuelle Iteration aller Elemente.)
Die Leistung beim Speichern oder Lesen großer oder komplexer Datenstrukturen ist schlecht, da diese manuell in Zeichenfolgen serialisiert oder aus Zeichenfolgen deserialisiert werden müssen. Wichtige Browserimplementierungen unterstützen nur Zeichenfolgen (obwohl die Spezifikation dies nicht sagt).
Die Kontinuität und Integrität der Daten muss gewährleistet sein, da die Daten praktisch unstrukturiert sind.
Web SQL-Datenbank
Web SQL Database ist eine strukturierte Datenbank, die über die gesamte Funktionalität und Komplexität einer typischen SQL-basierten relationalen Datenbank verfügt. Die indizierte Datenbank liegt irgendwo dazwischen. Die Web-SQL-Datenbank verfügt über Schlüssel-Wert-Paare in freier Form, ähnlich wie Web Storage, bietet aber auch die Möglichkeit, Felder anhand dieser Werte zu indizieren, sodass Suchvorgänge viel schneller sind.
Vorteile der Web SQL-Datenbank
Unterstützt von den wichtigsten mobilen Browsern (Android Browser, Mobile Safari, Opera Mobile) sowie einigen PC-Browsern (Chrome, Safari, Opera).
Als asynchrone API ist die Gesamtleistung sehr gut. Durch die Datenbankinteraktion wird die Benutzeroberfläche nicht gesperrt. (Die synchrone API ist auch für WebWorker verfügbar.)
Gute Suchleistung, da Daten anhand von Suchschlüsseln indiziert werden können.
Leistungsstark, da es das Transaktionsdatenbankmodell unterstützt.
Starre Datenstrukturen erleichtern die Aufrechterhaltung der Datenintegrität.
Schwächen der Web SQL-Datenbank
Veraltet, wird von IE oder Firefox nicht unterstützt und wird möglicherweise irgendwann von anderen Browsern eingestellt.
Die Lernkurve ist steil und erfordert Kenntnisse in relationalen Datenbanken und SQL.
Objektrelationale Impedanzinkongruenz
Reduzierte Agilität, da das Datenbankschema vordefiniert sein muss und alle Datensätze in der Tabelle derselben Struktur entsprechen müssen.
Indexierte Datenbank (IndexedDB)
Bisher haben wir gesehen, dass sowohl Web Storage als auch Web SQL Database verschiedene Stärken und Schwächen haben. Indexed Database entstand aus den Erfahrungen dieser beiden frühen APIs und kann als Versuch angesehen werden, die Vorteile beider zu kombinieren, ohne deren Nachteile in Kauf zu nehmen.
Die indizierte Datenbank ist eine Sammlung von „Objektspeichern“, in denen Objekte direkt abgelegt werden können. Dieser Speicher ähnelt ein wenig einer SQL-Tabelle, in diesem Fall gibt es jedoch keine Einschränkungen hinsichtlich der Struktur der Objekte, sodass nichts im Voraus definiert werden muss. Das ist also ein bisschen wie Web Storage, mit mehreren Datenbanken und jeder Datenbank mit mehreren Speichern. Aber im Gegensatz zu Web Storage bietet es auch wichtige Leistungsvorteile: eine asynchrone Schnittstelle, die Indizes für den Speicher erstellen kann, um die Suchgeschwindigkeit zu erhöhen.
Vorteile von IndexedDB
Insgesamt eine gute Leistung als asynchrone API. Durch die Datenbankinteraktion wird die Benutzeroberfläche nicht gesperrt. (Die Sync-API ist auch für WebWorker verfügbar.)
Gute Suchleistung, da Daten anhand von Suchschlüsseln indiziert werden können.
Unterstützt Versionskontrolle.
Leistungsstark, da es das Transaktionsdatenbankmodell unterstützt.
Da das Datenmodell einfach ist, ist auch die Lernkurve recht einfach.
Gute Browserunterstützung: Chrome, Firefox, Mobile FF, IE10.
Schwächen von IndexedDB
Sehr komplexe API, was zu einer großen Anzahl verschachtelter Rückrufe führt.
Dateisystem
Die oben genannten APIs eignen sich für Text und strukturierte Daten, aber wenn es um große Dateien und binäre Inhalte geht, brauchen wir etwas anderes. Glücklicherweise haben wir jetzt den FileSystem-API-Standard. Es gibt jeder Domain ein vollständiges hierarchisches Dateisystem, und zumindest unter Chrome handelt es sich dabei um echte Dateien auf der Festplatte des Benutzers. Bezüglich des Lesens und Schreibens einzelner Dateien baut die API auf der bestehenden File API auf.
Die FileSystem (Dateisystem)-API hat Vorteile
, die eine große Menge an Inhalten und Binärdateien speichern kann, die für Bilder, Audio, Video, PDF usw. geeignet sind.
Als asynchrone API weist sie eine gute Leistung auf.
Schwächen der FileSystem API
Sehr früher Standard, nur von Chrome und Opera unterstützt.
Keine Transaktionsunterstützung.
Keine integrierte Such-/Indizierungsunterstützung.
Schauen Sie sich den Code an
In diesem Abschnitt wird verglichen, wie verschiedene APIs dasselbe Problem lösen. Bei diesem Beispiel handelt es sich um ein „Geo-Mood“-Check-in-System, mit dem Sie Ihre Stimmung zeitlich und örtlich aufzeichnen können. Über Schnittstellen können Sie zwischen Datenbanktypen wechseln. In einer realen Situation könnte das natürlich etwas gekünstelt wirken, der Datenbanktyp wäre auf jeden Fall sinnvoller als der andere und die Dateisystem-API wäre für diese Anwendung einfach nicht geeignet! Zu Demonstrationszwecken ist es jedoch hilfreich, wenn wir verschiedene Wege sehen, um das gleiche Ergebnis zu erzielen. Beachten Sie außerdem, dass einige Codeausschnitte überarbeitet wurden, um die Lesbarkeit zu gewährleisten.
Jetzt können Sie unsere „Geo-Mood“-Anwendung ausprobieren.
Um die Demo interessanter zu gestalten, trennen wir die Datenspeicherung und verwenden standardmäßige objektorientierte Designtechniken. Die UI-Logik weiß nur, dass es einen Store gibt; sie muss nicht wissen, wie der Store implementiert ist, da die Methoden für jeden Store gleich sind. Daher kann der Code der UI-Ebene „store.setup()“, „store.count()“ usw. heißen. Tatsächlich verfügt unser Geschäft über vier Implementierungen, eine für jeden Speichertyp. Beim Start der Anwendung wird die URL überprüft und der entsprechende Store instanziiert.
Um die API-Konsistenz aufrechtzuerhalten, sind alle Methoden asynchron, d. h. sie geben Ergebnisse an den Aufrufer zurück. Die Implementierung von Web Storage ist sogar so, die zugrunde liegende Implementierung ist nativ.
In der folgenden Demo überspringen wir die Benutzeroberfläche und die Positionierungslogik und konzentrieren uns auf die Speichertechnologie.
Speicher erstellen
Für localStorage führen wir eine einfache Prüfung durch, um zu sehen, ob der Speicher vorhanden ist. Wenn es nicht vorhanden ist, erstellen Sie ein neues Array und speichern Sie es in localStorage unter dem Schlüssel checkins. Zuerst verwenden wir ein JSON-Objekt, um die Struktur in einen String zu serialisieren, da die meisten Browser nur die Speicherung von Strings unterstützen.
if (!localStorage.checkins) localStorage.checkins = JSON.stringify([]);
Wenn für eine Web-SQL-Datenbank die Datenbankstruktur nicht vorhanden ist, müssen wir sie zuerst erstellen. Glücklicherweise erstellt die openDatabase-Methode die Datenbank automatisch, wenn sie nicht vorhanden ist. Durch die Verwendung der SQL-Anweisung „falls nicht vorhanden“ wird sichergestellt, dass die neue Checksins-Tabelle nicht überschrieben wird, wenn sie bereits vorhanden ist. Wir müssen die Datenstruktur im Voraus definieren, dh den Namen und Typ jeder Spalte der Checksins-Tabelle. Jede Datenzeile stellt einen Check-in dar.
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"); } ); });
Der Start einer indizierten Datenbank erfordert einige Arbeit, da dafür ein Datenbankversionierungssystem aktiviert werden muss. Wenn wir eine Verbindung zur Datenbank herstellen, müssen wir klarstellen, welche Version wir benötigen. Wenn die aktuelle Datenbank eine frühere Version verwendet oder noch nicht erstellt wurde, wird das Ereignis onupgradeneeded ausgelöst ausgelöst werden. Wenn kein Upgrade erforderlich ist, wird das onsuccess-Ereignis sofort ausgelöst.
Eine weitere Möglichkeit besteht darin, einen „Stimmungs“-Index zu erstellen, damit passende Emotionen später schnell abgefragt werden können.
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); }; }; };
Zum Schluss starten Sie FileSystem. Wir kodieren jeden Check-in im JSON-Format in einer separaten Datei, die sich im Verzeichnis „checkins/“ befindet. Auch dies ist nicht die am besten geeignete Verwendung der FileSystem-API, für Demonstrationszwecke jedoch in Ordnung.
Besorgen Sie sich einen Zugriff auf das gesamte Dateisystem, um das Verzeichnis „checkins/“ zu überprüfen. Wenn das Verzeichnis nicht vorhanden ist, erstellen Sie es mit getDirectory.
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"); } ); }
Einen Check-in speichern (Check-in)
Mit localStorage müssen wir nur das Check-in-Array herausnehmen, am Ende eines hinzufügen und es dann speichern wieder. Wir müssen auch die JSON-Objektmethode verwenden, um es als Zeichenfolge zu speichern.
var checkins = JSON.parse(localStorage["checkins"]); checkins.push(checkin); localStorage["checkins"] = JSON.stringify(checkins);
Mit Web SQL Database geschieht alles in einer Transaktion. Wir möchten eine neue Zeile in der Checksins-Tabelle erstellen. Dies ist ein einfacher SQL-Aufruf, anstatt alle Check-in-Daten in den Befehl „insert“ einzufügen. Die echten Daten – die vier Werte, die wir speichern wollen – werden in der zweiten Zeile platziert. „?“-Elemente werden durch diese Werte ersetzt (checkin.time, checkin.latitude usw.). Die nächsten beiden Parameter sind Funktionen, die nach Abschluss des Vorgangs bzw. nach Erfolg bzw. Fehler aufgerufen werden. In dieser Anwendung verwenden wir für alle Vorgänge denselben gemeinsamen Fehlerhandler. Auf diese Weise ist die Erfolgsrückruffunktion das Handle, das wir an die Suchfunktion übergeben. Stellen Sie sicher, dass das Handle bei Erfolg aufgerufen wird, damit die Benutzeroberfläche benachrichtigt werden kann, wenn der Vorgang abgeschlossen ist (z. B. um die Anzahl der Check-ins zu aktualisieren). bis jetzt).
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 ); });
Sobald der Speicher eingerichtet ist, ist die Speicherung in IndexedDB ungefähr so einfach wie Web Storage, mit dem Vorteil der asynchronen Arbeitsweise.
var transaction = db.transaction("checkins", 'readwrite'); transaction.objectStore("checkins").put(checkin); transaction.oncomplete = handler;
Verwenden Sie die FileSystem-API, um eine neue Datei zu erstellen und das entsprechende Handle abzurufen, das mit der FileWriter-API gefüllt werden kann.
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 );
Nach Übereinstimmungen suchen
Die nächste Funktion findet alle Check-Ins, die zu einer bestimmten Emotion passen. So kann der Benutzer beispielsweise sehen, wann und wo er in letzter Zeit eine tolle Zeit hatte. Mit localStorage müssen wir jeden Check-in manuell durchlaufen und ihn mit der Stimmung der Suche vergleichen, um eine Liste mit Übereinstimmungen zu erstellen. Eine bessere Vorgehensweise besteht darin, einen Klon der gespeicherten Daten anstelle des tatsächlichen Objekts zurückzugeben, da die Suche ein schreibgeschützter Vorgang sein sollte. Daher übergeben wir jedes passende Eincheckobjekt zur Ausführung an die generische Methode clone().
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)这篇文章。