HTML5 Web Storage-- な機能を uupaa.js に実装してみた

Cookieよりも大容量のデータをクライアントサイドに保存する仕様。それが HTML5Web Storage です。
Web Storage はまだ策定中です。

Firefox3.5+, IE8+, Safari4+, Opera10.50+ など最新のブラウザでは既に利用可能ですが、「何年も待ってられない、今すぐ使いたい」ですよね?

そこで、クロスブラウザWeb Storage 相当の機能を uupaa.js に実装してみました。
# sessionStorage は実装していませんよ

デモ

http://pigs.sourceforge.jp/blog/20100104/20100104_uu.storage.htm

Firefox2+, Safari3.1+, IE6+, Google Chrome3+, Opera9.2+ で動作確認してます。

ストレージバックエンド

以下のオーダーで バックエンドが利用可能か試行します。

  1. Web Storage (window.localStorage) (Firefox3.5+, IE8+, Safari4+, Opera10.50+)
  2. IE Storage (userData behavior) (IE6+)
  3. Flash Storage (SharedObject) (Flash8 以上をインストールしている環境)
  4. Cookie Storage (document.cookie) (cookieが利用可能な環境)

Web Storage の I/F

http://www.w3.org/TR/webstorage/#storage

interface Storage {
  readonly attribute unsigned long length;
  getter DOMString key(in unsigned long index);
  getter any getItem(in DOMString key);
  setter creator void setItem(in DOMString key, in any data);
  deleter void removeItem(in DOMString key);
  void clear();
};

Web Storage を利用した、key/value pair のセットとゲットはこんな感じになります。

var db = window.localStorage;
var key = "keyword";
var val = "value";

db[key] = val; // setter
db.setItem(key, val); // これでもOK

alert(db[key]); // "value"  getter
alert(db.getItem(key)); // これでもOK

uupaa.js に実装した I/F

全て同期I/Fです

uu.mix(uu, {
  // --- Web Storage ---
  // [1][get all] uu.local() -> { key: "val", ... }
  // [2][get one] uu.local("key") -> "val"
  // [3][set]     uu.local("key", "val")
  local: uu.mix(uulocal, {
    nth:        uulocalnth,     // uu.local.nth(n) -> "key"
    get:        uulocalget,     // uu.local.get("key") -> "val"
    set:        uulocalset,     // uu.local.set("key", "val")
    size:       uulocalsize,    // uu.local.size() -> Number
    clear:      uulocalclear,   // uu.local.clear()
    ready:      uulocalready,   // uu.local.ready() -> Boolean
    remove:     uulocalremove,  // uu.local.remove("key")
    dbtype:     uulocaldbtype   // [protected] uu.local.dbtype() -> Number
  })
});
  • uu.local() で key/value ペアの全取得と 個別取得/登録 を
  • uu.local.nth() で n 番目の key を取得(イテレート用)(0 オリジン)
  • uu.local.get(key) で key に一致する value を取得, 無ければ null または 空文字列
  • uu.local.set(key, value) で pair を登録
  • uu.local.size() で pair数の取得
  • uu.local.clear() で 全pairの削除
  • uu.local.ready() が true ならいずれかのストレージを利用可能
  • uu.local.remove(key) で pair の削除
  • uu.local.dbtype() で現在オープンしているストレージの種類を取得
    • 0: オープンしていない(利用不能)
    • 1: Web Storage(localStorage)
    • 2: globalStorage
    • 3: openDatabase
    • 4: IE Storage
    • 5: Flash Storage
    • 6: Cookie Storage
    • 9: Wait (Storage のオープン待ち, Flash Storage で発生する可能性がある)

注意事項

  • Flash Storage が選択された場合は、Flash のロード + Flash からの応答待ちが裏で走ります(UI スレッドはブロックしないよ)
  • 応答待ちしている間は、uu.local.ready() が false を、uu.local.dbtype() が 9 を返します。
    • ちょっとお待ち下さい。
    • ただし、404 だと待っても無駄です
      • CookieStorage に切り替えて運用するなどの手もありますが、その辺の やさしさ は未実装です。

