Now browsing the archives for 5月, 2010.

XPCOM サービスへの頻繁なアクセスを効率化するテクニック

拡張機能や XUL アプリにて、 JavaScript から特定の XPCOM サービスを頻繁に使用するケースがよくあります。そのような場合に処理やソースコードを効率化するためのテクニックをいくつか紹介します。ここでは、例として nsIObserverService を頻繁に利用するケースを想定します。なお、CcComponents.classes, CiComponents.interfaces への参照です。

方式1: 毎回 XPCOM サービスを呼び出す

特に工夫をしない場合、以下のように XPCOM サービスを利用するたびに毎回そのスコープ内で呼び出し手続きを行うことになります。

var MyExtension = {
    init: function() {
        var observerSvc = Cc["@mozilla.org/observer-service;1"].
                          getService(Ci.nsIObserverService);
        observerSvc.addObserver(...);
    },
    uninit: function() {
        var observerSvc = Cc["@mozilla.org/observer-service;1"].
                          getService(Ci.nsIObserverService);
        observerSvc.removeObserver(...);
    },
};

方式2: グローバル変数として定義

最初に XPCOM サービスの呼び出しを行い、グローバル変数 gObserverSvc として参照を保持します。
この方式ですと、スクリプトロード直後(実際に XPCOM サービスを使うタイミングよりも前)に XPCOM サービスの呼び出しが行われます。したがって、その XPCOM サービスを必ずしも使うとは限らないケースには向いていません。

var gObserverSvc = Cc["@mozilla.org/observer-service;1"].
                   getService(Ci.nsIObserverService);

var MyExtension = {
    init: function() {
        gObserverSvc.addObserver(...);
    },
    uninit: function() {
        gObserverSvc.removeObserver(...);
    },
};

方式3: 拡張機能専用オブジェクトのプロパティとして定義

グローバル変数として定義せずに、拡張機能専用オブジェクト MyExtensionobserverSvc プロパティとして定義します。
この方式ですと、実際に XPCOM サービスを使うタイミング(observerSvc プロパティを参照した時)に XPCOM サービスの呼び出しが行われます。逆に、その XPCOM サービスを使わない場合は無駄に XPCOM サービスの呼び出しが行われることがない、という利点があります。

