Nuxt.js + Socket.ioでルームチャットアプリを作る ~ サーバーフレームワーク選べなくなっててつまずいた夏

# はじめに
Nuxt + Express + socket.ioでチャットみたいなリアルタイムWebアプリが作れるよ、という記事はいっぱいあったのですが、`create-nuxt-app`さんが3.Xにアップデートしてサーバーサイドフレームワーク（つまりExpress）を選択できなく（しなくてよく）なり途方にくれていたので記事に残します。
この方法だと、nodeもう一つ立ち上げてるだけっちゃだけなんだよなぁ...

`create-nuxt-app`のダウングレードの方法もありますが、なにやら[想い](https://github.com/nuxt/create-nuxt-app/issues/563)が込められているようなので。

# 前提
- `create-nuxt-app`: v3.2.0
- Nuxt on Docker（ただの趣味なのでDocker使わなくてもいい内容です）

# 最終形態
最終形態はこちらです。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/37974df9-f342-5c85-13d3-52e851a5772b.gif)

ソースコードはこちらです。 => [at946/demo_nuxt-socket.io_chat](https://github.com/at946/demo_nuxt-socket.io_chat)

# Nuxtアプリ作成
まず`create-nuxt-app`でNuxtアプリを作ります。（アプリ名は`chat`にしました）

```
$ docker run --rm -it -v `pwd`:/app -w /app node yarn create nuxt-app chat

Project name: chat
Programming language: JavaScript
Package manager: Yarn
UI framework: Bulma
Nuxt.js modules: None
Linting tools: None
Testing framework: None
Rendering mode: Universal (SSR / SSG)
Deployment target: Server (Node.js hosting)
Development tools: jsconfig.json
```
これでカレントディレクトリに`chat/`ディレクトリができます。ここがアプリ本体なので`chat/`配下をホームディレクトリとして活動していきます。

```
$ cd chat
```

# Docker系を準備
NuxtアプリをDockerで動かすために`Dockerfile`と`docker-compose.yml`を準備します。

```dockerfile:Dockerfile
FROM node:14.8

ENV HOME=/app     \
    LANG=C.UTF-8  \
    TZ=Asia/Tokyo \
    HOST=0.0.0.0

WORKDIR ${HOME}
COPY package.json ${HOME}
COPY yarn.lock ${HOME}

RUN apt -y update && \
    apt -y upgrade && \
    yarn install

COPY . ${HOME}
EXPOSE 3000 3001
CMD ["yarn", "run", "dev"]
```

```yaml:docker-compose.yml
version: "3"

services:
  nuxt:
    build: .
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - 3000:3000
      - 3001:3001
```

3000番ポートはNuxtアプリ、3001番ポートはSocket.ioのサーバーサイド用のポートとして使います。

Dockerイメージをビルドしてコンテナを立ち上げます。

```
$ docker-compose build
$ docker-compose up -d
```

`http://localhost:3000`にアクセスしてNuxtアプリが立ち上がればここまではOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/0599c9c8-f9c1-c2b2-63ca-3559b7112713.png)

```
$ docker-compose down
```

# socket.ioでチャット機能を作る
## socket.ioのインストール
まずはsocket.ioをインストールします。サーバーサイドのライブラリの`socket.io`とクライアントサイドのライブラリの`socket.io-client`が必要です。

```
$ docker-compose run --rm nuxt yarn add socket.io socket.io-client
```

## サーバーサイドをコーディング
`create-nuxt-app@3.X`で作ったアプリでは`server/`ディレクトリがないですね。その代わりにserverMiddleware使うといいらしいっす。これは外部APIとかと簡単に連携できるようにってやつらしいです。

まず`nuxt.config.js`にserverMiddlewareの設定を追加します。

```js:nuxt.config.js
export default {
  ...
  serverMiddleware: {
    'api': '~/api'
  },
  ...
}
```
`api/`ディレクトリ配下にサーバーサイドのコードを作ります。

```
$ mkdir api
$ touch api/index.js
```

