Playframework付属のサンプル'websocket-chat'を見てみる(2/4)

[前回](http://qiita.com/yyyske/items/d9e3cf69f7a6f5fb732b)の続き。今回はクライアント側のWebSocketの記述を中心に見ています。



##chatRoom.scala.html
```scala:chatRoom.scala.html
@(username: String)

@main(username) {
    
    <div class="page-header">
        <h1>Welcome to the chat room 
            <small>You are chatting as @username</small></h1>
    </div>
    
    <div id="onError" class="alert-message error">
        <p>
            <strong>Oops!</strong> <span></span>
        </p>
    </div>
    
    <div id="onChat" class="row">
        <div class="span10" id="main">
            <div id="messages">
            </div>
            <textarea id="talk"></textarea>
        </div>
        <div class="span4">
            <h2>Members</h2>
            <ul id="members">
            </ul>
        </div>
    </div>
    
    <script type="text/javascript" charset="utf-8"
        src="@routes.Application.chatRoomJs(username)"></script>
    
}
```
`main`のViewにはめ込んで表示している。
`#onError`の`div`にエラー表示、`#onChat`の`div`の左の列にチャットの内容、右の列にルーム内の人一覧(Members)を表示している。一番下でjavascriptを読み込んでいるが、指定の仕方が独特である。`routes`ファイルを見てみると以下のようになっている。

```text:routes
GET    /assets/javascripts/chatroom.js
    controllers.Application.chatRoomJs(username)
```
JSのsrc指定の部分は、実行時には

```html
src="/assets/javascripts/chatroom.js?username=a"
```
と変換されるため、クエリ文字列を渡しながらJSを取得しにいくことになる。routesファイルの定義により、`/assets/javascripts/chatroom.js`にアクセスした場合は`Application.java`の`chatRoomJs`アクションが呼ばれることになっている。ここでJSを生成し、ブラウザ側に返している。

```java:Application.java
public static Result chatRoomJs(String username) {
    return ok(views.js.chatRoom.render(username));
}
```
ちなみに`views`パッケージ内に`chatRoom.scala.js`という名前で配置したJSのテンプレートファイルは、コンパイルされると`views.js`パッケージに配置され、`views.js.chatRoom`という名前でアクセスできるようになる模様。

##chatRoom.scala.js
```java:chatRoom.scala.js
@(username: String)

$(function() {
    var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
    var chatSocket = 
        new WS("@routes.Application.chat(username).webSocketURL(request)")

    var sendMessage = function() {
        chatSocket.send(JSON.stringify(
            {text: $("#talk").val()}
        ))
        $("#talk").val('')
    }

    var receiveEvent = function(event) {
        var data = JSON.parse(event.data)

        // Handle errors
        if(data.error) {
            chatSocket.close()
            $("#onError span").text(data.error)
            $("#onError").show()
            return
        } else {
            $("#onChat").show()
        }

        // Create the message element
        var el = $('<div class="message"><span></span><p></p></div>')
        $("span", el).text(data.user)
        $("p", el).text(data.message)
        $(el).addClass(data.kind)
        if(data.user == '@username') $(el).addClass('me')
        $('#messages').append(el)

        // Update the members list
        $("#members").html('')
        $(data.members).each(function() {
            var li = document.createElement('li');
            li.textContent = this;
            $("#members").append(li);
        })
    }

    var handleReturnKey = function(e) {
        if(e.charCode == 13 || e.keyCode == 13) {
            e.preventDefault()
            sendMessage()
        }
    }

    $("#talk").keypress(handleReturnKey)

    chatSocket.onmessage = receiveEvent

})
```
ここがクライアント側のWebSocketを担っている要となっている模様。

`MozWebSocket`が定義してある場合はそっちを使うようFireFox対応がしてあるが、FF11からは`WebSocket`に統一された模様。

```js
var chatSocket = 
    new WS("@routes.Application.chat(username).webSocketURL(request)")
```
という記述をしておくと、実行時に

```js
var chatSocket = new WS("ws:\/\/localhost:9000\/room\/chat?username=a")
```
というWebSocket用プロトコルのURLに変換してくれる。ここではそのURLを用いてWebSocketオブジェクトを作成している。同時に、非同期で、サーバとのWebSocketコネクションの確立も行われているようである([The WebSocket API （日本語訳）](http://www.hcn.zaq.ne.jp/___/WEB/WebSocket-ja.html))。

routesファイルを見てみると以下のようになっており、この時`Application.java`の`chat`メソッドが呼ばれるのがわかる。

```text:routes
GET    /room/chat    controllers.Application.chat(username)
```
この時、サーバ側でもWebSocketオブジェクトを作成しブラウザ側に返している。これでサーバとのWebSocketコネクションが確立する。

```java:Application.java
public static WebSocket<JsonNode> chat(final String username) {
    return new WebSocket<JsonNode>() {
        
        // Called when the Websocket Handshake is done.
        public void onReady(WebSocket.In<JsonNode> in, WebSocket.Out<JsonNode> out){
            
            // Join the chat room.
            try { 
                ChatRoom.join(username, in, out);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    };
}
```

`chatRoom.scala.js`ではWebSocketコネクションの確立の他、

* sendMessage 
* receiveEvent 
* handleReturnKey 

の3つの関数を定義している。

`sendMessage`は、WebSocketへのメッセージ送信を行っており、テキストエリアの文字列をJSON化した後、WebSocketの`.send()`メソッドでサーバに送信している。送信が終わったらテキストエリアをクリアしている。

`receiveEvent`は、`chatSocket.onmessage`と紐付けられており、WebSocketでメッセージを受けとった際に行う処理が書いてある。
`onmessage`の引数にはWebSocketの戻り値としてJSONオブジェクトが帰ってくるので、まずそれをパースしている。JSONにエラーが入っていた場合はエラーを表示する。
エラーがない場合は、メッセージ用の`div`を追加してチャットメッセージ用の`div`の一番下に追加、ルームメンバー一覧も`ul`を毎回クリアして新たに`li`を組み直している。自分の発言にはそれとわかるようCSSのクラスを追加したりしている。

`handleReturnKey`は、テキストエリアでのEnterキーの挙動を定義している。

-----

長くなってきたのでここまで。[次回](http://qiita.com/yyyske/items/87c49e925237a208373e)はサーバ側の挙動について見ていく。

---
[Playframework付属のサンプル'websocket-chat'を見てみる(1/4)](http://qiita.com/yyyske/items/d9e3cf69f7a6f5fb732b)
[Playframework付属のサンプル'websocket-chat'を見てみる(3/4)](http://qiita.com/yyyske/items/87c49e925237a208373e)
[Playframework付属のサンプル'websocket-chat'を見てみる(4/4)](http://qiita.com/yyyske/items/a6f5090f555038d6ff01)