HTML5 × CSS3 × jQueryを真面目に勉強してみる – #2 WebStorage

2012.02.06

そんな訳で、HTML5のWebStorageに触れてみることにします。

※WebStorageと一言で言っても、セッションストレージローカルストレージがある訳ですが、今回はローカルストレージに焦点を当てていきます。


前置きのようなもの

ネイティブアプリケーションの持つアドバンテージのひとつに、アプリ特有のデータや状態を永続的に保存できるというのがあります。
その方法は、iniファイルやXMLファイルなど、いくつかの種類があります。

対するWebアプリケーションは、Cookieをローカルストレージとして使うのが長らく一般的とされてきました。
しかしこれには以下のような欠点があります。

  • CookieはすべてのHTTPリクエストに含まれる
  • そのため不要なデータを毎回送信してしまうので、Webアプリケーションのパフォーマンス低下を招く
  • SSL状況下でない限りデータが暗号化されないので、何かと危なっかしい
  • 容量が4KBしかないので、実用性に欠ける

Webアプリケーションと呼ぶからには、サーバーに送信されす、それでいて大容量の永続的なストレージが欲しいところです。

HTML5 Webストレージ

Internet ExplorerのuserDataや、Google Gearといった
ブラウザ固有の機能や、サードパーティ製プラグインというのは以前からありましたが、
正真正銘の標準機能としてHTML5からWeb Storageという仕様が登場しました。

各ブラウザ毎の対応状況

IE Firefox Safari Chrome Opera Android iPhone
8.0〜 3.5〜 4.0〜 4.0〜 10.5〜 2.0〜 2.0〜

うん、これなら使っても問題なさそうですね。IE8ですら対応してるとか誰が想像できたでしょう!?
そんな訳で、実際にコードを書いてみるとします。

WebStorageを使ってみる

Webストレージは、名前付きキーと値のペアで構成されます。
データをキーに紐付けて保存しておき、同じキーを使ってそのデータを取り出すというしくみです。
なんという単純構造でしょう!

ちなみにストレージ領域の最大容量は、5MBが仕様で定められています。
2012年2月時点では、どのブラウザもこの容量を順守しており、またWebアプリ側から容量を変更する手段もありません。

保存

localStorage.setItem(キー, 値);

取得

変数 = localStorage.getItem(キー);

指定したキーに紐づく値を削除

localStorage.removeItem(キー);

すべてのキーと値を削除

localStorage.clear();

入力した文字列を保存して出力するだけの簡単なサンプルを作ってみました。保存された文字列はブラウザを終了させても失われず、再度取り出すことが出来ます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Web Storage - Sample</title>
    <script type="text/javascript">
      function saveStorage(id) {
        var target = document.getElementById(id);
        var str = target.value;
        localStorage.setItem("message", str);
      }

      function loadStorage(id) {
        var target = document.getElementById(id);
        var msg = localStorage.getItem("message");
        target.innerHTML = msg;
      }

    </script>
    <!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
  </head>
  <body>
    <h1>Web Storage Sample</h1>

    <p id="msg"></p>
    <input type="text" id="input" />
    <input type="button" value="保存" onclick="saveStorage('input')" />
    <input type="button" value="読み込み" onclick="loadStorage('msg')" />
  </body>
</html>

ここで、もう少しWebStorageの仕様について突っ込んでみます。

  1. キーはユニークでないとダメで、値の変更(※上書き)は出来ても、キーの名称変更は出来ない
  2. 値は文字列型だけでなく、論理型や数値型など、JavaScriptで使われている型なら何でも保存できる
  3. ただし、値は実際には文字列として保存される

単純構造であるが故に、型といった面倒な情報はいっさい持ってくれません。
なので文字列以外の値を取り出す際は、parseInt()やparseFloat()といった関数を使って値を元のJavaScriptデータ型に変換するのがベターだったりします。

整数型へ変換する場合

var count = parseInt(localStorage["data.count"]);

論理型へ変換する場合

var hasItem = (localStorage["data.hasItem"] == "true");

Web Storageのイベント取得について

ストレージ領域が変化したかどうかを検出するには、storageイベントをハンドリングすることで実現できます。

storageイベントの発生条件
1.setItem()、removeItem()、clear()のいずれかが呼ばれる
2.実際にストレージ領域に変化が生じた

この二つの条件を満たすとstorageイベントが発生します。
ということは、対象のキーに既存の値をセットしたり、存在しないキーでremoveItem()を呼び出しても、storageイベントは発生しません。
ここは要チェックや。

※2012年2月の当ブログ執筆時点ではstorageイベントをなぜかハンドリング出来なくなっています。仕様に変更があったのか、実装方法に間違いがあるのか、現在調査しているとこです。

JSONでオブジェクトちっくに値を管理する

