Web アプリケーションのストレージについて・・

記事
IT・テクノロジー

# はじめに

Webアプリケーションでは、Webサーバ側にDBを持たせてデータを保持しておくことが多いかと思いますが、ブラウザのStorageに保存しておくこともあるかと思います。そこで、ブラウザで使用できるStorageについて調べてみました。

# Cookie

データモデル:構造化
永続性:端末
ブラウザ対応:100%
トランザクション:非対応
同期 / 非同期:同期

Cookie はクライアント側の汎用的な記憶領域として使用されていましたが、最近では新しい Storage API が公開され、Cookie の使用は推奨されていません。また、Cookie は、全てのリクエストに付与して送信されるため、性能の悪化の一因にもなり得ます。保存可能な容量は、4KBytesまでです。

Cookie は、以下の形でリクエストに付与されています。

```
Set-Cookie: <cookie-name>=<cookie-value>
```

Javascript で操作するには、以下のようにします。

```javascript
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
// "yummy_cookie=choco; tasty_cookie=strawberry"
```

# Local Storage

データモデル:キー値 
永続性:端末
ブラウザ対応:93%
トランザクション:非対応
同期 / 非同期:同期

Local Storage は、Cookie と違い、データを利用する時のみ送信されます。タブ間、ウィンドウ間でのデータの共有が可能です。保存可能な容量は、5MBytesまでです。

Javascript で操作するには、以下のようにします。

```javascript
localStorage.setItem('myCat', 'Tom');
const cat = localStorage.getItem("myCat"); // cat = "Tom"
localStorage.removeItem("myCat");
```

# Session Storage

データモデル:キー値 
永続性:セッション
ブラウザ対応:93%
トランザクション:非対応
同期 / 非同期:同期

Session Storage は、Local Storage に似ていますが、違いとしては、保存されたデータはページのセッションが終了するときに消去されるという点です。

Javascript で操作するには、以下のようにします。

```javascript
// sessionStorage にデータを保存する
sessionStorage.setItem('key', 'value');

// sessionStorage に保存したデータを取得する
const data = sessionStorage.getItem('key');

// sessionStorage に保存したデータを削除する
sessionStorage.removeItem('key')
```

# IndexedDB

データモデル:ハイブリッド
永続性:端末
ブラウザ対応:83%
トランザクション:対応
同期 / 非同期:非同期

IndexedDB では、ファイルや blob を含む構造化された多くのデータを保存することができます。IndexedDB は、非同期なため、他のアプリケーションの処理を妨げません。

使用する際は、まず、ブラウザがサポートしているかチェックします。

```javascript
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction || {READ_WRITE: "readwrite"};
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
if (!window.indexedDB) {
  window.alert("このブラウザーは安定版の IndexedDB をサポートしていません。IndexedDB の機能は利用できません。");
}
```

Javascript で操作するには、以下のようにします。

```javascript
const storeName = 'sampleStore';

// データベースを開く
const request = window.indexedDB.open("MyTestDatabase", 3);

request.onerror = function(event) {
  // request.errorCode に対して行うこと!
};

request.onsuccess = function(event) {
  const db = event.target.result;
  // テーブルを作成
  db.createObjectStore(storeName, {keyPath : 'id'});

  const trans = db.transaction(storeName, 'readwrite');
  const store = trans.objectStore(storeName);

  // データ挿入
  const data = {id : 'A1', name : 'test'};
  const putReq = store.put(data);

  putReq.onsuccess = function(){
    console.log('put data success');
  }

  const getReq = store.get(keyValue);

  getReq.onsuccess = function(event){
    // データ取得
    console.log(event.target.result);
  }

  trans.oncomplete = function(){
    // トランザクション完了時(putReq.onsuccessの後)に実行
    console.log('transaction complete');
  }
};

request.onupgradeneeded = function(event){
  //onupgradeneededは、DBのバージョン更新(DBの新規作成も含む)時のみ実行
  console.log('db upgrade');

  const db = event.target.result;
  // テーブルを作成
  db.createObjectStore(storeName, {keyPath : 'id'}); 
}
```

# キャッシュ API

データモデル:キー値
永続性:端末
ブラウザ対応:60%
トランザクション:非対応
同期 / 非同期:非同期

キャッシュ API は、サービスワーカーの仕様書で定義されていますが、必ずしもサービスワーカーと組み合わせる必要があるわけではありません。キャッシュの保存容量は、StorageEstimateを使って確認できます。

Javascript で操作するには、以下のようにします。

