Socket.IOを使ってシンプルなチャットアプリを作ってみました.

ユーザー管理とか同一名で利用されることを想定していないのでだいぶガバガバな実装です.

http://satopirka.com/chat/

構成

  • index.js(サーバーサイド)
  • index.html(クライアントサイド)
  • (mongodb)(db上にchatというdbとそのcollectionにusersとentriesを用意)

準備

$ npm i express
$ npm i socket.io
$ npm i mongodb
$ npm i moment
$ mongo
> use chat
switched to db chat
> db.createCollection("users")
{ "ok" : 1 }
> db.createCollection("entries")
{ "ok" : 1 }

実装

サーバーサイド

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const moment = require('moment');
const mongoClient = require('mongodb').MongoClient();

const url = 'mongodb://127.0.0.1:27017/chat'

let entries, users;
mongoClient.connect(url, (err, db) => {
  if (err) {
    console.log('db connection failure');
  } else {
    entries = db.collection('entries');
    users = db.collection('users');
  }
});

app.get('/', (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

function now() {
  return moment().format('YYYY-MM-DD HH:mm:ss');
}

function appendMessage(user, content, date) {
  entries.insert({
    'name': user,
    'content': content,
    'date': date
  });
  users.insert({'name': user});
  let option = '';
  if (content == 'disconnected' || content == 'connected') {
    option = ' disabled';
    content = ' ' + content;
  }
  io.emit('chat message', user + content + ' at ' + date, option);
}


io.on('connection', (socket) => {
  let user_name = null;

  /* disconnected */
  socket.on('disconnect', () => {
    let date = now();
    appendMessage(user_name, 'disconnected', date);
    io.emit('delete user', user_name);
    users.remove({'name': user_name}, (err, result) => {
      if (err) {
        console.log(result);
      }
    });
  });

  /* when user submit a chat message */
  socket.on('chat message', (msg) => {
    if (msg != '') {
      let date = now();
      appendMessage(user_name, ': ' + msg, date);
    }
  });

  /* new user */
  socket.on('user in', (msg) => {
    /* show the list of the history of the chat messages only for myself */
    entries.find().toArray((err, documents) => {
      documents.reverse().some((doc, i) => {
        let option = '';
        if (doc.content == 'disconnected' ||  doc.content == 'connected') {
          option = ' disabled';
        }
        /* only for myself */
        io.to(socket.id).emit('history', doc.name + ' ' + doc.content + ' at ' + doc.date, option);
        /* up to 50 messages */
        if (i == 50) {
          return true;
        }
      });

      /* save the chat message (with user_name and data) to the database */
      let date = now();
      user_name = msg;
      appendMessage(user_name, 'connected', date);
    });

    /* list the active users */
    users.find().toArray((err, documents) => {
      documents.some((doc, i) => {
        /* only for myself */
        io.to(socket.id).emit('add user', doc.name);
      });
      /* nortify the other active users of my login */
      io.emit('add user', msg);
    });
  });
});


http.listen(3000, '127.0.0.1', () => {
  console.log('listening on 127.0.0.1:3000');
});

クライアントサイド

<!doctype html>
<html>
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <!-- 略 -->
  </head>
  <body>
    <div class="container">
      <div class="row">
        <dir class="col-sm-12">
          <h1>簡易チャットシステム</h1>
          <form action="">
            <div class="form-group">
              <input id="m" autocomplete="off" class="form-control" />
            </div>
            <button class="btn btn-primary">Send</button>
          </form>
          <hr>
          <h4>アクティブなユーザー</h4>
          <div id="users"></div>
          <hr>
          <ul id="messages" class="list-group"></ul>
        </dir>
      </div>
    </div>
  </body>
  <script src="/socket.io/socket.io.js"></script>
  <script src="https://code.jquery.com/jquery-1.11.1.js"></script>
  <script>
    $(function () {
      let socket = io();
      let name = window.prompt('enter your name: ', 'anonymous');
      while(name == null) {
        name = window.prompt('enter your name: ', 'anonymous');
      }
      socket.emit('user in', name);
      $('form').submit(() => {
        socket.emit('chat message', $('#m').val());
        $('#m').val('');
        return false;
      });

      socket.on('chat message', (msg, option) => {
        $('#messages').prepend($('<li class="list-group-item' + option + '">').text(msg));
      });
      socket.on('history', (msg, option) => {
        $('#messages').append($('<li class="list-group-item' + option + '">').text(msg));
      });
      socket.on('add user', (msg) => {
        $('#users').append($('<button type="button" class="btn btn-light">').text(msg));
      });
      socket.on('delete user', (msg) => {
        $('#users').find('button:contains("' + msg + '")').remove();
      })
    });
  </script>
</html>

簡単な解説

まず,httpモジュールで作成したサーバーに対してconst io = require('socket.io')(http)でwebsocketを機能追加. その他にも,時間を取得するためにmomentモジュールや,dbにアクセスするためにmongodbモジュールを読み込んでいます. 今回は事前準備としてchatという名前のdbを作成し,その中にチャットの履歴を残すためのentriesと現在ログイン中のユーザーを保持するためのusersというcollectionを作成しており,

let entries, users;
mongoClient.connect(url, (err, db) => {
  if (err) {
    console.log('db connection failure');
  } else {
    entries = db.collection('entries');
    users = db.collection('users');
  }
});

で読み込みを行っております.
次に,ユーザーがアクセスしてきたときの処理を,

io.on('connection', (socket) => {
  //ここに記述
});

ここに記述しています.
その中に,

io.on('connection', (socket) => {
  socket.on(文字列, callback関数);
  socket.on(文字列, callback関数);
  socket.on(文字列, callback関数);
  ...
});

を足していきます.これは,クライントサイドにおいてsocket.emit(文字列, オプション)が実行されたときに,文字列が一致する場合にオプションを引数としてcallback関数が呼ばれます.
今回は新しくユーザーがアクセスしてきたときにクライアントサイドでsocket.emit('user in', name)(nameはユーザーに入力してもらった名前)を実行してあげて,サーバサイドでは,

socket.on('user in', (name) => {
  //過去に書き込まれたデータを表示する処理
  //データベースに書き込まれたアクティブユーザーの表示
});

といったことを実装しています.逆にクライアントサイドへ処理を渡したい場合は,サーバーサイドでsocket.emit(文字列, オプション)を実行してやればよいです.

デモとリポジトリ