Skip to content
uupaa edited this page Nov 14, 2014 · 66 revisions

WMCache.js は、ブラウザ上で、大容量かつ高速に動作するクライアントサイドストレージ機能を提供します。
これは現時点で提供できる、もっとも理想的なクライアントキャッシュの姿です。

クライアントストレージの歴史 も参照してください。

特徴

  • iOS 8 Mobile Safari, Chrome for Android をサポートしています
    • Android Browser はサポート対象外です。ChromeTrigger.js を使い、Chrome for Android にユーザを誘導してください
  • 大容量のストレージを提供します
  • ユーザがコントロール可能なブラウザキャッシュとして、またユーザデータの保存先(大容量のLocalStorage)としても利用できます
    • XHRで取得したデータをキャッシュし再利用できます
    • ブラウザ上で生成したデータの保存と読み込みができます(dotfile)
  • DataURI + Base64 + WebStorage/WebSQL を使った旧来の方法に比べ、無駄がなく、少ないメモリで高速に動作します
    • Blob, BlobURL, ArrayBuffer をサポートし、FileSystem, IndexedDB, Disk Quota API を使用しています
  • キャッシュコントロール機能があります
  • WMCache#gcWMCache#profile などのデバッグに役立つ機能を搭載しています
  • ゼロコンフィグレーションです。サーバ側へのAPIの設置や設定ファイルは不要です。js を読み込むだけで利用できます

端末毎のDISK QUOTA

端末毎のストレージの空き容量と、キャッシュ容量の表です。ストレージ容量の参考にしてください。
この表の QUOTA の値までキャッシュを貯めこむ事ができます。
iOS 8 では DiskQuota APIが機能しないため、端末のストレージの限界までデータを貯めこむ事ができます(が、実行すべきではないでしょう)。

DEVICE 端末の
ストレージ
空き容量
(QUOTA)
WMCacheから
使用可能な容量
iPhone 5 (iOS 8) 32GB 26.3GB 26.3GB
Nexus 7 (2012) 16GB 11.3GB 770MB
NW-Z1050 16GB(内蔵は2GBのみ) 526MB 35MB

API

WMCache

new WMCache(param:Object, callback:Function, errCallback:Function) でインスタンスを作成します。

ストレージの初期化成功で callback(cache:WMCache) をコールバックします。

エラー発生で errCallback(err:Error) をコールバックします。

param には name, allow, deny, garbage, limit を指定できます

param note
name:String = "void" アプリ名などのユニークな名前です。
ストレージ内部で
ディレクトリ名などに使用します
allow:URLStringArray = [] キャッシュするURLを指定します
deny:URLStringArray = [] キャッシュしないURLを指定します
garbage:URLStringArray = [] GC対象とするURLを指定します
limit:Integer = 0 キャッシュする容量を制限します。
0 は無制限になります。
単位は MB です

allow, deny, garbage, limit は WMCache#get に作用します。
詳しくは キャッシュコントロールを参照してください。

// code
var FS = global["WMFileSystemStorage"]; // FileSystem backend
var DB = global["WMIndexedDBStorage"];  // IndexedDB backend
var BH = global["WMBlackholeStorage"];  // Blackhole backend

function WMCache(param,         // @arg Object - { name, deny, allow, limit, garbage }
                 callback,      // @arg Function - cache ready callback(cache:WMCache, backend:StorageString):void
                 errCallback) { // @arg Function - error callback(err:Error):void
                                // @param.name String = "void" - application name
                                // @param.deny URLStringArray = [] - deny URL pattern
                                // @param.allow URLStringArray = [] - allow URL pattern
                                // @param.garbage URLStringArray = [] - garbage URL pattern
                                // @param.limit Integer = 0 - cache limit (unit MB)
    param = param || {};

//{@dev
    $valid($type(param,       "Object"),              WMCache, "param");
    $valid($type(callback,    "Function"),            WMCache, "callback");
    $valid($type(errCallback, "Function"),            WMCache, "errCallback");
    $valid($keys(param,       "name|deny|allow|garbage|limit"), WMCache, "param");
    $valid($type(param.name,  "String|omit"),         WMCache, "param.name");
    $valid($type(param.deny,  "URLStringArray|omit"), WMCache, "param.deny");
    $valid($type(param.allow, "URLStringArray|omit"), WMCache, "param.allow");
    $valid($type(param.garbage, "URLStringArray|omit"), WMCache, "param.garbage");
    $valid($type(param.limit, "Integer|omit"),        WMCache, "param.limit");
//}@dev

    var that = this;
    var name = param["name"] || "void";
    var deny = param["deny"] || [];
    var allow = param["allow"] || [];
    var garbage = param["garbage"] || [];

    this._limit = (param["limit"] || 0) * 1024 * 1024; // MB
    this._errCallback = errCallback;
    this._control = new WMCacheControl(allow, deny, garbage);
    this._storage = FS["ready"] ? new FS(name, _ready, errCallback) :
                    DB["ready"] ? new DB(name, _ready, errCallback) :
                                  new BH(name, _ready, errCallback);
    function _ready() {
        callback(that);
    }
}

