4
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

WebSocketを利用したチャット画面を作ってみる(Vuetify)

Last updated at Posted at 2020-12-06

概要

WebSocketのサーバを用意したので、今度はクライアントを作ってみることに。
Vuetifyを使って、簡単なチャット画面を作ってみる。

サーバについては↓の記事を参照のこと。
https://qiita.com/donraku/items/d4071fcb1802600bda67

結論

WebSocketを利用したチャット画面を作って、リアルタイムのやり取りができることを確認できた。

画面を3つ出して動かしたイメージは↓。

20201206.gif

詳細

  • チャット用のWebSocketのサーバが稼働していること
  • Vue.js + Vuetify の環境が用意済みであること

が動かす前提になるけど、その辺の詳細は省略。
vue-router で下記の画面を表示するように設定。

画面レイアウト部

上部にチャットのログ、下部に名前とメッセージの入力を配置。
送信ボタン、または、Enterでメッセージを送信することが可能とする。

画面イメージは上記の通り。

ChatSample.vue
<template>
  <v-app>
    <v-main>
      <v-container>
        <v-row class="text-center">
          <v-col cols="12">
            <v-card
              class="mx-auto my-12"
              elevation="2"
              max-width="500"
              color="grey lighten-5"
            >
              <v-card-title>Chat Sample</v-card-title>
              <v-divider></v-divider>
              <v-card-text
                ><v-row>
                  <v-col cols="12">
                    <v-container
                      ref="scrollTarget"
                      style="height: 450px"
                      class="overflow-y-auto"
                    >
                      <v-row v-for="(msg, i) in messages" :key="i" dense>
                        <v-col v-if="msg.ws_key != ws_key">
                          <div class="balloon_l">
                            <div class="face_icon">
                              <v-avatar :color="msg.avatar_color">
                                <span class="white--text">
                                  {{ msg.name }}
                                </span>
                              </v-avatar>
                            </div>
                            <p class="says">
                              {{ msg.message }}
                            </p>
                          </div>
                        </v-col>
                        <v-col v-else>
                          <div class="balloon_r">
                            <div class="face_icon">
                              <v-avatar :color="msg.avatar_color">
                                <span class="white--text">
                                  {{ msg.name }}
                                </span>
                              </v-avatar>
                            </div>
                            <p class="says">
                              {{ msg.message }}
                            </p>
                          </div>
                        </v-col>
                      </v-row>
                    </v-container>
                  </v-col>
                </v-row>
              </v-card-text>
              <v-divider></v-divider>
              <v-card-text>
                <v-row>
                  <v-col cols="3">
                    <v-text-field
                      label="名前"
                      v-model="name"
                      clearable
                    ></v-text-field>
                  </v-col>
                  <v-col>
                    <v-text-field
                      autofocus
                      label="メッセージ ※Enterでも送信できるよ"
                      v-model="message"
                      clearable
                      @keyup.enter="send_onClick"
                    ></v-text-field>
                  </v-col>
                </v-row>
                <v-btn class="info" small @click="send_onClick">
                  <v-icon>mdi-play</v-icon>送信
                </v-btn>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-main>
  </v-app>
</template>

スクリプト部

初期表示(created)で、WebSocketのサーバに接続。
オープンしたら、初回メッセージを送って、1回通信を行うようにしている。

メッセージ送信ではjson形式で情報をまとめて送信する。
メッセージ受信時、自分のメッセージなら右側に表示して、他人のメッセージなら左側に表示。

ChatSample.vue
<script>
// 初回メッセージ
const MSG_START = "###start###";

export default {
  name: "App",

  components: {},

  data: () => ({
    // UI items
    name: "名無し",
    message: "おはようございます。",

    // Vars
    connection: null,
    messages: [],
    ws_key: null,
    avatar_color: "",
  }),

  created: function () {
    // websocket 接続
    this.connection = new WebSocket("ws://localhost:8002/ws");

    // アバターの色をランダムに決める
    let random_color = "#";
    for (var i = 0; i < 6; i++) {
      random_color += "0123456789abcdef"[(16 * Math.random()) | 0];
    }
    this.avatar_color = random_color;

    const vm = this;

    // websocket オープン
    this.connection.onopen = function () {
      // 初回メッセージを送信する
      vm.sendMessageData(vm.avatar_color, MSG_START, vm.name);
    };

    // websocket メッセージ受信
    this.connection.onmessage = function (event) {
      // sample: {"key": "lu0x0uw5sEQvehHYOMBfIA==", "message_data": "{\"avatar_color\":\"#3370a8\",\"message\":\"###start###\"}"}
      const data_json = JSON.parse(event.data);
      const ws_key = data_json.key;
      const msg_json = JSON.parse(data_json.message_data);

      // 初回メッセージの場合はサーバで発行したkeyを保持しておく
      if (msg_json.message == MSG_START) {
        if (vm.ws_key == null && vm.avatar_color == msg_json.avatar_color)
          vm.ws_key = ws_key;
        return;
      }
      // 初回以外は、jsonをリストに追加する
      vm.messages.push({
        ws_key,
        avatar_color: msg_json.avatar_color,
        message: msg_json.message,
        name: msg_json.name,
      });
    };
  },

  updated: function () {
    this.scrollToEnd();
  },

  methods: {
    // メッセージ送信ボタンクリック
    send_onClick: function () {
      if (this.message == "") return;
      this.sendMessageData(this.avatar_color, this.message, this.name);
      this.message = "";
    },

    // メッセージ情報を送信
    sendMessageData: function (avatar_color, message, name) {
      const msg_json = {
        avatar_color,
        message,
        name,
      };
      this.connection.send(JSON.stringify(msg_json));
    },

    // チャットログを一番下までスクロール
    scrollToEnd() {
      this.$nextTick(() => {
        const chatLog = this.$refs.scrollTarget;
        if (!chatLog) return;
        chatLog.scrollTop = chatLog.scrollHeight;
      });
    },
  },
};
</script>

CSS部

チャットのログ部分は、ちょうど良いコンポーネントが見当たらなかったので、
いろいろ探してCSSで表現することに。
そのCSSが↓。

ChatSample.vue
<style scoped>
.balloon_l,
.balloon_r {
  margin: 10px 0;
  display: flex;
  justify-content: flex-start;
  align-items: flex-start;
}
.balloon_r {
  justify-content: flex-end;
}
.face_icon img {
  width: 80px;
  height: auto;
}
.balloon_r .face_icon {
  margin-left: 25px;
}
.balloon_l .face_icon {
  margin-right: 25px;
}
.balloon_r .face_icon {
  order: 2 !important;
}
.says {
  max-width: 300px;
  display: flex;
  flex-wrap: wrap;
  position: relative;
  padding: 10px;
  border-radius: 12px;
  background: #8ee7b6;
  box-sizing: border-box;
  margin: 0 !important;
  line-height: 1.5;
  /*   align-items: center; */
}
.says p {
  margin: 8px 0 0 !important;
}
.says p:first-child {
  margin-top: 0 !important;
}
.says:after {
  content: "";
  position: absolute;
  border: 10px solid transparent;
  margin-top: -3px;
}
.balloon_l .says:after {
  left: -26px;
  border-right: 22px solid #8ee7b6;
}
.balloon_r .says:after {
  right: -26px;
  border-left: 22px solid #8ee7b6;
}
</style>
4
11
0

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
4
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?