【2019年11月版】ブラウザ側でデータを保存するため各ブラウザにおけるローカルストレージの挙動の調査

#概要
このドキュメントではローカルストレージの説明と、各ブラウザでの動作を検証する。
ローカルストレージとは、データをブラウザ側に蓄積する仕組みである。

保存時に、サイトごとにKeyと文字の組み合わせでデータを格納する。

サイトごとの領域は、それぞれ独立しており、別のサイトで記録したデータを操作することはできない。
また、それぞれのサイト毎に保存できる上限は制限されている。その具体的サイズはブラウザ毎にことなる。

詳細な仕様は下記を参照のこと。
https://html.spec.whatwg.org/multipage/webstorage.html#the-localstorage-attribute

#実装例
下記にローカルストレージの実装例を紹介する。

##localStorage APIの使用例
localStorageの操作を行うAPIの使用例を紹介する。
このAPIは、処理が完了するまで制御の戻らない同期処理になっている。

###指定のキーにデータを格納する

```js
localStorage.setItem("KeyName","Data");
```

格納するデータは、**文字列**に過ぎない。
もし、オブジェクトを格納したい場合は、JSONを使用して変換をおこなうか、後述のstore.jsライブラリを使用すること。

サイト毎の容量を超えた場合は例外が発生する。
この時発生する例外は過去はブラウザによって異なっていたが、2019年11月時点で主要なブラウザではQuotaExceededErrorが発生する。

|ブラウザ名|例外の名前|
|:---------|:---------|
|FireFox 69.0.3|QuotaExceededError|
|Safari12.1.1|QuotaExceededError|
|Chrome 78|QuotaExceededError|
|IE11|QuotaExceededError|
|Microsoft Edge 44.18362.449.0|QuotaExceededError|


###指定のキーの取得

```js
console.log(localStorage.getItem("KeyName"))
```

存在しないキーを指定した場合はnullが返ってくる。

###指定のキーの削除

```js
localStorage.removeItem("keyName")
```

存在しないキーを指定してもエラーとならない。

###すべてのキーの削除

```js
localStorage.clear()
```

サイトに登録されているキーをすべて削除する。

###登録されているキーの列挙

```js
    var keys = [];
    for (var i = 0; i < localStorage.length; ++i) {
      keys.push(localStorage.key(i));
    }
    window.alert(keys.join("\n")); 
```

keyメソッドを使用すれば、キーの名称を取得できる。


##store.js　ローカルストレージ用のライブラリ
 __store.js__ 
https://github.com/marcuswestin/store.js

store.jsはローカルストレージの操作を行うMITライセンスのライブラリである。
ローカルストレージが使用できる場合は、それを利用し、IE6,IE7などではuserData を用いて同等の処理を行う。
このライブラリにオブジェクトを渡すとJSONに変換して格納する。

##storageイベント
setItem(), removeItem(), clear() メソッドが実行された時に、storageイベントが同じサイトを見ている、別のウィンドウで発行される。

以下にそのイベントを取得する例を示す。

```js
  window.addEventListener("storage", function (event) {
    var data = 'key:' + event.key +
               ' oldValue:' + event.oldValue +
               ' newValue:' + event.newValue +
               ' url:' + event.url +
               ' storageArea:' + event.storageArea;
    console.log(event);
  });
```

ブラウザを２つ開いて、片方でSetItemなどを行うと、もう片方でイベントが発行される。


#ブラウザ間の挙動の差
ここでは検証用のコードを使用して各ブラウザのローカルストレージの動作にどのような差があるか検証する。

 - FireFox 69.0.3 + Windows10
 - Safari12.1.1 + macOS 10.14.5
 - Chrome 78  + Windows10
 - IE11  + Windows10
 - Microsoft Edge 44.18362.449.0  + Windows10

##検証用コード
下記のサイトにアクセスする。
http://needtec.sakura.ne.jp/release/storageTest.html

