Vue.js + Vuex + VuetifyとFirebaseでSPAを作る(その4)
Vue, Vuex, VueRouter, Vuetifyおよび Firebase を使用してハリネズミ紹介 Web サイトを作成していきます。
上のヘッダー画像は作成するウェブサイトの完成図です。
各工程は以下の4部構成に分かれています。
- パート 1: Vuee のインストールと Vue ルータと Vue ルータを使用した SPA の構築
- パート 2: Vue Routerの使用
- パート 3: Vuex の使用と API へのアクセス
- パート 4: 認証に Firebase を使用する
これまでVueを使用したことがない初心者の方でも困らないよう詳しめに解説していきます。
前回行った内容
前回の投稿ではaxiosを使用してAPIを呼び出し、ハリネズミの一覧を取得し表示しました。
今回は最後にFirebaseを使用しユーザ認証とお気に入り機能を実装します。
Firebaseとは
Firebaseはデータベースや認証、ストレージの他にWEBアプリのホスティングなどインフラ周りの構築・管理などを不要にしてくれるバックエンドサービスです。
現在はGoogleに買収され、GCPのmBaaSサービスとなっています。
まずは上記リンクからアカウント作成しログインしてください。
サインイン機能の有効化
[プロジェクトを作成] をクリックし、任意の名前を入力します。
今回は HedgeHogs-Proud としてみました。
ただしProject IDは全世界で早い者勝ちとなっており、誰かが使用している間は重複した取得が不可となっています。
入力後、[続行] をクリックします。
Google アナリティクス の紐付けページに推移しますがこれはOFFにし [続行] をクリックします。
しばらくの間プロジェクト作成のローディングがされ、完了すると次のようなコンソールに移動します。
続いてアプリの追加を行います。
画面中央に表示されている4つの丸ボタンのうち、右から3番目の</>マークをクリックしてください。
適当なアプリ名を入力しホスティングのチェックを外して [アプリを登録] をクリックします。
上記画像のようなスニペットが表示されます。
右下のコピーボタンを押してクリップボートに貼り付けたら我々のアプリに取り込むために一旦エディター側へ戻りましょう。main.js
ファイルでfirebaseアプリケーションを初期化しApp.vue
で使用することが可能ですが、ゴチャゴチャするのを避けるためsrcフォルダーの中にfirebaseという新しいディレクトリを作成しこの中にindex.js
を用意することにします。
src/firebase/index.js
import firebase from "firebase";
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "AIzaSyD3JmMWlcna31pudlMxvpUFSeLgAf_nu_0",
authDomain: "hedgehogs-proud.firebaseapp.com",
databaseURL: "https://hedgehogs-proud.firebaseio.com",
projectId: "hedgehogs-proud",
storageBucket: "hedgehogs-proud.appspot.com",
messagingSenderId: "592208514293",
appId: "1:592208514293:web:1400c2131f9daeb398c9bc",
measurementId: "G-BN68C5Y91B"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
クリップボードの中身をそのまま貼り付けた後、<script>
タグを削除しファイルの先頭でfirebaseをインポートしました。
最後の行に記述している firebase.initializeApp(firebaseConfig);
の部分でfirebaseを初期化しています。
続いてプロジェクトにfirebaseをインストールします。
npm install firebase --save
毎度恒例ですがfirebaseをVueが認識できるようにアプリケーションへ追加します。main.js
を開き先ほど作成した設定ファイル(index.js
)をインポートしましょう。
main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import vuetify from "./plugins/vuetify";
import '@/firebase/'; // 追加
Vue.config.productionTip = false;
new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount("#app");
再びFirebaseコンソールに戻ります。
[Authentication] ボタンをクリックし [ログイン方法を設定] を押下します。
プロパイダーリストに推移したら “メール / パスワード" を選択しメールリンクを無効にして [保存] ボタンを押下します。
サインアップフォームの作成
前回の投稿で Join.vue
と Signin.vue
を作成しました。
この2つのページは最終的に殆ど同じコードで実装することが出来ます。
まずは最初にサインアップフォームを作成します。
Join.vue
コンポーネントを開き、まずは全てのコードを削除します。
Vuetifyには次のようなデフォルトのコンポーネント用レイアウト構造が用意されています。
- v-container
- v-layout
- v-flex
まずはtemplate内にこのレイアウトを作成していきます。
<template>
<v-container fill-height>
<v-layout align-center justify-cener>
<v-flex xs12 sm8 md4>
</v-flex>
</v-layout>
</v-container>
</template>
v-container
にはfill-heightを追加しフォームをウィンドウの垂直方向中央に配置するよう指定しています。v-flex
ではxs12 sm8およびmd4を追加しました、これはBootstrapの列幅の定義に似ており画面の幅を1~12の割合で指定する事ができます。
上記の例では、スマートフォン等の小さなデバイスではフォームは画面全体を意味する12列すべてを占有します。
タブレットなどのデバイスはsmで指定した定義に則りフォームは画面幅の3/4になります。
PCなどの中~大画面では、フォームは画面の1/3になります。
続いて v-flex
タグの中を次のように記述します。
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Join Form</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form ref="form" v-model="valid" lazy-validation>
<v-text-field
prepend-icon="person"
name="email"
label="Email"
type="email"
v-model="email"
:rules="emailRules"
data-cy="joinEmailField"
required
>
</v-text-field>
<v-text-field
prepend-icon="lock"
name="password"
label="Password"
type="password"
required
v-model="password"
:rules="passwordRules"
data-cy="joinPasswordField"
>
</v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
:disabled="!valid"
@click="submit"
data-cy="joinSubmitBtn"
>Join</v-btn
>
</v-card-actions>
</v-card>
まずv-flex
の内部では、v-card
を使用しclass ="elevation-12"
を v-card
に追加たことでしページの上に浮いているような外見にします。
また、フォーム上部には v-toolbar
を使用しColorにはprimaryを指定しました。
Vuetifyではデフォルトでテキストが黒文字になるため、白文字にするために dark
を v-toolbar
に追加しています。
次にバリデーションです。
ユーザーはフォームにメールアドレスとパスワードを記入する必要があります。
2つの v-text-field
を使用しこれらの値をキャプチャします。
このフィールドには v-model
と:rule
があり、ユーザが [Join] ボタンを押した際に定義したすべてのルールに対してフィールドの値が検証されます。
最後に [Join] ボタンではクリック時に submit メソッドを呼び出すようにしています。
さて、幾つかの v-model
を定義したため data
セクションに追加する必要があります。
メールアドレスとパスワードを検証するための emailRules
と passwordRules
を用意しましょう。
data() {
return {
valid: false,
email: '',
password: '',
emailRules: [
v => !!v || 'E-mail is required',
v => /.+@.+/.test(v) || 'E-mail must be valid'
],
passwordRules: [
v => !!v || 'Password is required',
v =>
v.length >= 6 ||
'Password must be greater than 6 characters'
]
};
},
emailとpasswordには、ユーザーがテキストフィールドにそれぞれ入力した値が含まれます。
Validは作成したすべてのルールに合格したかどうかを示します。
emailの場合はフィールドが空でないかを確認した後、入力された値が基本的な正規表現に一致することを確認しています。
passwordについてはフィールドが空でないことを確認した後、passwordの長さが少なくとも6文字であることを確認しています。
最後にメソッドを追加しましょう。
[Join]ボタンの押下時に呼び出されるsubmit()を作ります。
このメソッドは最初に上記ルールでフォームの値を検証し、合格した場合はuserJoinというVuexストアのアクションを呼び出しユーザーがフォームに入力したemailとpasswordを渡します。
methods: {
submit() {
if (this.$refs.form.validate()) {
this.$store.dispatch("userJoin", {
email: this.email,
password: this.password
});
}
}
}
VuexにuserJoinアクションを作成する
store.js
を開きuserJoinというアクションを作成しましょう。
userJoin({ commit }, { email, password }) {
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(user => {
commit('setUser', user);
commit('setIsAuthenticated', true);
router.push('/about');
})
.catch(() => {
commit('setUser', null);
commit('setIsAuthenticated', false);
router.push('/');
});
}
このアクションを呼び出すためのパラメータには commit
を代入します。
続いてfirebaseにユーザーを作成できるようにしていきます
まずは store.js
の冒頭でfirebaseをインポートしましょう。
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
import firebase from 'firebase'; //追加
Firebase Authenticationは createUserWithEmailAndPassword
というメソッドを提供しています。
このメソッドにユーザが入力しバリデーションにパスしたemailとpasswordを渡しましょう。
ユーザの登録に成功すると setUser
と setIsAuthentication
という2つのミューテーションを呼び出すようにします。
serJoin({ commit }, { email, password }) {
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(user => {
commit('setUser', user);
commit('setIsAuthenticated', true);
router.push('/about');
})
.catch(() => {
commit('setUser', null);
commit('setIsAuthenticated', false);
router.push('/');
});
}
続いてミューテーションを作成します。
setUser(state, payload) {
state.user = payload;
},
setIsAuthenticated(state, payload) {
state.isAuthenticated = payload;
}
setUser
ではユーザの状態をペイロードに設定し、setIsAuthenticated
ではisAuthenticatedの状態をペイロードに設定します。
最後にこれらのステートを追加しましょう。
state: {
hedgehogs: [],
apiUrl: "http://127.0.0.1:51310/api/",
user: null,
isAuthenticated: false
}
ユーザ作成のテスト
npm run serve
を実行しサーバーを起動したら [Join] ボタンをクリックし、適当なアドレスとパスワードを入力して [Join] を押下しましょう。
但し、押下しても何も反応はないのでFirebaseコンソールをブラウザで開きAuthentication を開いて確認します。
正常にユーザが追加できたことをクライアントに通知してあげる必要がありますが、先にLogin機能を作ってしまいましょう。Join.vue
で記述したコードを丸っとコピーし、Signin.vue
コンポーネントに貼り付けます。
その後ページタイトルを"Login form" に変更しボタンのラベルを “Login" に、呼び出すメソッド名を userLogin
に変更します。
Signin.vue
<template>
<v-container fill-height>
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4>
<v-card class="elevation-12">
<v-toolbar dark color="primary">
<v-toolbar-title>Login Form</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form ref="form" v-model="valid">
<v-text-field
prepend-icon="person"
name="email"
label="Email"
type="email"
v-model="email"
:rules="emailRules"
required
data-cy="signinEmailField"
></v-text-field>
<v-text-field
prepend-icon="lock"
name="password"
label="Password"
type="password"
data-cy="signinPasswordField"
v-model="password"
:rules="passwordRules"
required
></v-text-field>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
:disabled="!valid"
@click="submit"
data-cy="signinSubmitBtn"
>Login</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
export default {
name: "Signin",
data() {
return {
valid: false,
email: "",
password: "",
emailRules: [
v => !!v || "E-mail is required",
v => /.+@.+/.test(v) || "E-mail must be valid"
],
passwordRules: [
v => !!v || "Password is required",
v => v.length >= 6 || "Password must be greater than 6 characters"
]
};
},
methods: {
submit() {
if (this.$refs.form.validate()) {
this.$store.dispatch("userLogin", {
email: this.email,
password: this.password
});
}
}
}
};
</script>
<style scoped></style>
Signinフォームはこれだけで完成してしまいました。
続いてログイン用のアクションを作成しましょう。store.js
を開き、userLoginというアクションを作成します。
これはFirebaseを使用してユーザをログインさせます。
Firebaseが提供している signInWithEmailAndPassword
というメソッドを呼び出し、emailとpasswordを渡してやります。
ユーザの入力した値が正しかった場合、setUser
と setIsAuthentication
というミューテーションを呼び出します。
userLogin({ commit }, { email, password }) {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(user => {
commit('setUser', user);
commit('setIsAuthenticated', true);
})
.catch(() => {
commit('setUser', null);
commit('setIsAuthenticated', false);
});
}
Aboutへのリダイレクト
ユーザが正常にSignup もしくは Signin した際はAboutページへリダイレクトするようにしましょう。
前回の最後でPileColorを選択肢した際に表示させるv-cardへ [good] ボタンを追加したことを覚えていますか?
AboutページにはユーザーがGoodをしたハリネズミを表示させたいため、Firbaseのデータベースへハリネズミ情報を保存するようにします。
まずはユーザをAboutへリダイレクトさせるために store.js
へVueRouterをインポートします。
import router from "@/router";
次に先ほど作成した userLogin
の .then
と .catch
の最後にそれぞれ次の行を追記します。
.then(user => {
commit('setUser', user);
commit('setIsAuthenticated', true);
router.push('/about'); //追加
})
.catch(() => {
commit('setUser', null);
commit('setIsAuthenticated', false);
router.push('/'); //追加
});
これでユーザが正常にアカウント登録及びログインに成功した際はAboutページに、失敗した際はHomeページへリダイレクトさせられるようになりました。
ナビゲーションの更新
現在のナビゲーションには Signin
と Join
が設置されています。
ユーザが正常にログインした際はこの2つを非表示にし、代わりに [Logout] ボタンを表示したいと思います。
AppNavigation.vue
を開き、divタグで2つのボタンをグループ化しておきます。
その後現在指定しているclassを削除し、hidden-sm-and-down
を割り当て、v-if で isAuthenticate
を指定することでユーザが非認証時のみ表示されるようにします。
続いて、v-elseではログアウト用の新しいボタンを配置しましょう。
ユーザがこのボタンをクリックした際は logout
メソッドが呼び出されるようにします。
<div v-if="!isAuthenticated" class="hidden-sm-and-down">
<v-btn text to="/sign-in" data-cy="signinBtn">SIGN IN</v-btn>
<v-btn color="brown lighten-3" to="/join" class="nav-join" data-cy="joinBtn">JOIN</v-btn>
</div>
<div v-else>
<v-btn text to="/about">PROFILE</v-btn>
<v-btn outline color="gray" @click="logout" data-cy="logout">Logout</v-btn>
</div>
logout
メソッドも作成しましょう。
このメソッドはVuexアクション内の userSignOut
を呼び出すようにしておきます。
methods: {
logout() {
this.$store.dispatch("userSignOut");
}
}
また、ユーザが認証済みかどうかを確認する isAuthenticated
をcomputedに作成します。
computed: {
isAuthenticated() {
return this.$store.getters.isAuthenticated;
}
}
最終的にAppNAvigation.vueは次のようになります。
<template>
<span>
<v-navigation-drawer app v-model="drawer" class="brown lighten-2" dark disable-resize-watcher>
<v-list>
<template v-for="(item, index) in items">
<v-list-tile :key="index">
<v-list-tile-content>{{item.title}}</v-list-tile-content>
</v-list-tile>
<v-divider :key="`divider-${index}`"></v-divider>
</template>
</v-list>
</v-navigation-drawer>
<v-toolbar app color="brown darken-4" dark>
<v-toolbar-side-icon class="hidden-md-and-up" @click="drawer = !drawer"></v-toolbar-side-icon>
<v-spacer class="hidden-md-and-up"></v-spacer>
<router-link to="/">
<v-toolbar-title to="/">{{appTitle}}</v-toolbar-title>
</router-link>
<v-btn class="hidden-sm-and-down" to="/menu">Menu</v-btn>
<v-spacer class="hidden-sm-and-down"></v-spacer>
<div v-if="!isAuthenticated" class="hidden-sm-and-down">
<v-btn text to="/sign-in" data-cy="signinBtn">SIGN IN</v-btn>
<v-btn color="brown lighten-3" to="/join" class="nav-join" data-cy="joinBtn">JOIN</v-btn>
</div>
<div v-else>
<v-btn text to="/about">PROFILE</v-btn>
<v-btn outline color="gray" @click="logout" data-cy="logout">Logout</v-btn>
</div>
</v-toolbar>
</span>
</template>
<script>
export default {
name: "AppNavigation",
data() {
return {
appTitle: "HedgeHogs 🦔 ",
drawer: false,
items: [
{ title: "Menu" },
{ title: "Profile" },
{ title: "Sign In" },
{ title: "Join" }
]
};
},
computed: {
isAuthenticated() {
return this.$store.getters.isAuthenticated;
}
},
methods: {
logout() {
this.$store.dispatch("userSignOut");
}
}
};
</script>
<style scoped>
a {
color: white;
text-decoration: none;
}
.router-link-exact-active {
color: white;
text-decoration: none;
}
.v-toolbar__title {
color: white;
text-decoration: none;
}
</style>
Navigationで定義したアクションとゲッターを追加しましょう。store.js
を開き userSignout
というアクションを作成します。
このアクションは firebase.auth()
を使用してユーザをサインアウトさせてからuserステートをnullに設定し、 isAuthenticated
にもfalseをセットさせます。
userSignOut({ commit }) {
firebase
.auth()
.signOut()
.then(() => {
commit('setUser', null);
commit('setIsAuthenticated', false);
router.push('/');
})
.catch(() => {
commit('setUser', null);
commit('setIsAuthenticated', false);
router.push('/');
});
}
次にgettersセクションを追加します。isAuthenticated
はユーザ認証に基づいてtrueもしくはfalseを返させます。
getters: {
isAuthenticated(state) {
return state.user !== null && state.user !== undefined;
}
}
データベースへの追加
ユーザがログインすると気に入ったハリネズミにGoodをして自分のアカウントに追加できるようにします。
お気に入りにしたハリネズミは /about
ルートで確認できるようにしましょう。
これにはハリネズミ情報を保存するデータベースが必要になります。
Firebaseコンソールを開いて左側の Database をクリックします。
[データベースの作成] ボタンをクリックし、テストモードで開始
を選択肢して [次へ] をクリックします。
お好みのリージョンを選択し [完了] をクリックします。
続いてRealtime Databaseのルールを次のように変更します。
それではエディタに戻り、ハリネズミをデータベースへ保存しましょう。
HedgeHogsコンポーネントを開き、[good]ボタンをクリックした際にユーザがログインしていない場合はSigninページへリダイレクトするようにします。
<v-card-actions>
<v-btn color="green" dark @click="favoriteHedgehog(hedgehog)"
>good</v-btn
>
</v-card-actions>
次にログイン状態を判定するようcomputedに isAuthenticated
を作成します。
computed: {
hedgehogs() {
return this.$store.state.hedgehogs;
},
isAuthenticated() {
return this.$store.getters.isAuthenticated;
}
},
これでfavoriteHedgehogメソッドを作成する準備が整いました。
このメソッドではまずユーザがログインしているかを確認します。
未ログイン時は /sign-in
へリダイレクトさせ、ログイン時にはVuexのアクションを呼び出してデータベースのユーザアカウントへハリネズミを追加します。
methods: {
favoriteHedgehog(item) {
if (this.isAuthenticated) {
this.$store.dispatch('addFavolite', item);
} else {
this.$router.push('/sign-in');
}
}
}
次に store.js
にハリネズミをユーザへ追加するアクションを作成します。
このアクションではfirebaseを使用してusersというデータベースにハリネズミを登録します。
ユーザがfirebaseに登録された際払い出されているUIDを使用して一意のアカウントを特定します。
addFavolite({ state }, payload) {
firebase
.database()
.ref('users')
.child(state.user.user.uid)
.push(payload.name);
}
{{ state }}
は現在選択されているユーザの値を取得するために使用しています。
stateの中にはuserというオブジェクトがあり、更にuserというキーが入っており、uidが格納されています。
ややこしいですね。
このuidを使用して選択されたハリネズミの名前をデータベースへプッシュしています。
開発サーバを起動しログインをした後、適当なハリネズミをgoodするとfirebaseコンソールでは次のように情報が格納されている事が確認できます。
データベースにハリネズミが追加されたので、実際にユーザーのAboutページにハリネズミを表示させていきます。
まずAbout.vue
ファイルを開きます。
このページが読み込まれるたびにユーザーのgoodしたすべてのハリネズミを取得するようにします。
これを行うには、mounted() にgetFavolitesというメソッドを追加します。
このメソッドではユーザーのgoodした全てのハリネズミを取得するアクションをVuexストアで呼び出し、userFavolitesステートに保存します。
また、userFavolitesのcomputedプロパティを追加する事でストアのステートからuserFavolitesが返されるようになります。
export default {
name: 'About',
computed: {
userFavolites() {
return this.$store.state.userFavolites;
}
},
mounted() {
this.getFavolites();
},
methods: {
getFavolites() {
this.$store.dispatch('getUserFavolites');
}
}
};
次にstore.js
ファイルを開きgetUserFavolitesアクションを作成します。
ユーザーがログインすると、userと呼ばれるステートに保存します。
このステートにはfirebaseに登録されたときにそのユーザーに割り当てられた一意のuidが入っています。
このuidが含まれたデータベース内のすべてのレシピを取得し、userFavolitesにセットします。
getUserFavolites({ state, commit }) {
return firebase
.database()
.ref('users/' + state.user.user.uid)
.once('value', snapshot => {
commit('setUserFavolites', snapshot.val());
});
}
ミューテーションにはsetUserFavolitesを追加します。
setUserFavolites(state, payload) {
state.userFavolites = payload;
}
ステートにuserFavolitesを追加し、初期値に空の配列を割り当てておきます。
state: {
hedgehogs: [],
apiUrl: "http://127.0.0.1:51310/api/",
user: null,
isAuthenticated: false,
userFavolites: [] // 追加
},
About.vueファイルに戻り、ユーザのgoodした全てのハリネズミをループして表示させます。
<template>
<v-container>
<v-layout column>
<h1 class="title my-3">My Hedgehogs</h1>
<div
v-for="(item, idx) in userFavolites"
class="subheading mb-2"
:key="idx"
>
{{ item }}
</div>
<v-flex mt-4>
<v-btn color="primary" to="/menu">Go To Menu</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
ヘッダーテキストにはmy-3を追加する事でmargin-topとmargin-bottomを設定しタイトルとハリネズミの間にスペースを空けました。
その下では単純な v-for
で各ハリネズミをループして表示させていますが、1匹もgoodしていないユーザのケースでは空白が表示されます。
RootGuardの追加
現在の状態ではユーザのログイン状況に関わらずブラウザのURLに /about
を入力して直接アバウトページに移動する事が可能になってしまっています。
ログインしていない場合にはページへアクセスする事ができないようガードする必要があります。
router.js
を編集します。
RootGuradはMetaタグと連動して機能するので /about
ルートに authRequired
Metaタグを追加します。
{
path: "/about",
name: "about",
component: () => import("./views/About.vue"),
meta: {
authRequired: true
}
}
RootGuardは、Vueルーターの一部であるbeforeEachと呼ばれるメソッドでチェックされます。
beforeEachメソッドは authRequired
のMetaタグが含まれているかどうかすべてのルートをチェックします。
Metaタグが存在する場合ユーザーが認証されているかどうかを確認し、認証されない場合は /sign-in
ページにリダイレクトさせます。
ユーザーがログインしている場合はRootの続行が許可されます。
authRequiredメタタグを持たないページへルーティングする場合は無条件でルーティングが続行されます。
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.authRequired)) {
if (!store.state.isAuthenticated) {
next({
path: "/sign-in"
});
} else {
next();
}
} else {
next();
}
});
まとめ
4回に分け投稿してきましたが今回が最後となります。
Firebaseは非常に多機能で便利なサービスです、プランも無料プランが用意されているので個人での開発や検証に気軽に用いることができます。
今回のソースコードは以下のGitHubへアップロードしてあります。