肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

Firefox 3 的なFUELとJSMを存分に使って拡張機能開発をしよう!

というわけで,意味のわからないタイトルをつけてしまいましたが,Firefoxのアドオンを作るときに,Firefox 3 で使える,FUELとJavaScriptコードモジュール(JSM)をうまく使えば,これまでのFirefoxアドオン開発のわずらわしい部分が少し楽になるので,それについてちょいと書いてみようかと思います.
とはいえ,自分も最近使い出したくらいでそれほど詳しいわけでもなく大体がMDCに散らばった情報をよせあつめただけなので,間違っていることを書いていることがあるかもしれません.ツッコミもいただければと思います.

Firefox アドオン開発を始めるにあたって

雛形を公開します.
ダウンロードしてFirefoxにドラッグ&ドロップすればインストールできる形(xpi)で,公開します.以下からダウンロードできるのでしちゃってください.

ダウンロード

ためしにインストールしてみる場合

(追記) CodeReposに入れてみました>< *1

雛形に含まれるのは,

  • 汎用のinstall.rdf (名前とかパスとか変更して使ってください)
  • 汎用のchrome.manifest (同上)
  • 汎用のJavaScriptコードモジュール(定義してあるだけ)
  • 汎用のxulJavaScript(JSM読み込みくらい)
  • en,jaのLOCALE(chrome.manifestにも設定済み)
  • LinuxOS X,Windowsでのskinフォルダ(同上)
  • 超簡易build.sh(zipで固めてxpiにするだけの・・・w)

です.ステータスバーにこんにちはって表示されます.w
skinとかlocaleファイルの設置とchrome.manifestとか,調べてゼロからやろうとすると割かし良くわからなかったりもするので,あらかじめつくっておきました.
JSMも含めておきましたが,まぁ,使わないかもしれないですね.


その他開発環境とかは以下のサイトが非常に参考になると思います.

FUEL ってなに

要するに,ラッパーです.これまで,XPCOMという,C++などで書かれたライブラリをJavaScriptから呼ぶために,Component.classesからインスタンスをつくらなきゃいけなかったりとかよくわからないものを,ラッピングして使いやすくしたもの,と考えて問題ないと思います.

最新のFirefoxであれば,拡張機能を作れば,XUL上にApplicationオブジェクトが用意され,それを呼ぶだけで使えます.具体的には,例えば,以下のようにすると,Firefox エラーコンソールの メッセージ に文字列を出力します.

Application.console.log("hogehoge!!");


FUEL のリファレンスはMDCにも書いてあるので,見てみると良いかもしれません.


じゃあ早速その中からいくつか見てみます.見てみるといっても,まぁ,非常に断片的に,ですが.

Application は FUEL のボス的な存在

何ってかんじですが.fuelIApplication - Toolkit API | MDN に書いてありますが,これはXULスクリプトにプリロードされます.いやいやそれも何ってかんじですが,要するに何も考えずに Applicatoin とすれば使えるよということです.
で,他の FUEL オブジェクトは,Applicationオブジェクトのメンバとして定義されていますので,Application経由で使います.

なにが定義されているのかは,上記MDCに書いてあり,それぞれの FUEL のリファレンスを見ればメソッドも引数もちゃんと書いてあります.ということは私がブログをかくまでもな(r  ということですね.

そんなわけで,そのなかから,いくつかピックアップします.

FUEL :: Console

上の例でも出しましたが,開発にあたってかなり良く使うのは,Consoleじゃないでしょうか.
ref.) extIConsole - Toolkit API | MDN

エラーコンソールを開き,「メッセージ」にログを出力する

エラーコンソールというのは,ツール→エラーコンソール,と選択すると開く,以下のようなウィンドウです.この,「メッセージ」という欄に文字列を出力することができます.

