愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

メッセージングでまあまあ捗るかもしれない話

この記事はJavaScript Advent Calendar(オレ標準コース)の13日めのエントリイになります。
ちなみに家に帰った瞬間、マシンの時計がずれて12/14になってて、大分一人で焦りました。てへぺろ。ぺろぺろ。

この記事の題材はJavaScriptにおけるメッセージング(もどき)です。

で、メッセージングって何やねんと

JavaScriptで!メッセージング!というとその筋の人はwindow.postMessageを思い出すのかも知れませんが、
この記事では「メッセージング」という言葉をもっと広い意味に捉えて使っています。
だいたい、「あるオブジェクトがメッセージを受け取るオブジェクトを直接には知らなくても、特定の目的を持ったメッセージを投げて処理をさせることができるような仕組み」のことを「メッセージング」と呼んでいます。
すごい!すごい分かりにくい!
(ちなみにいわゆるプログラミング用語としての「メッセージパッシング」とは関係ないものとして考えてください)


分かりにくいのでコード書くと、こんなことが出来るやつのことです

    // 1.jsにて
    component1.pub("init", config);

    // 2.jsにて
    component2.sub("init", function(conf) {
        // 非同期にconfを受け取って何かする
    });

    // 3.jsにて
    component3.sub("init", function(conf) {
        // 非同期にconfを受け取って何かする
    });

component1はcomponent2やcomponent3 については何も知らない(component2やcomponent3 の参照をもってない) わけですが、"init"というメッセージをsubscribeしてる奴らに非同期にメッセージを届けて特定の処理を走らせることができます。
「節子、それObserverパターンやないか!」と思った人は正解です。Observerパターンもどきのことをメッセージングと呼んでいるだけです。

どういう時にうれしいのか

ここまでだと、メッセージングもどきが出来て何がうれしいのか、というのがよくわからないと思いますが、メッセージングが便利なのはあまりページ遷移をしないアプリ(Ajaxでモリモリページ内容を書き換えるアプリ風味のやつとか、いわゆるOne Page App的なやつとか)を作るときです。

例えば、複数の種類のモーダルウィンドウを使うアプリがあったとして、現在の画面にはユーザアクションによってどのモーダルウィンドウが出ているか分からないとします。そこに何らかのトリガーで(例えばAjaxによる画面遷移とか)画面中のモーダルウィンドウに全部消えてほしい!とかなった場合、メッセージングがない場合はそのトリガーと同時に

    // in ajax.js
    modal1.close();
    modal2.close();
    modal3.close();
    modal4.close();

というような処理を、for文とか使っても同じことですがベロベロベロ、っと書かないといけないわけです。
ここにさっきの仕組みがある場合、modalの初期化時に

    // modal.js
    this.sub("modalClose", function() {
        this.close();
    });

というのを書いておくと、トリガーを引く側では

    // ajax.js
    this.pub("modalClose");

という風にメッセージを投げるだけで、誰がどういうモーダルウィンドウを作って出しているか気にせず、かつajax.jsがmodal.jsおよびそのクライアントコードの中身に依存することなくすべてのモーダルウィンドウを閉じる処理を書くことができます。(prototypeを使う、という方法もありますがそっちだとajax.jsの中にModal.prototype.close(); とか書くハメになり結局ajax.jsがmodal.jsに依存してしまいます)

その他には、orientationchangeのような多くのコードの動作に影響を与えるイベントについて、
いろいろな所で同じeventをlistenしていちいち処理するのもあほらしいので、一つのモジュールの中で

    window.addEventListener("orientationchange", function(evt) {
        // ここに必要な前計算をまとめてしまう
        this.pub("orientationChange", data);
    });

という風に前処理およびEvent -> Messageの変換を行ってしまい、各コンポーネントの中では

    this.sub("orientationChange", function(data) {
        // dataを使って必要な処理をする
    });

という風に、イベントの受け口をまとめて、それからmessageで再ディスパッチするというような使い方もできます。DOM Mutation Eventを間引いて非同期化して再ディスパッチ、とかもアリですね。

メッセージングはEventと異なり、DOMの世界に縛られていないので「グローバルな通知」として柔軟に何にでも使えます。ちなみに、このようなメッセージングをアプリに導入するとDOM Eventと合わせて「なんか似たような仕組みをもつもの」が2つ存在することになってしまいますが、経験上似たようなことをするのに2通りの方法があってもプログラマーの脳は大丈夫です。たぶんそういうのが3つあると死ねます。

簡単な実装

簡単なメッセージングは、だいたい以下のようなコードで実現できます。ちなみに時間に追われているのでばぐっているかもしれません。メッセージングとか言っていますが中身は単なる関数コールです。

    var listeners = {};
    function Emitter() {}
    (function(ep) {
        ep.sub = function(type, callback) {
            if ( listeners[type] === void 0 ) {
                listeners[type] = [];
            }
            listeners[type].push({
                actor: this,
                callback: callback
            });
        };
        ep.pub = function {
            var args = Array.prototype.slice.call(arguments),
                type = args.shift();

            (listeners[type] || []).forEach(function(listener) {
                setTimeout(function() {
                    listener.callback.apply(listener.actor, args);
                }, 0);
            });
        };
    })(Emitter.prototype);

    var component1 = new Emitter();
    // bra bra...


まあ要は、誰にでも書けるような処理です。ここにsubscribe側のpriorityをつけたり、jQueryのようにmessage typeに関してnamespaceを使えるようにするのもカンタンですね。同じようなことをする単品ライブラリはいくらでもあると思うので敢えて列挙はしませんが、自分でミニマムなものを書いてしまっても十分用に足ると思います。(mofmof.jsにもmsgboxとかありますね!)

まとめ

明日になる!明日になっちゃう!と思いながら急いで書いたのでいささか伝わりづらい面があったかと思います。
あと実際に使ってみて、「これ便利だなー」という場面に遭遇しないと、なかなか便利さが分かりづらいかもしれません。
すべてのWebサイトであまねく使えるテクニックではないですが、今後いわゆるOne Page Appとかあまりページ遷移がないWebアプリケーションとかを作ろうという人は、メッセージングを導入して各モジュールを疎結合にして設計してみるのもいいかもしれません。便利です。たぶん。

次回はegtra-gさんです。お楽しみに!