GreasemonkeyスクリプトにjQueryを読み込む汎用スクリプト

書いた人: noriaki 2008,03月01日(土) 18:30

jQuery Loader

Greasemonkey スクリプトは非常に便利で私もいくつか書いてますが, Prototype ライブラリや jQuery ライブラリなどの外部ライブラリを利用したいことがよくあります.

しかしGreasemonkeyでは,全てのスクリプトは安全のために普通のwindowオブジェクトではなく XPCNativeWrapper によってラップされたwindowオブジェクトをスコープにして実行されます. そのため,既存のクラスを拡張するタイプのライブラリは読み込み後にunsafeWindowを介して利用しなければならないなどちょっと不便です.

そこでオススメしたいのが,jQueryです. jQueryは全ての機能をjQueryオブジェクトの中に実装しているため,Greasemonkeyによってラッピングされているかどうかを気にせず利用できるためです.

他の人も同じように思ったかは分かりませんが,GreasemonkeyでjQueryを利用する方法は以下のものがすでに紹介されています.

This is a simple snippet that helps us load the jQuery power into our userscripts with Greasemonkey.

jQuery & Greasemonkey: How to play nicely with jQuery and Greasemonkey

もはや jQuery なしでは JavaScript が書けない状態なんだけど、Greasemonkey で使うためには一工夫必要だったのでメモしておく。

Greasemonkey で jQuery を使うための覚え書き - 記憶は削除の方向で

ただ,前者の方法では実行されるページで毎回jQueryライブラリがダウンロードされるため実行までにやや時間がかかってしまいます. また,後者の方法ではfirefoxにキャッシュされたコードを読むので実行は早くなりますが,いくつか使えない機能がでるようです. さらに,同一ページに複数のGreasemonkeyスクリプトがjQueryを読み込んだ場合,先に読み込まれた方のjQueryオブジェクトが正常に動作しないといった問題もあります.

そこで,上記の問題を解決しつつ,それぞれの良い部分を取り入れた汎用のjQueryLoaderを作りましたのでご紹介します.

以下には,機能や利用方法,ソースコードなどを載せています.

これはなに?

Greasemonkeyスクリプト中でjQueryを利用するためのスクリプトスニペットです. 主な機能として,以下のような機能を備えており,簡単に利用できるようになっています.

  • URIを指定したjQueryのダウンロードと読み込み
  • URIを指定した任意のjQueryプラグインの読み込み
  • コードのキャッシュ機能
  • jQueryやプラグインのバージョンアップ対応
  • 他のjQuery利用スクリプトとの衝突回避

使い方

下記のソースコードをjQueryを利用したいGreasemonkeyスクリプトの末尾などにコピーし,それ以降の部分で以下のコードのように呼び出します.

var loader = new jQueryLoader(
  { // jQuery Core: required
    name: 'jQuery',
    version: '1.2.3',
    url: 'http://blog.fulltext-search.biz/javascripts/gm/jquery-1.2.3.min.js'
  },
  [
    { // jQuery Plugin: optional
      name: 'reflect',
      version: '1.0', // optional: use '1.0' if undefined
      url: 'http://blog.fulltext-search.biz/javascripts/gm/jquery.reflect.js'
    },
    {
      name: 'jCarouselLite',
      version: '1.0',
      url: 'http://blog.fulltext-search.biz/javascripts/gm/jquery.jcarousellite.pack.js'
    }
  ]
);
loader.namespace = 'somescriptname'; // 利用したスクリプト名などの適当な名前空間を指定
loader.load(function($j) {
  // do something with jQuery($j)
  //  (ex.) if(!$j('#WATCHFOOTER').is('*')) return;
});

jQueryLoaderの第1引数にはjQueryライブラリの情報を含めたプロパティオブジェクトを渡し,第2引数には同様の情報を含めたプラグイン情報の配列を渡します.

このとき,プラグインの名前は任意のものが利用できます.また,version情報は省略された場合1.0が利用されます.

その後,loader.loadメソッドをコールバック関数を引数にして呼び出すことによって,jQueryとプラグインを読み込み,それらの準備ができてからjQueryオブジェクトを引数にしてコールバック関数を実行します.

ソースコード

