Kanasan.JS Jetpack ワークショップ2009年12月30日 18時08分

Kanasan.JS Jetpack ワークショップに行ってきました。Jetpack は Firefox 用の簡易拡張プラットフォーム。私としては通常の拡張機能のほうが高い自由度を持てて好きなのですが、Firefox を広く一般に使ってもらうには、簡単なものを簡単に作れるようにするという点が重要なのでしょう。

最初に mollifier さんによる Jetpack 入門があった後、二人組になり各グループで Jetpack フィーチャーを作っていくという流れ。私は satyr さんとペアになったのですが、さすが satyr さん、ゴルファーだけあって見慣れぬ記法を次々と使ってきます。

var { href } = location;

上のコードは分割代入の省略形式で、

var { href: href } = location;

の略、さらには、

var href = location.href;

の略なのですが、JavaScript 1.8 以降 (Firefox 3 以降) での機能とあって、私自身も久しく頭から抜けていました。

そんなこんなで完成した (といっても実際のコーディングはほとんど satyr さんでしたが) のが、現在閲覧中のページの W3C Markup Validation Service での検証結果をステータスバーに表示する Jetpack フィーチャー、HTMLValidator for Jetpack です。

ちなみに開発中に引っかかったのが、特定の名前空間に属する要素の jQuery での扱い。http://validator.w3.org/check?uri=http%3A%2F%2Fnanto.asablo.jp%2Fblog%2F&output=soap12 のように output パラメータに値 soap12 を指定すると、検証結果が、

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<m:markupvalidationresponse env:encodingStyle="http://www.w3.org/2003/05/soap-encoding" xmlns:m="http://www.w3.org/2005/10/markup-validator">
    <m:uri>http://nanto.asablo.jp/blog/</m:uri>
    <m:checkedby>http://validator.w3.org/</m:checkedby>
    <m:doctype>-//W3C//DTD HTML 4.01 Transitional//EN</m:doctype>
    <m:charset>utf-8</m:charset>
    <m:validity>true</m:validity>
    ...
</m:markupvalidationresponse>
</env:Body>
</env:Envelope>

と XML 形式で返ってくるのですが、ここから jQuery で validity 要素を選択するには、res を返ってきた XML 文書ノードオブジェクトとして、

$(res).find("m\\:validity")

のように指定する必要があるのでした。

そもそも DOM の getElementsByTagName メソッドからして、引数に指定できるのは要素名 (非修飾名) のみと勘違いしていたのですが、実際は、名前空間に属する要素を選択するためには接頭辞も含めた修飾名を指定する必要があるとのこと。名前空間の指定に、語彙に対して常に一意な名前空間 URI ではなく、文書片ごとに変わる可能性のある名前空間接頭辞を用いるのは、大変気持ち悪いです。

Firefox Developers Conference 20092009年11月12日 02時26分

11 月 8 日に行われた Firefox Developers Conference 2009 にて、secondlife さんとともにはてなブックマーク Firefox 拡張に関する発表を行いました。発表資料は次の通りです。

資料中に約 3500 行の XML とありますが、この中には XBL 内に直接書かれた JavaScript コードの行数も含まれていると思います。XBL は、特定の機能を持つ要素が複数存在する場合に威力を発揮します。Mozilla のウィジェット (ボタンやメニューなど) のソースを読むためには XBL のあらましを知っておく必要があると思うのですが、意外と認知度は低いようでした。

他の発表では、「ビッツにおける拡張機能開発」のユーザーの反応やサポートに関する話題が興味深かったです。ソフトウェアを広く使ってもらうには、ユーザーの反応に対し、肯定的であれ否定的であれ、すばやく対応することが重要なんだと実感しました。(たとえば Piro さんは、ユーザーの要望に従わないことを、理由を挙げて丁寧に説明しています。)

また、Aza さん、amachang さん、Piro さんによるトークセッションでは、「グローバルにするためにはローカライズしなければならない」という Aza さんの言葉が心に残りました。以前、奥 (kazuho) さんからも同じ言葉を聞いたことがあり、世界に通用するソフトウェアを作る人間の意識を垣間見た気がします。

懇親会で Aza さんに、Jetpack の HTML を使った UI が、外観やアクセシビリティ API を通したアクセスの点で、OSネイティブのウィジェットや XUL を使った UI と異なってくることについてお聞きしたかったのですが、私自身質問の要旨をまとめきれず、ちぐはぐな応答になってしまいました。ただ、WAI-ARIA の話を出されていたので、そうした仕組みを通じて (あるいはそれを背後で行ってくれるライブラリを使って) OS からのアクセスも確保していこうということだと思います。

懇親会は料理のほうも素晴しく、私はもっぱらアボカドとえびのサラダと、Firefox 5 周年ケーキの切れ端をぱくついていました。はてなステッカー (前日人に尋ねられるまで配布されることを知らなかったのですが) が予想外の人気で、私もじゃんけん大会に参加したものの、あえなく 1 回戦敗退です。

普段 Web 上でお世話になっている方々や、Mozilla を陰日向から支えてきた方々と直接お会いすることができ、非常に楽しくまた有意義なイベントでした。拡張開発の機会を与えてくださったはてなの皆さん、発表の場を用意してくださった Mozilla Japan の皆さん、発表にお越しいただいた皆さん、そして Mozilla と Firefox に対して多大な貢献をされてきた皆さんに深く感謝します。

LiveCoding #6 の 22009年07月19日 23時40分

OSC Kansai に出た帰りに satzz 君に電話したら、ちょうど LiveCoding #6 をやっているとのことだったのでその足で寄ってきました。前回も第 6 回だった気がするのは何かの間違いでしょうか。

LiveCoding とはプログラミングの実演と実況を楽しみましょうというイベントで、出てくる言語も C から PHP まで色とりどり。まともとは思えないテクニックも多々披露されたわけですが、中でも驚愕だったのが ujihisa さんが書いた Ruby のコード、

def fib n, x = (n < 2) ? n : fib(n - 1) + fib(n - 2)
  x
end

fib 10 # => 55

Ruby ではデフォルト引数内で再帰呼び出しができるだなんてちっとも知りませんでした。

飛び入り LiveCoder も大歓迎といわれ、その特典である参加費 (お寿司 + カレー代 + etc.) キャッシュバックを目当てに手を挙げたのですが、ネタが思い浮かばず、bit.ly API の存在を教えてもらって「Web ページ閲覧中にリンク先が bit.ly 経由で何回クリックされたかを表示する Greasemonkey スクリプト」を作ることにしました。

[図: リンク先へのクリック回数を表示する Greasemonkey スクリプト]

Greasemonkey スクリプトでは let を使えないのについ let と打ってしまって var と打ち直したり、Greasemonkey と bit.ly と wedata の API リファレンスを読みふけったり、E4X で構築した要素を手っ取り早く使うために innerHTML を使おうと思ったら、そもそも要素を作成する必要がないことがわかって単なるテキストを innerHTML に代入する羽目になったり、それならとテキストノードを直接生成するように書き換えたりしているうちに制限時間 20 分はあっという間に過ぎ、45 分かけてどうにか動くものができるという有様。動くのは LDRize 対応サイトのみ、利用するには Firefox 3.5 以上と bit.ly のアカウントが必要です。

その場で cho45 が「こういうときは JSDeferred を使えば」とプッシュしていた気がするので、後日 JSDeferred を使って書き直してみました。JSDeferred をうまいこと使えているかは不明ですが。

ちなみに Greasemonkey スクリプトで let が使えないというのは、Components.utils.evalInSandbox で JavaScript のバージョンが指定できないという制限によるものです。この仕様は Firefox 3.5 で改善され、適切なバージョンを指定すればサンドボックス内でも letyield を使えるようになりましたが、Greasemonkey 側はまだ対応していません。

コーディングが終わった後はまったり過ごしていましたが、JSDeferred のロゴを作れば女子高生の間で JSDeferred が流行るんじゃないかという話になったため、実際に作ってみましたsecondlife さんに「何この 5 秒で描いたような」と dis られましたが、30 秒はかかっています。JSDeferred のトップページにもこのロゴを貼り付けたので、ナウなヤングにバカウケ間違い無しですね。

それと、Gist で Greasemonkey スクリプトを管理しようという話を聞きますが、皆さん具体的にどうやって管理しているんでしょうか? とりあえず Greasemonkey スクリプトを書いた後、gisty で投稿してそれを git remote で引っ張ってきてとやったのですが、これでいいのかよくわかりません (環境は Cygwin + Meadow)。

$ cd FIREFOX_PROFILE_DIR
$ cd gm_scripts/bitly_referred/
$ ls
bitly_referred.user.js
$ gisty post bitly_referred.user.js
Initialized empty Git repository in GISTY_DIR/149553/.git/
$ git init
$ git remote add origin git@gist.github.com:149553.git
$ git pull origin master
$ cat > .gitignore
*
!*.user.js
$ meadoww bitly_referred.user.js
$ git add .
$ git commit -m "Rewrite with JSDeferred"
$ git push

第 2 回 Firefox 出張ワークショップ発表資料2009年07月15日 02時17分

先日京都コンピュータ学院で開催されたオープンソースカンファレンス 2009 Kansai、その中の一セッション「第 2 回 Firefox 出張ワークショップ ~基礎から学べる拡張機能開発~」に講師として参加させていただきました。私の担当した後半、実際に拡張機能を作ってみる部分の資料及び完成版の拡張は以下になります。

「わからないことがあったとき、どうやって調べるか」をひとつの柱として話を進めていきたかったのですが、つたない進行で後半ややペースが押し気味になってしまい、終了時間を 5 分ほど過ぎて最低限動くものが完成するという有様でした。人によっては休憩時間もろくに取れない状態になってしまい申し訳ありません。

以下に質問のあった点、補足等を記します。

「進む」メニューの特定

DOM Inspector でコンテキストメニューの「進む」メニューを探すのにいちいち文書ツリーをたどっていったのですが、後ほどあさんから指摘があったように、クリックで要素を選択する機能を使ったほうがはるかに簡単でした。

具体的には、DOM Inspector でブラウザウィンドウを選択後、マウス選択ボタンをクリックし、ブラウザウィンドウに戻ったらアプリケーションキーまたは Shift + F10 キーでコンテキストメニューを表示させます。そうすれば後は「進む」メニューを直接クリックするだけで DOM Inspector 側で適切な要素が選択されます。

[図: DOM Inspector のマウス選択ボタン] [図: キー操作で表示したコンテキストメニュー]

マウス選択状態では右クリック (Mac では Ctrl + クリック) してもその要素が選択されてしまうので、文書ツリーをたどっていく必要があると思っていたのですが、キー操作でコンテキストメニューを表示させればいいというのは個人的に盲点でした。

名前付き関数式

ContextHistory オブジェクトの各メソッドを記述する際、

var ContextHistory = {
  init: function CH_init() { ... },
};

CH_init のような名前をつけていることへの質問がありました。名前をつけることでスタックトレースにその名前が出るようになり、デバッグが楽になります。つけないと anonymous function といった表示になり、名前から関数を特定できません。

XUL 要素の動的な生成

XUL、XHTML、SVG といった特定のXML応用の要素を生成するときは、document.createElementNS メソッドを使い、第 1 引数にその XML 応用の名前空間 URI を指定します。一応 XUL 文書中では createElement メソッドでも XUL 要素が生成されますが、そのような DOM 仕様にない Mozilla 独自の振る舞いに依存すべきではありません。さもなくば XUL と他の XML 応用とを組み合わせるときなどに混乱を招くことになります。

よく参照する Firefox のソースコード

Mozilla Cross-Reference で Mozilla の (Firefox の) ソースコードを閲覧・検索できます。私の場合、実際によく参照するのは次のような箇所です。

Firefox の機能とそのソースコードの場所
機能 Firefox 3.5 系統 最新開発版
ブラウザの UI /browser/base/content/ /browser/base/content/
DOM インターフェース /dom/public/idl/ /dom/interfaces/
UI 部品 /toolkit/content/widgets/ /toolkit/content/widgets/

数値への変換

JavaScript で +exprNumber(expr) と同じ意味であり、expr を数値へ変換した結果を返します。

ラベルの国際化

追加したメニューのラベルを、ロケールによらずツールバーの履歴ドロップダウンボタンのツールチップと同じにするのが目標でした。たとえば次のような手順が考えられます。

  1. DOM Inspector で履歴ドロップダウンボタンを選択し、その id を確認する。
  2. view-source:chrome://browser/content/browser.xul からその id を検索し、ツールチップ文字列を表す実体参照を見つける。
  3. MXR で実体名を検索し、その実体が含まれる DTD ファイルを見つける。
  4. MXR で見つかった DTD ファイル名と browser.xul のソースで参照している DTD の chrome URI とを見比べ、その DTD ファイルを指す chrome URI の見当をつける。
  5. オーバーレイファイルからその DTD を参照し、メニューのラベルとしてその実体参照を記述する。

[図: DOM Inspector で選択した履歴ドロップダウンボタン] [図: 履歴ドロップダウンボタンのソースコード] [図: MXR での実体名の検索結果] [図: browser.xul が参照する DTD]

<!DOCTYPE overlay [
  <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
  %browserDTD;
]>

修正の反映

最初のほうで QuickRestart を紹介した影響か、ソースコードを修正するたびに Firefox を再起動していた人が多かったですが、実は about:config から nglayout.debug.disable_xul_cache を true にしておけば、新しいウィンドウを開くたびに最新のソースコードが読み込みなおされます。ただし、XPCOM コンポーネントJavaScript モジュールはこの限りではありません。