```javascript
var CACHE_VERSION = 1;
var CURRENT_CACHES = {
  font: 'font-cache-v' + CACHE_VERSION
};

self.addEventListener('activate', function(event) {
  // CURRENT_CACHES で指定されていないすべてのキャッシュを削除します。
  // この例ではキャッシュは1つしかありませんが、同じロジックが
  // 複数のバージョン化されたキャッシュがある場合を処理します。
  var expectedCacheNamesSet = new Set(Object.values(CURRENT_CACHES));
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (!expectedCacheNamesSet.has(cacheName)) {
            // このキャッシュ名が「予期される」キャッシュ名のセットに存在しない場合は、削除します。
            console.log('Deleting out of date cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

self.addEventListener('fetch', function(event) {
  console.log('Handling fetch event for', event.request.url);

  event.respondWith(
    caches.open(CURRENT_CACHES.font).then(function(cache) {
      return cache.match(event.request).then(function(response) {
        if (response) {
          // event.request のエントリがキャッシュにある場合、レスポンスが定義され、それを返すことができます。
          // この例では、フォントリソースのみがキャッシュされることに注意してください。
          console.log(' Found response in cache:', response);

          return response;
        }

        // それ以外の場合、event.request のエントリがキャッシュにない場合、
        // レスポンスは undefined となり、リソースを fetch() する必要があります。
        console.log(' No response for %s found in cache. About to fetch ' +
          'from network...', event.request.url);

        // 後で cache.put() の呼び出しで使用する可能性があるため、リクエストで .clone() を呼び出します。
        // fetch() とcache.put() の両方がリクエストを「消費」するため、コピーを作成する必要があります。
        return fetch(event.request.clone()).then(function(response) {
          console.log(' Response for %s from network is: %O',
            event.request.url, response);

          if (response.status < 400 &&
              response.headers.has('content-type') &&
              response.headers.get('content-type').match(/^font\//i)) {
            // これにより、エラーであることがわかっているレスポンス(つまり、HTTP ステータスコード 4xx または 5xx)のキャッシュが回避されます。
            // また、フォントに対応するレスポンスのみをキャッシュする必要があります。
            // つまり、"font/" で始まる Content-Type レスポンスヘッダーを持ちます。
            // 不透明なフィルタされたレスポンスの場合、
            // レスポンスヘッダーにアクセスできないので、このチェックは常に失敗し、フォントはキャッシュされないことに注意してください。
            // すべての Google Web Fonts は CORS をサポートするドメインから提供されるため、ここでは問題になりません。
            // ただし、CORS をサポートしていないクロスオリジンドメインから他のリソースをキャッシュしようとしている場合は、注意が必要です。
            // レスポンスで .clone() を呼び出して、そのコピーをキャッシュに保存します。
            // そうすることで、制御されたページに戻る元のレスポンスオブジェクトを保持できます。
            console.log(' Caching the response to', event.request.url);
            cache.put(event.request, response.clone());
          } else {
            console.log(' Not caching the response to', event.request.url);
          }

          // 元のレスポンスオブジェクトを返します。これは、リソース要求を満たすために使用されます。
          return response;
        });
      }).catch(function(error) {
        // この catch() は、match() または fetch() 操作から発生する例外を処理します。
        // HTTP エラーレスポンス(404 など)は例外をトリガーしないことに注意してください。
        // 適切なエラーコードが設定された通常のレスポンスオブジェクトを返します。
        console.error(' Error in fetch handler:', error);

        throw error;
      });
    })
  );
});
```

# Web SQL

データモデル:構造化
永続性:端末
ブラウザ対応:77%
トランザクション:対応
同期 / 非同期:非同期

Web SQL は、SQL ベースのブラウザ DB です。HTML5の仕様には入っていませんが、多くのブラウザによってサポートされています。

```javascript
var name = 'localdb'
var version = '1.0'
var description = 'Web SQL Database'
var size = 2 * 1024 * 1024
// データベース接続
var sampledb = openDatabase(name, version, description, size);
// 実行
sampledb.transaction(
  function(tr){
    tr.executeSql('select id, name from user', []); // []内にはSQL文中の"?"に対応する値を入れる
  },
  function(error){
    // エラー発生時の処理。必要に応じて書き足す
  },
  function(){
    // 正常終了時の処理。必要に応じて書き足す
  }
);
```

# おわりに

Google としては、Local Storage はあまり推奨されていないようで、indexedDB や キャッシュ API などを使うことを勧めているようです。これは、Local Storage がブロッキングな処理でメインスレッドを使うことで処理に影響を与えるためです。しかし、できる限り多くのブラウザをサポートする Web サイトの場合、これではカバーしきれないこともあるかと思います。その辺りと調整しつつ、使う必要がありそうです。また、使えるブラウザだけ、indexedDB や キャッシュ API を使って、サポートしていないブラウザだけ Local Storage を使うといったやり方も検討すると良さそうです。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す