Node.js、Express、AngularJS、および MongoDB を使用してリアルタイム・アンケート・アプリケーションを作成する

2014年 7月 24日
PDF (450 KB)
 

Build a real-time polls application with Node.js, AngularJS, and MongoDB

04:53  |  Transcript
Author photo - Joe Lennon

Joe Lennon

CTO at ePubDirect

@joelennon

IBM Bluemix™ にサインアップ
無償のサービス、ランタイム、インフラを含むクラウド・プラットフォームが、新たなモバイルやウェブ・アプリのクイックな構築とデプロイを実現します。

私は最近、大人数の学生を相手に HTML5 についての講義をするなかで、その場で学生にアンケートをとって、その結果をリアルタイムで更新される形で表示したいと考え、そのためのアンケート・アプリを短時間で作成することにしました。このアプリは、アーキテクチャーを単純なものにして、使用するプログラミング言語やフレームワークも多くならないようにしたかったので、すべてを JavaScript で作成することに決めました。つまり、サーバー・サイドには Node.js と Express を、データベースには MongoDB を、そしてフロントエンドのユーザー・インターフェースには AngularJS を使用することにしました。

この MEAN スタック (Mongo、Express、Angular、Node) は、Web アプリケーションの開発およびデプロイメントを行うためのスタックとして、LAMP スタック (Linux、Apache、MySQL、PHP) のシンプルさをそのうち凌駕するかもしれません

プロジェクトのソース・コードの管理には DevOps Services (旧 JazzHub) を使用することにしました。それは、JazzHub には作成したコードを管理するための完全なバージョン管理システムが用意されていることに加え、クラウドでコードを編集するためのオンライン IDE や、プロジェクトを管理するためのアジャイルな機能も豊富に用意されているからです。また、DevOps Services は容易に Eclipse と統合できることから、BluemixCloud Foundry などのプラットフォームにワン・クリックでデプロイできるようにするための Eclipse プラグインを使用できることも理由の 1 つです。

アプリの作成に必要なもの

 

読む:Node.js: 基本を越えて

ステップ 1. 基本的な Express バックエンドを作成する

 

Eclipse で「Node (ノード)」パースペクティブに切り替え、新規 Node.js Express プロジェクトを作成します。私と同じく DevOps Services プロジェクトを作成する場合は、Node.js Express プロジェクトにも DevOps Services プロジェクトと同じ名前を付けます。テンプレート・エンジンとしては、Jade が選択されたままにします。Eclipse によって、単純な Express アプリを作成するために必要な npm モジュールが自動的にダウンロードされます。

Express アプリを実行する

 

プロジェクト・エクスプローラーで、作成したプロジェクトのルートに「app.js」があるのを見つけて右クリックし、「Run As (実行)」 > 「Node Application (Node アプリケーション)」の順に選択します。これで Web サーバーが起動され、アプリが Web サーバーにデプロイされます。次に、ブラウザーを開いて http://localhost:3000 にアクセスします。

図 1. Express アプリ・スタート画面
Express アプリ・スタート画面

基本的なフロントエンドを構成する

 

アンケート・アプリでは、一般的なユーザー・インターフェースとレイアウト用に Bootstrap フレームワークを使用します。Express アプリにいくらか変更を加え、これを反映してみましょう。まず、routes/index.js を開いて、次のように title プロパティーを Polls に変更します。

リスト 1. routes/index.js
        exports.index = function(req, res){
             res.render('index', { title: 'Polls' });
         };

次に、views/index.jade テンプレートに変更を加え、Bootstrap を読み込むようにします。Jade は簡便なテンプレート言語で、HTML にコンパイルされます。Jade ではインデントを使用するため、閉じタグが必要なくなることから、テンプレートのサイズが大幅に小さくなります。ここでは、メイン・ページのレイアウト用にのみ Jade を使用します。次のステップでは、Angular の部分テンプレートを使用して、このメイン・ページに機能を追加していきます。

