追悼 E4X (仮) 発表資料2013年05月27日 21時49分

Firefox 21 で E4X のサポートが削除されたのを受け、「追悼 E4X (仮)」というイベントが開催されました。東京での開催だったのですが、私自身仕様の邦訳を手がけるなど E4X には並々ならぬ想いがあり、京都から駆けつけた次第です。

来たからにはと私も「E4X と autovivification」という題で LT をしてきました。Perl でいうところの autovivification という機能が E4X にも備わっているという話です。ほかに「私と E4X」という発表 (むしろ自分語り) もしたのですが、こちらはその場限りのオフレコです。

追悼というだけあって皆さん E4X に対する熱い思いを語っていましたが、特に感心したのが Vimperator の対応の話です。それまで E4X を使っていた部分を、ECMAScript 6 での採用が検討されているテンプレートリテラルに置き換えたものの、それ自体現在の SpiderMonkey では実装されていません。そこで、chrome://liberator/....js と JavaScript ファイルを読み込んでいたのを、liberator://....js と独自プロトコルを介した読み込みに変更し、そのプロトコルハンドラの中でソースコード変換を行っているとのことでした。

私も製品のコード中で E4X を使っており、結果として後進に負の遺産を残すこととなってしまったのですが、一方で E4X があったから今の私があるというのもひとつの真実であり、E4X 仕様及びそれを実装した Mozilla に深く感謝します。

jQuery のバグを見つけてから修正されるまで2013年01月28日 02時17分

1 月 24 日に開催された Kyoto.js meetup 4 で「jQuery のバグを見つけてから修正されるまで」と題した発表を行いました。

jQuery へのコミットに関して 2 行でまとめるとすれば次のようになるでしょうか。

  1. jQuery のソースコードはショートコーディングの嵐なので心してかかる
  2. Contributing to jQueryjQuery Core Style Guidelines は必読

発表の筋書きは以下の通りです。


jQuery のバグを見つけてから修正されるまで


フォロー・ミー

  • nanto_vi (TOYAMA Nao)
  • 株式会社はてな アプリケーションエンジニア
    • クライアントサイド (JavaScript)
    • サーバーサイド (Perl)
  • jQuery をバリバリ使っている
    • Deferred
    • イベント
    • DOM 操作

ある日どこかで

街中にクリスマスの装飾が灯り始めるころ

うんうん、変わったよね

……


そういえば Deferred を使ったとき this の値はどうなるんだったっけ?

$.Deferred().done(function () {
    this // ← ココとか
}).then().done(function () {
    this // ← ココの値は?
});

jQuery 1.8.3 と jQuery 1.9 Beta 1 で結果が違う!

汚れなき悪戯

ふむふむ…… (コードを読む)

ふむふむ…… (コードを読む)

ふむふむ…… あれ?


これ、自分の書いたコードが動かなくなる?

$.Deferred().then(function () {
    // ここでの this が
    return $.Deferred().resolveWith(this, arguments);
}).done(function () {
    // ここにも引き継がれてほしい
    this
});

……


まずい!

バック・トゥ・ザ・フューチャー

どこを直せば自分のコードが動くのか

コードとにらめっこ

にらめっこ

にらめっこ

……


問題なのはここ、でもここを変えると先の変更の意味が失われるから……


こことここか!

羊たちの沈黙

直すならテストを書かないと

テストを書くならテストを走らせないと

テストを走らせるなら jQuery のビルド環境を作らないと

grunt? Node.js なら入ってるし楽勝でしょ

……


うわあ、この Node.js バージョンが古い!

うわあ、この OS バージョンが古い!

