機能が呼び出されたことを他のアドオンに通知する

あなたはアドオンを作る時、他のアドオンと連携しやすいようにするということを意識しているでしょうか?

例えば、複数のタブをまとめて一気に閉じる機能を設けるために、タブの配列を受け取ってそれらをすべて閉じる closeTabs() という関数を定義するとしましょう。

function closeTabs(aTabs) {
  gBrowser.dispatchEvent(event);
  aTabs.forEach(function(aTab) {
    gBrowser.removeTab(aTab);
  });
}

この機能を提供するアドオンの作者であるあなたにとっては、これ以上特に何も気にする事はありませんよね。

では、他のアドオン作者の人が、あなたのアドオンと連係して動作するアドオンを開発する場面を考えてみましょう。この関数が呼ばれたという事を他のアドオンから検知するにはどうすればよいでしょうか?

Firefox のユーザの多くは、複数のアドオンを組み合わせて使っています。アドオンを公開していると、「あなたのこのアドオンはとても素晴らしい! ところで自分は○○というアドオンを使ってるんだけど、あなたのアドオンの機能からもこの○○の機能を利用できるようにならないだろうか?」という風な要望が寄せられることもあります。私はそういう要望を受け取って初めてそのアドオンの存在を知ることが多いのですが、それでは連携してみようと思ってそのアドオンのソースコードを覗いてみて、途方に暮れてしまうことが少なくありません。そういう場合で一番多いのが、「このアドオンのこの機能が呼び出される直前に、何らかの処理を割り込ませたい。しかし、その方法がない。」というケースです。

もちろん、方法が全くないわけではない場合がほとんどです。例えばたいていの場合、以下のようにすれば任意の処理を割り込ませることができます。

eval('window.closeTabs = '+
     window.closeTabs.toSource().replace(
       '{',
       '{ AnotherAddon.onTabsClosed(); '
     )
);

しかし、このように eval() を使って関数を書き換えたり、あるいは関数全体を置き換えたりする方法は、同じ事をやろうとするアドオンが沢山存在すると破綻してしまいます。また、書き換え(置き換え)対象の関数の定義が変わった時にも、期待通りに動かなくなる可能性があります。

実際に Firefox 1.5 以前の古いバージョンの Firefox では、tabbrowser 要素の addTab()removeTab() といったメソッドを置き換えないと、「新しいタブが開かれた」「タブが閉じられた」「タブが移動された」といった場面で任意の処理を行う事はできませんでした(※厳密に言うと、間接的なやり方で実現できない事もなかったのですが……)。そのため、タブブラウズ機能に関係するアドオンを複数インストールするとそれらが衝突してまともに動かないということもざらにありました。

他のアドオン作者をこういう風に困らせてしまわないように、他のアドオンと連携しやすいアドオンを作る方法はないのでしょうか? それがこのエントリのテーマです。

複数のアドオン同士を連携しやすくする方法はいくつかありますが、その 1 つとして、DOM Level2 Events の仕組みを使う方法があります。例えば次のようにすると、前出の closeTabs() が呼ばれた時に「複数のタブを閉じようとしていますよ!」というイベントを通知することができます。

function closeTabs(aTabs) {
  // 新しいイベントオブジェクトを作る
  var event = document.createEvent('Events');
  // 好きなイベント名を付ける。
  // 後の引数はとりあえず true, false と書いておけばいい。
  event.initEvent('ClosingTabs', true, false);
  // イベントオブジェクトに任意のプロパティを持たせて
  // いろんな情報を送れる。
  event.tabs = aTabs; // 閉じられようとしているタブ
  // 任意の DOM 要素ノードの dispatchEvent() メソッドで、
  // イベントの通知を開始!
  gBrowser.dispatchEvent(event);

  ...
}

このようにカスタムイベントを通知するようにしておけば、他のアドオンの作者は以下のようにして closeTabs() が呼ばれたことを簡単に検知できます。

gBrowser.addEventListener(
  'ClosingTabs',
  function (aEvent) {
    // お、複数のタブが閉じられようとしているぞ!
    var tabs = aEvent.tabs;
    var labels = tabs.map(function(aTab) { return aTab.label; });
    alert(labels.join('\n')+
          '\n-----'+
          '\n以上の'+tabs.length+'個のタブがこれから閉じられます');
  },
  false
);

Firefox 2 以降でも、これと同じ方法で、タブが開かれたり閉じられたりといったイベントを検知できます。つまり、Firefox 本体と同様の処理をあなたのアドオンにも持たせるということですね。

「他のアドオンと連携する事なんて興味ないよ」と思うかもしれませんが、このテクニックは他のアドオンとの連携以外でも役に立ちます。

例えば closeTabs() を使う機能にさらに色々便利な機能を増やしたくなった時、closeTabs() の中にその都度コードを書き加えていっていると、そのうち関数がどんどん長くなってメンテナンスが大変になってきます。何百行、何千行という長い関数の中でうまく動かない部分がでてくると、原因箇所の特定も、バグの修正も困難です。

でも上記のようにカスタムイベントを定義してあれば、機能を増やしたくなった時は、そのイベントを監視するイベントリスナを新しく定義するだけで済みます。機能ごとに細かく関数を分ければメンテナンス性も高まります。

カスタムイベントは、他のアドオンとの連携にもあなた自身がアドオンをメンテナンスする手間の削減にも役立つ一石二鳥なテクニックです。ぜひ活用してみて下さい。