例えば以下のように関数を定義しておけば,logMsg() 関数を呼ぶと,エラーコンソールが開いていない場合は開き,開いている場合はフォーカスし,文字列を出力します.Objectだったら適当にforしちゃう感じにしておけばそれなりに使えるかもしれないです(てきとう・・・

function logMsg(string)
{
    Application.console.open();
    
    if (typeof string == 'object') {
        for (k in string) {
            logMsg(k + " : " + string[k]);
        }
    }
    Application.console.log(string);
}

FUEL :: activeWindow(Window)

現在アクティブであるウィンドウの,Window FUELオブジェクトを取得できます.

var w = Application.activeWindow;

このオブジェクトは,開いているタブや,現在アクティブなタブなどの情報をもっています.

新しいタブを開き,対象のURLに移動する

以下のようにします.

Application.activeWindow.open(
    Components.classes["@mozilla.org/network/io-service;1"]
      .getService(Components.interfaces.nsIIOService)
      .newURI("http://deadlinetimer.com/",
    null,
    null)
).focus();

結構ハマったのが,引数に,URL文字列を指定できないという点です.できればいいのに...!
引数に渡すのは,nsIURIオブジェクトで,これはComponents経由で取得しなければいけないのです.ちゅ・・ちゅうとはんぱだ.などと思いながら..

余談ですが,Components.classesやComponents.interfacesは,他のアドオン開発者がやっているように,私も,スクリプトの先頭で

const Cc = Components.classes;
const Ci = Components.interfaces;

などとしちゃってます.こうなっている場合,以下のような感じでかけたりなど.

Application.activeWindow.open(
    Cc["@mozilla.org/network/io-service;1"]
      .getService(Ci.nsIIOService)
      .newURI("http://deadlinetimer.com/",
    null,
    null)
).focus();

(追記) id:hogelog がmakeURI って関数があるよーっての教えてくれた.で,試してみたもののなぜか手元の環境で動かず・・・プロファイル変えてもだめだった.なんでだろ?一応 hogelog の環境では動いてるっぽいから使えるとは思うんだけど.
あ,そしてどっちにしろJSM内では Components 経由の方法しかつかえなそげ?

FUEL :: Extensions / Extension

拡張機能の情報を取得するのに,Extensionsと,ExtensionというFUELオブジェクトが使えます.Extensionsから,getすることで個々のExtensionオブジェクトを取得するのです.

で,ここで,拡張機能独自の設定を定義する場合について で書いてあることを思ったのですが,拡張機能を認識するためには,install.rdfにアプリケーションIDを登録しています.それにより,拡張機能の設定も作ればいいんじゃないの?と思うわけです.FUELを使って拡張機能の情報を得るにはこっちのほうが断然スマートですよね,多分?

拡張機能の情報を取得する (Extensionsから,Extensionを取得)

で,具体的には,どうするかというと,以下のようにgetで取得します.例えば,以下のは deadlinetimer.com 用に作っているアドオンの情報を取得するものです.

var dlt = Application.extensions.get('deadlinetimer@nequal.jp');

これで,「dlt」変数にExtensionオブジェクトが入っているので,さらにそこから拡張機能の設定を読み込んだりとかできます.

拡張機能の設定を書き込む/読み込む

上記のように dlt 変数にExtensionオブジェクトが入っているとして,次のようなことが出来ます.

// Extensionオブジェクトの prefs に拡張機能の設定オブジェクト(FUELのPreferenceBranchオブジェクト(https://developer.mozilla.org/ja/FUEL/PreferenceBranch)が入っています
var dltPrefs = dlt.prefs;

// 値があるかどうか調べる
// 以下は, extensions.deadlinetimer@nequal.jp.timerid の設定があるかどうかを調べている
if (dltPrefs.has("timerid")) {
    Application.console.log("あたいがあるよ!!!");
}

// 値をセットする
// 以下は, extensions.deadlinetimer@nequal.jp.timerid に 2 を設定している
dltPrefs.setValue("timerid", 2);

// 値を取得する
// 以下は, extensions.deadlinetimer@nequal.jp.timerid から値を取得する.0 をデフォルト値としている
var timerId = dltPrefs.getValue("timerid", 0);

このPreferenceBranchオブジェクトがかなり画期的なのは,値の型をほぼ気にしなくて良いという点です.
実はこれまでのアドオン開発では*2nsIPrefServiceから,nsIPrefBranchを取得し,getPrefTypeで設定の型を判別し,それぞれに対して,getBoolPref,getCharPref,getIntPrefなどというメソッドで値を取得したりしなければいけませんでした.getValue/setValueが実装されたことでそれを意識することなく設定の書き込みや読み込みができるようになったっぽいですね.

で,1点注意しなければいけないのは,そうはいっても設定自体に型は決まっている,という点です.

先のtimeridは,setValue("timerid", 2) をした時点で,Intになっています.これは,多分設定をresetしない限り変えられないとおもいます.
で,そのうえでそこに例えば "2" をセットしようとすると,Exceptionが投げられます.以下のようなものです.

エラー: uncaught exception: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIPrefBranch.setComplexValue]"  nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)"  location: "JS frame :: file:///C:/Program%20Files/Mozilla%20Firefox/components/fuelApplication.js :: prefs_sv :: line 996"  data: no]

もっとわかりやすい書き方はないのか,と思いますが,catchしない例外は,エラーコンソールにこのままこれが出力されます.

したがって,外部からの入力,例えばユーザが入力した値を,Intの型の設定に書き込みたい場合などは,必ず以下のようにparseIntしてあげます.

// timerid 変数にはなにかしらの入力
dltPrefs.setValue("timerid", parseInt(timerid));
拡張機能インストール後の初回起動時に何かする

これまた割かし便利なものが出来たのですが,その拡張が初回起動かどうかを判別することができます.

if (dlt.firstRun) {
    // なにかしらの処理
}

などとすれば,インストール直後に,説明ページのタブが開かれる,などの挙動も簡単に作ることができますね.

FUELその他

その他にも結構便利なものがありまして,大きく分けて,Application以下からオブジェクトを取得して使える,グローバルな(?)ものと,Extensions.get() から Extension オブジェクトを取得して使える,拡張機能専用のものにわけられます.
まぁ,そのあたりはMDCを見たほうがちゃんと書いてあるので良いかもしれません.

