Node.js + Socket.IOでチャットシステム (第4回)

こんにちはtatsyです。

前回(第3回の記事)の内容はほぼ、僕の趣味でファイル構成を整えただけでしたが、今回は本格的にチャットの機能を拡充していきます。


ログイン情報のemit

まず、基本的なところからでユーザがチャットにログインしたり、ログアウトしたりしたときに、その情報をemitして全クライアントに通知する処理を付け加えます。

やることは簡単でsocket.ioのconnectionイベントが発生したら、リモートからクライアントに向かって、表示したいメッセージをchat messageイベントとして投げるだけです。

ログアウトの場合も同じでdisconnectイベントが発生したらchat messageイベントをクライアントに投げます。

index.jsは次のように書き換わります(該当部分のみ抜粋)。

index.js

io.on('connection', function(socket) {
    io.emit('chat message', 'a user entered');
    socket.on('disconnect', function() {
        io.emit('chat message', 'a user exited');
    });

    socket.on('chat message', function(msg) {
        io.emit('chat message', msg);
    });
});

おそらく、ここまでの仕組みを理解していれば、これはそれほど難しくないのではないかと思います。


ログイン画面の表示

さて、ここから少し処理が複雑化します。

ログイン画面を表示したいのですが、現在は1枚分しかページがないので、ログイン画面用のJadeファイルを次のようにしましょう。

views/login.jade

extends layout

block content
    form(action="room", method="post")
        input(name="user", autocomplete="off")
        button Login

index.jadeと同様にlayout.jadeをextendsしてログインフォームの部分だけを書き換えます。

ちょっとかっこ悪いですが、チャットのメッセージ投稿画面をそのまま利用します。

そうしたら、最初につないだときに表示される画面がlogin.jadeになるようにindex.jsを編集します。

index.js

app.get('/', function(req, res) {
    res.render('login');
});

こうすると、以下のようにボタンがLoginになったものが最初に表示されるようになります。

chat_login

login.jadeではLoginボタンが押されたらhttp://localhost:3000/roomに対してPOST通信を行います。

現在はindex.jsにその処理を書いていないので、ボタンを押すと「Cannot POST /room」と表示されてしまうはずです。


ログイン処理とログイン名の表示

まず、”room/”へのPOST通信を処理するためにindex.jsにapp.post(url, callback)関数を追加します。

// roomへのPOST通信
app.post('/room', function(req, res) {
    res.render('index');
});

これで一応エラーは出なくなり、今まで通りのチャット画面が表示されますが、誰がログインしたのかという情報を取得できていません。

これを実現するためにExpress周りのミドルウェアとしてbody-parserを使います。いつも通りnpmでインストールです。

$ npm install --save body-parser

1点注意としてExpress3系ではbody-parser等のミドルウェアがExpressオブジェクトのメンバとして定義されていましたが、Express4系ではこれらのミドルウェアを個別にインストールしなくてはなりません。

index.jsでもbody-parserを使用するようにコードを書き換えます。

index.js

var express = require('express');
var bodyParser = require('body-parser');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.use(express.static(__dirname + '/public'));
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(bodyParser.urlencoded({extended:false}));
app.use(bodyParser.json());

ここまで書くと、formから送られてきたデータがreq.body.(フィールド名)で使えるようになります。

今回の例ではinputのnameがuserなので、req.body.userがフォームに入力されたユーザ名です。

次にこの取得されたユーザ名をクライアント側に渡す必要がありますが、これにはres.render関数の第2引数を使います。

前回まではres.render(filename)という呼び出し方をしてましたが、第2引数にはデータをしてすることができて、

app.post('/room', function(req, res) {
    res.render('index', {user: req.body.user});
});

のように書くことでデータをクライアント側に渡されます。

もちろん、渡している構造体にメンバーを複数持たせることもできます。

これで、クライアントがhttp://localhost:3000/roomにPOST通信で接続したときに、自分のログインユーザ名を取得することができます。

ただ、今回は現在繋いでいるすべてのsocketに対して、ユーザのログインを知らせたいので、これを再度サーバーに戻してemitする必要があります。

なのでindex.js側のイベントsocket.on関数でこれらの情報が使えるように改良します。

具体的な手順は

リモート: io.on(‘connection’) → socket.emit(‘connected’)

クライアント: socket.on(‘connected’) → socket.emit(‘user login’) (POST情報を引数に取る)

リモート: socket.on(‘user login’) (POST情報が引数で渡される)

という流れの処理を実行します。

繰り返しになりますが、socket.emit(event)に指定しているイベント名は任意のもので必ずしもconnectedやuser loginである必要はありません。

具体的なコードは以下のようになります。

index.js

io.on('connection', function(socket) {
    socket.emit('connected', {});

    socket.on('user login', function(user) {
        io.emit('chat message', user.name + ' entered');
        socket.on('disconnect', function() {
            io.emit('chat message', user.name + ' exited');
        });

        socket.on('chat message', function(msg) {
            io.emit('chat message', user.name + ': ' + msg);
        });
    });
});

public/js/chat.js

socket.on('connected', function(data) {
    socket.emit('user login', user);
});

ここでのポイントはsocket.onでイベントを受け取る際のcallback関数に渡すデータをsocket.emit関数の第2引数で指定していることです。

これでめでたく、誰がログインやログアウト、発言を行っているかが画面に表示されるようになります。

chat_with_username


さて、今回はチャットにログイン名などが表示されるようになりました。

次回は最終回で各ユーザがタイピング中かどうかを通知する機能を実装したいと思います。

最後までお読みいただき、ありがとうございました。