BlobBuilder で 外部ファイルの要らない WebWorkers はつくれる

WebWorkersで別タブ選択中もきっちり動く無敵タイマーをつくる - つまみ食う
http://d.hatena.ne.jp/mohayonao/20111108/1320756534


先日書いたこのエントリーで、別タブを選択したときも精度が落ちないタイマーを作った。
でも、外部ファイルが必要だったのでライブラリ的なものに組み込むときにパスを自己解決できないという問題があった。

// 問題点:muteki-timer.js の設置場所が制限される
//        もしくはライブラリの外から muteki-timer.js の場所を教えてあげる必要がある
var url = "muteki-timer.js";
var timer = new Worker(url); 


今回は外部ファイルを使わない無敵タイマー。以下手順

  1. BlobBuilderっていうのを使ってコードを組み立てる。
  2. すごいと評判*1 の createObjectURL でURLを取得
  3. new Worker(url) する


ちなみに前回の方法は

  1. 外部ファイルを書く (muteki-timer.js)
  2. url = "./muteki-timer.js"
  3. new Worker(url) する

MutekiTimerクラス

上記の方法で無敵タイマークラスを作った。
無敵タイマーが使えないときは通常の setInterval を使う。コードはエントリの最後。

デモ

無敵タイマー と 通常の setInterval の比較ページ。
別タブを選択して、しばらく放置して戻ってくると差が分かる。
http://mohayonao.herokuapp.com/mutekitimer

サンプル

無限インベンション
http://mohayonao.herokuapp.com/invention


6文字リズムマシン
http://mohayonao.hatenablog.com/entry/2011/11/21/202019


どちらも音のなるウェブ楽器で、別タブを選択中も音が鳴り続ける。
(以前は別タブを選択すると音が激しく途切れていた)

メモ

  • Chrome, Firefox で使える
  • Chrome では ファイル(htmlをダブルクリックして開いた状態)だと動かない
    • URL.createObjectURL が undefined を返す
  • Firefox は ローカル(127.0.0.1 or localhost)だと動かない??
    • ちょっと前にそれで困った覚えがあるけど、今はそうでもない。バージョン依存?
  • たしかに createObjectURL はすごい。よく分からないけどもっと凄いことに使えそう。

コード

    function MutekiTimer() {
        this.initialize.apply(this, arguments);
    }
    MutekiTimer.prototype = {
        initialize: function() {
            var url = (function() {
                var BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder;
                var URL = window.URL || window.webkitURL;
                var MutekiTimerBlob;
                if (!BlobBuilder || !URL) return null;
                
                MutekiTimerBlob = new BlobBuilder();
                MutekiTimerBlob.append("var timerId = 0;");
                MutekiTimerBlob.append("this.onmessage = function(e) {");
                MutekiTimerBlob.append("  if (timerId !== 0) {");
                MutekiTimerBlob.append("    clearInterval(timerId);");
                MutekiTimerBlob.append("    timerId = 0;");
                MutekiTimerBlob.append("  }");
                MutekiTimerBlob.append("  if (e.data > 0) {");
                MutekiTimerBlob.append("    timerId = setInterval(function() {");
                MutekiTimerBlob.append("    postMessage(null);");
                MutekiTimerBlob.append("    }, e.data);");
                MutekiTimerBlob.append("  }");
                MutekiTimerBlob.append("};");
                return URL.createObjectURL(MutekiTimerBlob.getBlob());
            }());
            if (url) {
                this._timer = new Worker(url);
                this.isMuteki = true;
            } else {
                this._timer = null;
                this.isMuteki = false;
            }
            this._timerId = 0;
        },
        setInterval: function(func, interval) {
            if (this._timer) {
                this._timer.onmessage = function(e) {
                    func();
                };
                this._timer.postMessage(interval);
            } else {
                if (this._timerId !== 0) {
                    clearInterval(this._timerId);
                }
                this._timerId = setInterval(function() {
                    func();
                }, interval);
            }
        },
        clearInterval: function() {
            if (this._timer) {
                this._timer.postMessage(0);
            } else {
                if (this._timerId !== 0) {
                    clearInterval(this._timerId);
                }
                this._timerId = 0;
            }
        }
    };