Now browsing the SCRAPBLOG weblog archives.

Jetpack SDK 0.5 の Reuqest API

Jetpack SDK 0.5 で追加された Request API を使用すると、 XMLHttpRequest によってWebサーバとデータを送受信する処理をより簡単に実装できます。特に、レスポンスがJSONかXML形式であるようなWebサービスのAPIを利用する場合に重宝しそうです。

この記事では、 Request API を使い、Twitter でキーワード「Firefox 4」を含むツイートの検索結果をコンソールへ列挙する例を紹介します。

Twitter のAPI仕様

実装に入る前に、 Twitter の検索用APIの仕様を簡単に示しておきます。

リクエスト

キーワード xxx で検索する場合、以下のようなURLへGETメソッドで送信する。
http://search.twitter.com/search.json?q=xxx

レスポンス

キーワードにマッチしたツイートのデータが、下記のようなJSON形式で返る。

{
    "results": [
        { "text": "(1番目のツイートの内容)", "from_user": "(1番目のツイートの発言者)" ... },
        { "text": "(2番目のツイートの内容)", "from_user": "(2番目のツイートの発言者)" ... },
        { "text": "(3番目のツイートの内容)", "from_user": "(3番目のツイートの発言者)" ... },
        ...
    ]
}

実装

まず、 require 関数でモジュールをインポートします。

var requests = require("request");

次に、 Request APIの Request コンストラクタを用いて、 Request オブジェクトのインスタンスを生成します。コンストラクタの引数には、以下のプロパティを有するオブジェクトを渡します。

プロパティ 概要
url データ送信先のURL
onComplete データ受信時(XHRでいうところの readyState == 4)のコールバック処理
headers 必要に応じてリクエストヘッダ(User-Agent や Referer)をオブジェクト形式でセットする。例:
headers: { "User-Agent": "MyApp", Referer: "http://..." },
content 必要に応じてリクエストのパラメータをオブジェクト形式でセットする。
contentType 必要に応じてHTTPヘッダの Content-Type の値をセットする。
デフォルトでは application/x-www-form-urlencoded

生成した Request インスタンスの get メソッドを呼び出すと、GETメソッドでリクエストが送信されます。なお、 post メソッドを呼び出すと、POSTメソッドでリクエストが送信されます。

// Request インスタンスの生成
var request = requests.Request({
    url: "http://search.twitter.com/search.json",
    content: { q: "Firefox 4" },
    onComplete: function () {
        // ToDo
    }
});
// GETメソッドで送信
request.get();

Webサーバからのレスポンスが返って onComplete メソッドがコールバックされると、 Request インスタンスの response プロパティからレスポンス内容(Response オブジェクト)を取得することができるようになります。 Response オブジェクトは色々なプロパティを有し、 json プロパティや xml プロパティでレスポンスのボディ部をJSONあるいはXML形式でパースした結果を取得したり、 headers プロパティでレスポンスのヘッダ部の各フィールド値を取得したりすることが可能です。

今回はレスポンスのボディ部をJSON形式でパースし、前述のAPI仕様にあるとおり "results" プロパティでツイートのデータの配列を取得し、配列の各要素に対して "from_user", "text" プロパティで発言者と発言内容を取得します。

    onComplete: function () {
        var results = this.response.json.results;
        results.forEach(function(result) {
            console.log("user: " + result.from_user);
            console.log("text: " + result.text);
        });
    }

TOP

Jetpack SDK 0.4 の Page Worker API

HTTP(S)によりWebサーバとデータを送受信するには、 xhr API で XMLHttpRequest のインスタンスを生成する方法や、 Jetpack SDK 0.5 で追加された Request API を使用する方法があります。しかし、これらのAPIではWebサーバからのレスポンスがHTMLデータだった場合、特定のノードにある文字列を抽出したりする処理がやや困難となります。