リスト 2. views/index.jade
        doctype 5
        html(lang='en')
          head
            meta(charset='utf-8')
            meta(name='viewport', content='width=device-width, 
	initial-scale=1, user-scalable=no')
            title= title
            link(rel='stylesheet', href='//netdna.bootstrapcdn.com/bootstrap/3.0.1/
	css/bootstrap.min.css')
            link(rel='stylesheet', href='/stylesheets/style.css')                
          body
            nav.navbar.navbar-inverse.navbar-fixed-top(role='navigation')
              div.navbar-header
                a.navbar-brand(href='#/polls')= title
            div.container
              div

アプリに加えた変更を確認するには、Eclipse 内で Web サーバー・プロセスを kill し、app.js ファイルを実行します。

図 2. Polls アプリのボイラープレート

注: Jade テンプレートを使用する際には、コードを適切にインデントするように注意しないと、面倒なことになります。また、複数のインデント・スタイルが混在すると、Jade でエラーが発生します。

ステップ 2. AngularJS を使用してフロントエンドのユーザー・エクスペリエンスを作成する

 

Angular の使用を開始するには、まず HTML ページに Angular を読み込んで、いくつかのディレクティブを追加する必要があります。views/index.jade テンプレート内で、html 要素を次のように変更します。
html(lang='en', ng-app='polls')

このファイルの head ブロック内に次のスクリプト要素を追加します。

リスト 3. Angular と Angular リソース・モジュールを読み込むスクリプト要素
        script(src='//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js')
        script(src='//ajax.googleapis.com/ajax/libs/angularjs/1.0.8
/angular-resource.min.js')

次に、テンプレートの body 要素に ng-controller 属性を追加する変更を行います (後ほどこの属性を使用して、ユーザー・インターフェースをコントローラーの論理コードにバインドします)。
body(ng-controller='PollListCtrl')

最後に、次のように ng-view 属性が含まれるようにテンプレートの最後の div 要素に変更を加えます。
div(ng-view)

Angular モジュールを作成する

 


Angular の優れた機能の 1 つが、バックエンド・モデルが変更されたときにビューが自動的に更新されるデータ・バインディング機能です。この機能のおかげで、DOM を操作するという厄介な作業が取り除かれるため、作成しなければならない JavaScript コードの量が劇的に減ります。

Express では、デフォルトでプロジェクトの public ディレクトリーから JavaScript ソース・ファイル、CSS スタイルシート、画像などの静的リソースが公開されます。public ディレクトリーに、javascripts という名前の新しいサブディレクトリーを作成し、このサブディレクトリーに、app.js という名前のファイルを作成します。このファイルには、アプリの Angular モジュールが格納されることになり、ユーザー・インターフェースで使用される routes とテンプレートが定義されます。

リスト 4. public/javascripts/app.js
        angular.module('polls', [])
          .config(['$routeProvider', function($routeProvider) {
            $routeProvider.
              when('/polls', { templateUrl: 'partials/list.html', controller: 
PollListCtrl }).
              when('/poll/:pollId', { templateUrl: 'partials/item.html', controller: 
PollItemCtrl }).
              when('/new', { templateUrl: 'partials/new.html', controller: 
PollNewCtrl }).
              otherwise({ redirectTo: '/polls' });
          }]);

Angular のコントローラーではアプリのスコープを定義するとともに、ビューとバインドさせるデータおよびメソッドも定義します。

リスト 5. public/javascript/controllers.js
        // Managing the poll list
        function PollListCtrl($scope) {
          $scope.polls = [];
        }
        // Voting / viewing poll results
        function PollItemCtrl($scope, $routeParams) {
          $scope.poll = {};
          $scope.vote = function() {};
        }
        // Creating a new poll
        function PollNewCtrl($scope) {
          $scope.poll = {
            question: '',
            choices: [{ text: '' }, { text: '' }, { text: '' }]
          };
          $scope.addChoice = function() {
            $scope.poll.choices.push({ text: '' });
          };
          $scope.createPoll = function() {};
        }

HTML 部分テンプレートを作成する

 

コントローラーからデータをレンダリングするために、Angular では HTML 部分テンプレートを使用します。HTML 部分テンプレートを使用すると、データを格納したり条件処理や反復処理などを実行したりする際に、プレースホルダーと式を使用できるようになります。public ディレクトリーに、partials という名前の新しいサブディレクトリーを作成します。このアプリでは 3 つの部分テンプレートを作成します。1 つ目の部分テンプレートは、選択できるアンケートのリストを表示するためのものであり、ここでは検索フィールドを使用してこのリストを簡単にフィルタリングするために、Angular を使用します。

リスト 6. public/partials/list.html
        <div class="page-header">
          <h1>Poll List</h1>
        </div>
        <div class="row">
          <div class="col-xs-5">
            <a href="#/new" class="btn btn-default"><span class="glyphicon 
glyphicon-plus"></span> New Poll</a>
          </div>
          <div class="col-xs-7">
            <input type="text" class="form-control" ng-model="query" 
placeholder="Search for a poll">
          </div>
        </div>
        <div class="row"><div class="col-xs-12">
<hr></div></div>
        <div class="row" ng-switch on="polls.length">
          <ul ng-switch-when="0">
            <li><em>No polls in database. Would you like to
 <a href="#/new">create one</a>?</li>
          </ul>
          <ul ng-switch-default>
            <li ng-repeat="poll in polls | filter:query">
              <a href="#/poll/{{poll._id}}">{{poll.question}}</a>
            </li>
          </ul>
        </div>
        <p>&nbsp;</p>

2 つ目の部分テンプレートでは、ユーザーがアンケートを表示できるようにするために、Angular の switch ディレクティブを使用して、ユーザーがアンケートに回答したかどうかを判定します。この判定に基づいて、そのアンケートに回答するためのフォームか、アンケートの回答結果を示すためのグラフが表示されます。

リスト 7. public/partials/item.html
        <div class="page-header">
          <h1>View Poll</h1>
        </div>
        <div class="well well-lg">
          <strong>Question</strong><br>{{poll.question}}
        </div>
        <div ng-hide="poll.userVoted">
          <p class="lead">Please select one of the following options.</p>
          <form role="form" ng-submit="vote()">
            <div ng-repeat="choice in poll.choices" class="radio">
              <label>
                <input type="radio" name="choice" ng-model="poll.userVote" 
	value="{{choice._id}}">
                {{choice.text}}
              </label>
            </div>
            <p><hr></p>
            <div class="row">
              <div class="col-xs-6">
                <a href="#/polls" class="btn btn-default" role="button"><span
class="glyphicon glyphicon-arrow-left"></span> Back to Poll
              </div>
              <div class="col-xs-6">
                <button class="btn btn-primary pull-right" type="submit">
	Vote &raquo;</button>
              </div>
            </div>
          </form>
        </div>
        <div ng-show="poll.userVoted">
          <table class="result-table">
            <tbody>
              <tr ng-repeat="choice in poll.choices">
                <td>{{choice.text}}</td>
                <td>
                  <table style="width: {{choice.votes.length
	/poll.totalVotes*100}}%;">
                    <tr><td>{{choice.votes.length}}</td></tr>
                  </table>
                </td>
              </tr>
            </tbody>
          </table>  
          <p><em>{{poll.totalVotes}} votes counted so far. <span 
ng-show="poll.userChoice">You voted for <strong>{{poll.userChoice.text}}
</strong>.</span></em></p>
          <p><hr></p>
          <p><a href="#/polls" class="btn btn-default" role="button">
<span class="glyphicon glyphicon-arrow-left"></span> Back to 
Poll List</a></p>
        </div>
        <p>&nbsp;</p>

最後となる 3 つ目の部分テンプレートでは、ユーザーが新しいアンケートを作成できるようにするフォームを定義します。このフォームで、ユーザーは 1 つの質問と 3 つの選択肢を入力するように求められます。他にも選択肢を追加できるようにするボタンが用意されています。複数の選択肢がないとアンケートにならないので、ユーザーが少なくとも 2 つの選択肢を入力していることを後で確認します。

リスト 8. public/partials/new.html
        <div class="page-header">
          <h1>Create New Poll</h1>
        </div>
        <form role="form" ng-submit="createPoll()">
          <div class="form-group">
            <label for="pollQuestion">Question</label>
            <input type="text" ng-model="poll.question" class="form-control" 
id="pollQuestion" placeholder="Enter poll question">
          </div>
          <div class="form-group">
            <label>Choices</label>
            <div ng-repeat="choice in poll.choices">
              <input type="text" ng-model="choice.text" class="form-control" 
placeholder="Enter choice {{$index+1}} text"><br>
            </div>
          </div>    
          <div class="row">
            <div class="col-xs-12">
              <button type="button" class="btn btn-default" ng-click=
"addChoice()"><span class="glyphicon glyphicon-plus">
</span> Add another</button>
            </div>
          </div>
          <p><hr></p>
          <div class="row">
            <div class="col-xs-6">
              <a href="#/polls" class="btn btn-default" role="button">
<span class="glyphicon glyphicon-arrow-left"></span> 
Back to Poll List</a>
            </div>
            <div class="col-xs-6">
              <button class="btn btn-primary pull-right" type="submit">
	Create Poll &raquo;</button>
            </div>
          </div>
          <p>&nbsp;</p>
        </form>

最後に、結果を表示するために style.css ファイルにいくつかの CSS 宣言を追加する必要があります。このファイルの中身を次のコードで置き換えます。

リスト 9. public/stylesheets/style.css
        body { padding-top: 50px; }
        .result-table {
          margin: 20px 0;
          width: 100%;
          border-collapse: collapse;
        }
        .result-table td { padding: 8px; }
        .result-table > tbody > tr > td:first-child {
          width: 25%;
          max-width: 300px;
          text-align: right;
        }
        .result-table td table {
          background-color: lightblue;
          text-align: right;
        }

この時点でアプリを実行すると、空のアンケート・リストが表示されます。新しいアンケートの作成を試みた場合、フォームが表示されて選択肢を追加することはできますが、アンケートを保存することはできません。次のステップではこれらをすべて連携させます。

ステップ 3. Mongoose を使用して MongoDB にデータを保存する

 

見る:An introduction to MongoDB (MongoDB の紹介)

データを保存するために、アプリでは、MongoDB ドライバーと Mongoose npm モジュールを使用します。これによってアプリが MongoDB データベースと通信できるようになります。これらのモジュールを取得するには、アプリのルート・ディレクトリーにある package.json ファイルを開き、dependencies セクションに次の行を追加します。

リスト 10. dependencies セクションに追加する行
        "mongodb": ">= 1.3.19",
        "mongoose": ">= 3.8.0",

ファイルを保存し、プロジェクト・エクスプローラーでこれを右クリックして、「Run As (実行)」 > 「npm install (npm のインストール)」の順に選択します。これで npm モジュールと追加の依存関係がインストールされます。

Mongoose モデルを作成する

 

アプリのルートに models という名前の新しいサブディレクトリーを作成し、そのサブディレクトリーの中で Poll.js という名前の新しいファイルを作成します。このファイルで Mongoose モデルを定義し、このモデルを使用して構造化された方法で MongoDB に対して照会を行ったりデータを保存したりします。

リスト 11. models/Poll.js
        var mongoose = require('mongoose');
        var voteSchema = new mongoose.Schema({ ip: 'String' });
        var choiceSchema = new mongoose.Schema({ 
          text: String,
          votes: [voteSchema]
        });
        exports.PollSchema = new mongoose.Schema({
          question: { type: String, required: true },
          choices: [choiceSchema]
        });

データ保存用 API routes を定義する

 

次に、アプリのルート・ディレクトリーにある app.js ファイルの中でいくつかの routes をセットアップし、Angular クライアント・サイド・コードから MongoDB に対して照会や更新を行ったりするために使用できる JSON エンドポイントを作成します。app.get('/', routes.index) という行を見つけ、その後に次のコードを追加します。

リスト 12. JSON エンドポイントを作成する
        app.get('/polls/polls', routes.list);
        app.get('/polls/:id', routes.poll);
        app.post('/polls', routes.create);

ここで、これらの関数を実装する必要があります。routes/index.js ファイルの中身を次のコードで置き換えます。

リスト 13. routes/index.js
        var mongoose = require('mongoose');
        var db = mongoose.createConnection('localhost', 'pollsapp');
        var PollSchema = require('../models/Poll.js').PollSchema;
        var Poll = db.model('polls', PollSchema);
        exports.index = function(req, res) {
          res.render('index', {title: 'Polls'});
        };
        // JSON API for list of polls
        exports.list = function(req, res) { 
          Poll.find({}, 'question', function(error, polls) {
            res.json(polls);
          });
        };
        // JSON API for getting a single poll
        exports.poll = function(req, res) {  
          var pollId = req.params.id;
          Poll.findById(pollId, '', { lean: true }, function(err, poll) {
            if(poll) {
              var userVoted = false,
                  userChoice,
                  totalVotes = 0;
              for(c in poll.choices) {
                var choice = poll.choices[c]; 
                for(v in choice.votes) {
                  var vote = choice.votes[v];
                  totalVotes++;
                  if(vote.ip === (req.header('x-forwarded-for') || req.ip)) {
                    userVoted = true;
                    userChoice = { _id: choice._id, text: choice.text };
                  }
                }
              }
              poll.userVoted = userVoted;
              poll.userChoice = userChoice;
              poll.totalVotes = totalVotes;
              res.json(poll);
            } else {
              res.json({error:true});
            }
          });
        };
        // JSON API for creating a new poll
        exports.create = function(req, res) {
          var reqBody = req.body,
              choices = reqBody.choices.filter(function(v) { return v.text != ''; }),
              pollObj = {question: reqBody.question, choices: choices};
          var poll = new Poll(pollObj);
          poll.save(function(err, doc) {
            if(err || !doc) {
              throw 'Error';
            } else {
              res.json(doc);
            }   
          });
        };

Angular サービスを使用してデータをフロントエンドにバインドする

 

この時点でバックエンドは、データベースに対して照会を行ったりアンケートを保存したりできるようにセットアップされていますが、通信方法を認識できるように Angular にいくつか変更を加える必要があります。これは Angular サービスを使用すると簡単に実現することができます。このサービスは、サーバー・サイドとの通信プロセスを単純な関数呼び出しにラップします。

リスト 14. public/javascripts/services.js
        angular.module('pollServices', ['ngResource']).
          factory('Poll', function($resource) {
            return $resource('polls/:pollId', {}, {
              query: { method: 'GET', params: { pollId: 'polls' }, isArray: true }
            })
          });

このファイルを作成したら、index.jade テンプレートに含める必要があります。次の行を head セクションの最後の script 要素の下に追加します。
script(src='/javascripts/services.js')

また、このサービス・モジュールを使用するように、Angular アプリに対して指示する必要もあります。これを行うには、public/javascripts/app.js を開き、最初の行を次のように変更します。
angular.module('polls', ['pollServices'])

最後に、データベースに対して照会を行ったりアンケートを保存したりするためのサービスを使用するように、Angular のコントローラーを変更します。public/javascripts/controllers.js ファイルの PollListCtrl を次のように変更します。

リスト 15. public/javascripts/controller.js
        function PollListCtrl($scope, Poll) {
          $scope.polls = Poll.query();
        }
   ...

ID によってアンケート用のクエリーを実行するように PollItemCtrl 関数を更新します。

リスト 16. public/javascripts/controller.js (続き)
...
        function PollItemCtrl($scope, $routeParams, Poll) {
          $scope.poll = Poll.get({pollId: $routeParams.pollId});
          $scope.vote = function() {};
        }
 ...

同様に、フォームが送信されるときに新しい回答データがサーバーに送信されるように、PollNewCtrl 関数に変更を加えます。

リスト 17. public/javascripts/controller.js (続き)
...
        function PollNewCtrl($scope, $location, Poll) {
          $scope.poll = {
            question: '',
            choices: [ { text: '' }, { text: '' }, { text: '' }]
          };  
          $scope.addChoice = function() {
            $scope.poll.choices.push({ text: '' });
          };
          $scope.createPoll = function() {
            var poll = $scope.poll;
            if(poll.question.length > 0) {
              var choiceCount = 0;
              for(var i = 0, ln = poll.choices.length; i < ln; i++) {
                var choice = poll.choices[i];        
                if(choice.text.length > 0) {
                  choiceCount++
                }
              }    
              if(choiceCount > 1) {
                var newPoll = new Poll(poll);       
                newPoll.$save(function(p, resp) {
                  if(!p.error) { 
                    $location.path('polls');
                  } else {
                    alert('Could not create poll');
                  }
                });
              } else {
                alert('You must enter at least two choices');
              }
            } else {
              alert('You must enter a question');
            }
          };
        }

アプリを実行する

 

もうすぐ完成です!この時点で、ユーザーはアプリを使用してアンケートの表示と検索、新規アンケートの作成、個々のアンケートに対する回答選択肢の表示を実行できるようになっているはずです。アプリを実行する前に、ローカルで MongoDB が実行されていることを確認してください。この確認は通常、ターミナル・ソフトまたはコマンド・プロンプトを開いて mongod コマンドを実行するだけの簡単なものです。アプリを実行するときは、ターミナル・ウィンドウを開いたままにしておきます。

図 3. アンケートの選択肢を表示する
アンケートの選択肢を表示する

アプリを実行したら、ブラウザーで http://localhost:3000 にアクセスし、アンケートをいくつか作成します。アンケートをクリックすると選択肢を表示することができますが、実際にはアンケートに回答したり結果を表示したりすることはまだできません。最後となる次のステップで、これをできるようにします。

ステップ 4. Socket.io を使用したリアルタイムでの回答

 


WebScoket を使用すると、サーバー・サイドは直接クライアント・サイドと通信を行ってメッセージを送信できるようになります。

作成しなければならない残りの機能は、アンケートに回答する機能のみです。アプリでユーザーがアンケートに回答できるようになると、ユーザーが回答した結果は、接続されているすべてのクライアント上で、リアルタイムで更新されるようになります。アンケートに回答する機能は、socket.io npm モジュールを使用すると簡単に実装することができるので、次はこの作業を行いましょう。

アプリのルート・ディレクトリーにある package.json ファイルを開いて、dependencies セクションに次の行を追加します。
"socket.io": "~0.9.16"

ファイルを保存して、パッケージ・エクスプローラーで右クリックし、「Run As (実行)」 > 「npm install (npm のインストール)」の順に選択して npm モジュールをインストールします。

次に、アプリのルートにある app.js ファイルを開き、ファイルの末尾にある server.listen... ブロックを削除して次のコードで置き換えます。

リスト 18. app.js
...
        var server = http.createServer(app);
        var io = require('socket.io').listen(server);

        io.sockets.on('connection', routes.vote);

        server.listen(app.get('port'), function(){
          console.log('Express server listening on port ' + app.get('port'));
        });

次に、index.jade テンプレートに変更を加えて socket.io クライアント・ライブラリーを読み込むようにします。アプリを実行すると、指定した場所で自動的にこのライブラリーを使用できるようになるので、このファイルは自分で見つける必要はありません。必ず、テンプレート内の、angular-resource ライブラリーを読み込んだ行の直後に次のコードを追加してください。
script(src='/socket.io/socket.io.js')

最後に、ユーザーが socket.io にメッセージを送信すると新しい回答が保存されるように、また更新された結果と共にメッセージがすべてのクライアントに送信されるように、アンケートへの回答機能を作成する必要があります。routes ディレクトリーにある index.js ファイルの末尾に次のコードを追加します。

リスト 19. routes/index.js
        // Socket API for saving a vote
        exports.vote = function(socket) {
          socket.on('send:vote', function(data) {
            var ip = socket.handshake.headers['x-forwarded-for'] || 
socket.handshake.address.address;    
            Poll.findById(data.poll_id, function(err, poll) {
              var choice = poll.choices.id(data.choice);
              choice.votes.push({ ip: ip });      
              poll.save(function(err, doc) {
                var theDoc = { 
                  question: doc.question, _id: doc._id, choices: doc.choices, 
                  userVoted: false, totalVotes: 0 
                };
                for(var i = 0, ln = doc.choices.length; i < ln; i++) {
                  var choice = doc.choices[i]; 
                  for(var j = 0, jLn = choice.votes.length; j < jLn; j++) {
                    var vote = choice.votes[j];
                    theDoc.totalVotes++;
                    theDoc.ip = ip;
                    if(vote.ip === ip) {
                      theDoc.userVoted = true;
                      theDoc.userChoice = { _id: choice._id, text: choice.text };
                    }
                  }
                }       
                socket.emit('myvote', theDoc);
                socket.broadcast.emit('vote', theDoc);
              });     
            });
          });
        };

注: なぜ通常の IP アドレスのプロパティーの前で 'x-forwarded-for' ヘッダーを探しているのかが疑問に思われる方のために説明しておくと、このようにしているのは、このアプリを負荷分散環境にデプロイした場合に必ず正しいクライアント IP が使用されるようにするためです。例えば、アプリを Bluemix や Cloud Foundry にデプロイした場合には、アプリが適切に機能するにはこのようにしておくことが不可欠です。

WebSocket にデータを送信する Angular サービスを追加する

 

ここまでで、WebSocket のバックエンド機能は完成しています。あとは、フロントエンドを連携させて、ソケット・イベントを送信したりリッスンしたりできるようにするだけです。これを行う最善の方法は、新しい Angular サービスを追加することです。public/javascripts フォルダーにある services.js ファイルの中身を次のコードで置き換えます。

リスト 20. public/javascripts/services.js
        angular.module('pollServices', ['ngResource']).
          factory('Poll', function($resource) {
            return $resource('polls/:pollId', {}, {
              query: { method: 'GET', params: { pollId: 'polls' }, isArray: true }
            })
          }).
          factory('socket', function($rootScope) {
            var socket = io.connect();
            return {
              on: function (eventName, callback) {
                socket.on(eventName, function () {  
                  var args = arguments;
                  $rootScope.$apply(function () {
                    callback.apply(socket, args);
                  });
                });
              },
              emit: function (eventName, data, callback) {
                socket.emit(eventName, data, function () {
                  var args = arguments;
                  $rootScope.$apply(function () {
                    if (callback) {
                      callback.apply(socket, args);
                    }
                  });
                })
              }
            };
          });

最後に、アンケートに回答する WebSocket メッセージを PollItemCtrl コントローラーがリッスンしたり送信したりするように、このコントローラーを編集する必要があります。元のコントローラーのコードを次のコードで置き換えます。

リスト 21. public/javascripts/controllers.js
...
        function PollItemCtrl($scope, $routeParams, socket, Poll) { 
          $scope.poll = Poll.get({pollId: $routeParams.pollId});
          socket.on('myvote', function(data) {
            console.dir(data);
            if(data._id === $routeParams.pollId) {
              $scope.poll = data;
            }
          });
          socket.on('vote', function(data) {
            console.dir(data);
            if(data._id === $routeParams.pollId) {
              $scope.poll.choices = data.choices;
              $scope.poll.totalVotes = data.totalVotes;
            }   
          });
          $scope.vote = function() {
            var pollId = $scope.poll._id,
                choiceId = $scope.poll.userVote;
            if(choiceId) {
              var voteObj = { poll_id: pollId, choice: choiceId };
              socket.emit('send:vote', voteObj);
            } else {
              alert('You must select an option to vote for');
            }
          };
        }
   ...

最終的な成果物の動作を表示する

 

ここまでの作業で、アンケート・アプリは完成しました。mongod が実行中であることを確認してから、Eclipse で Node アプリケーションを再度実行します。ブラウザーで http://localhost:3000 にアクセスし、いずれかのアンケートまでナビゲートして、そのアンケートに回答すると、結果が表示されるはずです。リアルタイムでの更新を確認するには、自分のローカル IP アドレスを調べ、localhost をそのアドレスに置き換えます。次に、ローカル・ネットワーク上にある別のマシン (スマートフォンやタブレット端末でも構いません) を使用してそのアドレスにアクセスします。別の端末でアンケートに回答すると、結果がその端末に表示されるとともに、メインのコンピューターのブラウザーにもその結果が自動的に表示されます。

図 4. アンケート結果の表示
アンケート結果の表示

次のステップ: さらなる開発とデプロイメント

 

ここで作成したアンケート・アプリは、出発点としてはなかなかのものですが、改善の余地は大いにあります。私がこのようなアプリを計画するときには、アジャイル手法に従って、ユーザー・ストーリーやエピックを定義し、プロジェクトを複数のスプリントに分けるようにしています。このプロジェクトでは DevOps Services を利用して、プロジェクトに付随するものやソース・コードをすべてひとまとめにして、クラウドでホストされるリポジトリーに保持することで、開発の作業を実にシンプルにしています。

読む:Project kickoff on DevOps Services (DevOps Services で始めるプロジェクト)

自分のアプリが気に入ったら、次のステップはそれを世界と共有することです。昔は単純なアプリでさえもデプロイするのは悪夢のような作業になる可能性がありましたが、ありがたいことにそんな時代は終わりました。IBM の Cloud Foundry 互換の新しい Bluemix プラットフォームを使用すると、数分もあれば最小限の構成で簡単にアプリケーションをクラウドにデプロイすることができます。

まとめ

 

今の時代は、開発者にとっては素晴らしい時代です。優れたアプリを開発するために自由に使えるフレームワークやツールが豊富にあるため、アプリの開発が容易かつ迅速に行えるだけでなく、より楽しいものになります。この記事では、MEAN スタック (Mongo、Express、Angular、Node) と呼ばれているテクノロジー・スタックを使用してアプリを作成する方法を説明しました。このスタックは、Web アプリケーションの開発およびデプロイメントにおいてシンプルさの代名詞ともなっている LAMP スタック (Linux、Apache、MySQL、PHP) をそのうち凌駕するかもしれません。私は、その日が待ち遠しいです。


関連トピック:Node.jsMongoDBJavaScript

コメントの追加

注意: HTML コードは、コメント内ではサポートされません。


残り 1000 文字

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Open source, Cloud computing
ArticleID=964349
ArticleTitle=Node.js、Express、AngularJS、および MongoDB を使用してリアルタイム・アンケート・アプリケーションを作成する
publish-date=07242014