HTML5 Web Storage-- な機能を uupaa.js に実装してみた
Cookieよりも大容量のデータをクライアントサイドに保存する仕様。それが HTML5 の Web 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+ で動作確認してます。
ストレージバックエンド
以下のオーダーで バックエンドが利用可能か試行します。
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〜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
リンク
ソースコード(長いよ)
// === 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 トラップな(余計な)コードが入ってます。
問題が無ければ、次回のリリースから利用可能になります。
# と思ってたけど、ちょっとコード直さないと…