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

##夏休みの自由工作
チャットルームを作れる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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65643/905c6894-9f8c-97f1-a048-0efda5b5d82c.png)

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

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

<details><summary>コード全体</summary><div>

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

```
</div></details>

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

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