```js:api/index.js
const app = require('express')()
const server = require('http').createServer(app)
const io = require('socket.io')(server)

io.on('connection', socket => {
  console.log(`socket_id: ${socket.id} is connected.`)

  // send-msgイベントを受け取ったらブロードキャストする
  socket.on('send-msg', msg => {
    socket.emit('new-msg', msg)
    console.log(`receive message: ${JSON.stringify(msg)}`)
  })
})

server.listen(3001)
```

シンプルに**send-msg**イベントをクライアントから受け取ったら`socket.emit`で**new-msg**イベントをブロードキャストします。

なのでクライアントはチャットを送信するときに**send-msg**イベントを送ればよくて、サーバーサイドから**new-msg**イベントを受け取ったら**msg**の内容を表示すればいい、ってことになります。

## クライアントサイドをコーディング
`pages/chat/index.vue`ファイルを作成・編集します。

```
$ mkdir pages/chat
$ touch pages/chat/index.vue
```

```vue:pages/chat/index.vue
<template>
  <section class="section">
    <div class="field">
      <div class="control">
        <input class="input" type="text" v-model="msg" @keypress.enter.exact="sendMessage">
      </div>
    </div>
    <article class="media" v-for="(msg, index) in msgs" :key="index">
      <div class="media-content">
        <div class="content">
          <p>
            <strong>{{ msg.name }}</strong>
            <br>
            {{ msg.text }}
          </p>
        </div>
      </div>
    </article>
  </section>
</template>

<script>
import io from 'socket.io-client'

export default {
  data() {
    return {
      msg: '',
      msgs: [],
      socket: ''
    }
  },
  mounted() {
    this.socket = io('http://localhost:3001')
    this.socket.on('new-msg', msg => {
      this.msgs.push(msg)
    })
  },
  methods: {
    sendMessage() {
      this.msg = this.msg.trim()
      if (this.msg) {
        const message = {
          name: this.socket.id,
          text: this.msg,
        }
        // イベント元はブロードキャストを受けないので自分でmessageを追加する
        this.msgs.push(message)
        // send-msgイベントでmessageをサーバーサイドに投げる
        this.socket.emit('send-msg', message)
        this.msg = ''
      }
    }
  }
}
</script>
```

# デモ１
ここまでできたら一度Dockerイメージをビルドして起動してみましょう。

```
$ docker-compose build
$ docker-compose up -d
```

複数タブで`http://localhost:3000/chat`にアクセスします。片方でメッセージを送信すると、もう片方にも表示されます！！

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/f5cec327-6ab6-6f97-ede9-48479e8f9e91.gif)

いえーい。

```
$ docker-compose down
```

# ルームをコーディング
ここまででSocket.ioを使ってリアルタイムにチャットすることができるようになりましたが、今のままでは誰でもこのサイトにアクセスしたらチャットが流れてくるようになっちゃいます。
特定の人たちとだけやり取りをしたい場合はSocket.ioのルームを使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/bed591ea-4c88-4f2d-759f-35d2aedc57cd.png)

