今回は前回にサーバーサイドの設定や実装を行いましたので、
フロント(画面)側を作成していきたいと思います。
全てのソースコードは以下に格納されています。
チャット画面(view)作成
- チャットルーム(作成)、チャットルーム詳細画面のためのviewを作成します。
- /resources/templatesの以下にroom.ftlとroomdetail.ftlを作成します。
- 今回は画面の開発はfreemarker形式で、ロジックはvue.jsで作成します。
- UIはbootstrapを利用しますが、ダウンロードするのではなくCDNを利用します。
room.ftl作成
<!doctype html>
<html lang="en">
<head>
<title>Websocket Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div class="row">
<div class="col-md-12">
<h3>チャットルーム一覧</h3>
</div>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">チャットルーム名</label>
</div>
<input type="text" class="form-control" v-model="room_name" v-on:keyup.enter="createRoom">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="createRoom">チャットルーム開設</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item list-group-item-action" v-for="item in chatrooms" v-bind:key="item.roomId" v-on:click="enterRoom(item.roomId)">
{{item.name}}
</li>
</ul>
</div>
<!-- JavaScript -->
/webjars/vue/2.5.16/dist/vue.min.js
/webjars/axios/0.17.1/dist/axios.min.js
<script>
var vm = new Vue({
el: '#app',
data: {
room_name : '',
chatrooms: [
]
},
created() {
this.findAllRoom();
},
methods: {
findAllRoom: function() {
axios.get('/chat/rooms').then(response => { this.chatrooms = response.data; });
},
createRoom: function() {
if(!this.room_name) {
alert("チャットルーム名を入力してください。");
return;
} else {
var params = new URLSearchParams();
params.append("name", this.room_name);
axios.post('/chat/room', params)
.then(
response => {
alert(response.data.name+"開設に成功しました。")
this.room_name = '';
this.findAllRoom();
}
)
.catch( response => { alert("チャットルーム開設に失敗しました。"); } );
}
},
enterRoom: function(roomId) {
var sender = prompt('ニックネームを入力してください。');
if(sender) {
localStorage.setItem('wschat.sender',sender);
localStorage.setItem('wschat.roomId',roomId);
location.href="/chat/room/enter/"+roomId;
}
}
}
});
</script>
</body>
</html>
- /resources/templates/chat/room.ftlを作成します。
- チャットルームを作成したり現在のチャットルームのリストを表示します。
- リストをクリックしたらチャットルームに参加します。
roomdetail.ftl作成
<!doctype html>
<html lang="en">
<head>
<title>Websocket ChatRoom</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/webjars/bootstrap/4.3.1/dist/css/bootstrap.min.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div class="container" id="app" v-cloak>
<div>
<h2>{{room.name}}</h2>
</div>
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text">内容</label>
</div>
<input type="text" class="form-control" v-model="message" v-on:keypress.enter="sendMessage">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="sendMessage">送る</button>
</div>
</div>
<ul class="list-group">
<li class="list-group-item" v-for="message in messages">
{{message.sender}} - {{message.message}}</a>
</li>
</ul>
<div></div>
</div>
<!-- JavaScript -->
/webjars/vue/2.5.16/dist/vue.min.js
/webjars/axios/0.17.1/dist/axios.min.js
/webjars/sockjs-client/1.1.2/sockjs.min.js
/webjars/stomp-websocket/2.3.3-1/stomp.min.js
<script>
//alert(document.title);
// websocket & stomp initialize
var sock = new SockJS("/ws-stomp");
var ws = Stomp.over(sock);
var reconnect = 0;
// vue.js
var vm = new Vue({
el: '#app',
data: {
roomId: '',
room: {},
sender: '',
message: '',
messages: []
},
created() {
this.roomId = localStorage.getItem('wschat.roomId');
this.sender = localStorage.getItem('wschat.sender');
this.findRoom();
},
methods: {
findRoom: function() {
axios.get('/chat/room/'+this.roomId).then(response => { this.room = response.data; });
},
sendMessage: function() {
ws.send("/pub/chat/message", {}, JSON.stringify({type:'TALK', roomId:this.roomId, sender:this.sender, message:this.message}));
this.message = '';
},
recvMessage: function(recv) {
this.messages.unshift({"type":recv.type,"sender":recv.type=='ENTER'?'[アラム]':recv.sender,"message":recv.message})
}
}
});
function connect() {
// pub/sub event
ws.connect({}, function(frame) {
ws.subscribe("/sub/chat/room/"+vm.$data.roomId, function(message) {
var recv = JSON.parse(message.body);
vm.recvMessage(recv);
});
ws.send("/pub/chat/message", {}, JSON.stringify({type:'ENTER', roomId:vm.$data.roomId, sender:vm.$data.sender}));
}, function(error) {
if(reconnect++ <= 5) {
setTimeout(function() {
console.log("connection reconnect");
sock = new SockJS("/ws-stomp");
ws = Stomp.over(sock);
connect();
},10*1000);
}
});
}
connect();
</script>
</body>
</html>
- /resources/templates/chat/roomdetail.ftlを作成します。
- チャットロームに参加した時の画面となります。
- クライアント間、メッセージをやり取りできます。
- 参加時はws-stompでサーバーに接続後チャットルームを登録するアクションを実装しました。
ws.subscribe(“/sub/chat/room/”+vm.$data.roomId, function(message) {……}
登録は「/sub/chat/room/チャットルーム番号」で登録することになりますが、このアドレスをTopicとしてサーバーからメッセージを発行します。
チャットルームでメッセージを入力したらサーバでtopic(”/sub/chat/room/チャットルーム番号”)でメッセージを発行(publish)しますが、これを登録した人はws.subscribeで待機していて発送されたメッセージを受けて処理が可能になることです。
テスト
- サーバーを実行して「localhost:8080/chat/room」を実行します。
- 最初はチャットルームがないので、チャットルーム名を入力して作成します。
- チャットルームを作成したらリストからチャットルームをクリックしてチャットに参加できます。

- アプリケーションを起動します。

- localhost:8080/chat/roomに接続します。

- チャットルームを作成します。

- リストからチャットルームをクリックします。
- ニックネームを入力してOKを押します。

- チャットルーム詳細画面です。
- ”[アラム]テストユーザー1さんが参加しました”と表示されれことを確認できます。
ブラウザをもう一つ開いてやり取りができるか確認してみましょう

- チャットルームが表示されています。

- リストからチャットルームをクリックします。
- ニックネーム(テストユーザー2)を入力してOKを押します。

- テストユーザー1の画面にはテストユーザー2さんが参加しましたと表示されます。

- 実際に内容に入力し「送る」ボタンを押してみましょう。
- テストユーザー1で入力した内容が2の画面でも表示されます。

逆にテストユーザー2から入力した内容がテストユーザー1の画面にも表示されます。
終わりに
前からWebsocketを利用してチャットアプリケーションを作成してみたかったのですが、Stompまで勉強ができてうれしいです。
Stompのpub/subを利用することでメッセージの移動経路が明確になり、チャットルームごとのクライアントsessionを保存していてメッセージを発送しなければならないロジックが簡単になりました。
まだ、細かいところまでは理解できていないところもありますが、分析が好きな自分にとっては分析することが多いので、楽しみです。