利用可能なストレージサイズの目安

    • 0: オープンしていない(利用不能)
    • 1: Web Storage(localStorage) → 不明(後で計る)
    • 2: globalStorage → 3〜5MBぐらいらしい
    • 3: openDatabase → 最小で200kB。要求すれば変更可能
    • 4: IE Storage → 最小で64kB
    • 5: Flash Storage → 最小で100kB
    • 6: Cookie Storage → だいたい4kB

@hotchpotch さんからの追加情報

@uupaa openDatabase は、現状実装してあるSafari4/Chrome4 Betaでは1ドメインあたり5MB固定(変更不可能)のようです
via http://twitter.com/hotchpotch/status/7362769845

IE Storage と Cookie Storage の有効期限

ローカル時間で、2032/01/01 にしてあります。

ソースコード(長いよ)

// === Storage ===
// depend: uu.js

// Web Storage:  http://www.w3.org/TR/webstorage/#storage
//         IE8:  http://msdn.microsoft.com/en-us/library/cc197062(VS.85).aspx
// Web Database: http://www.w3.org/TR/webdatabase/
//      Safari4: http://developer.apple.com/safari/library/documentation/iPhone/Conceptual/SafariJSDatabaseGuide/Name-ValueStorage/Name-ValueStorage.html
//
uu.waste || (function(win, doc, uu) {
var _db = 0,     // db object
    _dbtype = 0, // db type,  1 is localStorage,
                 //           2 is globalStorage,
                 //           4 is ieStorage,
                 //           5 is flashStorage,
                 //           6 is cookieStorage,
    _dbwait = 0,
    _persist = new Date(2032, 1, 1), // ieStorage, cookieStorage persist date
    _cookieReady = !!navigator.cookieEnabled,
    // --- use db ---
    _useLocalStorage  = win.localStorage  ? 1 : 0, // Web Storage
//  _useGlobalStorage = win.globalStorage ? 1 : 0, // globalStorage(DOM Storage)
    _useIEStorage     = uu.ie             ? 1 : 0, // userData behavior
    _useFlashStorage  = uu.ver.fl > 7     ? 1 : 0, // SharedObject
    _useCookieStorage = _cookieReady      ? 1 : 0; // Cookie

uu.mix(uu, {
  // --- cookie ---
  // [1][get all] uu.cookie() -> { key: "val", ... }
  // [2][get one] uu.cookie("key") -> "val"
  // [3][set]     uu.cookie("key", "val", option = void 0)
  cookie: uu.mix(uucookie, {
    get:        uucookieget,    // uu.cookie.get("key") -> "val"
    set:        uucookieset,    // uu.cookie.set("key", "val", option = void 0)
    clear:      uucookieclear,  // [1][clear all] uu.cookie.clear()
                                // [2][clear one] uu.cookie.clear("key")
    ready:      uucookieready   // uu.cookie.ready() -> Boolean
  }),
  // --- Web Storage ---
  // [1][get all] uu.local() -> { key: "val", ... }
  // [2][get one] uu.local("key") -> "val"
  // [3][set]     uu.local("key", "val")
  local: uu.mix(uulocal, {
    nth:        uulocalnth,     // uu.local.nth(n) -> "key"
    get:        uulocalget,     // uu.local.get("key") -> "val"
    set:        uulocalset,     // uu.local.set("key", "val")
    size:       uulocalsize,    // uu.local.size() -> Number
    clear:      uulocalclear,   // uu.local.clear()
    ready:      uulocalready,   // uu.local.ready() -> Boolean
    remove:     uulocalremove,  // uu.local.remove("key")
    dbtype:     uulocaldbtype   // [protected] uu.local.dbtype() -> Number
  })
});

// --- cookie ---
// uu.cookie - cookie accessor
// [1][get all] uu.cookie() -> { key: "val", ... }
// [2][get one] uu.cookie("key") -> "val"
// [3][set]     uu.cookie("key", "val", option = void 0)
function uucookie(a, b, c) { // @return Hash/String/void 0:
  return a === void 0 ? _uucookiegetall()
      : (b === void 0 ? _uucookiegetall : uucookieset)(a, b, c);
}

// uu.cookie.get - retrieve cookie
function uucookieget(key) { // @param String: "key"
                            // @return String/null: "val" or null
  return _uucookiegetall()[key];
}

// inner - get all cookies
function _uucookiegetall(prefix) { // @param String(= void 0): prefix filter
  var rv = {}, i, pair, ary, k, v, pfx = prefix || "", pz = pfx.length;

  if (_cookieReady && doc.cookie) {
    pair = doc.cookie.split("; ");
    for (i in pair) {
      ary = pair[i].split("=");
      k = ary[0];
      v = decodeURIComponent(ary[1] == null ? "" : ary[1]);
      if (pfx) {
        !k.indexOf(pfx) && (rv[k.slice(pz)] = v);
      } else {
        rv[k] = v;
      }
    }
  }
  return rv;
}

// uu.cookie.set - store cookie
function uucookieset(key,      // @param String: "key"
                     val,      // @param String: "val"
                     option) { // @param Hash(= {}): { domain, path, maxage }
                               //   maxage - Number/Date: 2 -> +2 days
                               //                         new Date(2010,1,1) -> expire 2010/1/1
                               //   domain - String:
                               //   path   - String:
  if (_cookieReady) {
    var rv = [], opt = option || {}, age = opt.maxage;

    rv.push(key + "=" + encodeURIComponent(val));
    opt.domain && rv.push("domain=" + opt.domain);
    opt.path   && rv.push("path="   + opt.path);
    if (age !== void 0) {
      rv.push("expires=" +
              (uu.isnum(age) ? new Date((+new Date) + age * 86400000)
                             : age).toUTCString());
    }
    (location.protocol === "https:") && rv.push("secure");
    doc.cookie = rv.join("; "); // store
  }
}

// uu.cookie.clear - clear cookie
// [1][clear all] uu.cookie.clear()
// [2][clear one] uu.cookie.clear("key")
function uucookieclear(key,      // @param String(= void 0): key
                       option) { // @param Hash(= {}): { domain, path }
  _uucookieclear(key, "", option);
}

// inner - clear cookies
function _uucookieclear(key,      // @param String(= void 0): "key"
                        prefix,   // @param String(= void 0): prefix filter
                        option) { // @param Hash(= {}): { domain, path }
  var pfx = prefix || "", opt = uu.arg(option, { maxage: -1 }), i,
      hash = key ? uu.hash(key, "") : _uucookiegetall(pfx);

  for (i in hash) {
    uucookieset(pfx + i, "", opt);
  }
}

// uu.cookie.ready
function uucookieready() { // @return Boolean
  return _cookieReady;
}

// --- storage ---
// [1][get all] uu.local() -> { key: "val", ... }
// [2][get one] uu.local("key") -> "val"
// [3][set]     uu.local("key", "val")
function uulocal(a, b) {
  if (a === void 0) { // [1]
    var rv = {}, v, i = 0, iz, ary, r;

    switch (_dbtype) {
    case 1: for (iz = _db.length; i < iz; ++i) {
              v = _db.key(i);
              rv[v] = _db.getItem(v);
            }
            break;
//{::
    case 4: ary = _ieLoadIndex();
            while ( (v = ary[i++]) ) {
              rv[v] = _db.getAttribute(v) || "";
            }
            break;
    case 5: r = _db.uulocalall();
            for (i in r) {
              v = r[i];
              (v === "__uu_typeof_null__") && (v = ""); // [!] restore null
              rv[i] = v;
            }
            break;
    case 6: rv = _uucookiegetall("uustorage");
//::}
    }
    return rv;
  }
  return (b === void 0 ? uulocalget : uulocalset)(a, b); // [2][3]
}

// uu.local.nth
function uulocalnth(nth) { // @param Number: index
                           // @return String: "key"
  var rv, hash;

  switch (_dbtype) {
  case 1: rv = _db.key(nth); break;
//{::
  case 4: rv = _ieLoadIndex()[nth]; break;
  case 5: rv = _db.uulocalnth(nth);
          (rv === "__uu_typeof_null__") && (rv = ""); // [!] restore null
          break;
  case 6: hash = _uucookiegetall("uustorage");
          rv = uu.hash.nth(hash, nth)[0];
//::}
  }
  if (rv == null) { throw new Error("INDEX_SIZE_ERR"); }
  return rv || "";
}

// uu.local.get
function uulocalget(key) { // @param String: "key"
                           // @return String/null: "val"
  if (!key) { return null; } // [HTML5 SPEC] null
  var rv = "";

  switch (_dbtype) {
  case 1: rv = _db[key]; break;
//{::
  case 4: _db.load("uustorage");
          rv = _db.getAttribute(key); break;
  case 5: rv = _db.uulocalget(key);
          (rv === "__uu_typeof_null__") && (rv = ""); // [!] restore null
          break;
  case 6: rv = _uucookiegetall("uustorage")[key];
//::}
  }
  return rv || null;
}

// uu.local.set
function uulocalset(key,   // @param String: "key"
                    val) { // @param String: "val"
  if (!key) { throw new Error("NOT_SUPPORTED_ERR"); }
  var ary;

  switch (_dbtype) {
  case 1: _db[key] = val; break;
//{::
  case 4: ary = _ieLoadIndex();
          ary.push(key);
          _db.setAttribute(key, val);
          _ieSaveIndex(ary);
          break;
  case 5: _db.uulocalset(key, val || "__uu_typeof_null__"); // [!] null trap
          break;
  case 6: uucookieset("uustorage" + key, val, { maxage: _persist });
//::}
  }
}

// uu.local.size
function uulocalsize() { // @return Number: -1 is unknown
  switch (_dbtype) {
  case 1: return _db.length;
//{::
  case 4: return _ieLoadIndex().length;
  case 5: return _db.uulocalsize();
  case 6: return uu.hash.size(_uucookiegetall("uustorage"));
//::}
  }
  return 0;
}

// uu.local.clear
function uulocalclear() {
  var ary, v, i = 0;

  switch (_dbtype) {
  case 1: _db.clear(); break;
//{::
  case 4: ary = _ieLoadIndex();
          while ( (v = ary[i++]) ) {
            _db.removeAttribute(v);
          }
          _ieSaveIndex([]);
          break;
  case 5: _db.uulocalclear(); break;
  case 6: _uucookieclear("", "uustorage");
//::}
  }
}

// uu.local.ready
function uulocalready() { // @return Boolean:
  return !!_dbtype;
}

// uu.local.remove
function uulocalremove(key) { // @param String: "key"
  if (!key) { throw new Error("NOT_SUPPORTED_ERR"); }
  var ary, i = 0;

  switch (_dbtype) {
  case 1: _db.removeItem(key); break;
//{::
  case 4: ary = _ieLoadIndex();
          _db.removeAttribute(key);
          i = ary.indexOf(key);
          i >= 0 && ary.splice(i, 1);
          _ieSaveIndex(ary);
          break;
  case 5: _db.uulocalremove(key); break;
  case 6: uucookieset("uustorage" + key, "", { maxage: -1 });
//::}
  }
}

// uu.local.dbtype
function uulocaldbtype() { // @return Number:
  return _dbwait ? 9 : _dbtype;
}

//{:: inner - load index(ieStorage)
function _ieLoadIndex() {
  _db.load("uustorage");
  var ary = _db.getAttribute("uulocalidx");

  return ary ? ary.split("\v") : [];
}
//::}

//{:: inner - save index(ieStorage)
function _ieSaveIndex(ary) {
  _db.setAttribute("uulocalidx", ary.join("\v"));
  _db.save("uustorage");
}
//::}

//{:: inner - setup ie storage
function _ieStorageInit() {
  var meta;

  uu.head(meta = uue("meta"));
  meta.addBehavior("#default#userData");
  meta.expires = _persist.toUTCString();
  return meta;
}
//::}

//{:: inner - setup flash storage
function _flashStorageInit() {
  _dbwait = 1;
  var div;

  uu.body(div = uu.div());
  return uu.flash(div, "externaluulocal", uu.config.imgdir + "uu.storage.swf", 1, 1);
}
//::}

uupub.flashStorageCallback = flashStorageCallback;

// uupub.flashStorageCallback - callback from FlashStorage
function flashStorageCallback(/* msg */) {
  _dbwait = 0;
  _dbtype = 5;
  uupub.flashStorage = uuvain;
}

// --- open storage object ---
_dbtype = (_useLocalStorage  && (_db = win.localStorage))  ? 1 // Firefox3.5+, IE8+, Safari4+, Opera10.50+
//      : (_useGlobalStorage && (_db = win.globalStorage)) ? 2 // Firefox2.0~3.0
        : (_useIEStorage     && (_db = _ieStorageInit()))  ? 4 // IE6~IE8
        : 0;
(_dbtype === 2) && (_db = _db[location.hostname]); // back compat

_dbtype || uu.ready(function() {
  _dbtype = (_useFlashStorage && (_db = _flashStorageInit())) ? 0 // Opera9.5~10.10, Safari3
          : _useCookieStorage ? 6 // for all
          : 0;
}, 2); // 2: high(system)

})(window, document, uu);
// uu.storage.flash.as
// mtasc -swf img/uu.storage.swf -header 1:1:1 -main -version 8 -strict uu.storage.flash.as
import flash.external.ExternalInterface;