```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
  <title>Storage Sample</title>
</head>
<body>
<p>
  KEY:
  <input id="storeKey" type="text" value="testData"></input>
</p>
<p>
  DATA:<BR>
  <textarea id="storeMsg" col="40" row="10"></textarea>
</p>
<p>
  EVENT:<BR>
  <select id="eventLog" multiple="multiple"></select>
</p>
<button id="update">update</button>
<button id="reload">reload</button>
<button id="testObject">testObject</button>
<button id="delete">delete</button>
<button id="clear">clear</button>
<button id="keyList">keyList</button>
<p>
  Big Data:<BR>
  <input id="bigSize" type="text" value="5242880"></input>
  <button id="updateBigData">大量データ書き込み</button>
</p>
<script>
  reloadStorage();
  // 指定のキーの値を更新
  document.getElementById('update').addEventListener('click', function() {
    let key = document.getElementById('storeKey').value;
    let contents = document.getElementById('storeMsg').value;
    localStorage.setItem(key, contents);
    console.log('localStorage.setItem(%s, %s)', key, contents);
  });
  
  // ストレージ読み込み
  function reloadStorage() {
    let key = document.getElementById('storeKey').value;
    let contents = localStorage.getItem(key);
    document.getElementById('storeMsg').value = contents;
    console.log('localStorage.getItem(%s) is %s', key, contents);
  }

  // 指定のキーの値を読み込む
  document.getElementById('reload').addEventListener('click', function(){
    reloadStorage();
  });
  

  // 指定のキーの値を削除
  document.getElementById('delete').addEventListener('click', function(){
    let key = document.getElementById('storeKey').value;
    localStorage.removeItem(key);
    console.log('localStorage.removeItem(%s)', key);
  });

  // 全て削除
  document.getElementById('clear').addEventListener('click', function(){
    localStorage.clear();
    console.log('localStorage.clear()');
  });

  // キーの一覧
  document.getElementById('keyList').addEventListener('click', function(){
    let keys = [];
    for (var i = 0; i < localStorage.length; ++i) {
      keys.push(localStorage.key(i));
    }
    window.alert(keys.join("\n")); 
  });

  // オブジェクトで書き込む
  document.getElementById('testObject').addEventListener('click', function(){
    let obj = {test:"123", value:123};
    localStorage.setItem('testObject', obj);
    console.log(localStorage.getItem('testObject'));
    window.alert(JSON.stringify(localStorage.getItem('testObject'))); 
    // オブジェクトは書き込んでも無意味。
    // [object Object]という文字列になるだけ
  });

  // 指定のサイズのデータを作成する
  document.getElementById('updateBigData').addEventListener('click', function(){
    try {
      let value = parseInt(document.getElementById('bigSize').value,10);
      let data = Array(value).join("x");
      let key = 'k';
      localStorage.clear();
      console.log('大量データ作成 key:%s size:%d', key, value);
      localStorage.setItem(key, data);
    } catch(e) {
      console.log(e);
      window.alert(e);
    }
  });

  // storageイベント
  window.addEventListener("storage", function (event) {
    let data = 'key:' + event.key +
               ' oldValue:' + event.oldValue +
               ' newValue:' + event.newValue +
               ' url:' + event.url +
               ' storageArea:' + JSON.stringify(event.storageArea);
    console.log('storage event : %o', event);
    let eventLog = document.getElementById('eventLog');
    let option = document.createElement("option");
    option.text = data;
    eventLog.appendChild(option);
  });

  // storagecommit イベント
  window.addEventListener("storagecommit", function (event) {
    console.log('storagecommit event : %o', event);
  });


</script>
</body>
</html>
```

##FireFox 69.0.3 + Windows10
__保存先：__ 
C:\Users\ユーザ名\AppData\Roaming\Mozilla\Firefox\Profiles\<profile folder>\webappsstore.sqlite
SQLiteの形式で以下のようなSQLで取得できる。

```sql
select * from webappsstore2;
```

 __イベントの挙動:__ 
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・clear済みで、再度、クリアしてもイベントは発行されない。

 __保存の限界:__
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

 __開発者ツールでの確認方法:__