WMCache.prototype.has

WMCache#has(url:URLString):Boolean は、キャッシュが存在する場合に true を返します。

// code
var PATH_NORMALIZE = /^\.\//;      // "./a.png" -> "a.png"

function WMCache_has(url) { // @arg URLString
                            // @ret Boolean
//{@dev
    $valid($type(url, "URLString"), WMCache_has, "url");
//}@dev

    return this._storage["has"](url.replace(PATH_NORMALIZE, ""));
}
var cache = new WMCache({}, function(cache) {
    cache.has("a.png"); // -> true or false
}, function(err) {});

WMCache.prototype.get

WMCache#get(url:URLString, callback:Function, options:Object = {}):void は、キャッシュがあればキャッシュを返し、キャッシュが無ければサーバからダウンロードしてキャッシュします。
先頭がドットから始まるファイルはキャッシュから直接取り出します。サーバにファイルを取得しに行きません。

結果は callback(url:URLString, data:Blob|File|ArrayBuffer|null, mime:MimeTypeString, size:Integer, cached:Boolean) で返します。
サーバにファイルが存在しない場合(404)やサーバでエラーが発生した場合(503等)は callback(url, null, "", 0, false) を返します。
通信エラーが発生した場合は errCallback を呼び出します

  • data は Blob, File または ArrayBuffer 型になります。FileSystem API が利用できる環境では Blob か File を、それ以外の場合は ArrayBuffer を返します。
  • mime はサーバから返された MimeType です。 "image/png" などになります。
    • URL から MimeType を取得する場合は WMMimeType.js を使用してください。
  • size はファイルサイズ(bytes)です
  • キャッシュを返した場合(サーバからファイルをダウンロードしなかった場合)に、cached が true になります

options には cors, reget を指定できます

options note
options.cors:Boolean = false xhr.withCredentials = true を行います
options.reget:Boolean = false キャッシュを無視して
サーバから再取得を行い
既存のキャッシュを上書きします
// code
function WMCache_get(url,       // @arg URLString
                     callback,  // @arg Function - callback(url:URLString, data:Blob|File|ArrayBuffer|null, mime:MimeTypeString, size:Integer, cached:Boolean):void
                     options) { // @arg Object = {} - { reget, cors, wait }
                                // @options.reget Boolean = false - force re-download from server.
                                // @options.cors Boolean = false - withCredentials value.
                                // @options.wait Boolean = false - wait for completion of writing.
                                // @desc fetch L2 or L3 cache and store.
//{@dev
    $valid($type(url,      "URLString"), WMCache_get, "url");
    $valid($type(callback, "Function"),  WMCache_get, "callback");
//}@dev

    options = options || {};

//{@dev
    $valid($type(options.reget, "Boolean|omit"), WMCache_get, "options.reget");
    $valid($type(options.cors,  "Boolean|omit"), WMCache_get, "options.cors");
//}@dev

    url = url.replace(PATH_NORMALIZE, "");

    var that = this;

    if ( DOT_FILE.test(url) ) {
        _fetch(this, url, callback);
        return;
    }

    if ( options["reget"] || !this._storage["has"](url) ) { // download?
        var xhr = new XMLHttpRequest();

        xhr["onerror"] = this._errCallback;
        xhr["onload"] = function() {
            var status = xhr["status"];
            if (status >= 200 && status < 300) {
                _loaded(xhr["response"], // Blob or ArrayBuffer
                        xhr["getResponseHeader"]("content-type"), // mime
                        parseInt(xhr["getResponseHeader"]("content-length"), 10)); // size
            } else {
                callback(url, null, "", 0, false); // 404 and other
            }
        };
        if (options["cors"]) { xhr["withCredentials"] = true; }
        xhr["responseType"] = this._storage instanceof FS ? "blob" : "arraybuffer";
        xhr["open"]("GET", url);
        xhr["send"]();
    } else { // fetch cached data
        this._storage["fetch"](url, function(data, mime, size) {
            callback(url, data, mime, size, true);
        });
    }

    function _loaded(data, mime, size) {
        var store = that._control["isStore"](url);

        if (store &&
            that._limit &&
            that._limit < that["size"]() + size) {
            store = false;
        }
        if (store) {
            if (options["wait"]) {
                that._storage["store"](url, data, mime, size, function(code) {
                    _handleStatusCode(code);
                    // Wait for completion of writing.
                    callback(url, data, mime, size, false);
                });
            } else {
                that._storage["store"](url, data, mime, size, _handleStatusCode);
                // Not wait for completion of writing.
                callback(url, data, mime, size, false);
            }
        } else {
            callback(url, data, mime, size, false);
        }
    }
    function _handleStatusCode(code) {
        switch (code) {
        case 200: break;
        case 413: console.log("QuotaExceededError");
                  that["gc"](); // auto gc
                  break;
        case 503: console.log("WriteError");
        }
    }
}