あと,イベントリスナ周りはちゃんと触ってないのでなんともいえないです.注意が必要だそうですが..

JSM化

と,FUELの紹介もそこそこですが,続いてFirefox 3 で使えるほげほげ機能の2つ目,JavaScriptコードモジュールについてちょっとだけ紹介します.

JSM化のメリット

これは簡単な話で,オブジェクトをシングルトン化できるという点です.
XULアプリは,XULごとにオブジェクトが存在しています.(という言い方でいいんだろうか)

例えば,親ウィンドウでHelloWorldオブジェクトを作成し,HelloWorld.greeting = "hello"; などとしたとします.

次に,サブウィンドウ定義するXULを呼び,サブウィンドウを生成したとします.そこで,同じJSファイルを読み込み,HelloWorld.greetingを出力しても,そこにはhelloは入っていません.新しいHelloWorldオブジェクトになるのです.

で,それでは困る面もあって,オブジェクトはグローバルに1つあればいい,という汎用なモジュールとかを定義できるようにしたものがJavaScriptコードモジュール(JSM)です.

JSM を使う

JSMを使うことはわりかし簡単で,やらなければいけないのは,

  • JSMを作成(.jsm)
  • chrome.manifest にモジュールディレクトリを登録
  • JSからJSMの呼び出し

の3つです.

まずは,アドオンのディレクトリのルートに,「modules」などをディレクトリを作成します.
で,chrome.manifestには

resource mymodules modules/

などと,resourceの登録をします.(この場合,mymodulesと名前をつけます)

そして,JSから,

Components.utils.import("resource://mymodules/module.jsm");

などとして,モジュールをインポートします(module.jsmというファイルにモジュールを作ってあるとして).

で,これでなにが出来るかというのは,module.jsmに書いてあること次第なのですが,「JSM内のコードは完全に隠蔽される」らしく,JSMからオブジェクトなどのシンボルを持ち出すには,

var EXPORTED_SYMBOLS = ["MyModule"];

などと,EXPORTED_SYMBOLSに配列でシンボル名を定義してあげなければいけません.
これにより,module.jsm で定義した MyModule オブジェクトなどを,呼び出したJSのほうから使えるようになります.

ああ,なんか説明がわずらわしいから雛形のほうを見てくれればいいかもしれませんww

JSM内でFUELを使う

とはいえ.

とはいえ,ですよ.

JSMを使うと,純粋なオブジェクトになるので,documentオブジェクトとかとれません!まぁ細かい説明はおいときますが(というかあまりできないのでw),通常,名前空間の指定なしに定義した変数は,一番上のwindowオブジェクトの中のシンボルになります*3.このあたりは,browser.xul を読み込んで Firebug の DOM で覗いてみると面白いかもしれませんね.


で,本題ですが,JSM内ではwindowオブジェクトやらdocumentオブジェクトやら,さらにFUELで使ってるApplicationオブジェクトもありませんので,JSM内でFUELを使いたい場合,これをComponents経由で呼び出さなければいけません.

そんなわけで,JSMの一番最初で,こんなコードを入れておきます.

const Cc = Components.classes;
const Ci = Components.interfaces;
var Application = Cc["@mozilla.org/fuel/application;1"].getService(Ci.fuelIApplication);

こうしておけば,以後JSM内でも,同じように Application.console.log などと書くことができるようになります.

JSM内でコネクションを張る

同じように,XMLHttpRequestなども呼べません.というか,このあたりを調べていてわかったのですが,XMLHttpRequestって,nsIJSXMLHttpRequestとnsIXMLHttpRequestの2つのインターフェースのインプリメントだったんですね...知らなかった.

まぁそれはともかく,だんだん説明が面倒になってきたので(ぉ),要するに,普通にXMLHttpRequestみたいなことをやるには,以下のように,Components経由でインスタンスを生成したりする必要があります.

var Req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
Req.QueryInterface(Ci.nsIDOMEventTarget);
Req.addEventListener("load", function()
{
    // ...
});

Req.QueryInterface(Ci.nsIXMLHttpRequest);
Req.open("GET", "http://example.com/", true);
Req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
Req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
Req.send(null);

このあたりも,実は気づきにくいですが,

の下のほうに書いてありました.やりましたね.

まとめと参考にすべきもの

そんなこんなで

FUELも.JSM化とかも若干情報が少ないかな?まあ無難にMDCやXULPlanetを探すのと,あと,去年の夏に出た Firefox 3 Hacks は,非常に参考になります.良本だと思います.

Firefox 3 Hacks ―Mozillaテクノロジ徹底活用テクニック

Firefox 3 Hacks ―Mozillaテクノロジ徹底活用テクニック

*1:そして早速drry先生に添削してもらた.上のxpiもそれにともなって修正済

*2:とかいって私の開発暦はFUEL後から始まっているのでそれほど苦労した経験があるわけではないのですがw

*3:という説明でいいのだろうか・・・.なんかこのあたりは自信ない><