いくら最大容量が5MBあるとはいえ、一つのキーに対し一つの値だけしか持てないようではとても管理しきれません。
ということでJSON形式をうまく使って複数の値を一つのオブジェクトでまとめてしまい、一つのキーで管理するというのをやってみます。

オブジェクト型のデータをJSON形式に変換

var data = new Object;
data.name = document.getElementById("name").value;
data.email = document.getElementById("email").value;
var jsonStr = JSON.stringify(data);

JSON.stringify()メソッドを使えば、オブジェクト型変数をイッパツでJSON形式に変換することが出来ます。

JSON形式の文字列をオブジェクト型に変換

var data = JSON.parse(jsonStr);

JSON.parse()メソッドの引数にJSON文字列を渡すことで、オブジェクト型に変換された値を取得することが出来ます。

簡易データベース的なアプリを作ってみる

現実的な使い方かどうかはビミョーですが、ここまでの総括ということで、アドレス帳アプリというものを作ってみるとします。

要件

  • 入力する項目は名前とメールアドレスの2つ
  • 入力したデータは日付をキーにしてローカルストレージ領域に保存する
  • アドレス一覧から任意の連絡先を選択して削除することが出来る
  • Reloadボタンクリックで、データを再取得
  • ClearDataボタンクリックで、ストレージ領域をすべて削除する
  • 登録、削除処理完了後は、アラーとメッセージを表示し、データを再取得する

まずはHTML

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Web Storage - Sample</title>
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
    <!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
  </head>
  <body>
    <p class="logo-bg">jQuery x HTML5 x CSS3</p>
    <div id="container">
      <header>
        <h1>Web Storage Sample</h1>
      </header>
      <fieldset>
        <p>
          <label for="name">Name</label>
          <input id="name" type="text" name="name" />
        </p>
        <p>
          <label for="email">Mail</label>
          <input id="email" type="email" name="email" />
        </p>
        <p><button id="regist">Regist</button></p>
      </fieldset>
      <table id="list">
        <caption>Member List</caption>
        <thead>
          <tr>
            <th></th>
            <th>Name</th>
            <th>E-Mail</th>
            <th></th>
          </tr>
        </thead>
        <tbody></tbody>
      </table>
      <button id="reload">Reload</button>
      <button id="clear">Clear Data</button>
    </div>
  </body>
</html>

可能な限りdivタグ等を多用せず、シンプルにとどめます。

続けてJavaScript

$(document).ready(function() {
  /** 登録ボタンクリック */
  $('#regist').click(function() {
    var time = new Date().getTime();
    var data = new Object();
    data.name = $('#name').val();
    data.email = $('#email').val();
    var str = JSON.stringify(data);
    //ローカルストレージ
    localStorage.setItem(time, str);
    alert("保存しました。");
    loadStorage();
  });

  /** データクリアボタンクリック */
  $('#clear').click(function() {
    localStorage.clear();
    alert("全てのデータを消去しました。");
    loadStorage();
  });

  /** リロードボタンクリック */
  $('#reload').click(loadStorage);

  /** ローカルストレージデータ読み込み */
  function loadStorage() {
    $("#list tbody").empty();
    var rec = "";
    for (var i=0; i<localStorage.length; i++) {
      var key = localStorage.key(i); //keyを取得
      var value = localStorage.getItem(key); //keyからJSON文字列を取得
      if (!value) {
        continue;
      }
      try {
        var data = JSON.parse(value); //JSONオブジェクトに変換
      } catch (event) {
        continue;
      }
      var date = new Date();
      date.setTime(key);
      var dateStr = date.toDateString() + " " + date.toLocaleTimeString();
      rec += "<tr id='" + key + "'><td><button class='delete' href='#'>delete</button></td>";
      rec += "<td>" + data.name + "</td>";
      rec += "<td>" + data.email + "</td>";
      rec += "<td><time datetime='" + dateStr + "'>" + dateStr + "</time></td>";
      rec += "</tr>";
    }
    $("#list tbody").append(rec);
    $('.delete').bind('click', delete_clickHandler);
  }

  /** 削除処理 */
  function delete_clickHandler(event) {
    var target = $(event.target).parents('tr').attr('id');
    localStorage.removeItem(target);
    alert('対象者を削除しました。');
    loadStorage();
  }
  //登録済みデータ読み込み
  loadStorage();
});

ローカルストレージデータ読み込みの処理では、ストレージ領域にあるデータをすべて取得しています。
そのため、中にはJSON形式に変換できないような値が含まれていることも当然考えられるので、
try catchすることで、JSONのパースエラーを回避しています。

デザインをCSS3で

