Chrome JavaScript
このセクションでは、JavaScript オブジェクトを効果的に扱う方法を見ていきます。拡張機能におけるもっとも一般的な chrome コードの例から始めましょう。これには、アプローチの仕方が複数あります。ここでは、私たちが見つけた最も効果的で一貫した方法を紹介します。
JavaScript オブジェクトをうまく管理する最初の手順は、Firefox のコードや他の拡張機能と競合しないことが分かっている名前空間を持つことです。名前空間の宣言は、そのファイル内で行うことが最善です。名前空間を宣言した JavaScript ファイルをすべての XUL ファイル内に含めてください。
/** * XULSchoolChrome 名前空間. */ if ("undefined" == typeof(XULSchoolChrome)) { var XULSchoolChrome = {}; };
名前空間の XULSchoolChrome が var を使用してどのように宣言されているか注意してください。名前空間を window chrome 内のどこからでも使用できるように、グローバルオブジェクトにする必要があります。
名前空間は通常の JavaScript オブジェクトであるため、どのような関数でも含められます。ここで、名前空間に含まれるすべてのオブジェクトで横断的に使用したい共通のユーティリティ関数やプロパティを記述するとよいでしょう。次の例では、オブザーバサービスなどの頻繁に使用される XPCOM サービスが名前空間のメンバに含められています。
/** * XULSchoolChrome 名前空間 */ if ("undefined" == typeof(XULSchoolChrome)) { var XULSchoolChrome = { /** * このオブジェクトの初期化 */ init : function() { this.obsService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); } }; /** * コンストラクタ */ (function() { this.init(); }).apply(XULSchoolChrome); };
JavaScript オブジェクトは、インデックス付き文字列の配列として扱うこともできます。
// 以下は同じ XULSchoolChrome.Hello = {}; XULSchoolChrome["Hello"] = {}; // 以下は同じ XULSchoolChrome.Hello.init(); XULSchoolChrome.Hello["init"]();
これは、動的に生成された名前を持つ属性や関数をセットしたい場合にとても役立ちます。これは、JavaScript の特異なプロパティの一つです。すべてのオブジェクトは、名前と値以外に何も割り当てられていません。どの JavaScript オブジェクトにも、好きな時に、関数や属性を追加したり置き換えたりできます。これは変な仕様に見えますが、コードが複雑になってきた時に助けになる強力な機能です。例えば、Firefox の chrome 内のオブジェクトのメソッドを置き換えることができるため、その通常行われる動作を変更できます。これは最終手段にすべきですが、場合によってはとても役立ちます。
通常、XUL ウィンドウの制御にそれほど多くのコードを必要としないため、一つの JavaScript ファイルだけで足りるでしょう。多くのコードを必要とする複雑な動作をさせる場合は、それらを複数のオブジェクトとファイルに分けるでしょう。XUL ウィンドウには、必要なだけ複数のスクリプトを含めることができます。
chrome オブジェクトを初期化するには、通常、初期化コードを window の "load" イベントハンドラから実行します。load イベントは、window 上の DOM が完全に読み込まれた後のそれらがユーザに表示される前に発生します。このタイミングで、ユーザに変更を加えられることなく、window 内の要素を操作したり変更したりできます。
/** * Controls the browser overlay for the Hello World extension. */ XULSchoolChrome.BrowserOverlay = { /** * オブジェクトの初期化 */ init : function(aEvent) { this._stringBundle = document.getElementById("xulschoolhello-string-bundle"); // ここで window の DOM を変更できます } // 他のコード }; window.addEventListener( "load", function() { XULSchoolChrome.BrowserOverlay.init(); }, false);
load ハンドラ内では、window を読み込み中に閉じることや、新しいウィンドウを開いたり、警告、ダイアログを表示することはできません (すべきではありません)。これらが行われる前に window の読み込みが完了しなければなりません。このような悪い UI は避けてください。本当にこのような動作が必要な場合は、遅延した後にコードが実行されるようにタイムアウトを設定します。
init : function(aEvent) { let that = this; this._stringBundle = document.getElementById("xs-hw-string-bundle"); window.setTimeout( function() { window.alert(that._stringBundle.getString("xulschoolhello.greeting.label")); }, 0); }
setTimeout 関数は、2 番目の引数で指定されたミリ秒後に 1 番目の引数の関数を実行します。この例では、関数がすぐに実行されるように遅延を 0 に設定しています。Firefox には、最低 10 ~ 15 ミリ秒の遅延がある (John Resig 氏のブログ記事 Analyzing Timer Performance を参照) ため、実際はすぐに実行されません。これは、window の読み込みを完了させるのに十分な時間です。
コールバック関数を引数として送る方法と this を参照する代わりの方法 (私たちは that と呼んでいます) に注目してください。これは、メソッドバインディングと呼ばれる JavaScript の機能 (癖と言った方が良いかもしれません) のために必要なことのすべてです。この方法の短所は、this 参照が期待通りの動作をしないことです。これにはいくつかの回避策がありますが、ここでは、そのうちの一つである、私たちが見つけた最もエレガントで読みやすいものを使用します。
JavaScript は、驚くべき柔軟性をもたらす機能を有していますが、Java などの他の言語のように厳格でないなど、いくつかの不便なところもあります。オブジェクトメンバを保護する private や public キーワードが無いことが、この明らかな例です。代わりに、private メンバや public メンバを名前の付け方で区別する方法がよく用いられています。これらのスコープは全く強制されていませんが、他のメンバとうまく使い分けて private メンバが使用されないようにできます。
練習問題
ここでは、chrome のある一面をテストするための短い練習問題を出します。Hello World 拡張を変更して、"Hello! This message has been shown 5 times." のように、メッセージが表示された回数を表示するようにしてください。 このカウンタを BrowserOverlay オブジェクト内の変数として保持し、メッセージが表示されるたびにカウントを増やしてください。
これが正しく動作したら、次のことを行ってください: メッセージを何回か開き、カウント数を増やします。次に、新しいウィンドウを開き、その新しいウィンドウからメッセージを表示してください。何が起こると思いますか? カウント数は何回になるでしょうか?
期待通りでなかったかもしれませんが、新しいウィンドウのカウントはリセットされました。ウィンドウごとにカウンタが保持されているため、拡張機能が期待通りの動作になりませんでした。これは、基本的な教えです: chrome はグローバルではなく、window 固有のものです。すべてのスクリプトやオブジェクトは、各ウィンドウに複製され、ウィンドウごとに独立した動作をします。ここに落とし穴があります。ほとんどの Firefox ユーザ、特にパワーユーザはいつでも一つのウィンドウしか開かないため、全体を見下ろすのはとても簡単です。一つのウィンドウでテストして動作するからといって油断してはいけません。必ず、複数のウィンドウを開いて拡張機能をテストしてください。
多くの場合、すべての開いた Firefox ウィンドウで矛盾の無いようにデータを統合する必要があります。これにはいくつかの方法があります。Preferences を用いる方法もそのうちの一つですが、これはチュートリアルの別のセクションで取り上げます。他の 2 つの方法は、JavaScript コードモジュール (Firefox 3 以降) や XPCOM を用いる方法です。
JavaScript コードモジュール
JavaScript コードモジュール (JSM としても知られています) は、Firefox 3 の新機能です。これは、ウィンドウ間ですべてを同期するための最善のツールです。セットアップはとても簡単です。初めにすべきことは、chrome.manifest にモジュールのエントリを追加することです。
resource xulschoolhello modules/
JavaScript コードモジュールは、chrome プロトコルとよく似た resource プロトコルでアクセスされます。chrome と同じように、パッケージ名とパスを定義します。ここでは、分かりやすくしておくために、JSM ファイルの場所をプロジェクトのルートの下の modules ディレクトリに置きます。例えば、このディレクトリの messageCount.js ファイルへアクセスする URL は次のようになります。
resource://xulschoolhello/messageCount.js
コードモジュールは、通常の JavaScript ファイルです。新しい名前やファイルの種類はでてきません。Mozilla は、これらのファイルの拡張子に .jsm を標準で使用していますが、.js でも良いことになっています。ここでは、開発者の環境でデフォルトのファイルの関連付けを変更しなくても済むように、.js を使用することにします。
JSM を追加した Hello World プロジェクトをダウンロードして、modules ディレクトリ内のファイルを含めるために必要なビルドシステムの変更箇所を確認してください。変更箇所は僅かです。modules ディレクトリ内に小さな Makefile.in ファイルを追加し、すべてを切り離して管理します。
セットアップについてはここで取り上げません。話を元に戻しましょう。JavaScript コードモジュールとは何でしょうか?
JavaScript コードモジュールは、宣言された要素が public であることを指定する、通常の JavaScript ファイルです。すべてのモジュールファイルは、次のように宣言で始めます。
var EXPORTED_SYMBOLS = [ "XULSchool" ];
EXPORTED_SYMBOLS は、このファイルが XULSchool という名前のオブジェクトだけを記述していることを Firefox に教える特別な識別子です。いくつかのオブジェクトや関数、変数をこのファイル上で宣言できますが、外側から見えるオブジェクトは、名前空間に使用している XULSchool だけです。名前空間を付けることにより、名前空間のオブジェクト以外にエクスポートするものについて心配する必要がなくなります。この名前空間内のすべてのオブジェクトも、XULSchool オブジェクトのメンバであるため、同様にエクスポートされます。
モジュールファイルは、次のコードで、chrome スクリプトや他のコードモジュールにインポートすることができます。
Components.utils.import("resource://xulschoolhello/messageCount.js");
このセクションで変更した Hello World のコードモジュールの例を見ていきましょう。ここでは、2 個のファイルを定義しています。一つは名前空間を宣言し、もう一つは前回の練習問題で取り上げたメッセージをカウントする関数です。
var EXPORTED_SYMBOLS = [ "XULSchool" ]; const Cc = Components.classes; const Ci = Components.interfaces; /** * XULSchool namespace. */ if ("undefined" == typeof(XULSchool)) { var XULSchool = {}; };
このコードを説明する必要はないでしょう。モジュールレベルで使用する名前空間を宣言しています。chrome 名前空間のオブジェクトはウィンドウごとに分かれているため、このように、chrome とは別の、すべてのウィンドウに対してユニークなモジュール名前空間のオブジェクトで定義する必要があります。コードモジュール上でウィンドウ固有のデータをセットすることは、問題を引き起こすだけです。そのため、何を chrome で定義し、何を定義すべきでないかを慎重に決めてください。
上記で宣言された 2 つの定数は、コードサイズを減らすために使用されます。私たちのコードでは、頻繁に XPCOM コンポーネントを使用する必要があるため、このようにしています。
this.obsService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
このコードを次のように短くできます。
this.obsService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
これら 2 つの定数は overlay 内で定義する必要はありません。これらはすでに、Firefox の browser.js ファイル内でグローバルに定義されています。ただし、独自のウィンドウを作成する時や、chrome の外のコードで動作させる時のみ定義する必要があります (また、SeaMonkey のメインウィンドウではこれらの定数が宣言されていません)。
モジュールが window のスコープの外側で動作する点は、強調する価値があります。chrome 内のスクリプトと異なり、モジュールは、window や document、openUILink のような他のグローバル関数などのオブジェクトへのアクセスがありません。これらはすべて UI コンポーネントと UI 操作であるため、chrome 内で実行されるほうがよいです。
私たちは、ほとんどのコードを静的オブジェクトや具体化する必要のない単独のオブジェクトを通して扱います。しかし、時々、クラスを定義したり複数のインスタンスを作成できるようにしたりする必要があります。よくある例として、ローカルデータベースやリモート API との対話があります。データは実体の配列に変換されることが多いため、クラスを通して提供されるのが望ましいです。次の例は、クラスを定義する方法の一つです。
/** * ユーザクラス。 Hello World ユーザを表す (中身は何でもよい) */ XULSchool.User = function(aName, aURL) { this._name = aName; this._url = aURL; }; /** * ユーザクラスのメソッド */ XULSchool.User.prototype = { /* ユーザの名前 */ _name : null, /* ユーザの URL */ _url : null, /** * ユーザ名の取得 * @ユーザ名を返す */ get name() { return this._name; }, /** * ユーザ URL の取得 * @ユーザ URL を返す */ get url() { return this._url; } };
この例では、Hello World 拡張のための架空の User クラスを定義しました。function キーワードを使用してクラスを定義することは変に思えますが、これが JavaScript での方法です。関数もまたオブジェクトなのです。クラスの定義は、同時にコンストラクタとしての役割も果たします。そして、他のすべてのメンバは prototype 属性を使用して定義できます。上記の例では、name と url メンバのための "getter" プロパティを定義しています。このように、User クラスが不変のインスタンスを持っているため、クラスを利用するオブジェクトはうまく振る舞い、変更すべきでないものは変更しません。
インスタンスの作成と使用する方法はとても簡単です。
let user = new XULSchool.User("Pete", "https://example.com/pete"); window.alert(user.name);
これは、JavaScript で一般的に行われる方法です。JSM や chrome、通常のウェブページ内でもこれを使用できます。実体はアプリケーション全体を通して使用される傾向があるので、私たちは、これらのクラスをモジュールレベルで定義することが最善のアプローチであると考えています。
JSM は、ウィンドウから独立したオブジェクトを扱うには最善の解決策です。続くセクションでは、XPCOM について取り上げます。これは、JSM より前に用いられた代替手段であり、Mozilla アプリケーションの基礎の一つです。次のセクションは、拡張機能開発において XPCOM を使用しなければならない多くの一般的な状況を取り上げるので読み飛ばさないでください。あなた自身のコンポーネントを XPCOM で実装することもあるかもしれません。
This tutorial was kindly donated to Mozilla by Appcoast.