LINE, Slack, MicrosoftのTeamsなど最近は仕事でもチャットアプリを利用する機会が増えていると思います。そんな中チャットアプリを自作できてないだろうか、チャップアプリはどのように実装するのかとチャットアプリに関心を持っている人も多数いるかと思います。そんな人におすすめなのが今回紹介するSocket.io, Express.jsとVue.jsを利用した簡易チャットアプリケーションの構築です。チャットのメッセージ通信についてはSockect.ioが行ってくれるのでメッセージのリアルタイムの送受信であれば簡単に実装することができます。

Socket.ioの設定がメインになりますが、メッセージの入力やクライアント間で送信しあうメッセージの表示に関する箇所はvue.jsを利用しているのでSocket.ioとvue.jsとの組み合わせも理解することができます。

Express.jsのインストールと設定

Express.jsのインストール

任意の名前のフォルダを作成します。ここではvue-chatというフォルダを作成しています。

npmを使ったJavaScriptのパッケージ管理を行うためnpm initコマンドでpackage.jsonファイルを作成します。


$ mkdir vue-chat
$ cd vue-chat
$ npm init -y

npm init -yコマンドを実行したvue-chatフォルダには、package.jsonファイルが作成されます。


$ npm install express

Express.jsの設定

vue-chatフォルダにindex.jsファイルを作成しExpressサーバの設定を開始します。

Expressサーバの設定では、ポートは3000に設定し、”/”にアクセスがあるとHello Worldを返します。


const app = require('express')();
const server= require('http').Server(app);

app.get('/', function(req, res){
  res.send('<h1>Hello world</h1>');
});

server.listen(3000, function(){
  console.log('listening on *:3000');
});	

nodeコマンドでサーバを起動します。


$ node index.js

ブラウザからhtpp://locahost:3000にアクセスするとHello Worldが表示されればExpressサーバの初期設定は完了です。

Express.jsでHello World
Express.jsでHello World

HTMLファイルの作成

“/”へのアクセスに対して、Hello Worldの文字列を返していましたが、index.htmlファイルの内容を返すようにルーティングの変更を行います。

htmlファイルの内容を返す場合はsendFileメソッドを利用します。__dirnameは現在更新しているindex.jsファイルが保存されているパスを取得しているので、index.htmlはindex.jsと同じフォルダに作成します。


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

index.htmlファイルの内容は下記の通りです。index.htmlファイルの中ではinput要素を追加し、メッセージの入力する場所を作成します。機能はまだ実装されていませんが、”Send”ボタンクリックするとメッセージが送信されul要素のliにメッセージが追加される形になります。


<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>
  </body>
</html>
index.jsのファイルの更新が完了したらnode index.jsを一度終了して再度node index.jsを実行してください。index.js更新毎にnode index.jsを再実行させるのは効率的ではないのでnpm install –save-dev nodemonでnodemonライブラリをインストールし利用することで更新を自動で反映してくれるようになります。nodemonインストール後にnpx node index.jsコマンドを実行してください。
fukidashi

ブラウザから”/”にアクセスすると以下の画面が表示されます。

簡易チャットの初期画面
簡易チャットの初期画面

Socket.ioのインストール

リアルタイムでのメッセージの送受信を行うためにはsocket.ioが必要になります。node installコマンドでsocket.ioのインストールを行います。


 $ npm install socket.io

Socket.ioの基本動作確認

インストールが完了したら、index.jsにsocket.ioに関する行を追加します。読み込んだsocket.ioとhttpサーバオブジェクトを使ってsocket.ioのインスタンスを作成しています。io.onではconnectionイベントを設定するとクライアント(ブラウザ)がサーバに接続するとconnectionイベントの内部の処理が実行されます。ここではクライアントがサーバに接続するとa user connectedがコンソールログに表示されます。


var app = require('express')();
var server = require('http').Server(app);
var io = require('socket.io')(server); //追加

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