function _fetch(that,       // @arg this
                url,        // @arg URLString
                callback) { // @arg Function - callback(url:URLString, data:Blob|File|ArrayBuffer, mime:MimeTypeString, size:Integer):void
    if ( that._storage["has"](url) ) {
        that._storage["fetch"](url, function(data, mime, size) {
            callback(url, data, mime, size, true);
        });
    } else {
        callback(url, null, "", 0, false); // 404
    }
}
cache.get("a.png", function(url, data, mime, size, cached) {
    cache.has("a.png"); // -> true
});

WMCache.prototype.list

WMCache#list():Object は、キャッシュの一覧とサイズを返します。

戻り値は { url: size, ... } の一覧です。
相対パス("./a.png") は 正規化され、先頭の "./" を除去した状態の相対パス("a.png") としてリストアップされます。

// code
function WMCache_list() { // @ret Object - { url: size, ... }
    return this._storage["list"]();
}
cache.get("a.png", function(url, data, mime, size, cached) {
    cache.list(); // -> { "a.png": 12345 }
});

WMCache.prototype.size

WMCache#size():Integer は キャッシュの合計をバイト数で返します。

// code
function WMCache_size() { // @ret Integer - total cache size
    return this._storage["size"]();
}
(cache.size() / 1024 / 1024).toFixed(1) + "MB"; // "24.1MB"

WMCache.prototype.drop

WMCache#drop(url:URLString, callback:Function):void は、url に一致するキャッシュを削除します。 一致するキャッシュが存在しない場合は何もしません。

// code
function WMCache_drop(url,        // @arg URLString
                      callback) { // @arg Function = null - callback():void
                                  // @ret Object - { url: size, ... }
    url = url.replace(PATH_NORMALIZE, "");
    this._storage["drop"](url, callback || function() {});
}

WMCache.prototype.store

WMCache#store(url:URLString, data:Blob|File|ArrayBuffer, mime:MimeTypeString, size:Integer, callback:Function):void は、データを直接ストレージに書き込みます。

// code
function WMCache_store(url,        // @arg URLString
                       data,       // @arg Blob|File|ArrayBuffer
                       mime,       // @arg MimeTypeString
                       size,       // @arg Integer
                       callback) { // @arg Function = null - callback(url:URLString, code:HTTPStatusCode, stored:Boolean):void
//{@dev
    $valid($type(url,      "URLString"),        WMCache_store, "url");
    $valid($type(data,     "Blob|File|ArrayBuffer"), WMCache_store, "data");
    $valid($type(mime,     "MimeTypeString"),   WMCache_store, "mime");
    $valid($type(size,     "Integer"),          WMCache_store, "size");
    $valid($type(callback, "Function|omit"),    WMCache_store, "callback");
//}@dev

    callback = callback || function() {};

    var that = this;
    var to = this._storage instanceof FS ? "blob" :
             this._storage instanceof DB ? "arraybuffer" : "";

    if (to) {
        _convert(data, to, mime, function(result) {
            that._storage["store"](url, result, mime, size, function(code) {
                callback(url, code, true); // stored
            });
        });
    } else {
        callback(url, 204, false); // fake response
    }
}