// External jQuery Loader
/** Usage:
var loader = new jQueryLoader(
  { // jQuery Core: required
    name: 'jQuery',
    version: '1.2.3',
    url: 'http://blog.fulltext-search.biz/javascripts/gm/jquery-1.2.3.min.js'
  },
  [
    { // jQuery Plugin: optional
      name: 'reflect',
      version: '1.0', // optional: use '1.0' if undefined
      url: 'http://blog.fulltext-search.biz/javascripts/gm/jquery.reflect.js'
    }
  ]
);
**/
function jQueryLoader() { this.initialize.apply(this, arguments); };
var Util = {
  bind: function() {
    if (arguments.length < 3 && arguments[1] === undefined) return arguments[0];
    var args = Util.toArray(arguments), __method = args.shift(), object = args.shift();
    return function() {
      return __method.apply(object, args.concat(Util.toArray(arguments)));
    }
  },

  toArray: function(iterable) {
    if (!iterable) return [];
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  },

  extend: function(dist, source) {
    for (var property in source) {
      if(dist == source[property]) continue;
      if(source[property] !== undefined) dist[property] = source[property];
    }
    return dist;
  },

  each: function(iteratorable, iterator) {
    if(iteratorable.length === undefined)
      for(var i in iteratorable) iterator.call( iteratorable[i], i, iteratorable[i] );
    else
      for(var i = 0, l = iteratorable.length, val = iteratorable[0];
        i < l && iterator.call(val,i,val) !== false; val = iteratorable[++i] );
  },

  map: function(elems, callback) {
    var ret = [];
    for(var i=0,l=elems.length; i<l; i++) {
      var value = callback(elems[i], i);
      if(value !== null && value != undefined) {
        if(value.constructor != Array) value = [value];
        ret = ret.concat(value);
      }
    }
    return ret;
  },

  parseQueryString: function(str) {
    var memo = str.split('&');
    for(var i=0,obj={},l=memo.length; i<l; i++) {
      var pair = memo[i];
      if((pair = pair.split('='))[0]) {
        var name = decodeURIComponent(pair[0]);
        var value = pair[1] ? decodeURIComponent(pair[1]) :undefined;
        if(obj[name] !== undefined) {
          if(obj[name].constructor != Array) obj[name] = [obj[name]];
          if(value) obj[name].push(value);
        } else {
          var dummy = parseInt(new Number(value), 10);
          obj[name] = isNaN(dummy) ? value : dummy;
        }
      }
    }
    return obj;
  },

  periodicalExecuter: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;
    Util.extend(this, {
      registerCallback: function() {
        this.timer = setInterval(Util.bind(this.onTimerEvent, this), this.frequency * 1000);
      },

      execute: function() {
        this.callback(this);
      },

      stop: function() {
        if (!this.timer) return;
        clearInterval(this.timer);
        this.timer = null;
      },

      onTimerEvent: function() {
        if (!this.currentlyExecuting) {
          try {
            this.currentlyExecuting = true;
            this.execute();
          } finally {
            this.currentlyExecuting = false;
          }
        }
      }
    });

    this.registerCallback();
  }
};
jQueryLoader.prototype = {
  cacheName: 'jQuery.Libraries',
  namespace: 'jQueryLoader',

  initialize: function(jquery, plugins) {
    this.jquery = jquery;
    this.plugins = plugins || [];
    this.downloaded = 0;
    this.permanents = eval(GM_getValue(this.cacheName, '({})'));
  },

  load: function(callback) {
    if(typeof callback != 'function') return;
    this.callback = callback;
    this._load(this.jquery);
    Util.each(this.plugins, Util.bind(function(i,lib) { this._load(lib); }, this));
    this.eval();
  },

  _load: function(lib) {
    lib.version = lib.version ? lib.version : '1.0';
    if(!this.permanents[lib.name] || !this.permanents[lib.name].script ||
       this.permanents[lib.name].version &&
       this.compareVersion(this.permanents[lib.name].version, lib.version) < 0) {
      if(!this.permanents[lib.name]) this.permanents[lib.name] = {};
      Util.extend(this.permanents[lib.name], lib);
      var self = this;
      GM_xmlhttpRequest({
        method: 'GET',
        url: this.permanents[lib.name].url,
        onload: function(res) {
          self.permanents[lib.name].script = encodeURI(res.responseText);
          GM_setValue(self.cacheName, self.permanents.toSource());
          self.downloaded++;
        },
        onerror: function(res) { GM_log(res.status + ':' + res.responseText); }
      });
    } else { this.downloaded++; }
  },

  eval: function() {
    if(this.plugins.length + 1 == this.downloaded) {
      this.insert(this.permanents['jQuery'].script);
      if(!unsafeWindow.__jQuery) unsafeWindow.__jQuery = {};
      this.insert("__jQuery['" + this.namespace + "'] = jQuery.noConflict(true);");
      var plugins = Util.map(this.plugins, Util.bind(function(plugin) {
        return this.permanents[plugin.name].script;
      }, this)).join("\n");
      this.insert([
        '(function(jQuery,$) {', plugins, "})(__jQuery['",
        this.namespace, "'],__jQuery['", this.namespace, "']);"
      ].join(''));
      this.wait();
    } else {
      setTimeout(Util.bind(function() { this.eval(); }, this), 10);
    }
  },

  wait: function() {
    if(unsafeWindow.__jQuery && unsafeWindow.__jQuery[this.namespace] &&
       unsafeWindow.__jQuery[this.namespace]().jquery == this.permanents['jQuery'].version) {
      this.callback(unsafeWindow.__jQuery[this.namespace]);
    } else {
      setTimeout(Util.bind(function() { this.wait(); }, this), 10);
    }
  },

  insert: function(script) {
    var lib = document.createElement('script');
    lib.setAttribute('type', 'text/javascript');
    lib.appendChild(document.createTextNode(decodeURI(script)));
    document.getElementsByTagName('head')[0].appendChild(lib);
  },

  compareVersion: function(current, latest) {
    var delta = 0;
    var curr = current.split('.');
    var ltst = latest.split('.');
    for(var i=0, len = curr.length >= ltst.length ? curr.length : ltst.length; i<len; i++) {
      var curr_num = parseInt(curr[i], 10);
      var ltst_num = parseInt(ltst[i], 10);
      if(isNaN(ltst_num) || curr_num > ltst_num) {
        delta = 1;
        break;
      } else if(isNaN(curr_num) || curr_num < ltst_num) {
        delta = -1;
        break;
      }
    }
    return delta;
  }
};

このエントリをdel.icio.usにブックマークしているユーザ数このエントリをdel.icio.usに追加する
このエントリをはてなブックマークしているユーザ数このエントリをはてなブックマークに追加する
 | Tags ,

コメント

  1. Paul Irish said 3日 later:

    Greasemonkey 0.8 will include the @require attribute which will allow you to specify external scripts that must be included. (such as jquery or yui)..

このエントリはアーカイブされています。
コメントする場合は、お手数ですが「このページのURL」を記載した上で、新しいエントリにお願いします。