はじめに
Firebase の勉強のためにチャットアプリを作ってみました 🤤
Google 認証 ・ Firestore ・ Vuetify ・ vuexfire を使っています 🙄
DEMO
https://neetschat.firebaseapp.com/
コードはこちらにあります
対象読者
Vue と Firebase をさらっと勉強して何か作ってみたい方
以下コードを抜粋したものです
Plugins
Firebase の設定
(firebase.initializeApp はご自身のプロジェクトの値を設定してください)
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/auth";
firebase.initializeApp({
apiKey: "xxxxxx",
authDomain: "xxxxxx.firebaseapp.com",
databaseURL: "xxxxxx.firebaseio.com",
projectId: "xxxxxx",
storageBucket: "xxxxxx.appspot.com",
messagingSenderId: "xxxxxx"
});
export default firebase;
ログインしているかの確認用
import firebase from "@/plugins/firebase";
function auth() {
return new Promise(resolve => {
firebase.auth().onAuthStateChanged(user => {
resolve(user || false);
});
});
}
export default auth;
Vuetify の設定
@mdi/font パッケージを追加してアイコンフォントを mdi に変更しました
Installing Icons — Vuetify.js
import "@mdi/font/css/materialdesignicons.css";
import Vue from "vue";
import Vuetify from "vuetify/lib";
import "vuetify/src/stylus/app.styl";
Vue.use(Vuetify, {
iconfont: "mdi"
});
Store
vuexfire を使うと firestore と state を bind してくれて捗ります。
getters で message と user 情報を map している箇所がありますが、
firestore には参照型があるそうなので、そっちを使ったほうがいいのかも
import Vue from "vue";
import Vuex from "vuex";
import firebase from "@/plugins/firebase";
import { vuexfireMutations, firestoreAction } from "vuexfire";
const firestore = firebase.firestore();
const provider = new firebase.auth.GoogleAuthProvider();
Vue.use(Vuex);
export default new Vuex.Store({
strict: true,
state: {
user: null,
users: [],
messages: [],
isLoaded: false
},
mutations: {
...vuexfireMutations,
setCredential(state, user) {
state.user = user;
},
setIsLoaded(state, isLoaded) {
state.isLoaded = isLoaded;
}
},
getters: {
messages: state => {
return state.messages.map(message => {
message.user = state.users.find(user => user.uid === message.uid);
return message;
});
},
user: state => state.user,
users: state => state.users,
isLoaded: state => state.isLoaded,
isLoggedin: state => !!state.user
},
actions: {
initUsers: firestoreAction(({ bindFirestoreRef }) => {
const usersCollection = firestore.collection("users");
bindFirestoreRef("users", usersCollection);
}),
initMessages: firestoreAction(({ bindFirestoreRef }) => {
const messagesCollection = firestore
.collection("messages")
.orderBy("createdAt", "desc");
bindFirestoreRef("messages", messagesCollection);
}),
signIn() {
firebase.auth().signInWithRedirect(provider);
},
async signOut({ commit }) {
await firebase.auth().signOut();
commit("setCredential", null);
},
async setCredential({ commit }, user) {
if (!user) return;
const usersCollection = firestore.collection("users");
await usersCollection.doc(user.email).set({
name: user.displayName,
uid: user.uid,
email: user.email,
icon: user.photoURL
});
commit("setCredential", {
name: user.displayName,
uid: user.uid,
icon: user.photoURL
});
},
addMessage(context, { content, uid }) {
const serverTimestamp = firebase.firestore.FieldValue.serverTimestamp();
firestore
.collection("messages")
.add({
content,
uid,
createdAt: serverTimestamp,
updatedAt: serverTimestamp
})
.then(docRef => {
console.log(`${docRef.id} successfully added!`);
})
.catch(error => {
console.log(error);
});
},
removeMessage(context, id) {
firestore
.collection("messages")
.doc(id)
.delete()
.then(function() {
console.log("message successfully deleted!");
})
.catch(error => {
console.log(error);
});
},
loadComplete({ commit }) {
commit("setIsLoaded", true);
}
}
});
App.vue
認証チェック ・ メッセージ と ユーザー情報の初期化 をしています。
<template>
<v-app>
<v-navigation-drawer v-model="drawer" fixed app>
<v-list dense>
<v-list-tile to="/">
<v-list-tile-action>
<v-icon>home</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Home</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/code">
<v-list-tile-action>
<v-icon>code</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>Code</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar color="indigo" dark fixed app prominent>
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
<v-toolbar-title>
<span>NEET</span>
<span class="font-weight-light">Chat</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items v-if="isLoaded">
<div v-if="isLoggedin" class="text-xs-center">
<v-menu v-model="menu" :close-on-content-click="false" offset-y>
<template v-slot:activator="{ on }">
<v-btn flat fab v-on="on">
<v-avatar>
<img :src="user.icon" />
</v-avatar>
</v-btn>
</template>
<v-card>
<v-list>
<v-list-tile avatar>
<v-list-tile-avatar>
<img :src="user.icon" :alt="user.name" />
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>{{ user.name }}</v-list-tile-title>
<v-list-tile-sub-title>
{{ user.email }}
</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-divider></v-divider>
<v-list>
<v-list-tile @click="signOut">
<v-list-tile-title>Signout</v-list-tile-title>
</v-list-tile>
</v-list>
</v-card>
</v-menu>
</div>
<v-btn v-if="!isLoggedin" flat @click="signIn">Signin Google</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-content>
<Loading v-if="!isLoaded" />
<router-view v-else />
</v-content>
<v-footer color="indigo" app>
<span class="white--text">© 2019</span>
</v-footer>
</v-app>
</template>
<script>
import { mapActions, mapGetters } from "vuex";
import auth from "@/plugins/auth";
import Loading from "@/components/Loading";
export default {
name: "App",
components: {
Loading
},
data: () => ({
drawer: null,
menu: false
}),
computed: {
...mapGetters(["user", "users", "messages", "isLoaded", "isLoggedin"])
},
async mounted() {
let user;
if (!this.user) user = await auth();
await Promise.all([
this.user
? Promise.resolve()
: this.$store.dispatch("setCredential", user || null),
this.users.length ? Promise.resolve() : this.$store.dispatch("initUsers"),
this.messages.length
? Promise.resolve()
: this.$store.dispatch("initMessages")
]);
this.$store.dispatch("loadComplete");
},
methods: {
...mapActions(["signOut", "signIn"])
}
};
</script>
Component
メッセージ入力
ログインユーザのみメッセージ入力できるように v-if
で表示切替しています。
絵文字入力には、シンプルで使いやすそうだった v-emoji-picker を使っています。
joaoeudes7/V-Emoji-Picker
関係ないですが絵文字って海外でも emoji で通じるんですね!
<template>
<v-flex v-if="isLoggedin" class="my-3" xs12 sm10>
<v-form ref="messageForm" v-model="valid" @submit.prevent="addMessage">
<v-text-field
ref="messageText"
v-model="newMessage"
:rules="required"
label="message"
prepend-inner-icon="mdi-emoticon"
append-outer-icon="mdi-send"
required
clear-icon="mdi-close-circle"
clearable
@click:clear="clearMessage"
@click:prepend-inner="toggleEmojiPicker"
@click:append-outer="addMessage"
@blur="$refs.messageForm.resetValidation()"
></v-text-field>
<v-dialog v-model="dialog" :hide-overlay="true" width="325px">
<VEmojiPicker
:pack="pack"
:show-search="false"
:emojis-by-row="8"
label-search="search"
@select="selectEmoji"
/>
</v-dialog>
</v-form>
</v-flex>
<v-flex v-else class="my-3" xs12 sm10>
<p class="font-weight-medium headline red--text">
Sign in. with your Google Account.
</p>
</v-flex>
</template>
<script>
import { mapGetters } from "vuex";
import VEmojiPicker from "v-emoji-picker";
import packData from "v-emoji-picker/data/emojis.json";
export default {
name: "MessageInput",
components: {
VEmojiPicker
},
data: () => ({
newMessage: "",
valid: true,
pack: packData,
required: [v => !!(v + "").trim() || "message is required"],
dialog: false
}),
computed: {
...mapGetters(["isLoggedin", "user"])
},
watch: {
dialog() {
if (!this.dialog) {
this.inputFocus();
}
}
},
mounted() {
this.inputFocus();
},
methods: {
async addMessage() {
if (!this.$refs.messageForm.validate()) {
return;
}
await this.$store.dispatch("addMessage", {
content: this.newMessage,
uid: this.user.uid
});
this.clearMessage();
},
selectEmoji(dataEmoji) {
if (!this.dialog) {
return;
}
if (!this.newMessage) {
this.newMessage = "";
}
this.newMessage += dataEmoji.emoji;
this.dialog = false;
this.inputFocus();
},
toggleEmojiPicker() {
this.dialog = !this.dialog;
},
inputFocus() {
this.$refs.messageText.focus();
},
clearMessage() {
this.$refs.messageForm.resetValidation();
this.newMessage = "";
}
}
};
</script>
メッセージリストの表示
自分が投稿したメッセージのみ削除できるようにしています。
<template>
<v-flex class="my-3" xs12 sm10>
<v-list three-line>
<transition-group name="slide-fade">
<template v-for="(message, index) in messages">
<v-divider v-if="index != 0" :key="index"></v-divider>
<v-list-tile :key="message.id" avatar>
<v-list-tile-avatar>
<img :src="message.user.icon" />
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-sub-title class="text--primary subheading">{{
message.content
}}</v-list-tile-sub-title>
<v-list-tile-sub-title>{{
fromNow(message.createdAt)
}}</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action v-if="user && user.uid == message.uid">
<v-btn icon ripple @click="removeMessage(message.id)">
<v-icon color="amber lighten-1">delete</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</template>
</transition-group>
</v-list>
</v-flex>
</template>
<script>
import { mapGetters } from "vuex";
import moment from "moment-timezone";
export default {
name: "MessageList",
computed: {
...mapGetters(["messages", "user"])
},
methods: {
removeMessage(id) {
this.$store.dispatch("removeMessage", id);
},
fromNow(timestamp) {
if (!timestamp) {
return;
}
return moment(timestamp.toDate().toISOString()).fromNow();
}
}
};
</script>
<style scoped>
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.5s ease;
}
.slide-fade-enter,
.slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}
</style>
さいごに
最後まで読んでいただきありがとうございました 😆
Firebase を触る前は「サーバレス何それって」感じだったんですが Firebase かなりイイ感じですね
なんと言っても無料でスタートできるって、スゴイですよね Google さんまじ太っ腹 😁