3
3

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 5 years have passed since last update.

VuejsとFirebaseで作る認証付きチャットルーム

Last updated at Posted at 2019-08-16

夏休みの自由工作

チャットルームを作れるWEBアプリをなるべく楽して作ってみる。

使用したもの

  • vue-cli
  • Vuejs
  • Vuetify
  • Firebase
  • Firestore
  • Authentication
  • hosting

手順

vue-cliでプロジェクトを作ります。

ついでにvuetifyも追加します。

$ vue create my-chatroom
$ vue cd mychatroom
$ vue add vuetify

firebaseの設定

あらかじめfirebaseのconsoleでプロジェクトを作っておきます。
firebase-tools(https://firebase.google.com/docs/cli?hl=ja)
を使ってfirebaseを使う準備をします。
firebase init するときについでにhostingの設定もしておきます。

$ npm install -g firebase-tools
$ firebase login
$ firebase init

とりあえずデプロイ

$ npm run build
$ firebase deploy

表示されたアドレスでVuetifyが表示されればOK

コーディング

Firebaseのコンソールからアプリを追加して認証情報を持ってくる。
firebasesetting.png

認証情報をファイルにコピペする。

Vuetifyのリストとか使いまくって実装

コード全体
App.vue
<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title class="headline text-uppercase">
        <span class="font-weight-light">MY CHATROOM</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn v-if="logined" secondary @click="googleLogout">
        <v-icon>mdi-google</v-icon>:logout
      </v-btn>
    </v-app-bar>
    <v-content>
      <v-container class="fill-height" fluid v-if="!logined">
        <v-row align="center" justify="center">
          <v-col cols="12" sm="8" md="4">
            <v-card class="elevation-12">
              <v-toolbar color="primary" dark flat>
                <v-toolbar-title>ログイン</v-toolbar-title>
              </v-toolbar>
              <v-card-text>Googleアカウントでログインしてください。</v-card-text>
              <v-card-actions>
                <v-spacer></v-spacer>
                <v-btn color="primary" @click="googleLogin()">Google Login</v-btn>
                <v-spacer></v-spacer>
              </v-card-actions>
            </v-card>
          </v-col>
        </v-row>
      </v-container>

      <v-container fluid v-if="logined" transition="scroll-x-transition">
        <v-row align="start" justify="center">
          <v-col xs="12" sm="6" :class="xsDisplayRooms">
            <v-list dense>
              <v-list-item>
                <v-text-field v-model="inputRoomName" :counter="18" label="Room Name" required></v-text-field>
                <v-btn color="primary" :disabled="!inputRoomName" @click="createRoom">部屋を作る</v-btn>
              </v-list-item>
              <v-list-item
                @click="roomId = item.id;roomName = item.name"
                v-for="item in rooms"
                :key="item.id"
              >
                <v-list-item-content>
                  <v-list-item-title>
                    {{item.name}}
                    <v-icon class="float-right">mdi-chevron-right</v-icon>
                  </v-list-item-title>
                </v-list-item-content>
              </v-list-item>
            </v-list>
          </v-col>
          <v-col xs="12" sm="6">
            <v-card :loading="cardLoading">
              <v-card-title>
                <v-icon
                  v-if="$vuetify.breakpoint.xs"
                  class="mr-2"
                  @click="roomId = null"
                >mdi-arrow-left</v-icon>
                {{roomName}}
              </v-card-title>
              <v-card-text id="messageArea" style="height:60vh;overflow:scroll">
                <v-list dense rounded>
                  <v-list-item v-for="item in messages" :key="item.id">
                    <v-list-item-content :class="alignMessage(item.userName)" color="primary">
                      <v-list-item-title class="py-4">{{item.message}}</v-list-item-title>
                      <v-list-item-subtitle>{{item.userName + '('+displayTime(item.createdAt.seconds)+ ')'}}</v-list-item-subtitle>
                    </v-list-item-content>
                  </v-list-item>
                </v-list>
              </v-card-text>
              <v-spacer></v-spacer>
              <v-card-actions v-if="roomId">
                <v-text-field
                  v-model="inputMessage"
                  max="200"
                  label="Message"
                  required
                  :loading="messageLoading"
                  :disabled="messageLoading"
                  v-on:keydown.enter="enterAddMessage"
                ></v-text-field>
                <v-btn color="primary" :disabled="!inputMessage" @click="addMessage">送信</v-btn>
              </v-card-actions>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
import moment from "moment";
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/auth";

const firebaseConfig = {
  apiKey: "*************************",
  authDomain: "*************************",
  databaseURL: "*************************",
  projectId: "*************************",
  storageBucket: "*************************",
  messagingSenderId: "*************************",
  appId: "*************************"
};
firebase.initializeApp(firebaseConfig);
const firebaseApp = firebase.firestore();
const firebaseAuth = firebase.auth();

export default {
  name: "App",
  data: () => ({
    drawer: null,
    logined: false,
    inputRoomName: null,
    roomId: null,
    rooms: [],
    roomName: null,
    messages: [],
    inputMessage: null,
    userName: null,
    messageLoading: false,
    cardLoading: false
  }),
  props: {
    source: String
  },
  mounted() {
    firebaseAuth.onAuthStateChanged(user => {
      console.log(user);
      if (!user) {
        this.deleteLoginUser();
      } else {
        this.userName = user.displayName;
        this.logined = true;
        firebaseApp
          .collection("room")
          .orderBy("createdAt", "desc")
          .onSnapshot(res => {
            this.rooms = [];
            res.docs.forEach(elem => {
              let room = elem.data();
              room.id = elem.id;
              this.rooms.push(room);
            });
          });
      }
    });
  },
  watch: {
    roomId() {
      if (this.roomId) {
        this.cardLoading = true;
        firebaseApp
          .collection("room")
          .doc(this.roomId)
          .collection("messages")
          .orderBy("createdAt")
          .onSnapshot(res => {
            this.messages = [];
            res.docs.forEach(elem => {
              let room = elem.data();
              this.messages.push(room);
              this.$nextTick(() => {
                this.scrollBottom();
              });
            });
            this.cardLoading = false;
          });
      }
    }
  },
  computed: {
    xsDisplayRooms() {
      if (this.$vuetify.breakpoint.xs) {
        if (this.roomId) {
          return "overlay-slide overlay-out";
        } else {
          return "overlay-slide";
        }
      } else {
        return "";
      }
    }
  },
  methods: {
    displayTime(timestamp) {
      return moment(timestamp * 1000).format("YYYY/MM/DD HH:mm");
    },
    alignMessage(messageName) {
      if (messageName === this.userName) {
        return "text-right";
      }
    },
    enterAddMessage() {
      if (event.keyCode !== 13) return;
      this.addMessage();
    },
    createRoom() {
      const name = this.inputRoomName;
      if (name) {
        firebaseApp
          .collection("room")
          .add({
            name: name,
            createdAt: new Date(),
            createUser: this.userName
          })
          .then(res => {
            this.name = null;
          });
      }
    },
    addMessage() {
      this.messageLoading = true;
      const message = this.inputMessage;
      if (message) {
        console.log("add");
        firebaseApp
          .collection("room")
          .doc(this.roomId)
          .collection("messages")
          .add({
            message: message,
            createdAt: new Date(),
            userName: this.userName
          })
          .then(res => {
            this.inputMessage = null;
          })
          .finally(() => {
            this.messageLoading = false;
          });
      }
    },
    scrollBottom() {
      let container = this.$el.querySelector("#messageArea");
      container.scrollTop = container.scrollHeight;
    },
    googleLogin() {
      const provider = new firebase.auth.GoogleAuthProvider();
      firebaseAuth
        .signInWithPopup(provider)
        .then(result => {
          this.userName = result.user.displayName;
          this.logined = true;
        })
        .catch(error => {
          this.deleteLoginUser();
        });
    },
    googleLogout() {
      firebaseAuth.signOut().finally(() => {
        this.deleteLoginUser();
      });
    },
    deleteLoginUser() {
      this.userName = null;
      this.logined = false;
    }
  }
};
</script>
<style lang="scss">
.overlay-slide {
  position: absolute;
  z-index: 100;
  left: auto;
  right: auto;
  height: 100vh;
  background: #fff;
  transition: transform 0.5s ease;
}
.overlay-out {
  transform: translate(-100%);
}
</style>

  • FirestoreからonSnapshotでroomsとmessagesの変更を随時受け取りつつ反映。
  • フォーム送信すると自動的にリストが更新される。便利。
  • 下までスクロールする方法がvuetifyになかったので無理やりjsで実装。もっといい方法がありそう。
  • Vuexもvue-routerも使わないシンプルなアプリ。無駄にPWA対応。
  • Googleログインの場合user.displayNameで名前が拾える。
  • 時刻周りの表示はmomentjsに丸投げ

感想

2時間くらいでできたけど、一番難しかったのはスマホの時の表示をどうするかだった。
こんな感じでプロトタイプを作る>公開までが爆速に出来上がりそうなので結構良さそうだね。Firebaseは。

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?