Skip to content

Commit

Permalink
Add push system using Socket.IO
Browse files Browse the repository at this point in the history
  • Loading branch information
hakobera committed Dec 20, 2011
1 parent e616940 commit 43bdb3c
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 4 deletions.
161 changes: 160 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ public/stylesheets/styel.css
connecting to: rtvote-test
> db.topics.find();

#### 投票機能を実装する
### 投票機能を実装する

 これで画面はほぼ完成したので、ついに投票機能を追加します。投票機能は1票投票する機能と、集計結果を取得する機能からなります。
ですので、2つの API を作成します。まずは、1票投票する `makeVote` から考えていきます。
Expand Down Expand Up @@ -1129,3 +1129,162 @@ views/vote.ejs

こんな感じでデータが入っていればOKです。また、ここでは省略していますが、テストを書くのも忘れないでください。テストが全て通ったら、一旦 git commit しましょう。

### 投票結果をリアルタイムで表示できるようにする

 ここまでで普通の投票システムが完成しましたが、ここに「リアルタイム」な要素を加えてみましょう。
投票するとその結果をリアルタイムに更新される円グラフで表示するようにしてみます。モジュールとしては、WebSocket 相当の機能を
ほぼ全ブラウザで利用できるようできる Node.js の代表的なモジュールである [Socket.IO](http://socket.io/) を利用します。

 Socket.IO を利用できるように package.json の dependencies に socket.io を追加します。
0.8系の最新版を取得できるようにバージョン指定は `"0.8.x"` とします。

package.json

, "dependencies": {
"express": "2.5.2"
, "ejs": ">= 0.0.1"
, "mongoskin": "0.2.2"
, "socket.io": "0.8.x"
}

 依存関係を追加したら忘れずに `npm install` しておきましょう。

$ npm install -d

 次に、Socket.IO をラップしたモジュールを lib/io.js というファイル名で作成します。

lib/io.js

 まずは初期化処理と設定部分。

var sio = require('socket.io'),
util = require('util');

/**
* io module
*/
var io = null;

/**
* Listen app and create Socket.IO server.
*
* @param {Object} app Express application
*/
exports.listen = function(app) {
if (!io) {
io = sio.listen(app);

io.configure('production', function() {
io.enable('browser client minification');
io.enable('browser client etag');
io.enable('browser client gzip');
io.set('log level', 1);
// Heroku is not support WebSocket
io.set('transports', [ 'xhr-polling' ]);
});

io.configure('development', function() {
io.set('transports', ['websocket']);
});
}
};

 Socket.IO の configure メソッドを利用すると環境ごとに設定を変更できます。
今回は Heroku では WebSocket が利用できないため、Heroku 上で動かす production モードでは xhr-polling を、開発環境では WebSocket を使うようにします。

 続いて、今回は Topic ごとに通知する内容(集計結果)が異なるため、クライアントをグループ化する必要があります。
ここでは Socket.IO のネームスペースの機能を利用してこれを実装しています。指定した topicId に対応する namespace が既に存在する場合はそれを、
存在しない場合は、新規に作成して返します。

/**
* Namespaces list
*/
var namespaces = {};

/**
* Get namespace for specified topic.
* If namespace is not found, create new one and return it.
*
* @param {String} topicId Topic ID to create namespace
* @return {Object} Socket.IO server for specified namespace.
*/
exports.namespace = function(topicId) {
if (namespaces[topicId]) {
return namespaces[topicId];
} else {
var namespace =
io.of('/' + topicId)
.on('connection', function(socket) {
console.log('connected on ' + topicId);
socket.on('error', function(e) {
console.error(util.inspect(e, true));
});
});
namespaces[topicId] = namespace;
return namespace;
}
};

 これで Socket.IO 関連の実装はできたので、これを app.js, route/index.js に組み込んでいきます。

app.jp

var express = require('express'),
routes = require('./routes'),
io = require('./lib/io'); // 追加

var app = module.exports = express.createServer();
io.listen(app); // 追加

route/index.js

 投票画面を表示する時に、namespace を作成します。

exports.showTopic = function(req, res, next) {
var topicId = req.param('topicId');
io.namespace(topicId); // 追加
...
};

 そして、投票があった時に namespace に対して結果をブロードキャストします。ここではまだ集計結果を取得する処理を書いていないので、
仮データとして topicId を返しておきます。

exports.makeVote = function(req, res, next) {
var topicId = req.param('topicId'),
selection = req.param('selection');

db.makeVote(topicId, selection, function(err, result) {
if (err) {
if (err instanceof db.EntityNotFoundError) {
res.json(err.message, 404);
} else {
res.json(err, 500);
}
} else {
res.json(result);
io.namespace(topicId).emit('update', { test: topicId }); // 追加
}
});
};

それではこれに対応するクライアントコードを書いていきましょう。

views/vote.ejs

Socket.IO のクライアントは Socket.IO モジュールが `/socket.io/socket.io.js` という URL で提供してくれるので、これを利用します。
また、`io.connect()` の引数には `/ + namespace名` を指定します。今回は namespace = topicId なので、topicId を指定しています。
最後に `update` イベントでデータを受け取れるように `socket.on` メソッドでリスナを設定しています。
まだ、集計結果は出力されていないので、とりあえずコンソールに出力して通信ができていることを確認します。

<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
(省略)

var socket = io.connect('/<%= topic._id %>');
socket.on('update', function(data) {
console.log(data);
});
</script>



6 changes: 4 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
* Module dependencies.
*/

var express = require('express')
, routes = require('./routes')
var express = require('express'),
routes = require('./routes'),
io = require('./lib/io');

var app = module.exports = express.createServer();
io.listen(app);

process.on('uncaughtException', function(e) {
console.error(e.message);
Expand Down
60 changes: 60 additions & 0 deletions lib/io.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
var sio = require('socket.io'),
util = require('util');

/**
* io module
*/
var io = null;

/**
* Namespaces
*/
var namespaces = {};

/**
* Listen app and create Socket.IO server.
*
* @param {Object} app Express application
*/
exports.listen = function(app) {
if (!io) {
io = sio.listen(app);

io.configure('production', function() {
io.enable('browser client minification');
io.enable('browser client etag');
io.enable('browser client gzip');
io.set('log level', 1);
// Heroku is not support WebSocket
io.set('transports', [ 'xhr-polling' ]);
});

io.configure('development', function() {
io.set('transports', ['websocket']);
});
}
};

/**
* Get namespace for specified topic.
* If namespace is not found, create new one and return it.
*
* @param {String} topicId Topic ID to create namespace
* @return {Object} Socket.IO server for specified namespace.
*/
exports.namespace = function(topicId) {
if (namespaces[topicId]) {
return namespaces[topicId];
} else {
var namespace =
io.of('/' + topicId)
.on('connection', function(socket) {
console.log('connected on ' + topicId);
socket.on('error', function(e) {
console.error(util.inspect(e, true));
});
});
namespaces[topicId] = namespace;
return namespace;
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"express": "2.5.2"
, "ejs": ">= 0.0.1"
, "mongoskin": "0.2.2"
, "socket.io": "0.8.x"
}
, "devDependencies": {
"mocha": "0.5.0"
Expand Down
6 changes: 5 additions & 1 deletion routes/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var db = require('../lib/db');
var db = require('../lib/db'),
io = require('../lib/io');

var MONGO_URL = process.env.MONGOHQ_URL || 'localhost/rtvote';
if (process.env.NODE_ENV === 'test') {
Expand Down Expand Up @@ -63,6 +64,8 @@ exports.findTopic = function(req, res) {
*/
exports.showTopic = function(req, res, next) {
var topicId = req.param('topicId');
io.namespace(topicId);

db.findTopic(topicId, function(err, result) {
if (err) {
if (err instanceof db.EntityNotFoundError) {
Expand Down Expand Up @@ -92,6 +95,7 @@ exports.makeVote = function(req, res, next) {
}
} else {
res.json(result);
io.namespace(topicId).emit('update', { test: topicId });
}
});
};
6 changes: 6 additions & 0 deletions views/vote.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<% } %>
</div>
</form>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
$(function() {
var selections = $('.selection');
Expand All @@ -30,5 +31,10 @@ $(function() {
alert('Error!');
});
});
var socket = io.connect('/<%= topic._id %>');
socket.on('update', function(data) {
console.log(data);
});
});
</script>

0 comments on commit 43bdb3c

Please sign in to comment.