StorageタブにてLocal Storageを選択する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/0f2e8067-6109-e232-6a93-1a831b59a4ac.png)


 __ユーザによるローカルストレージの削除方法:__ 
Optionsを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/4bc82ecf-58a0-3769-1427-ad4791742bae.png)

Privacy & Security タブにて、「Clear Data」ボタンを押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/4b375ba9-48c7-2ce3-5ba1-65ea87480a34.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/9fabd764-35b7-5bd7-9c53-85d2555f9446.png)

即時消える模様

__Private Windowsの時__
通常のモードで記録した内容は読み込めない
書き込み時にはエラーにはならない。
同じPrivate Windowにおいては書き込んだ内容を別のタブから読みだせる。※
ブラウザを終了して再度、Private Windowsで同じ開いた場合、前回記録した内容は読み込めない。

※新しいタブで開いた場合の挙動は以下の通り
①Private Window Aでローカルストレージを書き込む
②Private Window Bを新しいタブで開く
 →この時点では①の内容は取得できない。
③Private Window Aで①と同じキーでローカルストレージを書き込む
④Private Window Bにイベントが発生。以降③で記録した内容が取得できる

##Safari12.1.1 + macOS 10.14.5
__保存先：__ 
~/Library/Safari/LocalStorage/

アクセスできない場合は以下参照。
http://osxdaily.com/2018/10/09/fix-operation-not-permitted-terminal-error-macos/

フォルダを確認すると以下のようにURL事にSQLite3のファイルが存在する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/82d0e611-4d5a-4038-5450-96f28391f0ac.png)

ItemTableを確認するとキーとデータが格納されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/60e8150e-9edd-c0dd-2e25-f5d7c037d533.png)


 __イベントの挙動:__ 
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・clear済みで、再度、クリアしてもイベントは発行されない。

 __保存の限界:__
キー名を「k」とし、2621441文字をデータとして保存した場合にQuotaExceededError例外が発生する。

 __開発者ツールでの確認方法:__
ストレージタブを開いてローカルストレージで確認可能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/f1174e22-b585-f400-a7ee-7a6c646cd620.png)

 __ユーザによるローカルストレージの削除方法:__ 
環境設定を開く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/f9aef26f-4152-9820-9e77-bec407f31c20.png)

プライバシータブのWebサイトデータを管理を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/0d188531-85d2-2093-7650-3df10fda2a90.png)

すべてを削除ボタンを押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/33ca5187-ffc2-ad6e-7056-e11f0352ae59.png)

__プライベートウィンドウの時__
通常のモードで記録した内容は読み込めない
書き込み時にはエラーにはならない。
同じプライベートウィンドウの別タブで記述した内容は読みだせない
同じプライベートウィンドウの同じタブで記述した内容は別のページに遷移後、戻っても呼び出せる。
ブラウザを終了して再度、プライベートウィンドウを開いた場合、前回記録した内容は読み込めない。


##Chrome 78  + Windows10
__保存先：__ 
C:\Users\ユーザ名\AppData\Local\Google\Chrome\User Data\Default\Local Storage\leveldb

[LevelDB](https://github.com/google/leveldb)で記録している。

頑張れば中身見れそうだが、正直よくわからない。
https://scrapbox.io/mima3/leveldb

 __イベントの挙動:__ 
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・clear済みで、再度、クリアしてもイベントは発行されない。

 __保存の限界:__
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

 __開発者ツールでの確認方法:__
ApplicationタブにてStorage>Local Storageを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/57f276c7-b12e-d3de-6bae-85e1323271d1.png)


 __ユーザによるローカルストレージの削除方法:__ 
設定⇒詳細設定の表示⇒履歴データの削除より、「Cookie と他のサイトやプラグインのデータ」を削除する。

__シークレットモード時__
通常モードで記録した内容を読みだせない
書き込み時にはエラーにはならない。
同じシークレットモードの別タブでは書き込んだ内容を読みだせる。
ブラウザを終了して再度、シークレットモードで同じ開いた場合、前回記録した内容は読み込めない。