拡張の JavaScript の名前空間

JavaScript には名前空間が存在しないので、拡張のスクリプトを書くときは、Firefox 本体や別の拡張が使っている名前と競合しないように注意する必要があります。これは拡張から Firefox 本体を自由に操作できることの裏返しでもあります。

JAR ファイルの利点

拡張の構成ファイルを単独の JAR ファイルに固めることで、アクセスするファイルがひとつですむようになり、ファイルアクセスの負荷を減らすことができるそうです。

ローカルな拡張の自動更新

たとえば企業内のみで使いたい拡張があるというとき、自動更新に対応させるためには、SSL を通じて更新を配布するか、電子署名を使い拡張に公開鍵を含める必要があるそうです。

Greasemonkey スクリプトとイベントで通信2008年06月26日 08時12分

Greasemonkeyスクリプトとウインドウ間で安全に通信する」にて、DOM イベントを用いた Web ページと Greasemonkey スクリプトとの通信について述べられています。そちらでは dispatchEvent メソッドの返り値による 1 bit 通信に触れていますが、やはりもっと自由にデータをやり取りしたいもの。そのためにはどのような方法があるでしょうか。

独自プロパティ

真っ先に思いつくのは、Web ページ側でイベントオブジェクトを作成した際、独自プロパティを追加する方法ですが、これはだめです。Greasemonkey スクリプト側ではイベントオブジェクトの独自プロパティを取得できません。event.wrappedJSObject.myProperty のように wrappedJSObject を介せば取得できますが、せっかく安全のため Firefox 側でラッパーに包んでくれたのに、それを外すべきではありません。wrappedJSObject は危険です。

CustomEvent

こんなこともあろうかと DOM 3 Events 草案では CustomEvent というイベントが用意されています。これを使えばイベントオブジェクトに独自のデータを保持できます。しかし、Firefox 3 は DOM 3 Events に対応していません。終了。

CommandEvent

Firefox 3 では新しく CommandEvent というイベントに対応しています。これは XUL での利用を考えて追加されたものですが、Web ページから作成することもできます。CommandEvent には文字列型の command プロパティがあるので、ここにデータを格納できます。JSON などを使えば複雑な構造のデータもやり取り可能です。

// Greasemonkey スクリプト
document.addEventListener("GMPingCommand", function (request) {
  var response = document.createEvent("CommandEvent");
  response.initCommandEvent("GMPongCommand", true, false,
                            'You said "' + request.command + '"');
  document.dispatchEvent(response);
}, false);
// Web ページ
window.addEventListener("load", function (event) {
  var request = document.createEvent("CommandEvent");
  request.initCommandEvent("GMPingCommand", true, false,
                           "Hello, world!");
  document.dispatchEvent(request);
}, false);

document.addEventListener("GMPongCommand", function (response) {
  alert(response.command); // => You said "Hello, world!"
}, false);

DataContainerEvent

Firefox 3 では新しく DataContainerEvent というイベントに対応しています。これも XUL での利用を考えて追加されたものですが、Web ページから作成することもできます。DataContainerEvent に専用の初期化メソッドはありませんが、getData メソッドと setData メソッドがあります。つまり、このイベントオブジェクトは辞書として扱えるのです。基本的に任意の型の値を格納できますが、Web ページと Greasemonkey スクリプトの間で使おうとするとセキュリティ制限がかかります。

Web ページで値を設定し、Greasemonkey スクリプトでそれを取得する場合、プリミティブ値 (undefined、null、文字列値、数値、真偽値) はそのまま取得できますが、JavaScript のオブジェクトや配列は取得できず null が返ります。Window オブジェクトや DOM ノードオブジェクトはラッパーに包まれます。なお、存在しないキーの値を取得しようとすると null が返ります。

逆に、Greasemonkey スクリプトで値を設定し、Web ページでそれを取得する場合、JavaScript のオブジェクトや配列もそのまま返されます。しかし、権限の異なるオブジェクトをさらすことになるので、データのやり取りはプリミティブ値のみにとどめるべきでしょう。Window オブジェクトや DOM ノードオブジェクトに関しては、ラッパーを値に設定してもラッパーが外されて返されます。

