Web アプリケーションでは、ストレージがいくつか使える！

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

# Cookie
| データモデル | 永続性 | ブラウザ対応 | トランザクション | 同期 / 非同期 |
| --- | --- | --- | --- | --- |
| 構造化 | 端末 | 100% | 非対応 | 同期 |
※各項目の意味は[こちら](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage?hl=ja)です。

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% | 非対応 | 同期 |
※各項目の意味は[こちら](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage?hl=ja)です。

Local Storage は、Cookie と違い、データを利用する時のみ送信されます。タブ間、ウィンドウ間でのデータの共有が可能です。保存可能な容量は、5MBytesまでです。
Local Storage を使うべきではない理由は、[こちら](https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851)が参考になるかと思います。

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

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

# Session Storage
| データモデル | 永続性 | ブラウザ対応 | トランザクション | 同期 / 非同期 |
| --- | --- | --- | --- | --- |
| キー値 | セッション | 93% | 非対応 | 同期 |
※各項目の意味は[こちら](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage?hl=ja)です。

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

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

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

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

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

# IndexedDB
| データモデル | 永続性 | ブラウザ対応 | トランザクション | 同期 / 非同期 |
| --- | --- | --- | --- | --- |
| ハイブリッド | 端末 | 83% | 対応 | 非同期 |
※各項目の意味は[こちら](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage?hl=ja)です。

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% | 非対応 | 非同期 |
※各項目の意味は[こちら](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage?hl=ja)です。

キャッシュ API は、サービスワーカーの仕様書で定義されていますが、必ずしもサービスワーカーと組み合わせる必要があるわけではありません。キャッシュの保存容量は、[こちら](https://developer.mozilla.org/ja/docs/Web/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() の両方がリクエストを「消費」するため、コピーを作成する必要があります。
        // （https://fetch.spec.whatwg.org/#dom-request-clone を参照）
        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 レスポンスヘッダーを持ちます。
            // 不透明なフィルタされたレスポンス（https://fetch.spec.whatwg.org/#concept-filtered-response-opaque）の場合、
            // レスポンスヘッダーにアクセスできないので、このチェックは常に失敗し、フォントはキャッシュされないことに注意してください。
            // すべての Google Web Fonts は CORS をサポートするドメインから提供されるため、ここでは問題になりません。
            // ただし、CORS をサポートしていないクロスオリジンドメインから他のリソースをキャッシュしようとしている場合は、注意が必要です。
            // レスポンスで .clone() を呼び出して、そのコピーをキャッシュに保存します。
            // そうすることで、制御されたページに戻る元のレスポンスオブジェクトを保持できます。
            // （https://fetch.spec.whatwg.org/#dom-response-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;
      });
    })
  );
});
```

各 Cache のメソッドについて、[こちら](https://developer.mozilla.org/ja/docs/Web/API/Cache#Methods)をご確認ください。

# Web SQL
| データモデル | 永続性 | ブラウザ対応 | トランザクション | 同期 / 非同期 |
| --- | --- | --- | --- | --- |
| 構造化 | 端末 | 77% | 対応 | 非同期 |
※各項目の意味は[こちら](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage?hl=ja)です。

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 を使うといったやり方も検討すると良さそうです。

# 参考
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage