Client-side storage. This is a general term that encompasses several separate but related APIs: Web Storage, Web SQL Database, Indexed Database, and File Access. Each technology offers a unique way to store data on the user's hard drive—rather than on the server where the data is typically stored. This is mainly done for the following two reasons: (a) making the web app available offline; (b) improving performance. For a detailed explanation of client-side storage usage, see the HTML5Rocks article "Offline": What does it mean? Why should I care?"
Let’s take a look at what they have in common:
Common features
Client-based storage
In fact, the meaning of "client time storage" Yes, the data is passed to the browser's storage API, which stores the data in an area on the local device. This area is also where it stores other user-specific information such as personal preferences and cache. In addition to storing data, these APIs can be used to retrieve data and, in some cases, perform searches and batch operations.
Placed in a sandbox
All four storage APIs tie data to a single "origin".
Space Limits (Quotas)
You can imagine the chaos if any website was allowed to fill gigabytes of data on an unsuspecting hard drive. Therefore, browsers impose limits on storage capacity. If your app attempts to exceed the limit, the browser will usually display a dialog box asking the user to confirm the increase. You might think that browsers impose the same individual limit on all storage that can be used by a single origin, but most storage mechanisms are individually limited. This may change if the Quota API is adopted. But for now, think of the browser as a two-dimensional matrix whose dimensions are origin and storage. abc.example may allow up to 5MB of Web Storage and 25MB of Web SQL database, but is prohibited from using Indexed DataBase because the user is denied access. The Quota API puts the problem together and lets you query how much space is available and how much is in use.
In some cases, users can also see how much storage will be used first. For example, when users install an app in the Chrome App Store, they will be prompted to accept its permissions in advance, including storage limits. . There may be a value in the manifest (of the application) that is "unlimited_storage" (unlimited storage).
Database processing (Transactions)
Two "database" storage formats support data processing. The purpose is the same as that of normal relational database data processing: to ensure the integrity of the database. Database transactions (Transactions) prevent "race conditions" - this is when two sequences of operations are applied to the database at the same time, causing the results of the operations to be unpredictable and the database to be in questionable accuracy. Dubious accuracy status.
Synchronous and Asynchronous Modes
Most storage formats support synchronous and asynchronous modes. Synchronous mode is blocking, which means that the storage operation will be completely executed before the next line of js code is executed. Asynchronous mode will cause the following js code to be executed before the database operation is completed. The storage operation will be performed in the background. When the operation is completed, the application will receive notification in the form of a callback function being called. This function must be specified when calling.
You should try to avoid using synchronous mode. Although it seems relatively simple, it will block page rendering when the operation is completed, and in some cases even freeze the entire browser. You may have noticed this happening to websites or even applications. When you click a button, everything becomes unusable. When you are still wondering, did it crash? As a result, everything suddenly returned to normal.
Some APIs do not have an asynchronous mode, such as "localStorage". When using these APIs, you should carefully monitor performance and be ready to switch to an asynchronous API if it causes problems.
API Overview and Comparison
Web Storage
Web Storage is a persistent object called localStorage. You can use localStorage.foo = "bar" to save the value, and later use localStorage.foo to get it - even after the browser is closed and reopened. You can also use an object called sessionStorage, which works in the same way, except that it will be cleared when the window is closed.
Web Storage is a type of NoSQL key-value store.
Advantages of Web Storage
For several years, it has been used by all modern browsers Yes, it is also supported under iOS and Android systems (IE supports it starting from IE8).
Simple API signature.
Synchronous API, easy to call.
Semantic events keep other tabs and windows in sync.
Weaknesses of Web Storage
Performance is poor when storing large or complex data using the synchronous API (which is the most widely supported mode).
Lack of indexes results in poor performance when retrieving large or complex data. (Search operations require manual iteration of all items.)
Performance is poor when storing or reading large or complex data structures because manual serialization into strings or string deserialization is required. Major browser implementations only support strings (although the specification doesn't say so).
It is necessary to ensure the continuity and integrity of data because the data is effectively unstructured.
Web SQL Database
Web SQL Database is a structured database with all the functionality and complexity of a typical SQL-powered relational database. Indexed Database is somewhere in between. Web SQL Database has free-form key-value pairs, a bit like Web Storage, but also has the ability to index fields from these values, so searches are much faster.
Advantages of Web SQL Database
is supported by major mobile browsers (Android Browser, Mobile Safari, Opera Mobile) and some PC browsers (Chrome, Safari, Opera).
As an asynchronous API, overall performance is very good. Database interaction does not lock the user interface. (The synchronous API is also available for WebWorkers.)
Good search performance because data can be indexed based on search keys.
Powerful because it supports transactional database model.
Rigid data structures make it easier to maintain data integrity.
Weaknesses of Web SQL Database
Outdated, will not be supported by IE or Firefox, and may be phased out from other browsers at some stage.
The learning curve is steep and requires knowledge of relational databases and SQL.
Object-relational impedance mismatch.
Reduced agility because the database schema must be predefined and all records in the table must match the same structure.
Indexed Database (IndexedDB)
So far, we have seen that both Web Storage and Web SQL Database have various strengths and weaknesses. Indexed Database emerged from the experience of these two early APIs and can be seen as an attempt to combine the advantages of both without incurring their disadvantages.
Indexed Database is a collection of "object stores" into which objects can be placed directly. This storage is somewhat like a SQL table, but in this case there are no constraints on the structure of the objects, so nothing needs to be defined up front. So this is a bit like Web Storage, with multiple databases and each database having multiple stores. But unlike Web Storage, it also has important performance advantages: an asynchronous interface that can create indexes on the storage to increase search speed.
Advantages of IndexedDB
As an asynchronous API, it generally performs well. Database interaction does not lock the user interface. (The Sync API is also available for WebWorkers.)
Good search performance because data can be indexed based on search keys.
Support version control.
Powerful because it supports transactional database model.
Because the data model is simple, the learning curve is also quite simple.
Good browser support: Chrome, Firefox, mobile FF, IE10.
Weaknesses of IndexedDB
Very complex API, resulting in a large number of nested callbacks.
FileSystem
The above APIs are suitable for text and structured data, but when it comes to large files and binary content, we need something else. Fortunately, we now have the FileSystem API standard. It gives each domain a complete hierarchical file system, and at least under Chrome, these are real files on the user's hard drive. In terms of reading and writing individual files, the API builds on the existing File API.
FileSystem (File System) API has some advantages
It can store a large amount of content and binary files, which is very suitable for images, audio, video, PDF, etc.
As an asynchronous API, it has good performance.
Weaknesses of FileSystem API
A very early standard, only supported by Chrome and Opera.
No transaction support.
No built-in search/indexing support.
Look at the code
This section compares how different APIs solve the same problem. This example is a "geo-mood" check-in system, where you can record your mood in time and place. Interfaces let you switch between database types. Of course, in a real world situation this might seem a bit contrived, the database type would definitely make more sense than the other, and the file system API would simply not be suitable for this application! But for demonstration purposes, it helps if we can see different ways of achieving the same result. Also note that some code snippets have been refactored to preserve readability.
Now you can try our "geo-mood" application.
In order to make the Demo more interesting, we separate the data storage and use standard object-oriented design techniques. The UI logic only knows that there is one store; it does not need to know how the store is implemented because the methods are the same for each store. Therefore the UI layer code can be called store.setup(), store.count() and so on. In fact, our store has four implementations, one for each storage type. When the application starts, the URL is checked and the corresponding store is instantiated.
To maintain API consistency, all methods are asynchronous, that is, they return results to the caller. The implementation of Web Storage is even like this, the underlying implementation is native.
In the following demonstration, we will skip the UI and positioning logic and focus on the storage technology.
Create Store
For localStorage, we do a simple test to see if the storage exists. If it does not exist, create a new array and store it in localStorage under the checkins key. First, we serialize the structure into a string using a JSON object, since most browsers only support string storage.
if (!localStorage.checkins) localStorage.checkins = JSON.stringify([]);
For Web SQL Database, if the database structure does not exist, we need to create it first. Fortunately, the openDatabase method will automatically create the database if it does not exist; similarly, using the SQL statement "if not exists" ensures that the new checksins table will not be overwritten if it already exists. We need to define the data structure in advance, that is, the name and type of each column of the checksins table. Each row of data represents a check-in.
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"); } ); });
Indexed Database startup requires some work as it requires enabling a database versioning system. When we connect to the database, we must clarify which version we need. If the current database is using a previous version or has not been created yet, the onupgradeneeded event will be triggered. When the upgrade is completed, the onsuccess event will be triggered. If there is no need to upgrade, the onsuccess event will be triggered immediately.
Another thing to do is to create a "mood" index so that matching emotions can be queried quickly later.
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); }; }; };
Finally, start FileSystem. We will encode each check-in JSON in a separate file, and they will be under the "checkins/" directory. Again this isn't the most appropriate use of the FileSystem API, but it's fine for demonstration purposes.
Start getting a control handle in the entire file system to check the "checkins/" directory. If the directory does not exist, use getDirectory to create it.
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"); } ); }
Save a check-in (Check-in)
Using localStorage, we only need to take out the check-in array, add one at the end, and then save it again. We also need to use the JSON object method to save it as a string.
var checkins = JSON.parse(localStorage["checkins"]); checkins.push(checkin); localStorage["checkins"] = JSON.stringify(checkins);
With Web SQL Database, everything happens in a transaction. We want to create a new row in the checksins table. This is a simple SQL call. We use the "?" syntax instead of putting all the check-in data in the "insert" command. This is cleaner and safer. The real data - the four values we want to save - is placed on the second row. "?" elements will be replaced by these values (checkin.time, checkin.latitude, etc.). The next two parameters are functions that are called after the operation is completed, called after success and failure respectively. In this application, we use the same common error handler for all operations. In this way, the success callback function is the handle we pass to the search function - make sure the handle is called on success so that the UI can be notified when the operation is completed (for example, to update the number of check-ins so far).
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 ); });
Once the storage is set up, storing it in IndexedDB is as easy as Web Storage, with the advantage of working asynchronously.
var transaction = db.transaction("checkins", 'readwrite'); transaction.objectStore("checkins").put(checkin); transaction.oncomplete = handler;
Use the FileSystem API to create a new file and get the corresponding handle, which can be filled with the FileWriter API.
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 );
Search for matches
The next function finds all check-ins that match a specific emotion, for example, the user can see when and where they had a great time recently. Using localStorage, we have to manually iterate through each check-in and compare it to the sentiment of the search, building a list of matches. Better practice is to return a clone of the stored data rather than the actual object, since searching should be a read-only operation; so we pass each matching check-in object to the generic clone() method for operation.
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)这篇文章。