// Greasemonkey スクリプト
document.addEventListener("GMPingDataContainer", function (request) {
  alert(request.getData("number")); // => 42
  alert(request.getData("object")); // => null
  alert(request.getData("window"));
  // => [object XPCNativeWrapper [object Window]]

  var response = document.createEvent("DataContainerEvent");
  response.initEvent("GMPongDataContainer", true, false);
  response.setData("data",
                   'You said "' + request.getData("data") + '"');
  document.dispatchEvent(response);
}, false);
// Web ページ
window.addEventListener("load", function (event) {
  var request = document.createEvent("DataContainerEvent");
  request.initEvent("GMPingDataContainer", true, false);
  request.setData("number", 42);
  request.setData("object", { foo: 42 });
  request.setData("window", window);
  request.setData("data", "Hello, world!");
  document.dispatchEvent(request);
}, false);

document.addEventListener("GMPongDataContainer", function (response) {
  alert(response.getData("data")); // => You said "Hello, world!"
}, false);

MessageEvent

Firefox 3 では新しく MessageEvent というイベントに対応しています。これは HTML 5 草案で定義されているもので、文書間メッセージングなどで利用されます。文書間メッセージングについては、現在好評発売中の WEB+DB PRESS Vol. 45 の連載「JavaScrit + ブラウザ探検 第 2 回 気になる Firefox 3 の新機能」に詳しく載っていますので、ぜひ購入しましょう。MessageEvent には文字列型の data プロパティがあるので、ここにデータを格納できます。

// Greasemonkey スクリプト
document.addEventListener("GMPingMessage", function (request) {
  var response = document.createEvent("MessageEvent");
  response.initMessageEvent("GMPongMessage", true, false,
                            'You said "' + request.data + '"',
                            location.protocol + "//" + location.host,
                            "", window);
  document.dispatchEvent(response);
}, false);
// Web ページ
window.addEventListener("load", function (event) {
  var request = document.createEvent("MessageEvent");
  request.initMessageEvent("GMPingMessage", true, false,
                           "Hello, world!",
                           location.protocol + "//" + location.host,
                           "", window);
  document.dispatchEvent(request);
}, false);

document.addEventListener("GMPongMessage", function (response) {
  alert(response.data); // => You said "Hello, world!"
}, false);

ここでは initMessageEvent メソッドの第 5 引数 origin と第 7 引数 source にそれぞれページの生成元と Window オブジェクトを渡しましたが、これは空文字列と null でも構わないでしょう。

XULCommandEvent

Greasemonkey スクリプトではなく拡張機能ですが、分割ブラウザイベントを用いた API を提供しています。そこで使われているのが XULCommandEvent で、上記三つとは異なり Firefox 2 以下でも使えるという利点があります。ただし、直接文字列を格納することはできないので、別のイベントのイベント名を使うという荒業に出ています。ちなみにこの API は Stylish の作者の提案をきっかけに作られたそうです。

独自属性

以上の方法はイベントのみで完結する、すなわち文書構造をいじらないことを前提としたものでした。文書構造を変更し、時には不正な HTML 文書になってもいいというのなら、HTML 要素を追加したり要素に独自属性を追加したりすることで、その要素、属性経由で値をやり取りできます。Firebug がとっているのと同じような方法です。

名前空間

イベントを用いて通信する以上、あらかじめイベント名を決めておく必要があります。ここで、安易にイベント名を決めてしまうと、複数の Greasemonkey スクリプトや拡張機能の間で名前が衝突してしまうかもしれません。そこで、DOM 3 Events 草案ではイベントの種類を特定するのに、従来のイベント名に加えて名前空間 URI を用いるようになっています。しかし、Firefox 3 は DOM 3 Events に対応していません。終了。

と終わってしまうのもなんなので、ここは DOM 2 Events の流儀に従うことにしましょう。MyGMMessage のようにイベント名に接頭辞 (この例では MyGM) をつけるのです。ただし、DOM という接頭辞は DOM Events によって予約されているので使ってはいけません。DOMContentLoaded や DOMMouseScroll などはだめな名前の典型です。

なお、DOM 3 Events のことも考えれば、イベント名は XML 名前空間における NCName、すなわち名前空間接頭辞を含まない XML の要素名に使える文字列にすべきでしょう。

まとめ

個人的には、Firefox 3 を対象にするなら CommandEvent がよいと思います。上で書いた中では使い方も一番シンプルですし、やり取りするデータが文字列に限定されるので余計な心配を減らせます。

さて、使えそうなイベントが三つも追加された Firefox 3 ですが、これ以外にもさまざまな新機能が搭載されています。現在好評発売中の WEB+DB PRESS Vol. 45 の連載「JavaScrit + ブラウザ探検 第 2 回 気になる Firefox 3 の新機能」に詳しく載っていますので、ぜひ購入しましょう。大切なことなので 2 回言いました。