🧊

NodeとSocket.IOで作るルーム機能つきチャットのサンプル

というわけで、最近勉強してたSocket.IOでサンプルを作りました。
このソースをまるごと持って行ってIPなりポートなり調整すれば動くはずです。

おかげでロジック自体はシンプルになったけど、使い勝手云々いじると必然的にクライアント側のコードが多く・・。
今回は久しぶりにjQueryも使ってます。

仕様

  • 部屋とニックネームを決めて入室し、その部屋でリアルタイムチャット
  • 違う部屋の内容は見れない
  • エンターキーでPOSTできる
  • 一回入った部屋を出て、違う部屋に入れる
  • スマートフォンでもキレイに見れる
  • システムメッセージとユーザーのメッセージは住み分ける

などなど。

ソース

Html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="format-detection" content="telephone=no">
<title>Hello Socket.IO</title>
<style>
/* 後述 */
</style>
</head>
<body>
<div id="container">
  <header>
    <h1>Socket IO chat</h1>
    <button id="leave">Leave</button>
  </header>
  <div id="page1">
    <p>
      This is sample of socket.IO chat.<br>
      Select [Room] and type your [Name].<br>
      And press [Join] to start!
    </p>
    <div class="init">
      <label>
        <span class="title">Room:</span>
        <select id="room">
          <option value="roomA">#A</option>
          <option value="roomB">#B</option>
          <option value="roomC">#C</option>
        </select>
      </label>
      <label>
        <span class="title">Name:</span> 
        <input type="text" value="John" id="name" maxlength="10"/>
      </label>
    </div>
    <button id="join">Join</button>
  </div>
  <div id="page2">
    <ul id="view" class="list"></ul>
    <div class="msgBox">
      <input type="text" placeholder="Please type messages here." id="message" />
    </div>
  </div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script>
// 後述
</script>
</body>
</html>

Css

body {
  margin:0;
  padding:0;
}

p {
  margin:0;
}

h1 {
  position:fixed;
  top:0;
  margin:0;
  padding:8px 0;
  width:100%;
  height:32px;
  background:#000;
  color:#fff;
  text-align:center;
  font-size:1.2rem;
  line-height:32px;
}

#leave {
  position:fixed;
  top:8px;
  right:8px;
  padding:3px 5px;
  height:32px;
  border-radius:8px;
  background:#165e83;
  color:#fff;
}

#page1,#page2 {
  margin-top:48px;
}

#page1 {
  padding:8px;
}

.init {
  margin:16px auto;
  padding:8px;
  border:1px solid #ccc;
  background:#eee;
}

.init > label {
  display:block;
  margin-bottom:8px;
  height:44px;
  font-size:1.4rem;
  line-height:44px;
}

.init > label > .title {
  display:inline-block;
  width:30%;
  color:#aaa;
}

.init > label > input[type="text"] {
  display:inline-block;
  width:61%;
  height:38px;
  font-size:1.2rem;
}

#page1 > button {
  display:block;
  box-sizing:border-box;
  margin:0 auto;
  width:100%;
  height:44px;
  border-radius:4px;
  background:#0094c8;
  color:#fff;
  font-size:1.5rem;
  -webkit-appearance:none;
}

#page2 .msgBox {
  margin:8px auto;
  padding:0 8px;
  text-align:center;
}

.msgBox input[type="text"] {
  -webkit-box-sizing:border-box;
  width:100%;
  height:38px;
  font-size:1.2rem;
}

.list {
  margin:0 auto;
  padding:0;
  border-top:1px solid #ccc;
  border-bottom:1px solid #fff;
  list-style:none;
}

.list li {
  padding:8px;
  border-top:1px #fff solid;
  border-bottom:1px #ccc solid;
  font-size:1.1rem;
}

.list li:nth-child(even) {
  background:#eee;
}

Client side JavaScript