class FlashStorage {
  public function FlashStorage() {
  }
  private function obj():SharedObject {
    return SharedObject.getLocal("FlashStorage");
  }
  private function eiall():Object {
    var so:SharedObject = obj(), rv:Object = {}, key:String, val:String;

    for (key in so.data) {
      val = so.data[key];
      if (val === null) {
        val = "__uu_typeof_null__"; // [!] AS2("") -> JS(null) null trap
      }
      rv[key] = val;
    }
    return rv;
  }
  private function einth(n:Number):String {
    var so:SharedObject = obj(), i:Number = -1, key:String;

    for (key in so.data) {
      if (++i === n) {
        return key;
      }
    }
    return "__uu_typeof_null__"; // [!] AS2("") -> JS(null) null trap
  }
  private function eiget(key:String):String {
    var rv:String = obj().data[key];

    return (rv === null) ? "__uu_typeof_null__" : rv; // [!] AS2("") -> JS(null) null trap
  }
  private function eiset(key:String, val:String):Void {
    var so:SharedObject = obj();

    so.data[key] = val;
    so.flush();
  }
  private function eisize():Number {
    var so:SharedObject = obj(), i:String, j:Number = 0;

    for (i in so.data) {
      ++j;
    }
    return j;
  }
  private function eiclear():Void {
    var so:SharedObject = obj();

    so.clear();
    so.flush();
  }
  private function eiremove(key:String):Void {
    var so:SharedObject = obj();

    delete so.data[key];
    so.flush();
  }
  static function main() {
    var obj:Object = new FlashStorage(),
        fn:Function = ExternalInterface.addCallback;

    fn("uulocalall", obj, obj.eiall);
    fn("uulocalnth", obj, obj.einth);
    fn("uulocalget", obj, obj.eiget);
    fn("uulocalset", obj, obj.eiset);
    fn("uulocalsize", obj, obj.eisize);
    fn("uulocalclear", obj, obj.eiclear);
    fn("uulocalremove", obj, obj.eiremove);

    ExternalInterface.call("uupub.flashStorageCallback", "from flash");
  }
};

反省会

  • 初めての連続
    • Cookie 以外は、へぇぇ〜状態
    • ExternalInterface で 丸1日浪費した感がある
  • 去年やるはずだったのに、間に合わなかった。
  • 容量オーバーで保存できない場合に、仕様では QUOTA_EXCEEDED_ERR 例外を投げる事になっていますが、これから実装します。
  • uu.storage.flash.as のコンパイルmtasc でやってます。
    • null("") が ExternalInterface を通ると "null" に変換されちゃうので、
    • null だったら "__uu_typeof_null__" で渡して、js 側で null("") に戻すという null トラップな(余計な)コードが入ってます。

問題が無ければ、次回のリリースから利用可能になります。
# と思ってたけど、ちょっとコード直さないと…