WMCache.prototype.clear

WMCache#clear は、全てのキャッシュを削除します。

// code
function WMCache_clear(callback) { // @arg Function = null - callback():void
//{@dev
    $valid($type(callback, "Function|omit"), WMCache_clear, "callback");
//}@dev

    this._storage["clear"](callback || function() {});
}

WMCache.prototype.gc

WMCache#gc は、不要になったキャッシュを削除し、ストレージの空き容量を増やします。

new WMCache の garbage で指定した URL と一致するキャッシュを削除します。

削除するキャッシュは以下の式で決まります。

if (!garbage.length) {
    全てのキャッシュを削除する(例外としてdotfileは削除しない)
} else {
    garbage と一致するキャッシュを削除する(例外としてdotfileは削除しない)
}

dotfile

先頭がドットで始まるパスを WMCache.js では dotfile と呼んでいます。
dotfile は GC の処理対象外になるため、大容量LocalStorage として利用できます。

  • .a.png ▶ dotfile です
  • .http://example.com/a.png ▶ dotfile です
  • ../node_modules/dir/a.png ▶ dotfile ではありません
  • ./a.png ▶ dotfile ではありません
// code
function WMCache_gc() { // @desc Drop unnecessary cache.
    this._control["gc"](this);
}

function WMCacheControl_gc(cache) { // @arg WMCache
    var list = cache["list"](); // { url: size, ... }
    var target = [];
    var gz = this._garbage.length;

    if (gz) {
        // garbage が指定されている場合は、
        // URLのパターンにマッチする url を削除する。
        // ただし、先頭がドットで始まる dotfiles は削除しない
        for (var url in list) {
            if ( !DOT_FILE.test(url) ) {
                for (var i = 0; i < gz; ++i) {
                    if (WMURL["match"](this._garbage[i], url)) {
                        target.push(url);
                    }
                }
            }
        }
    } else {
        // garbage が指定されていない場合は、
        // 全ての url を削除対象とする。
        // ただし、先頭がドットで始まる dotfiles は削除しない
        for (var url in list) {
            if ( !DOT_FILE.test(url) ) {
                target.push(url);
            }
        }
    }
    target.forEach(function(url) {
        cache["drop"](url);
    });
}

WMCache.prototype.getText

WMCache#getText(url:URLString, callback:Function, options:Object = {}):void は WMCache#get と同様に機能します。

結果を callback(url:URLString, text:String, mime:String, size:Integer, cached:Boolean) で受け取ります。

// code
function WMCache_getText(url,       // @arg URLString
                         callback,  // @arg Function - callback(url:URLString, text:String, mime:String, size:Integer, cached:Boolean):void
                         options) { // @arg Object = {} - { reget, cors, wait }
    this["get"](url, function(url, data, mime, size, cached) {
        _convert(data, "text", mime, function(result) {
            callback(url, result, mime, size, cached);
        });
    }, options);
}

function _toBlob(data, mime) {
    return data instanceof Blob ? data
                                : new Blob([data], { "type": mime });
}

function _convert(data, to, mime, callback) {
    var method = "";

    switch (to) {
    case "text":    method = "readAsText"; break;    // Blob|ArrayBuffer -> Text
    case "blob":    callback( _toBlob(data, mime) ); break;
    case "bloburl": callback( URL["createObjectURL"](_toBlob(data, mime)) ); break;
    case "dataurl": method = "readAsDataURL"; break; // Blob|ArrayBuffer -> DataURL
    case "arraybuffer":
        if (data instanceof Blob) {
            method = "readAsArrayBuffer";
        } else {
            callback(data);
        }
    }
    if (method) {
        var reader = new FileReader();

        reader["onloadend"] = function() {
            callback(reader["result"]);
        };
        reader[method]( _toBlob(data, mime) );
    }
}

