21
17

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-18

はじめに

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

create-nuxt-appのダウングレードの方法もありますが、なにやら想いが込められているようなので。

前提

  • create-nuxt-app: v3.2.0
  • Nuxt on Docker(ただの趣味なのでDocker使わなくてもいい内容です)

最終形態

最終形態はこちらです。
test.gif

ソースコードはこちらです。 => 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で動かすためにDockerfiledocker-compose.ymlを準備します。

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"]
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

$ 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の設定を追加します。

nuxt.config.js
export default {
  ...
  serverMiddleware: {
    'api': '~/api'
  },
  ...
}

api/ディレクトリ配下にサーバーサイドのコードを作ります。

$ mkdir api
$ touch api/index.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.emitnew-msgイベントをブロードキャストします。

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

クライアントサイドをコーディング

pages/chat/index.vueファイルを作成・編集します。

$ mkdir pages/chat
$ touch pages/chat/index.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>

デモ1

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

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

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

test.gif

いえーい。

$ docker-compose down

ルームをコーディング

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

image.png

出典:Rooms | Socket.IO

今回は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をブロードキャストする」機能の改修を行います。

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.idhttp://localhost:3000/chat/xxxxxx部分を取得することができます。

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>

以上です!

デモ2

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

test.gif

まとめ

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

Reference

21
17
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
17