16 April 2013

HTML5 Web Message のイントロダクション

An Introduction to HTML5 web messaging - Dev.Opera を読んだメモ。MessageChannel は知らなかったので。

導入

Web messaging は異なるブラウジングコンテキスト間で、DOM を介すことなく、データを共有する手段だ。これには cross-document messaging (window.postMessage() など) と channel messaging (MessageChannel) の 2 種類がある。

Message イベント

Cross document messaging, channel messaging, server-sent events, web sockets はすべて message イベントを発火させる。そのためまずは message イベントを見ていこう。

message イベントは MessageEvent interface で定義されている。5 つのリードオンリー属性を持つ。

  • data
    • string 形式のデータ
  • origin
    • データ送信元のオリジン
  • lastEventId
    • このメッセージイベントの ID
  • source
  • ports

cross-document messaging と channel messaging の場合 lasttEventId は空だ。また MessageEvent は DOM の Event interface を継承しているが、バブリングしない、キャンセルできない、デフォルトのアクションを持たないという特徴がある。

Cross-document messaging

cross-document message を送るにはまずブラウジングコンテキストを作る必要がある。方法は新しい window を作るか、iframe の window を refer するかの 2 通りだ。メッセージは postMessage() で送信する。引数は次の 2 つ。

  • message
    • 送信するメッセージ
  • targetOrigin
    • メッセージを送るオリジン

message パラメーターには string だけでなく object や データオブジェクト (File や ArrayBuffer) や配列も渡すことが可能だ。ただし IE8, 9 と Fx3.6 以下は string しか対応していない。

targetOrigin パラメーターにアスタリクスを設定すると送信するオリジンを絞ることがなくなる。データの漏洩につながるのでこの項目はきちんと設定することが推奨される。/ を設定すると同一オリジンに限定される。

var iframe = document.querySelector('iframe');
var button = document.querySelector('button');

var clickHandler = function(){
    // iframe.contentWindow refers to the iframe's window object.
    iframe.contentWindow.postMessage('The message to send.','http://dev.opera.com');
}

button.addEventListener('click',clickHandler,false);

postMessage() でメッセージが送信されると、受信側の message イベントが発火する。

var messageEventHandler = function(event){
    // check that the origin is one we want.
    if(event.origin == 'http://dev.opera.com'){
alert(event.data);
    }
}
window.addEventListener('message', messageEventHandler,false);

受信側のブラウジングコンテキストが postMessage() を受け取る準備ができているかどうかをチェックしたい場合、受信側でロードが完了したら送信側へ message を送信、送信側は受信側からのメッセージを listen し、とどいたらメッセージを送るようにすれば良い。

var clickHandler, messageHandler, button;

button = document.querySelector('button');

clickHandler = function(){
    window.open('otherpage.html','newwin','width=500,height=500');
}

button.addEventListener('click',clickHandler,false);

messageHandler = function(event){
    if(event.origin == 'http://foo.example'){
        event.source.postMessage('This is the message.','http://foo.example');
    }
}

window.addEventListener('message',messageHandler, false);
var loadHandler = function(event){
    event.currentTarget.opener.postMessage('ready','http://foo.example');
}
window.addEventListener('DOMContentLoaded', loadHandler, false);

Channel messaging

channel messaging はブラウジングコンテキスト間のダイレクトな双方向通信を提供する。 cross-document messaging 同様に DOM は介さず、 port 間の通信を行う。channel messaging は複数オリジン間の通信に便利だ。

MessageChannel と MessagePort オブジェクト

MessageChannel オブジェクトを作ると 2 つの関連するポートが作られる。一つは送信側に、もうひとつは他のブラウジングコンテキストに転送される。

それぞれのポートは MessagePort オブジェクトで、次のメソッドを持つ。

  • postMessage()
    • チャンネルを通じてメッセージを送る
  • start()
    • このポートで受けたメッセージのディスパッチを開始する
  • close()
    • ポートを閉じる

MessagePort オブジェクトは onmessage 属性を持つ。これは message イベントを listen する代わりのものだ。

ポートとメッセージの送信

ここでは、ドキュメントの中に 2 つの iframe があり、片方からもう片方へメッセージを送る例を示す。

ひとつめの iframe では以下を行う。

  • MessageChannel オブジェクトの作成
  • ひとつの MessageChannel ポートを親ドキュメントに転送する。これをもう片方の iframe へ転送してもらうためだ。
  • もう片方のポートでイベントを listen する。メッセージを受信するため。
  • ポートをオープンして受信可能な状態にする。
var loadHandler = function(){
    var mc, portMessageHandler;

    mc = new MessageChannel();

    // Send a port to our parent document.
    window.parent.postMessage('documentAHasLoaded','http://foo.example',[mc.port2]);

    // Define our message event handler.
    portMessageHandler = function(portMsgEvent){
        alert( portMsgEvent.data );
    }

    // Set up our port event listener.
    mc.port1.addEventListener('message', portMessageHandler, false);

    // Open the port
    mc.port1.start();
}

window.addEventListener('DOMContentLoaded', loadHandler, false);

親ドキュメントはポートを受け取るとそれをもう一つの iframe へ post する。

var loadHandler = function(){
    var iframes, messageHandler;

    iframes = window.frames;

    // Define our message handler.
    messageHandler = function(messageEvent){
        if( messageEvent.ports.length > 0 ){
            // transfer the port to iframe[1]
            iframes[1].postMessage('portopen','http://foo.example',messageEvent.ports);
        }
    }

    // Listen for the message from iframe[0]
    window.addEventListener('message',messageHandler,false);
}

window.addEventListener('DOMContentLoaded',loadHandler,false);

最後に、2 つ目の iframe はポートを受け取るとメッセージを送信する。メッセージは最初の iframe の portMsgHandler 関数がハンドルする。

var loadHandler(){
    // Define our message handler function
    var messageHandler = function(messageEvent){

        // Our form submission handler
        var formHandler = function(){
            var msg = 'add  to game circle.';
            messageEvent.ports[0].postMessage(msg);
        }
        document.forms[0].addEventListener('submit',formHandler,false);
    }
    window.addEventListener('message',messageHandler,false);
}

window.addEventListener('DOMContentLoaded',loadHandler,false);

サンプルコードは簡略化されていて、本来ならば MessageChannel がサポートされているかのチェックや、 origin が予期しているものかどうかのチェックをすべきである。

参考

cover photo by Sergio Aguirre