もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

AngularJSとFirebaseでリアルタイムチャットアプリ作ってみたよ

先月、社内の有志で各々好きなテーマを決めて1日もくもく勉強するという催しを開催したのですが、僕はクライアントサイドフレームワークを触ってみようーということでAngularJSを初めて触ってみました。以下の内容は1日でふんわり勉強しながら書いた内容なので間違ってたらどしどしご指摘ください。

※場所はムービングスクワッドさんのオフィスを借りました。須田さんありがとうございました!会社はこちら

AngularJSとは

Google主導で開発しているクライアントサイドMVCフレームワークです。他にも同様のものはBackbone.jsとかKnockout.jsとかあったりします。最近リリースされたnoteというサービスはAngularJSを使っているみたいですね。
参考:noteをAngularJSで構築した話

こんなことができる

以下のコードはAngularJSのサイトに掲載されているサンプルです。

<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.10/angular.min.js"></script>
  </head>
  <body>
    <div>
      <label>Name:</label>
      <input type="text" ng-model="yourName" placeholder="Enter a name here">
      <hr>
      <h1>Hello {{yourName}}!</h1>
    </div>
  </body>
</html>

タグの中に見慣れないng-XXがあると思いますが、これはAngularJSとHTMLのエレメントを関連づけているコードです。ちなみにプレフィックスのngはAngularのA "ng" ularから取っているらしいです。

サンプルはng-model="yourName"というコードでyourNameというモデルを定義してテキストボックスに割り当てています。で、下のh1の {{yourName}} で定義したモデル名が設定されています。ここがAngularJSがバインドする部分になります。
このHTMLをブラウザで表示すると{{yourName}}は表示されません。代わりにテキストボックスに何か入力すると{{yourName}}の部分に入力した値が自動で表示されるようになります。

jQueryで同じようなことをする場合、テキストボックスに対してkeyupイベントでテキストボックスの値を取得→h1のテキストにセットするという処理をデリゲートするみたいなコードを書くのでMappingだけで片付くのであればかなり楽ですね。

チャットしてみる

チャットと言えばNode.js/socket.ioなんかでお手軽に作る事ができる時代になったわけですが、AngularJSでもFirebaseというサービスと組み合わせてチャットが作れたりします。

github-pagesに立ててます。
http://rei-m.github.io/angular_chat/

※自由に投稿してもらって構いません。個人情報の投稿は見っけたら削除します。

では、少しだけコードを解説していきます。

まず index.htmlです。

<!doctype html>
<html ng-app="chatApp">
  <head>
    <meta charset="UTF-8">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.6/angular.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0-beta.7/angular-route.min.js"></script>
    <script src="https://cdn.firebase.com/js/client/1.0.11/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/0.7.1/angularfire.min.js"></script>
    <script src="js/index.js?11"></script>
  <link rel="stylesheet" href="css/style.css" />
  </head>
  <body>
  <header id="pg_head">
    <h1>チャットっぽい何かを作ってみた</h1>
  </header>
  <div ng-view></div>
  <fotter>

  </fotter>
  </body>
</html>

headでAngularJSのモジュールとFirebaseのモジュールを読み込んでいます。FirebaseというのはWebsocketとデータストアを提供してくれるPaasっぽいサービスです。FirebaseはAngularJSと連携するためのモジュールを提供しているのでそれを使います。

後は以下のコード。htmlのng-app属性はAngularJSのアプリであることを定義していて、自動でAngularJSの起動対象となります。divの方はAngularJSがバインドするview部分です。

<html ng-app="chatApp">
  <div ng-view></div>

次にViewを2枚作っています。この内容がURLに応じて先のng-viewにバインドされます。
まずはchatで表示する名前を入力するview。

view/regist.html

<div id="registUserDiv">
  名前を入力してボタンを押してね<br>
  <input type="text" ng-model="name" placeholder="おなまえ" value=""><br>
  <button ng-click="regist()">チャットを開始する</button><br>
</div>

ng-model="name"がnameモデル、ng-click="regist()"がregist関数とそれぞれAngularJSがMappingしています。

次にchat本体を表示するviewです。

view/chat.html

<div id="messagesDiv">
  <table id="ct_messageTable" border="0" cellspacing="0" >
    <tr ng-repeat="msg in messages" message-list>
      <td class="ct_left">
        {{msg.from}}<br>
        <span class="ct_date">({{msg.date}})</span>
      </td>
      <td class="ct_right">
        {{msg.body}}
      </td>
    </tr>
  </table>
</div>
<hr>
<span>{{inputName}}さん</span><br>
<input type="text" ng-model="msg" ng-keydown="addMessage($event)" placeholder="メッセージを入れてEnterを押してね。画像のURLも貼れるよ" style="width:800px;">

{{}}で囲まれているところはAngularJSがバインドするところです。SmartyやJadeなどのテンプレートエンジンと同じような見た目ですね。
新しく出てきたのはこの部分のng-repeat属性です。

    <tr ng-repeat="msg in messages" message-list>
      <td class="ct_left">
        {{msg.from}}<br>
        <span class="ct_date">({{msg.date}})</span>
      </td>
      <td class="ct_right">
        {{msg.body}}
      </td>
    </tr>