一方、 Jetpack SDK 0.4 で追加された Page Worker API を用いると、不可視のフレーム(iframe 要素)内に指定したURLのHTMLドキュメントをロードし、パースされた結果をDOM操作することが可能となります。この記事では、 Page Worker API を用いて Wikipedia の Internet に関するページを不可視のフレームに読み込んで見出し(H2 > SPAN要素)を列挙する例を紹介します。

基本的な使い方

まず、 require 関数でモジュールをインポートします。

var pageWorker = require("page-worker");

Page Worker APIの Page コンストラクタを用いて、 Page オブジェクトのインスタンスを生成します。コンストラクタの引数には、以下のプロパティを有するオブジェクトを渡します。

プロパティ 概要
content 不可視のフレームにロードするURL、またはHTMLソース
onReady ロード完了時のコールバック処理
allow 不可視のフレーム内でのスクリプト実行を許可するかどうかなどを指定するためのオプション。
allow: { script: false } でスクリプトの実行を不許可に設定することが可能。

生成した Page インスタンスを add メソッドへ渡すと、不可視のフレーム内への読み込みが開始されます。

var page = pageWorker.Page({
    content: "http://en.wikipedia.org/wiki/Internet",
    onReady: function() {
        // ToDo
    }
});
pageWorker.add(page);

