socket.io で Web空間に神社を建立

#は？
兎にも角にも成果物です。
https://web-shrine.herokuapp.com/

#はじめに
会社でWebSocket勉強会が開催されたので、試しに遊んでみようということで作成しました。
あまり時間もなく凝ったことはできないので、代わりにネタに走った結果がこれです。

使用技術は以下の通りです。

* HTML5
* CSS3（flexbox）
* Node.js（express.js）
* jQuery（Angularで書き直したい）
* socket.io

作成したプロダクトの公開にHerokuを用いています。

#コード

フロント側とバック側に分けて説明します。

##フロントエンド
今どきjQuery使うのもどうかとは思いましたが、あまり時間がなかったので許してください。
そのうちAngular使って書き直したいです。

HTML部は特にこれといって解説する部分はないので、scriptタグ内に絞って解説していきます。

```javascript:index.html
const socket = io();

socket.on('connect', () => {
    console.log('Server Connected')
})

socket.on('receive_money', (obj) => {
    console.log('現在のお賽銭額', obj.value)
    $('#money').animate(
        { opacity: "toggle" },
        0
    );
    $('#money').text(obj.value.toLocaleString())
    $('#money').animate(
        { opacity: "toggle" },
        200
    );
})

socket.on('receive_throw', (obj) => {
    if ($('#throw-money-list li').length >= 50) {
        $('#throw-money-list li:last-child').remove();
    }
    $('#throw-money-list').prepend(
        $('<li>').text(obj.value.toLocaleString() + '円が投げ込まれました！')
    )
})

socket.on('receive_money_list', (array) => {
    $('#money-list').empty();
    array.reverse();
    $.each(array, (index, element) => {
        $('#money-list').append(
            $('<li>').text(element.amount.toLocaleString() + '円 奉納'),
        )
    });
})

socket.on('receive_throw_time_by_amount', (obj) => {
    $('#time_1').text(obj['1'])
    $('#time_5').text(obj['5'])
    $('#time_10').text(obj['10'])
    $('#time_50').text(obj['50'])
    $('#time_100').text(obj['100'])
    $('#time_500').text(obj['500'])
    $('#time_1000').text(obj['1000'])
    $('#time_5000').text(obj['5000'])
    $('#time_10000').text(obj['10000'])
})

socket.on('receive_wish', (array) => {
    console.log('お願いを受信', array)
    $('#wish-list').empty();
    $.each(array, (index, element) => {
        $('#wish-list').prepend(
            $('<div style="margin: 12px;">').append(
                $('<dt>').text(element.name + ' さん - ' + new Date(element.time).toLocaleString()),
                $('<dd class="ema">').append(
                    $('<span class="ema-text">').text(element.wish)
                )
            )
        )
    });
})

socket.on('receive_worshipers', (num) => {
    $('#worshipers').text(num);
})


function throwMoney(num) {
    console.log('賽銭', num, '円')
    socket.emit('throw_money', { value: num });
    if ($('#throw-money-list li').length >= 50) {
        $('#throw-money-list li:last-child').remove();
    }
    $('#throw-money-list').prepend(
        $('<li>').text(num + '円を投げ込みました！')
    )
}

function sendWish() {
    console.log('お願いの送信')
    const name = $('#wish-form [name=name]').val();
    const wish = $('#wish-form [name=wish]').val();
    console.log(name, wish)
    if (!wish) {
        return;
    }
    socket.emit('send_wish', { name: name || '名無し', wish: wish, time: new Date() });
}
```

socket.ioはon関数で受信、emit関数で送信時の動作を定義することができます。
両関数は第一引数で名前を設定することができ、フロントエンドとバックエンドで同じ名前のon-emitの対を作ることで、双方向データ通信を実現しています。

``receive_money``は現在の合計賽銭額が変更された時に発火するon関数です。
\#moneyをアニメーションさせながら書き換えているだけです。

