10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue.js with TypeScript で Firebase を使う

Last updated at Posted at 2020-08-16

弊社福岡オフィスでだいたい月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>

これで、

  • ログインしていない状態でトップにアクセスすると HomeAbout しかない
  • About にアクセスしようとするとヘッダーのないログインページにリダイレクト
  • ログインに成功すると Logout が表示されているトップに戻される

という機能を実装できました。

課題

兎にも角にも TypeScriptを使い切れていない 感がすごいです。
特にVuexでTypeScriptを使い切れなかったというか、ちょっと力尽きた感あります。

まあ、取り扱うデータが増えたら、考えようと思います。

蛇足

最初はVueのClass APIを使ってやろうと思っていたんだけれど、やってくうちに「え、なにこれ、どうしたらいいか全然わからん」ということが増えました。特にVuex周り。

で、調べていくうちに デコレータ使わない Vue.js + TypeScript で進んだ「LINEのお年玉」キャンペーン を見つけて、「なるほどなー、時代はClass APIじゃないのか」となって、ある程度作ったところで方向転換したりしました。

参考

順不同

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?