/*  =00 Basic Setting
-----------------------------------------------------*/
body {
  margin: 0 auto;
  padding: 0 32px;
  font-family: Helvetica, Arial, Helvetica, sans-serif;
  background: #C38BB0;
  background: -moz-linear-gradient(top, #92C5E0 0%, #C38BB0 100%) fixed;
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #92C5E0), color-stop(100%, #C38BB0)) fixed;
  background: -webkit-linear-gradient(top, #92C5E0 0%, #C38BB0 100%) fixed;
  background: -o-linear-gradient(top, #92C5E0 0%, #C38BB0 100%);
  background: -ms-linear-gradient(top, #92C5E0 0%, #C38BB0 100%) fixed;
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#92c5e0', endColorstr='#c38bb0', GradientType=0 );
}

h1{
  font-size: 28px;
  line-height: 22px;
  margin-bottom: 22px;
}

.logo-bg {
  position: absolute;
  z-index: -1;
  top: -.4em;
  left: 0px;
  color: #8ABAD4;
  font-size: 600%;
  font-weight: bold;
  white-space: nowrap;
}


/*  =01 Container
-----------------------------------------------------*/
#container {
  margin-top: 40px;
  border: 10px solid #fff;
  padding: 40px 20px;
  background: #f0f0f0;
  box-shadow: 0 2px 5px rgba(0,0,0,0.75);

}


/*  =02 Regist Form
-----------------------------------------------------*/
fieldset {
  padding: 18px;
  background: #222222;
  border-radius: 11px;
  display: table;
  margin-bottom: 40px;
}
  fieldset p {
    display: table-cell;
    padding-left: 20px;
    vertical-align: bottom;
  }
    fieldset p label {
      color: #E3E3E3;
      font-size: 120%;
      display: block;
    }

  fieldset input[type="text"],
  fieldset input[type="email"],
  fieldset input[type="url"] {
    display: table-cell;
    width: 180px;
    height: 20px;
    border: 1px solid #D7D7D7;
  }


/*  =03 table
-----------------------------------------------------*/
table {
  border-collapse: collapse;
  text-align: left;
  margin-bottom: 20px;
  width: 100%;
}

table caption {
  margin-bottom: 10px;
  font-size: 28px;
  font-weight: bold;
  text-align: left;
}

table thead {
  border-bottom: 3px solid #09c;
}

table thead th {
  padding: 0 10px;
  text-align: left;
}

table tbody tr td {
  padding: 10px;
}
  table tbody tr:nth-child(even) {
    background: #DDD;
  }

  table tbody tr:nth-child(odd) {
    background: #fff;
  }

  table tbody tr:hover {
    background: #09c;
    border-radius: 4px;
  }
    table tbody tr:hover td {
      color: #fff;
    }

  table tbody tr .delete {
    visibility: hidden;
  }
    table tbody tr:hover .delete {
      visibility: visible;
    }


/*  =01 Button
-----------------------------------------------------*/
button {
  color: #6E6E6E;
  font: bold 12px Helvetica, Arial, sans-serif;
  text-decoration: none;
  padding: 4px 18px;
  position: relative;
  display: inline-block;
  text-shadow: 0 1px 0 white;
  -webkit-transition: border-color .218s;
  -moz-transition: border .218s;
  -o-transition: border-color .218s;
  transition: border-color .218s;
  background: #F3F3F3;
  background: -webkit-gradient(linear,0% 40%,0% 70%,from(whiteSmoke),to(#F1F1F1));
  background: -moz-linear-gradient(linear,0% 40%,0% 70%,from(whiteSmoke),to(#F1F1F1));
  border: solid 1px;
  border-radius: 4px;
  -webkit-border-radius: 4px;
  -moz-border-radius: 4px;
  margin-right: 10px;
  border-image: initial;
}
  button:hover {
      color: #333;
      border-color: #999;
      -moz-box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2) -webkit-box-shadow:0 2px 5px rgba(0, 0, 0, 0.2);
      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
  }

  button:active {
      color: #000;
      border-color: #444;
  }

button.delete {
  color: #fff;
  text-shadow: 0 -1px 0 #333;
  padding: 4px;
  border-radius: 12px;
  -webkit-border-radius: 12px;
  -moz-border-radius: 12px;
  background: #E75D59;
  background: -webkit-gradient(linear,0% 40%,0% 70%,from(#E75D59),to(#990000));
  background: -moz-linear-gradient(linear,0% 40%,0% 70%,from(#E75D59),to(#E6584C));

}

ページ背景やボタンにグラデーションを指定します。


まとめ

WebStorageはリレーショナルデータベースの様に複雑なデータ管理や検索といった使い方をするには無理があります。
例えばオンラインゲームの状態保存や、アカウント作成時における入力状態の保存といった用途には使えるのではないかと思えます。
いろいろと可能性を探ってきたいものです。