m_shige1979のときどきITブログ

プログラムの勉強をしながら学習したことや経験したことをぼそぼそと書いていきます

Github(変なおっさんの顔でるので気をつけてね)

https://github.com/mshige1979

Mojolicious+Angularjsでwebsocket

angularjsでwebsocket

Controllerだけではうまくいかないことがあるのでfactoryなどが必要になってくる

実装

default.html.ep
<!DOCTYPE html>
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel="shortcut icon" href="<%= url_for '/favicon.ico' %>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="utf-8">
    <link rel='stylesheet prefetch' href='/css/bootstrap.min.css'>
    <link rel='stylesheet prefetch' href='/css/main.css'>
    <script src='/js/jquery-2.1.1.min.js'></script>
    <script src='/js/bootstrap.min.js'></script>
    <script src='/js/angular.min.js'></script>
    <script src='/js/ui-bootstrap-tpls-0.12.0.min.js'></script>
    <script src='/js/main.js'></script>
  </head>
  <body>
    <div class="header">
        <div class="title">
            <a href="/"><%= title %></a>
        </div>
    </div>
    <div class="content">
    <%= content %>
    </div>
    <div class="footer">
        <div class="copyright">
            @m_shige1979
        </div>
    </div>
  </body>
</html>

このへんはいままでのものを流用しているのでぶっちゃけ手抜きです

index.html.ep
% layout 'default';
% title 'おれおれつぶやき君';
<div class="index" ng-app="myApp">
  <div ng-controller="MainCtrl">
        <div class="row head">
            さみしくなるまでなんかをつぶやけ
        </div>
        <div class="row detail">
            <input type="hidden" ng-model="EchoScoket.path" ng-init="EchoScoket.path='<%= url_for('echo')->to_abs %>'" />
            <div class="col-md-12 block1">
                <form class="form" role="form">
                  <div class="form-group">
                    <div class="input-group" style="width: 100%;">
                      <input type="text" class="form-control" placeholder="Message" ng-model="message" ng-keydown="handleKeydown($event)">
                    </div>
                  </div>
                  <button type="button" class="btn btn-default" ng-click="messageClear()">clear</button>
                  <button type="button" class="btn btn-primary" ng-click="messageSend()">send</button>
                </form>
            </div>
        </div>
        <div class="row detail">
            <div class="col-md-12 block1">
                <div class="message_block">
                    <div class="message_item" ng-repeat="item in EchoScoket.messageList">
                        俺:{{item.text}}
                    </div>
                </div>

            </div>
        </div>
    </div>
</div>

※echoのURLをどのようにしてng-modelに割り当てようかと思ったけどng-initでなんとかなりました。

main.js
var myApp = angular.module('myApp', ['ui.bootstrap']);

myApp.factory('EchoScoket', function($rootScope, $log){

    var service = {};
    var ws;

    service = {
        // websocket path
        path: "",

        //
        messageList: [],

        // 接続
        connect: function(){
            // オブジェクト生成
            ws = new WebSocket(service.path);
            $log.log(ws);

            // 接続
            ws.onopen = function(){
                $log.log("websocket connect");
            };

            // メッセージ受信
            ws.onmessage = function(message){
                $log.log("websocket connect")

                var res = JSON.parse(message.data);

                // 先頭に追加する
                service.messageList.unshift(res);

                // すぐに反映されないのでここで同期する
                $rootScope.$apply(service.messageList);
            };

            // 切断
            ws.onclose = function(){
                $log.log("websocket disconnect")
            }

        },

        // メッセージ送信
        send: function(message){
            if(message != ""){
                ws.send(message);
            }
        }
    };

    return service;
});

myApp.controller('MainCtrl', function($scope, $log, EchoScoket){

    $scope.EchoScoket = EchoScoket;
    $scope.message = "";

    // message clear
    $scope.messageClear = function(){
        $scope.message = "";
    }

    // message send
    $scope.messageSend = function(){
        EchoScoket.send($scope.message);
        $scope.message = "";
    }

    // enter key send
    $scope.handleKeydown = function(e) {
        if (e.which == 13) {
            EchoScoket.send($scope.message);
            $scope.message = '';
        }
    }

    // 初回にのみ実行され、websocketの接続処理を実施
    $scope.$watch('EchoScoket.path', function(newVal, oldVal) {
        EchoScoket.connect();
    });
});

factoryをシングルトンパターンにしてscopeにすることで値を変更されたことを監視する$watchと連携できるようにしています。
websocketの通信部分は基本そのままで値の反映に誤差があるので$rootScope.$applyで同期をとるようにしました。

app.psgi
use Mojolicious::Lite;
use DateTime;
use Encode;
use JSON;
use utf8;

# 接続人数
my $clients = {};

# Template with browser-side code
get '/' => 'index';

# WebSocket echo service
websocket '/echo' => sub {
  my $c = shift;

  my $id = sprintf "%s", $c->tx;
  $clients->{$id} = $c->tx;

  # Opened
  $c->app->log->debug('WebSocket opened.');

  # Increase inactivity timeout for connection a bit
  $c->inactivity_timeout(300);

  # Incoming message
  $c->on(message => sub {
    my ($c, $msg) = @_;

    $c->app->log->debug('mesage->' . $msg);

    my $dt   = DateTime->now( time_zone => 'Asia/Tokyo');

    for (keys %$clients) {
      my $_msg = "$msg";
      $clients->{$_}->send(JSON->new->utf8(0)->encode({
        hms  => $dt->hms,
        text => $_msg,
       }));
    }

  });

  # Closed
  $c->on(finish => sub {
    my ($c, $code, $reason) = @_;
    $c->app->log->debug("WebSocket closed with status $code.");
  });
};

app->start;

※ベースとしたものは
http://mojolicio.us/perldoc/Mojolicious/Guides/Cookbook#WebSocket-web-service
であとは以前作成したものを流用しました。

問題点

これ外部にあげて動くかな?
外部のサーバではnginxなどのサーバを経由するのでちょっと動かせるか不安なことがある。
ちょっとローカルvmで実験してみよう

なんか、本番環境が絡むとモチベーションが下がってしまうのでなんとかしないといけない…

以前試したこと

starmanをそのまま試したら動かなかったハンドシェイクとかが上手く渡っていないようなのでmorboだけでできるか実験していく予定

websocketを使用したアプリを作成してみたいので私の今後の課題になるかも…