``receive_throw```は賽銭が投げ入れられた時に発火します。
こちらも#throw-money-listが50件までに収まるように追加された額をリストに追加しているだけ。

``receive_money_list``は``receive_throw``と一見同じように見えますが、こちらはサーバーが起動してから現在までの奉納の履歴を取得して発火します。throwは「ユーザーがサイトにアクセスした後に、だれかが賽銭を投げ入れたとき」に発火するので、自分がアクセスしたときにはリストは空の状態ですが、こちらはサーバー起動時からの履歴を表示することができます。

``receive_throw_time_by_amount``は、上記で用いた方法を応用して、サーバーが起動してから賽銭箱に何回何円を入れたのかを取得して発火します。

``receive_wish``もまた同様の方法で、ユーザーが入力したお願いを取得して発火します。

``receive_worshipers``は、現在サイトを閲覧しているユーザー数を取得して発火します。
人数を書き換えているだけです。

続いてemit関数ですが、全てボタンのクリックイベントに紐づけてあります。

``throw_money``は賽銭を投げ込む動作です。賽銭の額を引数にとり、バックエンド側にemit関数として送信しています。
送信履歴をフロント側で書き換えることで、「hoge円を投げ込みました！」と「hoge円が投げ込まれました！」の文書の差を作っています。
これはバックエンド側でemitする対象を選択することが出来る機能によるものです。後述します。

``send_wish``は単純にバックエンド側にお願いを送信するものです。

##バックエンド

```javascript:app.js
const express = require('express')
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);

let num = 0;
const moneyList = [];
const wishList = [];
const throwTimeByAmount = {
    '1': 0,
    '5': 0,
    '10': 0,
    '50': 0,
    '100': 0,
    '500': 0,
    '1000': 0,
    '5000': 0,
    '10000': 0
}

app.use('/', express.static('public'))

io.on('connection', (socket) => {
    console.log('socket connected', socket.client.conn.server.clientsCount);
    io.emit('receive_money', {value: num})
    io.emit('receive_wish', wishList)
    io.emit('receive_money_list', moneyList)
    io.emit('receive_worshipers', socket.client.conn.server.clientsCount)
    io.emit('receive_throw_time_by_amount', throwTimeByAmount)

    socket.on('throw_money', (obj) => {
        console.log('賽銭が投げられました', obj)
        moneyList.push({amount: obj.value});
        num += obj.value;
        throwTimeByAmount[obj.value]++; 
        io.emit('receive_money', {value: num})
        io.emit('receive_money_list', moneyList)
        io.emit('receive_throw_time_by_amount', throwTimeByAmount)
        socket.broadcast.emit('receive_throw', {value: obj.value})
    })

    socket.on('send_wish', (obj) => {
        console.log('お願いが送信されました', wishList, obj)
        wishList.push(obj);
        io.emit('receive_wish', wishList)
    })

    socket.on('disconnect', () => {
        io.emit('receive_worshipers', socket.client.conn.server.clientsCount)
    })
    
})

http.listen(process.env.PORT || 3000, () => console.log('Server Open'))
```

``const io = require('socket.io')(http);``とおまじないをかけることで、ioオブジェクトが使えます。

ただ、バックエンド側では最初に``io.on('connection', (socket) => {})``を定義して、接続が確立したことを確認する必要があります。
コールバック関数の中にon-emit関数を書くことで、フロント側と同様に使えます。

最初数行のemit関数では、単純にバックエンドで管理されている変数をフロント側に送信しています。

```javascript:app.js
socket.on('throw_money', (obj) => {
    console.log('賽銭が投げられました', obj)
    if (moneyList.length >= 50) {
        moneyList.shift();
    }
    moneyList.push({ amount: obj.value });
    num += obj.value;
    throwTimeByAmount[obj.value]++;
    io.emit('receive_money', { value: num })
    io.emit('receive_money_list', moneyList)
    io.emit('receive_throw_time_by_amount', throwTimeByAmount)
    socket.broadcast.emit('receive_throw', { value: obj.value })
})
```
先ほど「バックエンド側でemitする対象を選択することが出来る機能」があると記しましたが、さっそく``throw_money``で用いています。
単純に``socket.emit()``としてしまうと、自分が送信した賽銭履歴も自分に返ってきてしまい、「投げ入れました」と「投げ込まれました」が同時に出力されてしまうのですが、``socket.broadcast.emit``とすることで、**自分以外にemit**することができます。
この機能にはほかにもバリエーションがあるので、公式ドキュメントで探してみてください。

その他のemit関数は説明するまでもないと思いますので割愛します。

#感想
小学生の時に、Java appletで実装されていたチャットでインターネットに触れた私としては、こんなに簡単にリアルタイム双方向通信が実装できるとは思わず、なんとも隔世の感を禁じえません。

socket.io、すっげー（小並感）

#参考資料
* [socket.io 公式](https://socket.io/)
* [Node.js + Express + Socket.ioで遊んでみた - RAKUS Developers Blog | ラクス エンジニアブログ](http://tech-blog.rakus.co.jp/entry/2018/03/14/084947)