$(function(io, $) {
  var socket = io.connect('http://localhost/chat'),
    $html = $('html,body'),
    page1 = $('#page1'),
    page2 = $('#page2').hide(),
    leaveBtn = $('#leave').hide(),
    roomBox = $('#room'),
    nameBox = $('#name'),
    joinBtn = $('#join'),
    msgBox = $('#message'),
    msgList = $('#view');

  socket.on('connected', function() {
    console.log('[System]Welcome to simple chat!');
  });

  joinBtn.on('click', function() {
    var r = roomBox.val();
    var n = nameBox.val();
    chat(r, n);
  });

  function chat(room, name) {
    socket.json.emit('init', {
      'room': room,
      'name': name
    });
  }

  socket.on('initialized', function() {
    page1.hide();
    page2.show();
    leaveBtn.show();
  });

  leaveBtn.on('click', function() {
    socket.emit('leave');
    page2.hide();
    leaveBtn.hide();
    page1.show();
    msgList.empty();
  });

  function send() {
    var data = msgBox.val();
    socket.json.send(data);
    msgBox.val('');
  }

  msgBox.focus(function() {
    page2.bind('keypress', sendByTypeEnter);
  });
  msgBox.blur(function() {
    page2.unbind('keypress', sendByTypeEnter);
  });

  var sendByTypeEnter = function(e) {
    if (e.keyCode === 13) {
      send();
    }
  };

  function update(data, systemFlag) {
    var list = $('<li>').html(data);
    if (systemFlag) {
      list.css('color', '#aaa');
    }
    msgList.append(list);
    $html.animate({
      scrollTop: list.offset().top
    }, 'fast');
  }

  socket.on('message', function(data) {
    if (data) {
      update(data);
    }
  });
  socket.on('System', function(data) {
    var systemFlag = true;
    if (data) {
      update(data, systemFlag);
    }
  });

}(io, jQuery));

Server side JavaScript

var app = require('http').createServer(handler),
  io = require('socket.io').listen(app),
  fs = require('fs')
  app.listen(8080);

function handler(req, res) {
  fs.readFile(__dirname + '/index.html', function(err, data) {
    if (err) {
      res.writeHead(500);
      return res.end('Error loading index.html');
    }
    res.writeHead(200);
    res.end(data);
  });
}

function getRoomAndNameObj(client, obj) {
  client.get('room', function(err, $room) {
    obj.room = $room;
  });
  client.get('name', function(err, $name) {
    obj.name = $name;
  });
  return obj;
}

var socket = io.of('/chat').on('connection', function(client) {
  client.emit('connected');

  client.on('init', function(req) {
    client.set('room', req.room);
    client.set('name', req.name);

    socket.to(req.room).emit('System', '[' + req.name + '] has come.');
    client.emit('System', 'Join [' + req.room + '] as [' + req.name + ']. Enjoy!');
    client.join(req.room);

    client.emit('initialized');
  });

  client.on('message', function(data) {
    var obj = {};
    getRoomAndNameObj(client, obj);
    socket.to(obj.room).emit('message', obj.name + ': ' + data);
  });

  client.on('leave', function() {
    var obj = {};
    getRoomAndNameObj(client, obj);
    if (obj.name) {
      socket.to(obj.room).emit('System', '[' + obj.name + '] has left..');
    }
    client.leave(obj.room);
  });

  client.on('disconnect', function() {
    var obj = {};
    getRoomAndNameObj(client, obj);
    if (obj.name) {
      socket.to(obj.room).emit('System', '[' + obj.name + '] has disconnected..');
    }
  });
});

いろいろ中途半端なのは突っ込んじゃダメです。

所感

そもそも最初に検証したかったのは、Socket.IOもといWebsocketでどんなことができそうかを把握することと、3G回線で使い物になるのかという点。

できそうなこと

やっぱNodeもそうやけど、APIのエンドポイントみたいな使い方がしっくりきそう。
結局Nodeは単純にサーバーやので、どういう使い方するか・・やけど。

Httpに代わるWebsocketという意味では、いろいろアイデアが沸いてきました。
やっぱTwitterのアレとLocationのアレでアレしてみたい。

3Gで

個人的に収穫だと思ったのはこっち。
一旦コネクションを張るまではやっぱ遅いんですけど、繋いでからは上々かと。
アップロードはモタつきがあったりSocket.IOさまのお力に頼りまくりですが、ダウンロードは早かったです。

今回のチャットのサンプルでも、閲覧専用にしてしまえばストレスなくスイスイでした。
もっと最適化できる部分もあると思いますが、個人的には満足。

サーバー側でエグい処理をさせて、結果だけを落とすってパターンで何か作ってみようと画策中です。

PCは?って言われると・・早いけどAjaxとかでも良いんじゃ・・と思ってしまう。
リアルタイム性は置いておくならば。