不可視のフレームへの読み込みが完了すると、 Page インスタンスの window および document プロパティ経由で不可視のフレーム内のDOMへアクセスすることができるようになります。以下の例では、不可視のフレームへの読み込み完了時、 HTMLのURLおよびタイトルを取得してコンソールへ表示し、さらにH2要素直下にあるSPAN要素の中身の文字列を列挙して表示します。

    onReady: function() {
        var url = this.window.location.href;
        var title = this.document.title;
        console.log(url + "
" + title);
        var elts = this.document.querySelectorAll("h2 > span");
        Array.forEach(elts, function(elt) {
            console.log(elt.textContent);
        });
        pageWorker.remove(this);
    }

なお、 onReady の最後で Page オブジェクトのインスタンス自身を PageWorker の remove メソッドへ渡すことで、 window および document プロパティが削除され、不可視のフレーム内でのページ読み込みに使用されたメモリが解放されます。

TOP

Jetpack SDK 0.4 の Simple Storage API

拡張機能の設定値のような少量のデータを保存する際には Preferences Service API を使用しますが、より多くのデータを永続的に(Firefox を終了しても保持されるように)保存するには、 Simple Storage API が便利です。

基本的な使い方

はじめに require 関数でライブラリをインポートします。

var simpleStorage = require("simple-storage");

もっとも単純な方法は、 storage プロパティへ直接値をセットする方式です。以下は “test” という文字列を保存した後、値を取り出す例です。

simpleStorage.storage = "test";
console.log(simpleStorage.storage);    // test

文字列だけでなく、数値、配列、オブジェクトなどをセットして保存可能です。

simpleStorage.storage = { "foo": 100, "bar": 200 };
console.log(simpleStorage.storage.foo);    // 100

上記は以下のように書くことも可能です。

simpleStorage.storage.foo = 100;
simpleStorage.storage.bar = 200;

データの保存先

Simple Storage APIを使って保存したデータは、プロファイルフォルダ配下の jetpack{パッケージマニフェストのID}simple-storagestore.json へJSON形式で保存されます。また、ファイルへの出力タイミングは Jetpack SDK 0.5 時点では Firefox 終了時のみですが、将来的には Firefox 使用中も最大で5分に1回出力される仕様となるようです。

データ保存可能サイズ

ひとつの拡張機能が保存できるデータサイズは5MBまでとなっています。 Simple Storage API の quotaUsage プロパティにて現在のデータ使用量(5MBを1.0とした割合)を調べたり、 onOverQuota プロパティにてデータ使用量が最大値を超過したときのコールバック処理を追加したりすることも可能です。ただし、 Jetpack SDK 0.5 時点では正常に動作しないようです。

console.log(simpleStorage.quotaUsage);
simpleStorage.onOverQuota = function() {
    // データ使用量が最大値を超過したときのコールバック処理
};

TOP

Jetpack SDK 0.4 の Widget と Private Browsing API 使用例

この記事では、 Jetpack 0.4 で新たに追加された4つの標準APIのうち、 WidgetPrivate Browsing を使用して実際に機能を開発する手順を解説します。なお、解説はメインプログラムの作成手順以降となります。 Jetpack SDK 自体の基本的な使い方については、 はじめての Jetpack SDK 0.2 を参照してください。

完成イメージ

プライベートブラウジングを開始/停止するためのボタンを有する Jetpack 拡張機能を、以下のように3段階に分けて実装します。

  • フェーズ1: Widget API を使ってUIを追加する
  • フェーズ2: Private Browsing API を使ってボタン押下時にプライベートブラウジングを開始/停止する
  • フェーズ3: Simple Dialog API (自作ライブラリ)を使ってプライベートブラウジング停止確認ダイアログを表示する

フェーズ1: Widget API を使ってUIを追加する

一般的な拡張機能では Firefox のメニューバー、ツールバー、ステータスバーなど様々な箇所にUIを追加することが可能で、それゆえUIの一貫性が保たれなくなる問題があります。 Jetpack では Widget API を使ってUIを追加することで、すべての拡張機能のUIの一貫性が保たれるようになります。現在のところ、 Widget API ではステータスバー上部に現れる拡張機能専用のぶっといバーにボタン型UIが追加可能ですが、このUI仕様は今後変更される予定です(参考)。

フェーズ1では、 Widget API を使って非常に単純なボタン型UIを追加します。まず、 main.js 内に以下のように記述して Widget API のモジュールを読み込みます。

const widgets = require("widget");

引き続き main.js へ以下のような main 関数を記述します。

exports.main = function(options, callbacks) {
    var button = widgets.Widget({
        label: "Start/Stop Private Browsing",
        image: "chrome://browser/skin/Privacy-48.png",
        onClick: function(event) {
            // ToDo
        }
    });
    widgets.add(button);
};

はじめに Widget API の Widget(options) コンストラクタを用いてボタンのインスタンスを生成します。コンストラクタの引数 options には下記の3つのプロパティを指定します。

プロパティ 詳細
label ボタンに対する説明の文言。画面に表示されないが、アクセシビリティの観点から指定が必要。
image ボタンのアイコン画像のURL。アイコン画像は24×24ピクセルにリサイズされる。
onClick ボタン押下時に実行するコールバック関数。

なお、 content プロパティに HTML をセットすることで、単純なボタンではなく凝ったUIを作ることも可能です(参考)。

生成したボタンのインスタンスは、 Widget API の add メソッドを用いて実際に追加します。ここまでで、ひとまず「cfx run -a firefox」コマンドで動作確認し、ボタンが表示されることを確認してください。なお、ボタンが配置されるバーは Ctrl+Shift+U で表示/非表示可能です。

フェーズ2: Private Browsing API を使ってボタン押下時にプライベートブラウジングを開始/停止する

フェーズ2では、 Private Browsing API を使ってボタン押下時にプライベートブラウジングを開始/停止できるようにします。はじめに main.js の冒頭部分に以下の内容を追加し、 Private Browsing API モジュールを読み込みます。

const privateBrowsing = require("private-browsing");

さらに、 Widget(options) コンストラクタの onClick オプションを以下のように修正します。

        onClick: function(event) {
            privateBrowsing.active = !privateBrowsing.active;
        }

Private Browsing API の active プロパティは真偽値で、現在プライベートブラウジング中かどうかを調べたり、値を変更することでプライベートブラウジングを開始/停止することができます。

フェーズ3: Simple Dialog API を使ってプライベートブラウジング停止確認ダイアログを表示する

Private Browsing API にはプライベートブラウジング開始/停止の直前/直後に呼び出すコールバック関数を追加/削除するためのメソッドが用意されています。フェーズ3では、プライベートブラウジング停止直前に確認ダイアログを表示する機能を実装します。

まず、 main.js の冒頭部分に以下の内容を追加し、自作ライブラリである Simple Dialog API モジュールを読み込みます。

const simpleDialog = require("simple-dialog");

Simple Dialog API の詳細は はじめての Jetpack SDK 0.2 の「自作ライブラリの作成」の項を参照してください。記載されているソースコードをコピペして main.js と同一フォルダ内に「simple-dialog.js」として格納してください。

次に、 main 関数内の最後に以下の内容を追加します。

    privateBrowsing.onBeforeStop = function(cancel) {
        var yes = simpleDialog.confirmYesNo("Do you want to stop private browsing?");
        if (!yes)
            cancel();
    };

Private Browsing API の onBeforeStop プロパティへ、プライベートブラウジング停止直前に呼び出されるコールバック関数をセットします。コールバック関数内では Simple Dialog API の confirmYesNo メソッドを使って確認メッセージを表示し、ユーザが「いいえ」ボタンを押したらコールバック関数の引数 cancel を実行し、停止処理をキャンセルします。

なお、 onBeforeStop.add(callback) メソッドを使ってコールバック関数 callback を追加したり、 onBeforeStop.add([callback1, callback2, ...]) のようにして複数のコールバック関数を一括して追加することも可能です。

動作確認

ボタンを押下することで、プライベートブラウジングが開始/停止されることを確認してください。また、停止直前に停止確認ダイアログが表示され、「いいえ」ボタンを押下するとプライベートブラウジングが停止されないことを確認してください。

TOP

Jetpack SDK 0.4 でのパッケージマニフェストの id プロパティの仕様変更

Jetpack SDK 0.4 ではパッケージマニフェスト(package.json)の id プロパティに関する仕様変更があり、自分で決めたIDを明示的に記述することができなくなり、代わりにSDKによって自動生成されるようになりました。

SDK 0.3 以前の仕様

SDK 0.3 以前のバージョンでは、 id プロパティの取り扱いについて以下2通りの方式がありました。

方式1: 明示的に記述する

hello-world@xuldev.org のようにメールアドレス形式のIDを自分で決めて、パッケージマニフェスト中に id プロパティとして明示的に記述する方式です。「cfx xpi」コマンドによってXPIインストーラを生成すると、この id プロパティが拡張機能のインストールマニフェストの <em:id> タグの値になります。今まで一般的な拡張機能を開発したことがある方であればこちらの方式のほうが馴染み易いと思います。

方式2: SDKで自動生成する

パッケージマニフェストには id プロパティを記述せず、SDKによってIDを自動生成する方式です。「cfx xpi」コマンドでXPIインストーラを生成すると、SDKによって自動的にIDが生成され、XPIインストーラ中のインストールマニフェストの <em:id> タグの値となります。この方式ですと、XPIインストーラを生成するたびに毎回異なるIDが自動生成されるため、新しいバージョンを作って Firefox へインストールするたびに別物の拡張機能としてインストールされてしまう問題がありました。

SDK 0.4 での仕様

SDK 0.4 以降では、 SDK 0.3 以前での方式1のように自分で決めたIDを明示的に記述することはできなくなり、IDはSDKで自動生成された公開鍵・秘密鍵のペアとして管理されるようになりました。

これまで方式1のようにパッケージマニフェスト中に自分で決めたIDを id プロパティとして記述していた場合、いったん id プロパティを削除してください。 id プロパティが記述されていない状態で、SDKで「cfx run」または「cfx xpi」コマンドを実行すると、以下のようなメッセージとともにIDが自動生成されてパッケージマニフェストに id プロパティの記述が追加されます。

No 'id' in package.json: creating a new keypair for you.
package.json modified: please re-run 'cfx run'

また、このとき、「~/.jetpack/keys/」(Windows の場合「%USERPROFILE%.jetpackkeys」)配下に、自動生成されたIDに対応する公開鍵・秘密鍵のペアなどが記述されたファイルが生成されます。

再度「cfx run」または「cfx xpi」を実行すると、今度は普通に起動またはXPIインストーラが生成されます。

複数のPCで開発する場合

SDK 0.4 では、パッケージのソースファイルを丸ごと別のPCへコピーしてから「cfx run」や「cfx xpi」コマンドを実行するとエラーとなります。もしあなたがそのパッケージの正規開発者ならば、前述の公開鍵・秘密鍵のペアなどが記述されたファイルも一緒にコピーして適切なディレクトリへ配置する必要があります。もしあなたがそのパッケージの派生版を開発しようとしているのならば、パッケージマニフェストの id プロパティを削除して再度SDKによって自動生成する必要があります。このような仕組みによって、そのパッケージが正当な開発者によって作られたものであることが Firefox 側で検証可能となります。

TOP

Jetpack SDK 0.4 で cfx コマンドのユーザ定義オプションを設定する

Jetpack SDK 0.4 では、 cfx コマンド実行時に頻繁に使うオプションをあらかじめ local.json に記述しておき、 cfx コマンド実行時に簡単に呼び出すことが可能となりました。

cfx run のオプション

Jetpack SDK では、拡張機能を動作確認するときに「cfx run」コマンドを用いますが、このとき、通常のインストール先とは異なる Firefox を起動して実行したい場合に「-b」オプションを付加したり、指定したプロファイルから起動したい場合「-P」オプションを付加します。以下は testuser というプロファイルで Firefox 3.7 上で拡張機能の動作確認をする例です。

cfx run -a firefox -b "%ProgramFiles%Mozilla Firefox 3.7firefox.exe" -P "%appdata%MozillaFirefoxProfiles    estuser"

cfx コマンドのユーザ定義オプション

前述のような長いオプションを毎回入力するのは面倒です。そこで、 Jetpack SDK 0.4 で導入された local.json に頻繁に使用するオプションを記述しておくと、 cfx コマンドから簡単に呼び出し可能となります。まず、 SDK の展開先フォルダ直下に local.json というファイルを作成し、下記のような内容を記述します。

{
  "configs": {
    "ff37": [
      "-a", "firefox",
      "-b", "C:Program FilesMozilla Firefox 3.7firefox.exe",
      "-P", "C:UsersadminAppDataRoamingMozillaFirefoxProfilestestuser"
    ]
  }
}

すると、 cfx コマンドの --use-config オプションにより local.json に記載した ff37 という名前のユーザ定義オプションが有効になります。 --use-config=ff37 の代わりに -g ff37 としても構いません。

cfx run --use-config==ff37

もちろん cfx run だけでなく cfx xpi などでも同様の方式で local.json に定義したオプションを呼び出し可能です。なお、 local.json 内ではバックスラッシュを「」とエスケープすること、また Windows の環境変数は使用できないことにご注意ください。

cfx コマンドのデフォルトオプション

default という名前のユーザ定義オプションを記述すると、これは --use-config オプション未指定時のデフォルトオプションとして自動的に呼び出されます。例えば、 local.json へ以下のように記述を追加します。

{
  "configs": {
    "default": [
      "-a", "firefox",
      "-P", "C:UsersadminAppDataRoamingMozillaFirefoxProfilestestuser"
    ],
    "ff37": [
      "-a", "firefox",
      "-b", "C:Program FilesMozilla Firefox 3.7firefox.exe",
      "-P", "C:UsersadminAppDataRoamingMozillaFirefoxProfilestestuser"
    ]
  }
}

すると、単に cfx run などを実行したとき、設定名 default で定義したデフォルトオプションが有効となります。

TOP

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