io.on('connection', function(socket){
  console.log('a user connected'); //追加
});

server.listen(3000, function(){
  console.log('listening on *:3000');
});

クライアント側になるindex.htmlファイルには以下のsocket.ioのクライアントスクリプトタグを追加します。ioの引数には接続を行うサーバのURLを設定します。Expressサーバがlocalhostのポート3000で起動しているので接続することができますが、Expressサーバが起動していない時や異なるポート番号を入力、URLが間違っている場合はエラーとなります。


	</form>
	<script src="/socket.io/socket.io.js"></script>
	<script>
		const socket = io();
          //const socket = io('http://localhost:3000);
	</script>
</body>
io()は引数にURLをとることができます。URLを入れない場合は、window.locationオブジェクトからURLを取得しています。本環境ではio(‘http://localhost:3000’)と設定しても動作します。window.locationが何かわからない場合は、console.log(window.location)をconst socket = io()の下に記述してブラウザでアクセスするとオブジェクトが取得できhrefプロパティにURLが入っていることが確認できます。
fukidashi

ブラウザでアクセスを行うとサーバとクライアント間で接続が行われ、connectionイベントによりサーバ側でa user connectedが表示されます。


$ node index.js
listening on *:3000
a user connected

index.htmlのクライアント側でサーバと接続が行われたかどうかconnetイベントにより確認を行うことができます。connectイベントにより接続が完了するとsockectのconnectedプロパティがtrueになることでサーバに接続できていることを確認できます。


const socket = io('http://localhost:3000');
socket.on('connect', () => {
    console.log(socket.connected); // true
  });

簡易チャットの構築

簡易チャットを構築するためには、Socket.ioを使ってリアルタイムにサーバ、クライアント間でデータの送受信が行えていることを確認する必要があります。まずはクライアントからサーバにメッセージを送ります。

以後サーバとクライアントという言葉が出てきますが、サーバはExpress.jsサーバのことで設定はすべてindex.jsに記述していきます。クライアントはブラウザのことを表し、すべての設定はindex.htmlの中に記述していきます。
fukidashi
リアルタイムとはブラウザでメッセージを送信するとページのリロードを行うことなく、入力したメッセージがすぐにページに反映されることを意味しています。
fukidashi

input要素に入力したデータをサーバに送るためには、input要素に入力したデータを取得する必要があります。入力したデータをvue.jsを使って取得します。取得はvue.jsである必要はなくJavaScriptでもjQuery, Reactでも行うことは可能です。

vue.jsの利用

今回はメッセージを入力する箇所(input要素)とサーバから送信されてくる各クライアントのメッセージを表示する箇所にvue.jsを利用します。メッセージはul要素を利用してリスト化します。

簡単にvue.jsを利用することができるcdnを使います。


<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

クライアントからサーバへのメッセージ送信

入力したメッセージをvue.jsで取得するためにinput要素にv-modelでデータバインディングを行います。入力した値はtextInputプロパティを通してvue.js側で取得することができます。


<input id="m" autocomplete="off" v-model="textInput"/>

button要素にclickイベントを設定します。button要素をクリックするとsendMessageメソッドが実行されます。sendMessageメソッドはvue.jsで設定を行います。


<button v-on:click.prevent="sendMessage">Send</button>
button要素をクリックするとデフォルトの動作によりブラウザのリロードが発生するためデフォルトの動作を停止させるためpreventを設定しています。
fukidashi

上記の設定を踏まえてvue.jsの設定を行います。vue.jsを使う場合はvue.jsの制御化におく要素を指定する必要があります。index.htmlに追加したul要素とform要素の外側にdiv要素を追加し、idをappとします。


<body>
	<div id="app">
    <ul id="messages"></ul>
    <form action="">
    //略
    </form>
    </div>

v-modelバインディングで設定したtextInputとボタンのclickイベントが正常に動作するか確認を行います。


	</div>
	<script src="/socket.io/socket.io.js"></script>
	<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
	<script>
		const socket = io('http://localhost:3000');
		const app = new Vue({
			el: '#app',
			data: {
				textInput: '',
			},
			methods: {
				sendMessage() {
					console.log('click send Button')
					console.log(this.textInput);
				}
			}
		});
	</script>
</body>

input要素に文字列を入力してSendボタンをクリックするとブラウザのデベロッパーツールに”click send Button”のメッセージとinput要素に入力した文字列が表示されれば正常に動作しています。

textInputの情報が取得できることが確認できたので、サーバに入力した文字列を送信するためsockedt.ioのemitイベントを設定します。


sendMessage() {
  socket.emit('chat message', this.textInput);
  this.textInput = '';
}

入力したメッセージはSendボタンをクリックするとvue.jsのsendMessageが実行され、textInputプロパティのデータ(=入力した文字列)がsocket.emitメソッドを通して、Expressサーバに送信されます。

送信の際にメッセージを識別するためにイベント名chat messageを設定しています。

サーバのメッセージ受信設定

次に送信したメッセージをExpressサーバ側(index.js)で受信する設定を行います。socketのonメソッドの引数にクライアント側のemitで使用したイベント名chat messageを設定し、受け取ったメッセージをコンソールに表示させます。


io.on('connection', function(socket){
  socket.on('chat message', function(msg){
    console.log('message: ' + msg);
  });
});

ブララザ(クライアント)の入力フォームにメッセージを入力して、sendボタンをクリックするとサーバ側に入力したメッセージが表示されることが確認できます。表示される場所はnode index.jsコマンドの下に表示されます。

初めてのメッセージ入力
初めてのメッセージ入力

$ node index.js
listening on *:3000
message: はじめてのSocket.ioを使ったメッセージ

わずか数行のコードでブラウザ上で入力した文字列をサーバ側でリアルタイムに取得することができました。

サーバからクライアントへのメッセージ送信

クライアントからサーバへメッセージを送信することができたので、次はサーバからクライアントにメッセージの送信を行います。

ここでのクライアントはサーバに接続しているブラウザすべてを表しています。メッセージの送信したブラウザだけでなくメッセージを入力せずsocket.ioによりサーバに接続しているブラウザも含みます。
fukidashi

先ほどのサーバの受信設定では受け取ったメッセージはconsole.logを使って表示していましたが、今度は受け取ったメッセージをio.emitメソッドを使って受け取ったメッセージをそのままクライアント(ブラウザ)に送信します。


io.on('connection', function(socket){
  socket.on('chat message', function(msg){
  	// console.log('message: ' + msg);
    io.emit('chat message', msg);
  });
});

クライアント側での受信設定

クライアント側ではサーバから送られてくるメッセージを受信する必要があります。

メッセージの受信は、サーバと同様にsocket.onメソッドで行うことができます。メッセージを入力するブラウザで確認するとまずinput要素に文字列を入力すると入力した後にブラウザのコンソールログに入力したメッセージが表示されます。サーバに送信してまたサーバからそのメッセージが返されることを確認できます。


var socket = io();

socket.on('chat message',function(msg){
  console.log(msg)
})
メッセージの送信
メッセージの送信
サーバから戻ってきたメッセージがコンソールに表示
サーバから戻ってきたメッセージがコンソールに表示

メッセージを入力していないブラウザでもメッセージを受け取ることができるのか確認するためにもう一つブラウザを起動します。

起動後はブラウザのコンソールを開いておいてください。サーバに接続しているブラウザのどちらかでメッセージを入力すると両方のブラウザのコンソールに入力したメッセージがリアルタイムで表示されます。

この時点でチャットアプリの基本が完成したことを理解できるかと思います。

vue.jsを使ってメッセージをリスト化

先ほどはサーバから受け取ったメッセージはコンソールログに表示させるだけでした。次はブラウザ(クライアント)で送信したメッセージをブラウザの画面上にリスト化するためにvue.jsを利用します。

messagesプロパティを新たに追加し、サーバから送信されてくるメッセージをこのmessagesプロパティの中に追加していきます。messagesは配列として設定するためメッセージを次々追加することができます。追加したメッセージをブラウザ上に表示するためにvue.jsのv-forティレクティブを利用します。messagesは配列としてメッセージが保持されているので、v-forによりメッセージ1つ1つを取り出して表示しています。


<ul id="messages">
  <li v-for="message in messages">{{ message }}</li>
</ul>

先ほどまではブラウザのコンソールにメッセージを表示させていたため、受け取ったメッセージはvue.jsの範囲外で処理を行っていました。vue.jsの中でメッセージを受信できるようにsocket.onイベントをライフサイクルフックのmountedに設定します。mountedに追加するこことでvue.jsの初期化中にsockect.onイベントが登録されるため初期化後にイベントを受け取ることができます。

受け取ったメッセージをmessagesプロパティに追加するためにライフサイクルフックのmountedを利用します。


<script>
	const socket = io('http://localhost:3000');
	const app = new Vue({
		el: '#app',
		data: {
			textInput: '',
			messages: [],
		},
		methods: {
			sendMessage() {
				socket.emit('chat message', this.textInput);
				this.textInput = '';
			}
		},
		mounted() {
			socket.on('chat message', (msg) => {
				this.messages.push(msg);
			});
		}
	});
</script>

以上で設定は完了で複数のブラウザを起動してメッセージを入力してみましょう。それぞれのブラウザで入力したメッセージがどちらのブラウザにもリスト化されて表示されます。

ブラウザAでの表示
ブラウザAでの表示
ブラウザBでの表示
ブラウザBでの表示

メッセージの表示方法の確認

サーバ側ではクライアントからメッセージをemitするとsocket.on(‘chat message’…)の中でio.emitによりすべてのクライアントにサーバからメッセージを送信していました。

クライアントが接続してきたら接続したきたクライアントのみにメッセージを送信することができます。その場合はio.emitではなくsocket.emitメソッドを利用します。


io.on('connection', function(socket){
  socket.emit('chat message','ようそこチャットアプリへ');
  socket.on('chat message', function(msg){
    io.emit('chat message', msg);
  });
});

ブラウザでアクセスすると以下のメッセージが表示されます。

アクセスしたブラウザ上に表示
アクセスしたブラウザ上に表示

次にアクセスしてきたブラウザ以外にメッセージを表示したい場合はsocket.broadcast.emitを利用することができます。


io.on('connection', function(socket){
  socket.emit('chat message','ようそこチャットアプリへ');
  socket.broadcast.emit('chat message','新しいユーザが接続しました。')
  socket.on('chat message', function(msg){
    io.emit('chat message', msg);
  });
});

別のブラウザがサーバへの接続を完了すると他のブラウザに以下のメッセージが表示されます。

他のブラウザがアクセスするとメッセージが表示
他のブラウザがアクセスするとメッセージが表示

アクセスした直後のブラウザにはようこそチャットアプリへのメッセージのみ表示されます。

アクセスしたブラウザ上に表示
アクセスしたブラウザ上に表示

接続が行われた時にメッセージを表示させることができたので、接続が切れた時にもメッセージを表示させる方法を確認します。

disconnectイベントを利用することで接続が切れたことを他のブラウザにメッセージで伝えることができます。


io.on('connection', function(socket){
  socket.emit('chat message','ようそこチャットアプリへ');
  socket.broadcast.emit('chat message','新しいユーザが接続しました。')
  socket.on('disconnect',function(){
    io.emit('chat message','あるユーザの接続が切れました')
  })
  socket.on('chat message', function(msg){
    io.emit('chat message', msg);
  });
});

socket.ioとExpress.jsとVue.jsを利用した簡易チャットアプリの完成です。これだけの短いコードでリアルタイムのチャットアプリケーションを作成することができました。