var MyExtension = {
    get observerSvc() {
        return Cc["@mozilla.org/observer-service;1"].
               getService(Ci.nsIObserverService);
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式4: 一度取得した参照をキャッシュ

方式3は、XPCOM サービスを使う(observerSvc プロパティを参照する)たびに毎回 XPCOM サービスの呼び出しが行われるという欠点があります。一方、こちらの方式4では、 observerSvc プロパティが初めて参照された際に、取得した XPCOMサービスへの参照を _observerSvc プロパティ(先頭にアンダーバー付きのプライベート的なプロパティ)として保持し、二回目以降のサービス使用時は保持した参照を返します。

var MyExtension = {
    _observerSvc: null,
    get observerSvc() {
        if (!this._observerSvc) {
            this._observerSvc = Cc["@mozilla.org/observer-service;1"].
                                getService(Ci.nsIObserverService);
        }
        return this._observerSvc;
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式5: 一度取得した参照をキャッシュ(改良版)

方式4をさらに改良し、プロパティの数を節約したバージョンです。
observerSvc プロパティへの初回参照時に、プロパティ自身を XPCOM サービスへの参照へと置き換えます。
return a = b; という書き方により、 ab を代入して、さらに a の値を返します。

var MyExtension = {
    get observerSvc() {
        delete this.observerSvc;
        return this.observerSvc = Cc["@mozilla.org/observer-service;1"].
                                  getService(Ci.nsIObserverService);
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式6: 一度取得した参照をキャッシュ(クラス対応)

方式5のような単独のオブジェクトではなく、クラスのプロパティとする場合、理由はよくわかりませんが以下のように書く必要があるようです。

function MyExtensionClass() { ... }

MyExtensionClass.prototype = {
    get observerSvc() {
        var svc = Cc["@mozilla.org/observer-service;1"].
                  getService(Ci.nsIObserverService);
        this.__defineGetter__("observerSvc", function() svc);
        return svc;
    },
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

方式7: XPCOMUtils.defineLazyServiceGetter を使う

XPCOMUtils.jsm という標準の JavaScript モジュールをインポートすると、方式5は以下のように defineLazyServiceGetter を使って書くことができます。ただし方式7は Firefox 3.6 (Gecko 1.9.2) 以降限定です。また、 browser.xul 内であれば Firefox 本体側ですでにモジュールがインポート済みなので、拡張機能側で改めてインポートする必要はありません。

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

var MyExtension = {
    init: function() {
        this.observerSvc.addObserver(...);
    },
    uninit: function() {
        this.observerSvc.removeObserver(...);
    },
};

XPCOMUtils.defineLazyServiceGetter(
    MyExtension, "observerSvc", "@mozilla.org/observer-service;1", "nsIObserverService"
);

方式8: Services.jsm を使う

Services.jsm という標準の JavaScript モジュールをインポートすると、ごく一部の XPCOM サービスへの参照を手軽に取得できます。 nsIObserverService であれば Services.obs で参照できます。実は Services.jsm モジュール内部では XPCOMUtils.defineLazyServiceGetter が使用されており、また、すべての拡張機能および Firefox 本体でシングルトンの参照を保持できるという JavaScript モジュールの特性を考えると、最も効率的な方式といえるでしょう。ただし方式8は Firefox 3.7 (Gecko 1.9.3) 以降限定です。また、 browser.xul 内であれば Firefox 本体側ですでにモジュールがインポート済みなので、拡張機能側で改めてインポートする必要はありません。

Components.utils.import("resource://gre/modules/Services.jsm");

var MyExtension = {
    init: function() {
        Services.obs.addObserver(...);
    },
    uninit: function() {
        Services.obs.removeObserver(...);
    },
};

方式9: 独自モジュール内で定義する

方式8に近いですが、拡張機能独自の JavaScript モジュールを作り、頻繁にアクセスする XPCOM サービスへの参照はそのモジュールのプロパティとして定義(その際に方式5や方式7を利用)してしまう手もあります。たくさんの XUL ウィンドウから XPCOM サービスへアクセスするような規模の大きい拡張機能に向いています。
また、そもそも拡張機能のメインプログラムがモジュール化されている場合は、そのモジュール内で方式5や方式7を使ってキャッシュを行うことで効率化が可能です。

私見

個人的には多くの場合は方式5で、グローバル変数を増やしても他への影響が少ない拡張機能独自のウィンドウ内で、それほど効率化を意識する必要の無い場面や単に面倒な場合は方式2、という感じです。最近の Firefox 本体のソースコードを見ると方式7,8が主流となりつつあるようですが、古いバージョンの Firefox にも対応しなければならない拡張機能としては、今のところは対応する Firefox のバージョンが限られてしまうのが難点です。

TOP

Jetpack SDK 0.3 コンテキストメニューAPIの使用例

先日 Jetpack SDK 0.3 がリリースされました。このバージョンのSDKではブラウザ上でのコンテキストメニューへ項目を追加するための context-menu API などが導入されており、少しずつですが実用的な機能を手軽にを開発できるようになってきました。この記事では context-menu API を実際に使用し、右クリックメニューから現在のページの短縮 URL を bit.ly で表示する機能を開発する手順を解説します。なお、 Jetpack SDK 自体の基本的な使い方については、 はじめての Jetpack SDK 0.2 を参照してください。

パッケージマニフェストの作成

Jetpack SDK 0.3 を展開したフォルダの下の packages フォルダ下に、これから作成するパッケージのルートフォルダである context-menu-example フォルダを作成し、その中に以下のような内容のファイル package.json を作成します。

{
    "id": "context-menu-example@xuldev.org",
    "name": "context-menu-example",
    "fullName": "Jetpack: Context Menu Example",
    "version": "0.1",
    "description": "An example for using the context menu API.",
    "author": "Gomita <gomita@xuldev.org>"
}

注意: Jetpack SDK 0.4 に対応させる場合はパッケージマニフェスト内の “id” プロパティを削除してください。

なお、 Jetpack SDK 0.2 から 0.3 でパッケージマニフェストの仕様が若干変更されており、 “name” プロパティの値はパッケージのルートフォルダのフォルダ名と同一であり、なおかつスペースを含まない値とする必要があります。また、 “name” プロパティは無くても構いません。一方、アドオンマネージャ上で表示するためのパッケージの名称は “fullName” プロパティとして宣言してください。詳細はSDK ドキュメント をご覧ください。

パッケージマニフェストを作成したら、いったんパッケージが正常に読み込まれることを確認します。 OS が Windows の場合、コマンドプロンプトから binactivate で Jetpack SDK 0.3 を活性化させ、 cfx docs コマンドで SDK ドキュメントを開き、「Package Reference」の下に「context-menu-example」が表示されることを確認してください。

プログラムの作成

ここからはプログラムの作成に取り掛かります。パッケージのルートフォルダ内にプログラム格納用の lib フォルダを作成し、さらにその下にメインプログラム main.js を作成してください。 exports.main メソッドには、以下のようにコンテキストメニューへ項目を追加する処理を記述します。

exports.main = function(options, callbacks) {
    var contextMenu = require("context-menu");
    var newItem = contextMenu.Item({
        label: "Shorten with bit.ly",
        onClick: handleClick
    });
    contextMenu.add(newItem);
};

コンテキストメニューへ新しい項目を追加するには、まず require 関数で context-menu API のモジュールを読み込み、 contextMenu.Item コンストラクタでメニュー項目のインスタンスを生成し、 contextMenu.add で実際に使用できるようにします。 contextMenu.Item コンストラクタの引数は、下表に挙げるようなプロパティを有するオブジェクトです。なお、 context-menu API の詳細は SDK ドキュメント を参照してください。

プロパティ 概要
label メニュー項目の表示文言。現在のところ日本語は使用不可。
data メニュー項目に関連付ける文字列。 xul:menuitem 要素の value 属性となる。
onClick メニュー項目をクリックしたときに呼び出されるコールバック関数。コールバック関数には contextObjitem の2つの引数が渡される。詳細は後述。
context メニュー項目を特殊な場面(画像上、リンク上などで右クリックしたとき)だけ表示させる場合に指定する。指定しない場合は純粋なページ上での右クリック時のみ表示される。

次に、 contextMenu.Item コンストラクタの引数オブジェクトの onClick プロパティに指定したコールバック関数 handleClick の内容を記述します。 exports.main の外側に以下の内容を追加してください。

function handleClick(contextObj, item) {
    var url   = contextObj.window.top.location.href;
    var title = contextObj.window.top.document.title;
    url   = encodeURIComponent(url);
    title = encodeURIComponent(title);
    require("tab-browser").addTab("http://bit.ly/?v=3&u=" + url + "&s=" + title);
}

onClick プロパティで指定したコールバック関数には contextObjitem の2つの引数が渡されます。前者は window, document, node という3つのプロパティを有する特殊なオブジェクトで、後者はクリックしたメニュー項目自体のインスタンスです。 contextObj.window プロパティはコンテキストメニューを開いた対象のページの DOM の window オブジェクトですので、 contextObj.window.location.href で現在のページの URL を取得することができます。ここでは、フレーム内部で右クリックした場合を考慮して window.top から最上位のフレームの URL およびタイトルを取得します。

引き続き取得した URL およびページのタイトルを encodeURIComponent 関数でエンコードし、 bit.ly の URL へパラメータとして付加してタブで開きます。ここで、タブで開く処理には Jetpack SDK 0.3 で導入された tab-browser API を使用します。現時点では tab-browser API は未完成のためか SDK ドキュメントには仕様が記載されていませんが、 addTab メソッドによって引数で渡した URL を新しいタブで開いたりすることが可能です。

動作確認

以上で実装は完了です。 cfx run -a firefox コマンドで動作確認しましょう。なお、 Jetpack SDK 0.3 から追加された機能として、以下のように -P オプションで指定したパスのプロファイルディレクトリから Firefox を起動して動作確認をすることができます。毎回クリーンなプロファイルを使用するのではなく、一定の動作確認専用プロファイルを使用したい場合に便利です。

cfx run -a firefox -P "%appdata%MozillaFirefoxProfilesjetpack.testpilot"

Jetpack SDK 0.4 では頻繁に使用するオプションを local.json に記述して呼び出すことが可能です(詳細)。

XPI インストーラ生成

上記手順で開発したパッケージを配布するには、 cfx xpi コマンドで XPI インストーラを生成します。生成したインストーラを Add-ons for Firefox にアップロードして公開することも可能なようです。なお、この手順で生成したインストーラは、将来的には Firefox の再起動無しにインストール・アンインストールが可能となる予定ですが、現時点では通常の拡張機能同様に再起動が必要となります。

TOP

xul:textbox のプレースホルダー文字列

modest に投稿した記事と同内容です。

注意: この記事は Firefox 3.7 での新機能について触れています。

HTML のテキストボックス

Firefox 3.7a5pre では HTML 5 の仕様である placeholder 属性 が実装されており、以下のようにしてテキストボックスにヒント用文字列を表示させることができます。

<input type="text" placeholder="Your Name">

XUL のテキストボックス

一方、 XUL の textbox 要素では Firefox 3.0 にてすでに同様の機能が emptytext 属性として実装済みですが、 Firefox 3.7a5pre では placeholder 属性でもヒント用文字列を表示することができます。互換性維持のため emptytext 属性も引き続き利用可能ですので、 Firefox 3.6 と 3.7 両対応の拡張機能などでは emptytext 属性を使用したほうが良いでしょう。

<textbox emptytext="Your Name" />
<textbox placeholder="Your Name" />

また、以下のように xul:textbox 要素の emptyText (大文字小文字に注意)および placeholder プロパティを使って JavaScript で動的にヒント用文字列をセットすることも可能です。

document.getElementById(...).emptyText = "Your Name";
document.getElementById(...).placeholder = "Your Name";

TOP

nsIEventListenerService でDOMイベントリスナを列挙する

modest に投稿した記事と同内容です。

nsIEventListenerService というXPCOMサービスを使うと、 XUL や HTML ドキュメント内のある要素に対してどんなDOMイベントリスナが追加されているかを調べることができます。以下は、 browser.xul にてFirefoxの「ホーム」ボタンに追加されたイベントリスナをエラーコンソールに列挙するサンプルです。

var els = Cc["@mozilla.org/eventlistenerservice;1"].
          getService(Ci.nsIEventListenerService);
var infos = els.getListenerInfoFor(document.getElementById("home-button"), {});
infos.forEach(function(info) {
    Application.console.log(info.type + " => " + info.toSource());
});

nsIEventListenerService の getListenerInfoFor メソッドは、引数で渡した要素のイベントリスナの情報を、 nsIEventListenerInfo オブジェクトの配列として返します。さらに、各 nsIEventListenerInfo オブジェクトについて、 type プロパティでイベントリスナの種類(click, keypress, mousedown など)を調べたり、 JavaScript のリスナであれば toSource() で内容を文字列化したりできます。ただし、 nsIEventListenerInfo オブジェクトはイベントリスナそのものではないので、 getListenerInfoFor で取得したイベントリスナを removeEventListener で削除する、といったことはできません。あくまでもデバッグ用です。

なお、 nsIEventListenerService は Firefox 3.6 (Gecko 1.9.2) 以降で利用可能です。

TOP

一定時間ドラッグオーバーし続けたら処理を実行する

modest に投稿した記事と同内容です。

拡張機能(XULアプリ)にて、一定時間ドラッグオーバーし続けたときに何らかの処理を実行したい、例えばツールバーに配置したボタン上にブラウザタブを3秒間ドラッグオーバーし続けたら、そのボタンをクリックしたものとみなして処理を実行したいとします。

これは、HTML5のドラッグ&ドロップAPIを使い、ドラッグオーバーし続けた際に dragover イベントが繰り返し発生する特性を利用すると、以下のように実装可能です。

以下は、ボタン上に何かを3秒間ドラッグオーバーし続けると、テキストボックスに現在時刻を表示するサンプルです。なお、サンプルコード全量はこちらに置いてあります。 chrome 権限は不要ですので、ダウンロードして拡張子を.xulにしてFirefoxで開けば、動作確認可能です。

XUL:

<button label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragover="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '
';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {

    _dragStartTime: null,

    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter": 
                // ドラッグオーバー開始時、ドラッグオーバー開始時刻をセット
                this._dragStartTime = Date.now();
                break;
            case "dragover": 
                // ドラッグオーバー中、ドラッグオーバー開始時刻からの経過時間を調べる
                if (this._dragStartTime && Date.now() - this._dragStartTime > 3000) {
                    // 3秒以上経過したら、ドラッグ開始時刻をリセットし、処理を実行する
                    this._dragStartTime = null;
                    event.target.doCommand();
                }
                break;
        }
    }

};

タイマーを用いた実装方式

ドラッグオーバー開始時(dragenter イベント発生時)に setTimeout で一定時間後に処理を実行するためのタイマーを設定し、ドラッグオーバー終了時(dragleave イベント発生時)に clearTimeout でタイマーを解除する、という実装方式ももちろん可能です。

XUL:

<button id="myButton"
        label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragleave="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '
';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {
    _dragOverTimer: null,
    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter": 
                // dragenterイベントが二回連続で発生した場合への対策
                if (this._dragOverTimer)
                    return;
                // ドラッグオーバー開始時にタイマーを設定
                this._dragOverTimer = setTimeout(function() {
                    document.getElementById("myButton").doCommand();
                }, 3000);
                break;
            case "dragleave": 
                // ドラッグオーバー終了時にタイマーを解除
                clearTimeout(this._dragOverTimer);
                this._dragOverTimer = null;
                break;
        }
    }
};

TOP