WMCache.prototype.getJSON

WMCache#getJSON(url:URLString, callback:Function, options:Object = {}):void は WMCache#get と同様に機能します。

結果を callback(url:URLString, json:Object, mime:String, size:Integer, cached:Boolean) で受け取ります。

// code
function WMCache_getJSON(url,       // @arg URLString
                         callback,  // @arg Function - callback(url:URLString, json:Object, mime:String, size:Integer, cached:Boolean):void
                         options) { // @arg Object = {} - { reget, cors, wait }
    this["get"](url, function(url, text, mime, size, cached) {
        _convert(data, "text", mime, function(result) {
            callback(url, JSON.parse(result), mime, size, cached);
        });
    }, options);
}

WMCache.prototype.getBlob

WMCache#getBlob(url:URLString, callback:Function, options:Object = {}):void は WMCache#get と同様に機能します。

結果を callback(url:URLString, blob:Blob, mime:String, size:Integer, cached:Boolean) で受け取ります。

// code
function WMCache_getBlob(url,       // @arg URLString
                         callback,  // @arg Function - callback(url:URLString, blob:Blob|File, mime:String, size:Integer, cached:Boolean):void
                         options) { // @arg Object = {} - { reget, cors, wait }
    this["get"](url, function(url, data, mime, size, cached) {
        _convert(data, "blob", mime, function(result) {
            callback(url, result, mime, size, cached);
        });
    }, options);
}

WMCache.prototype.getBlobURL

WMCache#getBlobURL(url:URLString, callback:Function, options:Object = {}):void は WMCache#get と同様に機能します。

結果を callback(url:URLString, blobURL:BlobURLString, mime:String, size:Integer, cached:Boolean) で受け取ります。

// code
function WMCache_getBlobURL(url,       // @arg URLString
                            callback,  // @arg Function - callback(url:URLString, blobURL:BlobURLString, mime:String, size:Integer, cached:Boolean):void
                            options) { // @arg Object = {} - { reget, cors, wait }
    this["get"](url, function(url, data, mime, size, cached) {
        _convert(data, "bloburl", mime, function(result) {
            callback(url, result, mime, size, cached);
        });
    }, options);
}
cache.getBlobURL(url, function(url, blobURL, mime) {
    if (/image/.test(mime)) { // "image/png", ...
        var img = new Image();
        img.src = blobURL;
        document.body.appendChild(img);
    }
});

WMCache.prototype.getArrayBuffer

WMCache#getArrayBuffer(url:URLString, callback:Function, options:Object = {}):void は WMCache#get と同様に機能します。

結果を callback(url:URLString, buffer:ArrayBuffer, mime:String, size:Integer, cached:Boolean) で受け取ります。

// code
function WMCache_getArrayBuffer(url,       // @arg URLString
                                callback,  // @arg Function - callback(url:URLString, buffer:ArrayBuffer, mime:String, size:Integer, cached:Boolean):void
                                options) { // @arg Object = {} - { reget, cors, wait }
    this["get"](url, function(url, data, mime, size, cached) {
        _convert(data, "arraybuffer", mime, function(result) {
            callback(url, result, mime, size, cached);
        });
    }, options);
}
var ctx = new AudioContext();
var soundBuffer = null

cache.getArrayBuffer(url, function(url, data, mime) {
    ctx.decodeAudioData(data, function(decodedBuffer) {
        soundBuffer = decodedBuffer;
    });
});

WMCache.prototype.clean

WMCache#clean():void はストレージを初期化します

function WMCache_clean() {
    this._storage["clean"]();
}

WMCache.prototype.profile

WMCache#profile():void はストレージの使用状況やキャッシュ容量をダンプします。

現在は、以下の情報を提示します。

  • 全てのキャッシュを取得する時間を計測し、DevTools のタイムライングラフに取得タイミングと時間をマッピングします
    • 使用する際は、DevTools を起動しタイムラインタブを表示しておいてください
  • 全てのキャッシュの key と size をダンプします
  • L2(ローカルストレージキャッシュ)の使用状況(USED)と、Disk Quota APIから得られた情報(USED, QUOTA)をダンプします
    • L2 の他にも L1(オンメモリキャッシュ)、L3(ネットワークキャッシュ)といった概念が存在します
      • ブラウザで扱えるメモリには制限があるため、L1 をサポートせず L2 のみをサポートしています
      • L1, L2, L3 は CPUのキャッシュ用語からの流用です

