リアルタイムで更新するランキング画面を”手抜き”実装してみた

# はじめに

ゲームセンターによく「最後にスコアのランキング表示」が出るゲームがあったと思うのですが、
それをちょっとWeb上で実装したいな〜って思ったので実装してみました

▼ イメージ
<img width="500px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/e18f9818-b1ac-e90b-954b-27dab3966699.jpeg"/>

ただ、単純にランキング表示するだけだとつまらない（？）ので、
WebSocket通信も使って**「外部からスコアデータを受信したらランキングをリアルタイムで更新」**も作ってみました

# 完成イメージ

![ranking-websocket.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153051/69e1a123-3fc6-2289-96ad-16f7ad6b546b.gif)
新規データがどれかわかりやすいように無駄に色つけてみたりしてますが、まぁそこはお好みでw
コードは [GitHub](https://github.com/RyBB/RealtimeRanking) に上げてます

にしても、画面リロードなしでしかも別のブラウザから画面いじれるってすごい仕組みだなぁ・・

# 構成図

わりとシンプルです
ランキング表示の画面はVueでサクっと作ってます。ほぼテンプレそのまま
データ送信は開発者ツールのコンソール上でサクッと。サーバー側はNode.jsでサクサクっと

> お惣菜や唐揚げが手抜き料理ってなるなら、今回の実装こそまさに**手抜き**実装

ただこれが言いたかっただけだったり・・・

<img src="https://docs.google.com/drawings/d/e/2PACX-1vQwhOHYJxMyVYVALG2ZHLdpVnh2aT_gSFsnHhMiFxrgIqGnoZhu58TKeVqNzJK8Ht-hgjAgb8NKX68t/pub?w=553&amp;h=473">

# フロント側の準備

とりあえずvue-cliでファイル群を生成します
vue-cliが入ってない方はまずvue-cliをインストールしてください
インストール方法はググればすぐ出てきます（手抜き）

```bash
# vueのテンプレ生成
$ vue create sample
## defaultを選択してできあがるまでちょっと待つ

# フォルダ移動
$ cd sample
```

こんな感じのディレクトリ構造になってると思います

```
.
├── README.md
├── babel.config.js
├── node_modules/
├── package.json
├── public/
├── src
│   ├── App.vue ⭐修正
│   ├── assets
│   ├── components/
│   │    └── HelloWorld.vue ⭐修正
│   └── main.js
└── yarn.lock
```

修正するのは ```.vue```ファイルの2つだけ

```vue:App.vue
<template>
  <div id="app">
    <HelloWorld/>   ←ここらへんをシンプルに
  </div>
</template>
 ~ 他は変更箇所ないので省略 ~
```

デフォルトだといらない要素があるので削除します

# HelloWorld.vue の修正

今回のメインファイルなのでそこそこ手を加えます。全体は [GitHub](https://github.com/RyBB/RealtimeRanking/blob/master/src/components/HelloWorld.vue) で確認してください！
HTML、JS、CSSごとに説明します！

## HTML: ランキングのテーブル表示

ランキング自体は ```<table></table>``` で作ってます
要素はデータ追加に合わせてどんどん増えていくので、 ```<tr></tr>``` を **v-for** でスコアデータ分ループさせるようにしてます

あとで、このスコアデータの配列にデータを追加する仕組みを作るので、
結果的に**「データを追加する → スコアデータの配列が増える → v-for でループする数が増える → 行が増える」**となります
ここらへんの動的処理がVueだとめっちゃ楽な気がします

```vue:HelloWorld.vue
<template>
  <div class="hello">
    <table class="ranking-table">
      <thead>
        <tr>
          <th>RANK</th>
          <th>NAME</th>
          <th>SCORE</th>
        </tr>
      </thead>
      <transition-group tag="tbody">
        <tr v-for="(post, index) in allScoreData" :key="post.id">
          <td :class="{newRecord: post.isNew}">{{index + 1}} 位</td>
          <td :class="{newRecord: post.isNew}">{{post.name}}</td>
          <td :class="{newRecord: post.isNew}">{{post.score}}</td>
        </tr>
      </transition-group>
    </table>
  </div>
</template>
〜〜
```

データを追加した際に、ランキングの行の追加でアニメーションを設定してますが、
こちらにはVueの [**transition**](https://jp.vuejs.org/v2/guide/transitions.html) を使っています

> transizion
> 自分もよくわかっていないですが、Vueでいい感じにアニメーション（トランジション）を設定できるやつっぽいw

## JS: Websocket通信

JSでは主にWebSocket通信と受信したデータを配列に格納する処理をしてます

```vue:HelloWorld.vue
〜〜
<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      allScoreData: [], // {id: Number, name: String, score: Number, isNew: Boolean}
    }
  },
  mounted() {
    const socket = new WebSocket('ws://localhost:5001');
    socket.onmessage = event => {
      this.addData(JSON.parse(event.data));
    }
  },
  methods: {
    addData(data) {
      // 既存のスコアのisNewをfalseにする (= 新規レコードのフラグを外す)
      const list = this.allScoreData.map(val => {
        return {
          id: val.id,
          name: val.name,
          score: val.score,
          isNew: false
        };
      });

      list.push({
        id: list.length + 1,
        name: data.name,
        score: Number(data.score),
        isNew: true
      });

      // 並び順をスコアの降順に変更
      this.allScoreData = list.sort((a,b) => (a.score > b.score ? -1 : 1))
    },
  }
}
</script>
〜〜
```

今回、WebSocketではNAMEとSCOREの2種類のデータを扱うので、JSONにしてやりとりしてます
サーバー上で動かすNode.jsは適当なポート（5001）でやってます

## CSS: アニメーション

transitionの設定項目についてはドキュメントに書いてあります
適当にパラメータの数値をいじるとアニメーションが変化します

```vue:HelloWorld.vue
〜〜
<style>
.ranking-table {
  margin: auto;
  width: 50vw;
}
.newRecord {
  background-color: #ff7f50;
}

/* animation */
.v-leave-active,
.v-enter-active {
  transition: opacity .5s, transform .5s;
}
.v-leave-to,
.v-enter {
  opacity: 0;
  transform: translateX(200px);
}
.v-leave,
.v-enter-to {
  opacity: 1;
}
.v-move {
  transition: transform .5s;
}
</style>
```

# サーバー側の準備

Node.jsで動かすプログラムもシンプル！
Websocket用ライブラリの **ws** をつかって、ひたすら待機しながらデータ来たら横に流すだけ

```javascript:server.js
const ws = require('ws').Server;
const wsServer = new ws({ port: 5001 });

wsServer.on('connection', server => {
  server.on('message', message => {
    wsServer.clients.forEach(client => {
      client.send(message);
    });
  });
});

console.log('websocket起動中...');
```

接続するclient(ブラウザ)が複数あるので、forEach を使って接続してる全部のclientにデータを送り返してます
あとはこれは ```Node server.js``` で実行するのみ

# 補足

今回は送られてきたデータをただリアルタイムにランキング表示するだけなのですが、
実際に使おうとすると **データを保存したい** ってなると思います
（現状ではランキング側のブラウザを再読み込みするとリセットされるので）

その場合は ~~AWSのDynamo~~ WebDBとして扱いやすい [**kintone**](https://kintone.cybozu.co.jp/) とか使うといいですよ！！（宣伝）

kintoneの使い方はこちらでまとめてます！！

> ▼ kintoneの使い方 (データベース編)
> https://qiita.com/RyBB/items/daabb9b60d804ee2242f

# おわりに

さくっと記事書くつもりがなんだかんだ説明が長くなってしまった・・・

今回はWebsocketにローカルのNode.jsサーバー使ってますが、AWSのAPI GatewayもWebSocketに使えるので
「API Gateway + Lambda + DynamoDB」って構成も可能です！（従量課金ですが）

ランキング画面は外部ディスプレイとかに表示しっぱなしにしておいて、IoTデバイスとかでデータ追加したらこっちも変わる！とか面白そう

それでは！≧(+･` ཀ･´)≦