夏休みの自由工作
チャットルームを作れる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のコンソールからアプリを追加して認証情報を持ってくる。

認証情報をファイルにコピペする。
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は。