##Internet Explore 11
__保存先：__ 
C:\Users\ユーザ名\AppData\LocalLow\Microsoft\Internet Explorer\DOMStore\0FOJP942
XML形式

 __イベントの挙動:__ 
 __・自分のWindowで行った操作のイベントでも、自分のWindowで発行される。__ 
 __・setItemで前回値と同じ場合でもイベントが発行される。__ 
・removeItemで存在しないキーを指定した場合でも、イベントは発行されない。
 __・clear済みで、再度、クリアした時に、イベントは発行される__ 
・XMLにデータが書き込まれた時に、操作を行ったウィンドウだけにstoragecommitイベントが発行される。このイベントの取得例は下記の通り

```js
  window.addEventListener("storagecommit", function (event) {
    console.log("storagecommit");
    console.log(event);
  });
```

・大量のデータをsetItemした場合の挙動が不安定
例：
　30000文字をsetItemする。⇒storageイベントが発生する。
　もう一度、30000文字をsetItemする。⇒storageイベントが発生しない
　一旦、キーを削除する。⇒storageイベントが発生する
　30000文字をsetItemする。⇒storageイベントが発生する。


 __保存の限界:__
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

 __開発者ツールでの確認方法:__
なし

 __ユーザによるローカルストレージの削除方法:__ 
オプション⇒「全般」タブ⇒閲覧の履歴の削除にて、「クッキーとWebサイトデータ」を選択して削除後、IEを再起動する。
http://msdn.microsoft.com/ja-jp/library/ie/bg142799%28v=vs.85%29.aspx

##Microsoft Edge 44.18362.449.0  + Windows10
__保存先：__ 
C:\Users\ユーザー名\AppData\Local\Packages\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\AC\#!001\MicrosoftEdge\User\Default\DOMStore\E2WN2O9U
XMLファイルに格納されている。

 __イベントの挙動:__ 
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・**clear済みで、再度、クリアした場合、イベントが発行される。**

 __保存の限界:__
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

 __開発者ツールでの確認方法:__
ストレージタブを選択する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/f723b3e9-5111-9a8f-abe1-edc40e735f9e.png)


 __ユーザによるローカルストレージの削除方法:__ 
履歴データの消去から削除する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/268b227c-b267-5703-b3f7-be900bea89ee.png)
※対象のページが開いている間は削除したはずのローカルストレージが使えるようなので、対象ページを閉じる。


__InPrivateブラウズの場合__
通常モードで記録した内容を読みだせない
書き込み時にはエラーにはならない。
同じInPrivateブラウズの別タブでは書き込んだ内容も読みだせない。
別ページに遷移すると書き込んだ内容が読めなくなる。
ブラウザを終了して再度、InPrivateブラウズで同じ開いた場合、前回記録した内容は読み込めない。


#まとめ
ローカルストレージを使用することで容易にクライアントサイドに情報を保持できる。
最近の主要なブラウザでは、その挙動は近くなっている。（マイクロソフト系を除く）

store.jsを用いれば、それはさらに容易になる

大きなデータの扱うのには適さない。この理由は二つある。
１つは同期処理のため、大きなデータを扱うと、そこで処理が止まる。
もう一つはサイト毎にストレージの上限が決められており、それは精々数MBであるからだ。

ストレージの上限は「わからない」ものとして処理した方が良い。

ユーザーがブラウザーの機能で、ローカルストレージをバックアップする手段はないので、消えたら困るデータは格納すべきでない。もし、格納する場合は、エクスポートの機能は必要だろう。

IE11を使用する場合、ストレージのイベントの使用は避けた方が良い。

IE11やEdgeで[sinon.js](https://sinonjs.org/)などでlocalStorage.setItemなどのMockやFakeを作成できない。localStorage.setItemにたいして関数の上書きができないためである。

また、以下の方法でLocalStorageを無効にされている可能性があるので、必ず使える前提は問題がある。
**主要ブラウザでCookie / JavaScript / Web Storageを無効にする方法まとめ**
https://qiita.com/Udomomo/items/32b1c84807e562b8ce79

以上。