出典：[Rooms | Socket.IO](https://socket.io/docs/rooms/)

今回は`http://localhost:3000/chat/1`とか`http://localhost:3000/chat/2`とか、パスパラメーターでルームを分けて、ルーム内だけでチャットできるようにしてみます。

処理の流れは

1. clientからserverにルーム参加イベントを送る
2. serverがclient（socket）をルームに登録しておく
3. clientからserverにルームにsend-msgイベントを送る
4. serverがルームの全てのsocketにmsgをブロードキャストする

です。

## サーバーサイドをコーディング
サーバーサイドでは、「**clientからのルーム参加イベントを受けとり、ルームに参加させる**」機能の追加と、「**send-msgイベントでルームIDを受け取り、そのルームに対してmsgをブロードキャストする**」機能の改修を行います。

```diff:api/index.js
  io.on('connection', socket => {
    console.log(`socket_id: ${socket.id} is connected.`)
+
+   // joinイベントを受け取ったらルームに登録する
+   socket.on('join', roomId => {
+     socket.join(roomId)
+     console.log(`socket_id: ${socket.id} joined in room ${roomId}`)
+   }) 
+
-   // send-msgイベントを受け取ったらブロードキャストする
-   socket.on('send-msg', msg => {
-     socket.broadcast.emit('new-msg', msg)
+   // send-msgイベントを受け取ったら指定のルームにブロードキャストする
+   socket.on('send-msg', (msg, roomId) => {
+     socket.to(roomId).emit('new-msg', msg)
      console.log(`receive message: ${JSON.stringify(msg)}`)
    })
  })

server.listen(3001)
```
クライアントから`roomId`を受け取って処理してます。

`socket.join`はルーム登録の処理です。引数にルームの名前を渡すだけの簡単メソッドですが、今回はクライアントから受け取るパスパラメーターの値をルーム名にします。

ルームへのブロードキャストは`socket.to(room name).emit(...)`でできます。これも先程と大きくは変わらないので理解しやすいですね。

## クライアントサイドをコーディング
クライアントサイドでは、「**Socketコネクションと同時にルームに参加する**」機能の追加と「**メッセージ送信時にroomIdを送る**」機能の改修をします。

まず、`roomId`となるパスパラメーターを受け取るためにファイル名を変更してからファイルを編集していきます。

```
$ mv pages/chat/index.vue pages/chat/_id.vue
```
これで`this.$route.params.id`で`http://localhost:3000/chat/xxx`の`xxx`部分を取得することができます。

```diff:pages/chat/_id.vue
  <script>
  import io from 'socket.io-client'

  export default {
    data() {
      return {
        msg: '',
        msgs: [],
        socket: ''
      }
    },
    mounted() {
      this.socket = io('http://localhost:3001')
+     this.socket.emit('join', this.$route.params.id)
      this.socket.on('new-msg', msg => {
        this.msgs.push(msg)
      })
    },
    methods: {
      sendMessage() {
        this.msg = this.msg.trim()
        if (this.msg) {
          const message = {
            name: this.socket.id,
            text: this.msg,
          }
          this.msgs.push(message)
          // send-msgイベントでmessageをサーバーサイドに投げる
-         this.socket.emit('send-msg', message)
+         this.socket.emit('send-msg', message, this.$route.params.id)
          this.msg = ''
        }
      }
    }
  }
  </script>
```
以上です！

# デモ２
デモしてみましょう。
上の２画面が`http://localhost:3000/chat/1`に、下の2画面が`http://localhost:3000/chat/2`にアクセスしています。
それぞれチャットしたい相手とだけチャットできていますね！

![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/37974df9-f342-5c85-13d3-52e851a5772b.gif)

# まとめ
この記事では`create-nuxt-app`のバージョンアップで過去記事の通りにはできなかったNuxt+Socket.ioのリアルタイムチャットをlocalで動くまで頑張ってみました。
普通に数日間つまづいてたので誰かの助けになれば幸いです！
また、NuxtやSocket.ioについてはBeginnerなので、こうしたほうがよりよいよ！などアドバイスいただけると嬉しいです。

# Reference
- [Socket.IO](https://socket.io/docs/)
- [nuxt/create-nuxt-app: Create Nuxt.js App in seconds.](https://github.com/nuxt/create-nuxt-app)
- [API: serverMiddleware プロパティ - NuxtJS](https://ja.nuxtjs.org/api/configuration-servermiddleware/)
- [senchalabs/connect: Connect is a middleware layer for Node.js](https://github.com/senchalabs/connect#appusefn)
- [Nuxt.js (Vue.js) + Express + Socket.IO でリアルタイムWeb (チャット) を体験する - Qiita](https://qiita.com/rema424/items/c1ef92625ecaf4421e6f)
- [Nuxt.js + socket.ioでルーム機能付きチャットアプリケーションのサンプル | 技術と物語](https://shoopon.com/archives/102)
- [socket.io - Dynamically joining rooms in Vue SocketIO - Stack Overflow](https://stackoverflow.com/questions/54629494/dynamically-joining-rooms-in-vue-socketio)