msg in messageという値が入っていますが、チャット情報が入っているmessage配列から1件ずつmsgに取り出して中のtdタグに表示していくというような処理をAngularJSに命令していることになります。for in ループで先頭からまわすようなイメージですね。

で、最後にAngularJSのコントローラとなるindex.jsです。ちと長いので全てのコードは解説しませんが、おおまかに言うと冒頭でURLに応じてコントローラとng-viewに表示するviewを指定して、後半で各コントローラを定義して初期処理やng-clickなどのイベント処理を実装して行くという形です。

index.js

angular

  // モジュール読み込み
  .module('chatApp', ['firebase', 'ngRoute'])

  // プロパティセット
  .value('fbURL', 'https://fiery-fire-8368.firebaseio.com/chat/')

  // factoryセット
  .factory('Schema', function($firebase, fbURL) {
    return $firebase(new Firebase(fbURL));
  })

  // Rooting
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller:'registUserCtl',
        templateUrl:'view/regist.html'
      })
      .when('/chat/:userName', {
        controller:'chatCtl',
        templateUrl:'view/chat.html'
      })
      .otherwise({
        redirectTo:'/'
      });
  })

  // ユーザーの名前を登録するコントロール
  .controller('registUserCtl', function($scope, $location) {

    // チャット開始ボタン押下
    $scope.regist = function() {
      if($scope.name){
        // チャット画面へ
        $location.path('/chat/'+$scope.name);
      }else{
        alert('未入力やぞ!');
      }
    };
  })

  // Viewバインド時のイベントに介入
  .directive("messageList",function(){
    return function(scope, element, attrs){
      var _msg = scope.msg;
      var _re = /^(http|ftp):\/\/.+.(jpg|gif|png)$/;
      var _chkBody = _msg.body.toLowerCase();
      if(_re.test(_chkBody)){
        element[0].innerHTML = '<td class="ct_left">' + _msg.from + '<br><span class="ct_date">(' + _msg.date + ')</span></td><td class="ct_right"><img src="' + _msg.body + '"></td>';
      }
    };
  })

  // チャットを行うコントロール
  .controller('chatCtl', function($scope, $location, $routeParams, Schema) {

    // ユーザー名が未入力の場合はリダイレクト
    if(!$routeParams.userName){
      $location.redirectTo('/');
    }

    // 初期化
    $scope.msg = '';

    // 渡ってきたユーザー名をバインド
    $scope.inputName = $routeParams.userName;

    // 登録済みのチャット情報をバインド
    $scope.messages = Schema;

    // メッセージ入力
    $scope.addMessage = function(e) {

      // Enter以外はリターン
      if (e.keyCode != 13) return;

      // 何かしら入力されていたら登録
      if($scope.msg !== ''){

        var _date = new Date(),
            _year = _date.getFullYear(),
            _month = _date.getMonth() + 1,
            _day = _date.getDate(),
            _hour = _date.getHours(),
            _minute = _date.getMinutes(),
            _second = _date.getSeconds();

        $scope.messages.$add({
          from: $scope.inputName,
          body: $scope.msg,
          date: _year + '-' + _month + '-' + _day + ' ' + _hour + ':' + _minute + ':' + _second
        });

        // 入力が終わったら初期化
        $scope.msg = '';
      }
    };
  });

この部分はhipchatっぽく画像のURLが投稿されたらリンクじゃなくて画像を表示したいなーというところで足してみました。チャット投稿時に投稿内容がhttpから始まって拡張子がjpg/gif/pngだったらバインドする内容を書き換えるというようなことをしています。

  .directive("messageList",function(){
    return function(scope, element, attrs){
      var _msg = scope.msg;
      var _re = /^(http|ftp):\/\/.+.(jpg|gif|png)$/;
      var _chkBody = _msg.body.toLowerCase();
      if(_re.test(_chkBody)){
        element[0].innerHTML = '<td class="ct_left">' + _msg.from + '<br><span class="ct_date">(' + _msg.date + ')</span></td><td class="ct_right"><img src="' + _msg.body + '"></td>';
      }
    };
  })

※自分の環境で試してみたい!という方はそのまま使ってもいいのですが、それだと僕のFirebaseアカウントにデータが紐づくのでFirebaseのアカウントを登録した後、以下のコードのURLを自分のもので書き換えて下さい。

  // プロパティセット
  .value('fbURL', 'https://fiery-fire-8368.firebaseio.com/chat/')

所感

Node.jsでチャットアプリを試してみた事もありますが、今回のやり方はかなりお手軽ですね。ただストレージたるFirebaseのURLがクライアント側にもろ見えなので使う際は注意して下さい。Web Socket部分はNodeとAngularJSで作ってサーバサイドでFirebaseを保存先として使うのが実用的な気がします。(できるのかな?)AngularJS自体も仕事で使ってみたいのですが、いかんせん仕事で触っているプロダクトはIE8以下もサポートしとるので動くのか。。。こちらを読んだ感じ、IE8でも動きそうですが果たしてどうなることやら。。。チャレンジしてみたくはありますが。

今回のコードはGitHubに置いてあるので自由に遊んで下さい。
https://github.com/rei-m/angular_chat

では。