うわあ、この Python (ry

……


沈黙


そしてクリスマスが過ぎ、正月が過ぎた

風と共に去りぬ

やっぱり正月松の内は休まないとね

ってなになに……


げげー、1.9-stable ブランチ!


もはや一刻の猶予も許されない

禁じられた遊び

ビルド環境がないならテスト環境を作ればいいじゃない

俺にはこの SpiderMonkey JavaScript Shell がある!


秘技、テストのコピペ改変!

荒野の用心棒

nanto_vi 「これ直さへんとあかんちゃう?」

J 「プルリクエストにしてーな」

nanto_viプルレクったでー

D 「ここ冗長やろ。もっと削れるやろ

nanto_vi (そない言われても…… これ以上どう切り詰めろいうんや……)

nanto_vi (しゃあないな、この条件分岐をもっと前に持ってきて……)

nanto_vi (あれ、この変数、フラグに流用できるちゃうか……)

nanto_viどや!

D 「まあええんちゃうか」

素晴らしき哉、人生!

D 「修正取り込んだで。おーきに」

J 「僕と契約して貢献者になってよ!


めでたしめでたし


※ この物語は実話を基にしたフィクションです。

jQuery で HTTP 接続するときの書き方2012年12月16日 19時41分

12 月 13 日に Kyoto.js の第 3 回 meetup で、「jQuery で HTTP 接続するときの書き方」と題した 5 分間のライトニングトークを行いました。以下にその内容を一部再構成して収録します。


こんにちは、nanto_vi です。今日は jQuery で HTTP 接続をするときの書き方について話します。

皆さん jQuery を使うことも多いかと思います。jQuery で HTTP 接続をするとき、古いサンプルだと次のような書き方が載っています。

$.ajax({
    url: '/foo/bar',
    data: { baz: 'qux' },
    success: function (data) {
        console.log(data);
    },
});

接続完了時の処理をコールバック関数として $.ajax() に渡してやる形ですね。しかし、現在この書き方は非推奨となっており、替わりに次のように書きます。

$.ajax({
    url: '/foo/bar',
    data: { baz: 'qux' },
}).done(function (data) {
    console.log(data);
});

$.ajax() の返り値に対して、done メソッドで接続完了後の処理を登録する形です。この書き方の何がいいかといえば、返り値を使いまわしたり done メソッドを複数回呼び出したりして、完了後の処理を後から追加できるところです。昔の書き方では完了後の処理がコールバック関数として $.ajax() の内部に格納されていましたが、現在は「完了後の処理」だけを $.ajax() の外部に (「プロミス」として) 取り出すことが可能になったわけですね。

今「内部の処理を外部に取り出す」と言いましたが、この言葉はどこかで聞き覚えがありませんか。そう、内部イテレータと外部イテレータです。

内部イテレータは、オブジェクトが個々の要素に対する処理を受け取り、オブジェクト内部で自身の各要素に適用させる形、

// 内部イテレータの使用例
$('p').each(function () {
    doSomethingWith(this);
});

外部イテレータは、オブジェクトから「各要素を列挙する」という機能だけをオブジェクト外部に取り出す形です。

// 外部イテレータの実装例と使用例
$.fn.iterator = function () {
    var i = 0, n = this.length, self = this;
    return {
        hasNext: function () { return i < n; },
        next: function () { return self[i++]; }
    };
};

var iterator = $('p').iterator();
while (iterator.hasNext()) {
    var element = iterator.next();
    doSomethingWith(element);
}

JavaScript 1.7 以降ではジェネレータという機能により、内部イテレータのような書き方で外部イテレータを生成することができます。

さて、「処理を内部に持ってしまっている」というのは、イベントハンドラの登録も同じですね。

$(document).on('click', function (event) {
    ...
});

イベント発生時の処理をコールバック関数として jQuery オブジェクト内部に渡していますが、これを外部に持ち出すことはできないのでしょうか。

実はそれを可能にするものとして Reactive Extensions (Rx) があります。Rx 入門記事の図にもあるように、Rx を使うと空間にまたがる要素 (配列など) の列挙と時間にまたがる要素 (イベントなど) の列挙を統一的に扱うことができます。

Reactive Extensions は主に .NET Framework 上で利用されていますが、JavaScript での実装として RxJS が公開されています。

ObserverパターンとIteratorパターンは同じだったんだよ!なんだってー!

neue cc - .NET Reactive Framework メソッド探訪第一回:FromEvent

そう、ObserverパターンとIteratorパターンは同じなのだよ、ナンダッテー!

neue cc - Reactive Extensions for JavaScript

(このあたりで時間切れ)

IE 6/7 で文書間通信を実現するための一案2011年12月08日 23時32分

HTML5-WEST.jp 飲み会 UST というのがあるそうで、「参戦希望者募集」とお誘いを受けたのですが、「参戦」というからには何かしら戦の準備を整えねばなるまいと、以前から考えていたことを夜なべして実装しました。

いわゆる HTML5 の文書間通信、window.postMessage() を IE 6/7 でどう再現するかという話で、about:blank を指す隠しフレームを二つ用い、window.name を介することで双方向の通信を実現しています。

どうも IE での about:blank は、その空白ページを読み込ませた文書の生成元を継承するらしく、たとえば http://example.org/ から location.href = 'about:blank'; を実行すれば http://example.org を生成元とする about:blank になります (少なくともそのように見えます)。これを利用し、一つのフレームに二つの文書から交互に about:blank を読み込ませあうことで、そのフレームの window.name を両文書で共有できるという仕組みです。

about:blank を読み込ませるのに location.reload() を使えば履歴に余分な項目を残すこともありませんし、フラグメント識別子を使った場合と違ってデータ量にもだいぶ余裕が出ます。

chrome URL からファイル一覧を取得する2011年01月12日 01時41分

Firefox にて、ディレクトリを指す chrome URL から、そのディレクトリ以下の全ファイルの URL を返すサンプルコードを書きました。

ChromeFiles.get("chrome://browser/content/");
/* => [ "chrome://browser/content/NetworkPanel.xhtml",
 *      "chrome://browser/content/aboutDialog.css",
 *      ...,
 *      "chrome://browser/content/browser.css",
 *      "chrome://browser/content/browser.js",
 *      "chrome://browser/content/browser.xul",
 *      ... ]
 */

特徴として、実際のファイルが (.jar または .xpi に) パッケージ化されているかどうかに関わらず、ファイル一覧を取得できることが挙げられます。ソースコード全体は上記 Gist へのリンクを参照してもらうとして、以下は各関数の解説です。

var ChromeFiles = {
    get: function CF_get(spec) {
        const ios = Cc['@mozilla.org/network/io-service;1'].
                    getService(Ci.nsIIOService);
        let uri = ios.newURI(spec, null, null);
        return this.getByURI(uri);
    },

    ...
};

単に、文字列として受け取った URL を nsIURI のインスタンスにして、getByURI() へ処理を委譲しているだけです。

getByURI: function CF_getByURI(uri) {
    // 1. ディレクトリを指す URL にする
    let baseURI = uri.clone().QueryInterface(Ci.nsIURL);
    baseURI.path = baseURI.directory;

    // 2. chrome URL からローカルファイルシステムでの URL へ変換
    const registry = Cc['@mozilla.org/chrome/chrome-registry;1'].
                     getService(Ci.nsIChromeRegistry);
    let localURI = registry.convertChromeURL(baseURI);

    // 3. ディレクトリ中のファイル名を取得
    let leafNames = null;
    if (localURI instanceof Ci.nsIFileURL) {
        leafNames = this.getLeafNamesByDirectory(localURI.file);
    } else if (localURI instanceof Ci.nsIJARURI) {
        leafNames = this.getLeafNamesByJARURI(localURI);
    } else {
        throw new Error('Unknown URI: ' + localURI.spec);
    }

    // 4. ディレクトリのパスとファイル名を結合
    let baseSpec = baseURI.spec;
    return leafNames.sort().map(function (leafName) baseSpec + leafName);
},
  1. chrome URL において、chrome://{package}/content/chrome://{package}/content/{package}.xul同じリソースを表しnsIIOService#newURI() の引数に前者を渡しても返ってくる URI オブジェクトの spec は後者になります。確実にディレクトリを指す URI オブジェクトを得るためには、自分で URI オブジェクトのプロパティを変更しなければなりません。

    ところが、newURI() で作られたオブジェクトは可変でない (immutable な) ことがあり、このときプロパティに値を設定しようとすると例外が発生します。chrome URL の場合 clone() で生成した URI オブジェクトは可変になるので、まずは URI オブジェクトを複製します。

    パスからディレクトリ部分だけを抜き出すのは、nsIURL インターフェースの directory プロパティを使うのが簡単です。nsIURL インターフェースを経由すれば、ディレクトリ以外にもファイル名や拡張子などをすぐに取得できます。

  2. chrome URL からローカルファイルシステム上でのファイル位置をあらわす URL への変換は、nsIChromeRegistry#convertChromeURL() で一発です。これにより得られる URL は大抵の場合 file URL か jar URL かのいずれかです。

  3. ディレクトリ中のファイル名一覧を配列として取得します。処理本体は file URL の場合と jar URL の場合で別になります。

    なお、QueryInterface() を使わなくとも、instanceof 演算子で nsIFileURL インターフェースを実装していることを確認できたなら、それ以降は nsIFileURLfile プロパティから nsIFile オブジェクトを取得できます。

  4. 得られたファイル名一覧の順序はわからないので、辞書順で並べ替えます。その後にディレクトリ部分を表す chrome URL と結合すれば、ディレクトリ直下のファイルを指す chrome URL の一覧が得られます。

getLeafNamesByDirectory: function CF_getLeafNamesByDirectory(dir) {
    let files = dir.directoryEntries;
    let leafNames = [];
    while (files.hasMoreElements()) {
        let file = files.getNext().QueryInterface(Ci.nsIFile);
        if (file.isFile())
            leafNames.push(file.leafName);
    }
    return leafNames;
},

ディレクトリを表す nsIFile オブジェクトから、その子ファイルの名前一覧を取得します。単に子ファイルを列挙していき、それがディレクトリなどでないときにファイル名を取得するだけです。

getLeafNamesByJARURI: function CF_getLeafNamesByJARURI(jarURI) {
    // 1. ZipReader を作成
    let zip = this.openZipReader(jarURI.JARFile);

    try {
        // 2. ディレクトリ直下のファイルのパスを取得
        let baseEntry = jarURI.JAREntry;
        let pattern = baseEntry + '?*~' + baseEntry + '?*/*';
        let entries = zip.findEntries(pattern);

        // 3. ファイル名部分だけを抜き出し、返す
        let leafNames = [];
        while (entries.hasMore())
            leafNames.push(entries.getNext().substring(baseEntry.length));
        return leafNames;
    } finally {
        zip.close();
    }
},

jar URI は jar:file://path/to/file.jar!/path/to/entry/ のような形で表されます。nsIJARURI インターフェースは、file://path/to/file.jar の部分を示す JARFile プロパティ (返ってくるのは nsIURI オブジェクト) と、/path/to/entry/ の部分を指す JAREntry プロパティ (返ってくるのは文字列) を持っています。

  1. JAR ファイル (または XPI ファイル) は ZIP 書庫なので、内部のファイル情報を読み取るためには nsIZipReader のインスタンスを作成し書庫を開く必要があります。

  2. 書庫内部のファイル名一覧を取得するには findEntries() を使います。ここで指定するファイル名のパターンにおいて、"?" は任意の1文字を、"*" は任意の文字列を、"pattern1~pattern2"pattern1 にマッチするが pattern2 にはマッチしないものを表します。

    "/path/to/entry/*" というパターンでは /path/to/entry/ 自信も含まれてしまうので、ディレクトリではないファイルだけを抽出するために "/path/to/entry/?*" と指定します。また、それだけだと子孫ディレクトリ中のファイルも含まれるので、"/path/to/entry/?*/*" を除外してやります。

  3. findEntries() で得られた値にはディレクトリ部分も含まれるので、その部分は切り取ってファイル名だけにします。

openZipReader: function CF_openZipReader(uri) {
    let zip = Cc['@mozilla.org/libjar/zip-reader;1'].
              createInstance(Ci.nsIZipReader);
    if (uri instanceof Ci.nsIFileURL) {
        // 1. file URL なら単にそのファイルを開く
        zip.open(uri.file);
    } else if (uri instanceof Ci.nsIJARURI) {
        // 2. jar URL なら JAR ファイル内部のファイルを開く
        let innerZip = this.openZipReader(uri.JARFile);
        zip.openInner(innerZip, uri.JAREntry);
    } else {
        throw new Error('Unknown URI: ' + uri.spec);
    }
    return zip;
},
  1. JAR ファイルがローカルファイルシステム上に直接存在するなら、単に open() メソッドにファイルオブジェクトを渡して開くだけです。

  2. 開こうとする JAR ファイルが別の書庫内に存在することもあります。その場合は openInner() に、JAR ファイルが含まれる書庫と、その書庫内での JAR ファイルのパスを指定してやります。

    nsIZipReader#openInner() は Firfox 4 で追加されたものですが、Firefox 4 より前では jar URL がネストすることはないといっていいので、ここで使っても問題ないでしょう。

たとえば Firefox 4 Beta でツリー型タブを使うと、chrome://treestyletab/content/ の実体は jar:jar:file://{profile}/extensions/treestyletab@piro.sakura.ne.jp.xpi!/chrome/treestyletab.jar!/content/treestyletab/ といった URL になりますが、上記のようにすればその内部のファイル構成を知ることができます。

また、resource://gre/modules/XPCOMUtils.jsm といった resource URL に関しても、nsIResProtocolHandler#resolveURI() を使えばローカルファイルシステム上の URL へ変換でき、上と同様にファイル一覧の取得などが可能になります。