弊社福岡オフィスでだいたい月1くらいで「開発合宿」なるものをやってまして、その時の個人のテーマとして「VueでFirebase使う」というのをやったので、そのまとめです。
まあ、実際には、開発合宿(日帰り)の3倍くらい時間使ってやってますけどねw
この記事のゴール
Vue.js with TypeScript で、Firebaseの認証機能を使ってログイン・ログアウトができるようになる。
前提
- Vue CLIでプロジェクトを作っている
- Firebaseでもプロジェクト作成済み
- Vue.extend()を使ってます
- Vue.jsもTSも勉強中なので、なにか突っ込みどころがあればぜひ。
環境
"firebase": "^7.17.2",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuefire": "^2.2.4",
"vuex": "^3.4.0"
Vue プロジェクトにFirebaseをインストール
# NPM
npm install firebase —save
# Yarn
yarn add firebase
Firebase認証情報
env ファイル作成
touch .env.development.local
以下の環境変数を設定。
値はFirebaseのプロジェクトから持ってきて入れます。
VUE_APP_FIREBASE_API_KEY=""
VUE_APP_FIREBASE_AUTH_DOMAIN=""
VUE_APP_FIREBASE_DB_URL=""
VUE_APP_FIREBASE_PROJECT_ID=""
VUE_APP_FIREBASE_STORAGE_BUCKET=""
VUE_APP_FIREBASE_MESSAGING_SENDER_ID=""
Firebaseの認証情報を持ったファイルを作成。
mkdir src/firebase
mkdir src/firebase/types
touch src/firebase/types/credentials.ts
touch src/firebase/credentials.ts
中身は以下
// src/firebase/types/credentials.ts
interface Config {
apiKey: string;
authDomain: string;
storageBucket: string;
databaseURL: string;
projectId: string;
messagingSenderId: string;
}
export interface Credentials {
config: Config;
}
// src/firebase/credentials.ts
import { Credentials } from './types/credentials'
export const credentials: Credentials = {
config: {
apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
storageBucket: process.env.VUE_APP_FIREBASE_STORAGE_BUCKET,
databaseURL: process.env.VUE_APP_FIREBASE_DB_URL,
projectId: process.env.VUE_APP_FIREBASE_PROJECT_ID,
messagingSenderId: process.env.VUE_APP_FIREBASE_MESSAGING_SENDER_ID
}
}
Vuefire のインストール
VueとFirebaseのやりとりを簡潔化してくれる vuefire
というパッケージをインストール。
npm i vuefire
main.ts
でvuefireを読み込む。
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import { firestorePlugin } from 'vuefire'
Vue.config.productionTip = false
Vue.use(firestorePlugin)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
各種設定
firebaseアプリの初期化
touch src/firebase/app.ts
中身
import Firebase from 'firebase/app'
import { credentials } from './credentials'
export const App: Firebase.app.App = Firebase.initializeApp(credentials.config)
認証
touch src/firebase/auth.ts
中身
import { App } from './app'
import firebase from 'firebase'
import 'firebase/auth'
export const Auth: firebase.auth.Auth = App.auth()
DB (Firestore)
touch src/firebase/db.ts
中身
import { App } from './app'
import firebase from 'firebase'
import 'firebase/firestore'
export const DB: firebase.firestore.Firestore = App.firestore()
Vuexの設定
今回は「ログインしているかどうか」の状態をVuexで管理することにします。
また、「ヘッダーを表示するかどうか」の状態もVuexで管理することにします。(ヘッダーに関しては後述)
まずは RootState
を定義した types
を作ります。
mkdir src/store/types
touch src/store/types/index.ts
store/types/index.ts
の中身
export interface RootState {
loggedIn: boolean;
showHeader: boolean;
}
store/index.ts
を書き換えます。
import Vue from 'vue'
import Vuex, { Commit, StoreOptions } from 'vuex'
import { RootState } from '@/store/types'
import { User } from 'firebase'
Vue.use(Vuex)
const store: StoreOptions<RootState> = {
state: {
loggedIn: false,
showHeader: true
},
getters: {
loggedIn (state: RootState): boolean {
return state.loggedIn
},
showHeader (state: RootState): boolean {
return state.showHeader
}
},
mutations: {
updateLogInState (state: RootState, loggedIn: boolean): void {
state.loggedIn = loggedIn
},
updateShowHeaderState (state: RootState, showHeader: boolean): void {
state.showHeader = showHeader
}
},
actions: {
updateLogInState ({ commit }: { commit: Commit }, user: User): void {
commit('updateLogInState', user !== null)
},
updateShowHeaderState ({ commit }: { commit: Commit}, showHeader: boolean): void {
commit('updateShowHeaderState', showHeader)
}
}
}
export default new Vuex.Store<RootState>(store)
これで設定は以上!
Firebase を使った認証
予めFirebase側で認証情報を登録しておきます。
今回はメールアドレスです。
App.vue の書き換え
まずは main.ts
にて、ログイン状態を調べてVuexの状態ををアップデートしてから、Vueをインスタンス化するように書き換えます。
App.vue
内で beforeCreate
してもいいけど、それだと非同期処理のため、ラグが発生してしまうので、このようにしました。
import Vue from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'
import { firestorePlugin } from 'vuefire'
import { Auth } from '@/firebase/auth'
Vue.config.productionTip = false
Vue.use(firestorePlugin)
const initVue = () => {
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
}
Auth.onAuthStateChanged(user => {
store.dispatch('updateLogInState', user)
.then(initVue)
})
ログイン画面作成
次は、ログイン画面を作ります。
一旦CSSを放棄して作ってますので、お好みでスタイルを入れてください。
touch src/views/Login.vue
中身
<template>
<div id="login">
<form novalidate @submit.prevent="login">
<p class="">Sign in</p>
<ul v-if="errors.length" class="errors">
<li v-for="error in errors" :key="error" class="error">{{error}}</li>
</ul>
<div>
<input v-model="email" type="email" required />
<input v-model="password" type="password" required />
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { Auth } from '@/firebase/auth'
interface UserLoginInfo {
email: string;
password: string;
errors: object[];
}
export default Vue.extend({
name: 'login',
components: {
},
data (): UserLoginInfo {
return {
email: '',
password: '',
errors: []
}
},
methods: {
login (event: any) {
this.errors = []
if (!this.errors.length) {
Auth.signInWithEmailAndPassword(this.email, this.password)
.then(() => this.$router.push('/'))
.catch(err => this.errors.push(err.message))
}
}
}
})
</script>
ルーティング設定
続いて、 router
の設定です。
今回は、最初から設定されている About
ページにアクセスするには認証が必要になるようガードしました。
About
の設定に meta: {requiresAuth: true}
を設定し、 router.beforeEach
で前述の meta
が設定されているかどうかを確認しています。
また、同様に、 noHeader: true
という meta を見て、ヘッダーを表示するページなのかどうかを確認し、Vuexの状態を更新しています。
import Vue from 'vue'
import VueRouter, { Route, RouteConfig } from 'vue-router'
import store from '@/store'
import Home from '../views/Home.vue'
import About from '@/views/About.vue'
import Login from '@/views/Login.vue'
import { Auth } from '@/firebase/auth'
Vue.use(VueRouter)
const routes: Array<RouteConfig> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
noHeader: true
}
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
function requiresAuth (to: Route): boolean {
return to.matched.some(route => route.meta.requiresAuth) && !Auth.currentUser
}
function hasNoHeader (to: Route): boolean {
return !to.matched.some(route => route.meta.noHeader)
}
router.beforeEach((to, from, next) => {
if (requiresAuth(to)) {
next('/login')
return
}
store.dispatch('updateShowHeaderState', hasNoHeader(to))
.then(() => { next() })
})
export default router
ヘッダーを切り出し、ログアウト機能をつける
デフォルトでは App.vue
で持っているナビゲーション部分をヘッダーとして切り出し、ログアウト機能を加えました。
あと、ログイン画面ではヘッダーを表示しないようにしました。
ただ、この機能は router と Vuex で持つのが正しい気がしてきた。。。
やっぱり気になったので、 router で管理するようにしました。
touch src/components/Header.vue
中身
<template>
<div id="nav" v-show="showHeader">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
<span v-if="loggedIn"> | <a href="" @click.prevent="logout">Logout</a></span>
</div>
</template>
<style lang="scss">
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
<script lang="ts">
import Vue from 'vue'
import { mapGetters } from 'vuex'
import { Auth } from '@/firebase/auth'
export default Vue.extend({
name: 'Header',
computed: {
...mapGetters([
'loggedIn',
'showHeader'
])
},
methods: {
logout (): void {
Auth.signOut().then(() => {
Auth.onAuthStateChanged(() => {
if (this.$router.currentRoute.path !== '/') this.$router.push('/')
})
})
}
}
})
</script>
App.vueを修正して完成
最後に、HeaderコンポーネントをApp.vueに追加する形に修正して終了。
<template>
<div id="app">
<Header />
<router-view/>
</div>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
<script lang="ts">
import Vue from 'vue'
import Header from '@/components/Header.vue'
export default Vue.extend({
name: 'App',
components: {
Header
}})
</script>
これで、
- ログインしていない状態でトップにアクセスすると
Home
とAbout
しかない -
About
にアクセスしようとするとヘッダーのないログインページにリダイレクト - ログインに成功すると
Logout
が表示されているトップに戻される
という機能を実装できました。
課題
兎にも角にも TypeScriptを使い切れていない 感がすごいです。
特にVuexでTypeScriptを使い切れなかったというか、ちょっと力尽きた感あります。
まあ、取り扱うデータが増えたら、考えようと思います。
蛇足
最初はVueのClass APIを使ってやろうと思っていたんだけれど、やってくうちに「え、なにこれ、どうしたらいいか全然わからん」ということが増えました。特にVuex周り。
で、調べていくうちに デコレータ使わない Vue.js + TypeScript で進んだ「LINEのお年玉」キャンペーン を見つけて、「なるほどなー、時代はClass APIじゃないのか」となって、ある程度作ったところで方向転換したりしました。
参考
順不同