-
Notifications
You must be signed in to change notification settings - Fork 0
WMCache
WMCache.js は、ブラウザ上で、大容量かつ高速に動作するクライアントサイドストレージ機能を提供します。
これは現時点で提供できる、もっとも理想的なクライアントキャッシュの姿です。
クライアントストレージの歴史 も参照してください。
- iOS 8 Mobile Safari, Chrome for Android をサポートしています
- Android Browser はサポート対象外です。ChromeTrigger.js を使い、Chrome for Android にユーザを誘導してください
- 大容量のストレージを提供します
-
端末の空きストレージの10%を利用できます
- ユーザにストレージの使用許可を求めるダイアログを表示しません
- 容量不足になると自動的にGCが実行されます
-
端末の空きストレージの10%を利用できます
- ユーザがコントロール可能なブラウザキャッシュとして、またユーザデータの保存先(大容量のLocalStorage)としても利用できます
- XHRで取得したデータをキャッシュし再利用できます
- ブラウザ上で生成したデータの保存と読み込みができます(dotfile)
- DataURI + Base64 + WebStorage/WebSQL を使った旧来の方法に比べ、無駄がなく、少ないメモリで高速に動作します
- Blob, BlobURL, ArrayBuffer をサポートし、FileSystem, IndexedDB, Disk Quota API を使用しています
- キャッシュコントロール機能があります
- WMCache#gc や WMCache#profile などのデバッグに役立つ機能を搭載しています
- ゼロコンフィグレーションです。サーバ側へのAPIの設置や設定ファイルは不要です。js を読み込むだけで利用できます
端末毎のストレージの空き容量と、キャッシュ容量の表です。ストレージ容量の参考にしてください。
この表の 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 |
- new WMCache インスタンスを作成します
- WMCache#has キャッシュがあれば true を返します
- WMCache#get キャッシュがあればキャッシュを返し、キャッシュが無ければサーバからダウンロードしてキャッシュします
- WMCache#list キャッシュの一覧とサイズを返します
- WMCache#size キャッシュ合計をバイト数で返します
- WMCache#drop キャッシュを削除します
- WMCache#store データを直接ストレージに書き込みます
- WMCache#clear 全てのキャッシュを削除します
- WMCache#gc 不要なキャッシュを削除します
- WMCache#getText WMCache#get と同様に機能し、結果を Text(文字列) で受け取ります
- WMCache#getJSON WMCache#get と同様に機能し、結果を JSON Object で受け取ります
- WMCache#getBlob WMCache#get と同様に機能し、結果を Blob で受け取ります
-
WMCache#getBlobURL WMCache#get と同様に機能し、結果を BlobURL で受け取ります
- 画像のキャッシュや WebFont のキャッシュで使用します
-
WMCache#getArrayBuffer WMCache#get と同様に機能し、結果を ArrayBuffer で受け取ります
- WebAudio API等で使用します
- WMCache#clean ストレージを初期化します
- WMCache#profile ストレージの使用状況やキャッシュ容量をダンプします
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#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#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#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#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#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#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#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#gc は、不要になったキャッシュを削除し、ストレージの空き容量を増やします。
new WMCache の garbage で指定した URL と一致するキャッシュを削除します。
削除するキャッシュは以下の式で決まります。
if (!garbage.length) {
全てのキャッシュを削除する(例外としてdotfileは削除しない)
} else {
garbage と一致するキャッシュを削除する(例外として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#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#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#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#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#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#clean():void はストレージを初期化します
function WMCache_clean() {
this._storage["clean"]();
}
WMCache#profile():void はストレージの使用状況やキャッシュ容量をダンプします。
現在は、以下の情報を提示します。
- 全てのキャッシュを取得する時間を計測し、DevTools のタイムライングラフに取得タイミングと時間をマッピングします
- 使用する際は、DevTools を起動しタイムラインタブを表示しておいてください
- 全てのキャッシュの key と size をダンプします
- L2(ローカルストレージキャッシュ)の使用状況(USED)と、Disk Quota APIから得られた情報(USED, QUOTA)をダンプします
- L2 の他にも L1(オンメモリキャッシュ)、L3(ネットワークキャッシュ)といった概念が存在します
- ブラウザで扱えるメモリには制限があるため、L1 をサポートせず L2 のみをサポートしています
- L1, L2, L3 は CPUのキャッシュ用語からの流用です
- L2 の他にも L1(オンメモリキャッシュ)、L3(ネットワークキャッシュ)といった概念が存在します
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 + ")";
}
}
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 にするなどの手順が必要です。