function WMCache_profile() {
    if (WMCacheProfile) {
        WMCacheProfile["dump"](this);
    }
}

function WMCacheProfile_get(cache,      // @arg WMCache
                            callback) { // @arg Function = null - callback(info:Object):void
                                        // @info.L2_LIST Object - { url: size, ... }
                                        // @info.L2_USED Integer - L2 used bytes. storage payload
                                        // @info.DEVICE_USED Integer - Storage used bytes.
                                        // @info.DEVICE_QUOTA Integer - Storage quota(cap) bytes.
                                        // @desc get L1, L2 and temporary disk quota.
    var task = new Task(2, function(err, buffer) {
            if (callback) {
                callback(buffer);
            }
        });
    var list = cache["list"]();

    if (global.chrome) {
        _chromeDevToolsProfile(list);
    }
    _collectStorageData(list);

    cache["quota"](function(used, quota) {
        task["set"]("DEVICE_USED",  used);
        task["set"]("DEVICE_QUOTA", quota);
        task["pass"]();
    });

    function _chromeDevToolsProfile(list) {
        // https://developer.chrome.com/devtools/docs/console#marking-the-timeline
        console.time("cache fetch elapsed");
        console.timeline("cache");
        //console.profile("CPU prifile");
        var keys = Object.keys(list);
        var fetchTask = new Task(keys.length, function() {
                setTimeout(function() {
                    //console.profileEnd("CPU prifile");
                    console.timelineEnd("cache");
                    console.timeEnd("cache fetch elapsed");
                }, 20);
            });
        keys.forEach(function(url, index) {
            cache.get(url, function() {
                console.timeStamp(index);
                fetchTask.pass();
            });
        });
    }

    function _collectStorageData(list) {
        var used = 0;
        for (var url in list) {
            used += list[url];
        }
        task["set"]("L2_LIST",  list);
        task["set"]("L2_USED", used);
        task["pass"]();
    }
}

function WMCacheProfile_dump(cache) { // @arg WMCache
    WMCacheProfile_get(cache, function(info) {
        if (global.chrome) {
            console.table(_table(info.L2_LIST));
            console.table({
                L2:        { USED: unit(info.L2_USED),     QUOTA: "NO DATA" },
                DiskQuota: { USED: unit(info.DEVICE_USED), QUOTA: unit(info.DEVICE_QUOTA) }
            });
        } else {
            console.dir(info.L2_LIST);
            console.dir({
                L2:        { USED: unit(info.L2_USED),     QUOTA: "NO DATA" },
                DiskQuota: { USED: unit(info.DEVICE_USED), QUOTA: unit(info.DEVICE_QUOTA) }
            });
        }
    });

    function _table(list) {
        var result = [];
        for (var id in list) {
            result.push({ ID: id, SIZE: list[id] });
        }
        return result;
    }
    function unit(n) {
        n = n || 0;

        if (n < 1024) {
            return (n.toString()).slice(-6) + "B";
        }
        if (n < 1024 * 1024) {
            return ((n / 1024).toFixed(1)).slice(-6) + "KB (" + n + ")";
        }
        if (n < 1024 * 1024 * 1024) {
            return ((n / 1024 / 1024).toFixed(1)).slice(-6) + "MB (" + n + ")";
        }
        return ((n / 1024 / 1024 / 1023).toFixed(1)).slice(-6) + "GB (" + n + ")";
    }
}

Backend Storage

WMCache.js は、データの保存先として WMFileSystem.js, WMIndexedDB.js, WMBlackholeStorage.js の3つのストレージを使っています。

  • WMFileSystem.js - FileSystem API を使います
  • WMIndexedDB.js - IndexedDB を使います
  • WMBlackholeStorage.js - どこにも保存しません。dev/null です

WMFileSystem.js, WMIndexedDB.js, WMBlackholeStorage.js は WMCache.js に同梱されています。

各DBの内容を DevTools から直接参照することもできます。

FileSystem を DevTools でみるためには chrome://flags で Enable Developer Tools experiments を ON にするなどの手順が必要です。