koa と socket.io を使用したチャットアプリを作成する。
koa に socket.io を組み込んだ、koa.io を使用する。
(koa.io は現在開発中だが、とりあえず動作に支障はなかった。
ただ、 socket.io をそのまま組み込んでもコード量はそれほど変わらないと思われる。)
koa.ioのインストール
koa.io は koa を含んでいるので、先に koa をアンインストールしてからインストールする。
$ npm uninstall --save koa
$ npm install --save koa.io
サーバー側の処理
app.js
// app.js
var app = require('koa.io')();
var route = require('koa-route');
var serve = require('koa-static');
var views = require('koa-views');
var moment = require('moment');
// ectをテンプレートエンジンとして指定
app.use(views(__dirname + '/views', {
map: {
html: 'ect'
}
}));
// -- routing --
// GET /chat
app.use(route.get('/chat', function *chat(){
yield this.render('chat.ect', {
title: 'CHAT',
version: '1.0.0'
});
}));
// static files
app.use(serve(__dirname + '/public'));
// -- WebSocket --
app.io.use(function* (next){
console.log('[connect]');
yield* next;
console.log('[disconnect]');
if (this.username) {
this.broadcast.emit('message', { message: 'logout: ' + this.username});
this.username = '';
}
});
app.io.route('login', function*(next, args){
console.log('[login] ' + JSON.stringify(args));
this.username = args.username;
var msg = { message: 'login: ' + this.username };
this.broadcast.emit('message', msg);
this.emit('message', msg);
});
app.io.route('logout', function*(next){
console.log('[logout] ' + this.username);
if (this.username) {
this.broadcast.emit('message', { message: 'logout: ' + this.username});
this.username = '';
}
});
app.io.route('message', function*(next, args){
console.log('[message] ' + JSON.stringify(args));
var msg = {
date: moment().format('hh:mm:ss'),
username: this.username,
message: args.message
};
this.broadcast.emit('message', msg);
this.emit('message', msg);
});
app.listen(3000);
app.io
の箇所が koa.io が socket.io をラッピングしている部分。
非常に簡単に WebSocket のサーバーサイド処理が書ける。
クライアント側の処理
今回はViewを knockoutjs で作成する。
(ちなみに… knockoutjs の Model や ViewModel を ES2015 で書けないか試行錯誤してみたが、肝心の ko.observable
の変数が上手く認識できず、断念。)
html (ECT)
views/layout.ect
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%- @title %></title>
<link rel="stylesheet" href="/honoka/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/chat"><%- @title %></a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="/version">About</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<% content %>
</div>
<footer class="footer">
<div class="container">
<% include 'views/footer.ect' %>
</div>
</footer>
<script src="/jquery/jquery.min.js"></script>
<script src="/socketio/socket.io.js"></script>
<script src="/knockout/knockout.js"></script>
<script src="/js/client.js"></script>
</body>
</html>
socket.io.js, knockout.js を追加。
chat.ect
<% extend 'views/layout.ect' %>
<div class="row" data-bind="visible: !user.isLogin()">
<form class="form-inline" id="login-form">
<div class="form-group">
<label for="username">お名前</label>
<input type="text" class="form-control" id="username" placeholder="User Name"
data-bind="value: user.username">
</div>
<button class="btn btn-primary" id="login"
data-bind="click: user.login">ログイン</button>
</form>
</div>
<div class="row" data-bind="visible: user.isLogin()">
<form class="form-inline" id="login-form">
<div class="form-group">
<p class="form-control-static">ようこそ <span data-bind="text: user.username"></span> さん!</p>
</div>
<button class="btn btn-xs btn-default" id="logout"
data-bind="click: user.logout">ログアウト</button>
</form>
</div>
<div class="row" data-bind="visible: user.isLogin()">
<form class="form-inline" id="chat-form">
<div class="form-group">
<label for="message">メッセージ</label>
<input type="text" class="form-control" id="message" placeholder="Message"
data-bind="value: message.message">
</div>
<button class="btn btn-primary" id="send"
data-bind="click: send">送 信</button>
</form>
</div>
<div class="row" data-bind="visible: user.isLogin()">
<div class="panel panel-default">
<div class="panel-heading">チャットルーム</div>
<div class="panel-body" id="response">
<div class="row" data-bind="foreach: messageList">
<div class="col-xs-1" data-bind="text: date"></div>
<div class="col-xs-1" data-bind="text: username"></div>
<div class="col-xs-10" data-bind="text: message"></div>
</div>
</div>
</div>
</div>
上から順に、
- ログインフォーム (ログイン後に隠す)
- ログアウトボタン (ログイン後に表示)
- メッセージフォーム (ログイン後に表示)
- チャット欄 (ログイン後に表示)
knockoutjs のデータバインドでデータをやり取りするように実装。
js
// client.js
// user model
var User = function User(chat){
var self = this;
self.username = ko.observable('');
self.isLogin = ko.observable(false);
self.login = function(){
// 接続処理
self.isLogin(true);
chat.login(self.username());
};
self.logout = function(){
// 切断処理
chat.logout(self.username());
self.isLogin(false);
self.username('');
};
};
// message model
var Message = function Message(prm, chat){
var self = this;
var opts = {
date: '',
username: '',
message: ''
};
if (!prm) {
prm = {};
}
$.extend(opts, prm);
self.date = opts.date;
self.username = opts.username;
self.message = ko.observable(opts.message);
self.send = function(){
if (chat) {
// メッセージ送信
chat.send({ message: self.message() });
self.message('');
}
};
};
// Chat class
var Chat = function Chat(messageList){
var self = this;
var socket = io.connect();
socket.on('connect', function(){
console.log('connect');
});
socket.on('disconnect', function(){
console.log('disconnect');
});
self.login = function(username){
socket.emit('login', { username: username });
socket.on('message', function(data){
console.log(data);
messageList.push(new Message({
date: data.date,
username: data.username,
message: data.message
}));
});
};
self.send = function(msg) {
socket.emit('message', msg);
};
self.logout = function(username) {
socket.emit('logout', { username: username });
socket.off('message');
};
};
// application view model
var AppViewModel = function AppViewModel(){
var self = this;
// 受信したメッセージ
self.messageList = ko.observableArray([]);
// socket.ioのラッパー
var chat = new Chat(self.messageList);
// models
self.user = new User(chat);
self.message = new Message({}, chat);
// 送信
self.send = function(){
self.message.send();
};
};
$(function(){
ko.applyBindings(new AppViewModel());
});
socket.io の処理は Chat クラスに押し込んだが、結果としてコードがゴチャゴチャになった気がする…