クライアント側のストレージ。これは、Web ストレージ、Web SQL データベース、インデックス付きデータベース、およびファイル アクセスなど、複数の個別ではあるが関連する API を包括する一般的な用語です。各テクノロジーは、通常データが保存されるサーバーではなく、ユーザーのハード ドライブにデータを保存する独自の方法を提供します。これは主に次の 2 つの理由で行われます。(a) Web アプリをオフラインで利用できるようにする。(b) パフォーマンスを向上させる。クライアント側のストレージ使用量の詳細については、HTML5Rocks の記事「オフライン」: どういう意味ですか? を参照してください。なぜ私が気にする必要があるのですか?」
それらの共通点を見てみましょう:
共通の機能
クライアントベースのストレージ
実際、「クライアント側の時間ストレージ」とは、データがブラウザのストレージ API に渡され、そこにデータが保存されることを意味します。データ個人の設定やキャッシュなど、他のユーザー固有の情報も保存されるローカル デバイスの領域。これらの API は、データの保存に加えて、データの取得にも使用でき、場合によっては検索やバッチ操作を実行することもできます。
サンドボックス化
4 つのストレージ API はすべて、データを単一の「オリジン」に結び付けます。
クォータ
Web サイトが何の疑いもなくハード ドライブにギガバイトのデータを書き込むことを許可されたら、大混乱になることは想像できるでしょう。したがって、ブラウザーはストレージ容量に制限を課します。アプリが制限を超えようとすると、通常、ブラウザーはユーザーに増加の確認を求めるダイアログ ボックスを表示します。ブラウザーは、単一のオリジンで使用できるすべてのストレージに同じ個別の制限を課していると思われるかもしれませんが、ほとんどのストレージ メカニズムは個別に制限されています。 Quota API が採用されると、これが変わる可能性があります。ただし、ここではブラウザを、原点とストレージを次元とする 2 次元のマトリックスとして考えてみましょう。 abc.example では、最大 5MB の Web ストレージと 25MB の Web SQL データベースを許可できますが、ユーザーはアクセスを拒否されているため、インデックス付きデータベースの使用は禁止されています。 Quota API は問題をまとめて、利用可能なスペースの量と使用中のスペースの量をクエリできるようにします。
場合によっては、ユーザーは使用されるストレージの量を事前に確認することもできます。たとえば、ユーザーが Chrome App Store にアプリをインストールするときに、ストレージ制限を含むそのアクセス許可を事前に受け入れるよう求められます。 (アプリケーションの) マニフェストには、「unlimited_storage」(無制限のストレージ) という値が存在する可能性があります。
データベース処理 (トランザクション)
2 つの「データベース」ストレージ形式がデータ処理をサポートします。目的は通常のリレーショナル データベースのデータ処理と同じで、データベースの整合性を確保することです。データベース トランザクション (トランザクション) は、「競合状態」を防ぎます。これは、2 つの一連の操作が同時にデータベースに適用され、操作の結果が予測不能になり、データベースの精度が疑わしい状態になることです。
同期モードと非同期モード
ほとんどのストレージ形式は同期モードと非同期モードをサポートしています。同期モードはブロッキングです。これは、js コードの次の行が実行される前にストレージ操作が完全に実行されることを意味します。非同期モードでは、データベース操作が完了する前に次の js コードが実行されます。ストレージ操作はバックグラウンドで実行され、操作が完了すると、アプリケーションは呼び出されたコールバック関数の形式で通知を受け取ります。この関数は呼び出し時に指定する必要があります。
同期モードの使用は比較的簡単に見えますが、操作の完了時にページのレンダリングがブロックされ、場合によってはブラウザ全体がフリーズすることがあります。ウェブサイトやアプリケーションでさえ、ボタンをクリックするとすべてが使用できなくなるという現象が発生していることに気づいたかもしれませんが、クラッシュしたのではないかと考えています。その結果、すべてが突然正常に戻りました。
「localStorage」などの一部の API には非同期モードがありません。これらの API を使用する場合は、パフォーマンスを注意深く監視し、問題が発生した場合には非同期 API に切り替える準備をしておく必要があります。
API の概要と比較
Web Storage
Web Storage は、localStorage と呼ばれる永続オブジェクトです。 localStorage.foo = "bar" を使用して値を保存し、後でブラウザを閉じて再度開いた後でも、localStorage.foo を使用して値を取得できます。 sessionStorage というオブジェクトを使用することもできます。これは、ウィンドウを閉じるときにクリアされる点を除いて、同じように機能します。
Web Storage は、NoSQL キー/値ストアの一種です。
Web Storage の利点
数年前からすべての最新のブラウザーでサポートされており、iOS および Android システム (IE8 のサポートが開始されてからは IE) でもサポートされています。 )。
シンプルな API 署名。
同期 API、呼び出しが簡単。
セマンティック イベントにより、他のタブとウィンドウの同期が維持されます。
Web Storage の弱点
同期 API (最も広くサポートされているモード) を使用すると、大規模または複雑なデータを保存する場合のパフォーマンスが低下します。
インデックスが不足していると、大規模なデータや複雑なデータを取得するときにパフォーマンスが低下します。 (検索操作では、すべての項目を手動で反復する必要があります。)
文字列への手動シリアル化または文字列の逆シリアル化が必要なため、大規模または複雑なデータ構造を保存または読み取りする場合、パフォーマンスが低下します。主要なブラウザ実装は文字列のみをサポートします (ただし、仕様にはそのように記載されていません)。
データは実質的に構造化されていないため、データの継続性と整合性を確保する必要があります。
ウェブ SQL データベース
Web SQL Database は、典型的な SQL ベースのリレーショナル データベースのすべての機能と複雑さを備えた構造化データベースです。インデックス付きデータベースはその中間です。 Web SQL Database には、Web Storage に似た自由形式のキーと値のペアがありますが、これらの値からフィールドにインデックスを付ける機能も備えているため、検索がはるかに高速になります。
Web SQL データベースの利点
主要なモバイル ブラウザ (Android ブラウザ、Mobile Safari、Opera Mobile) および一部の PC ブラウザ (Chrome、Safari、Opera) でサポートされています。
非同期 API として、全体的なパフォーマンスは非常に優れています。データベースの対話ではユーザー インターフェイスはロックされません。 (同期 API は WebWorkers でも使用できます。)
検索キーに基づいてデータのインデックスを作成できるため、優れた検索パフォーマンス。
トランザクションデータベースモデルをサポートしているため強力です。
厳格なデータ構造により、データの整合性の維持が容易になります。
Web SQL データベースの弱点
時代遅れのため、IE や Firefox ではサポートされず、ある段階で他のブラウザから段階的に廃止される可能性があります。
学習曲線は急勾配であり、リレーショナル データベースと SQL の知識が必要です。
オブジェクト リレーショナル インピーダンスの不一致。
データベース スキーマを事前定義する必要があり、テーブル内のすべてのレコードが同じ構造に一致する必要があるため、俊敏性が低下します。
インデックス付きデータベース (IndexedDB)
これまで、Web Storage と Web SQL Database の両方にさまざまな長所と短所があることがわかりました。インデックス付きデータベースは、これら 2 つの初期の API の経験から生まれ、欠点を負うことなく両方の利点を組み合わせようとする試みとして見ることができます。
インデックス付きデータベースは、オブジェクトを直接配置できる「オブジェクト ストア」のコレクションです。このストレージは SQL テーブルに似ていますが、この場合、オブジェクトの構造に制約がないため、事前に何も定義する必要はありません。したがって、これは Web Storage に似ており、複数のデータベースと各データベースに複数のストアがあります。ただし、Web Storage とは異なり、ストレージ上にインデックスを作成して検索速度を向上できる非同期インターフェイスという重要なパフォーマンス上の利点もあります。
IndexedDB の利点
非同期 API として、一般的に優れたパフォーマンスを発揮します。データベースの対話ではユーザー インターフェイスはロックされません。 (同期 API は WebWorkers でも利用できます。)
検索キーに基づいてデータのインデックスを作成できるため、優れた検索パフォーマンス。
バージョン管理をサポートします。
トランザクションデータベースモデルをサポートしているため強力です。
データモデルがシンプルなので、学習曲線も非常にシンプルです。
優れたブラウザのサポート: Chrome、Firefox、モバイル FF、IE10。
IndexedDB の弱点
非常に複雑な API により、多数のネストされたコールバックが発生します。
ファイルシステム
上記の API はテキストや構造化データに適していますが、大きなファイルやバイナリ コンテンツの場合は他のものが必要になります。幸いなことに、現在では FileSystem API 標準が存在します。これにより、各ドメインに完全な階層ファイル システムが提供され、少なくとも Chrome では、これらはユーザーのハード ドライブ上の実際のファイルになります。個々のファイルの読み取りと書き込みに関して、この API は既存のファイル API に基づいて構築されています。
FileSystem (ファイル システム) API には、大量のコンテンツとバイナリ ファイルを保存できるという利点があり、画像、音声、ビデオ、PDF などに非常に適しています。
非同期 API として、優れたパフォーマンスを備えています。
FileSystem API の弱点
非常に初期の標準で、Chrome と Opera でのみサポートされています。
トランザクションのサポートはありません。
組み込みの検索/インデックス作成のサポートはありません。
コードを見てください
このセクションでは、異なる API が同じ問題をどのように解決するかを比較します。この例は、時間と場所の気分を記録できる「ジオムード」チェックイン システムです。インターフェイスを使用すると、データベースの種類を切り替えることができます。もちろん、現実の状況では、これは少し不自然に見えるかもしれません。データベース タイプは他のタイプよりも明らかに合理的であり、ファイル システム API は単にこのアプリケーションには適していません。ただし、デモンストレーションの目的では、同じ結果を達成するためのさまざまな方法を確認できれば役立ちます。また、読みやすさを維持するために一部のコード スニペットがリファクタリングされていることにも注意してください。
「geo-mood」アプリを試してみましょう。
デモをより興味深いものにするために、データ ストレージを分離し、標準のオブジェクト指向設計手法を使用します。 UI ロジックは、ストアが 1 つあることだけを認識します。メソッドは各ストアで同じであるため、ストアがどのように実装されているかを知る必要はありません。したがって、UI レイヤー コードは、store.setup()、store.count() などと呼び出すことができます。実際、私たちのストアには、各ストレージ タイプに 1 つずつ、計 4 つの実装があります。アプリケーションが起動すると、URL がチェックされ、対応するストアがインスタンス化されます。
API の一貫性を維持するために、すべてのメソッドは非同期です。つまり、呼び出し元に結果を返します。 Web Storage の実装も同様で、基礎となる実装はネイティブです。
次のデモでは、UI と位置決めロジックをスキップし、ストレージ テクノロジーに焦点を当てます。
ストアの作成
localStorage の場合、ストレージが存在するかどうかを確認する簡単なチェックを行います。存在しない場合は、新しい配列を作成し、checkins キーの下の localStorage に保存します。まず、ほとんどのブラウザは文字列ストレージのみをサポートしているため、JSON オブジェクトを使用して構造を文字列にシリアル化します。
if (!localStorage.checkins) localStorage.checkins = JSON.stringify([]);
Web SQL Database の場合、データベース構造が存在しない場合は、最初にそれを作成する必要があります。幸いなことに、データベースが存在しない場合、openDatabase メソッドはデータベースを自動的に作成します。同様に、「存在しない場合」SQL ステートメントを使用すると、新しい checksins テーブルが既に存在する場合は上書きされません。事前にデータ構造、つまり、checksins テーブルの各列の名前と型を定義する必要があります。データの各行はチェックインを表します。
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"); } ); });
インデックス付きデータベースを使用するには、データベース バージョン管理システムを有効にする必要があるため、開始するにはいくつかの作業が必要です。データベースに接続するときは、必要なバージョンを明確にする必要があります。現在のデータベースが以前のバージョンを使用している場合、またはまだ作成されていない場合は、アップグレードが完了すると onupgradeneeded イベントがトリガーされます。引き起こされる。アップグレードする必要がない場合は、onsuccess イベントがすぐにトリガーされます。
もう 1 つは、一致する感情を後ですぐに照会できるように、「気分」インデックスを作成することです。
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); }; }; };
最後に、FileSystemを起動します。各チェックイン JSON を別のファイルにエンコードし、「checkins/」ディレクトリーに置きます。繰り返しますが、これは FileSystem API の最も適切な使用法ではありませんが、デモンストレーションの目的には問題ありません。
「checkins/」ディレクトリをチェックするために、ファイルシステム全体で制御ハンドルの取得を開始します。ディレクトリが存在しない場合は、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"); } ); }
チェックインの保存 (Check-in)
localStorage を使用すると、チェックイン配列を取り出し、最後に 1 つ追加して、再度保存するだけです。また、JSON オブジェクト メソッドを使用して文字列として保存する必要があります。
var checkins = JSON.parse(localStorage["checkins"]); checkins.push(checkin); localStorage["checkins"] = JSON.stringify(checkins);
Web SQL Database では、すべてがトランザクションで発生します。 checksins テーブルに新しい行を作成します。これは、すべてのチェックイン データを「insert」コマンドに入れる代わりに、「?」構文を使用します。実際のデータ (保存したい 4 つの値) は 2 行目に配置されます。 「?」要素はこれらの値 (checkin.time、checkin.latitude など) に置き換えられます。次の 2 つのパラメータは、操作の完了後に呼び出される関数で、それぞれ成功後と失敗後に呼び出されます。このアプリケーションでは、すべての操作に同じ共通のエラー ハンドラーを使用します。このように、成功コールバック関数は検索関数に渡すハンドルです。操作が完了したときに UI に通知できるように (たとえば、チェックイン数を更新するため)、成功時にハンドルが呼び出されることを確認します。これまでのところ)。
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 ); });
ストレージをセットアップしたら、IndexedDB への保存は Web Storage と同じくらい簡単で、非同期で動作するという利点があります。
var transaction = db.transaction("checkins", 'readwrite'); transaction.objectStore("checkins").put(checkin); transaction.oncomplete = handler;
FileSystem API を使用して新しいファイルを作成し、対応するハンドルを取得します。これには 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 );
一致を検索
次の関数は、特定の感情に一致するすべてのチェックインを検索します。たとえば、ユーザーは最近いつどこで楽しんだかを確認できます。 localStorage を使用すると、各チェックインを手動で繰り返し、検索のセンチメントと比較して、一致するリストを作成する必要があります。検索は読み取り専用操作である必要があるため、実際のオブジェクトではなく、保存されたデータのクローンを返すことをお勧めします。そのため、一致する各チェックイン オブジェクトを操作のために汎用 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)这篇文章。