Nuxt.js と Firebase（Firestore）を使って認証と DB 保存を実装する

この記事は [Nuxt.js #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nuxt-js-2) の 20日目の記事です。

今回の記事では、このアドベントカレンダーのためだけに作った謎ゲーム、「Nuxt の戦闘力をあげる」で使った技術についてご紹介します。
[growing-trees](https://growing-trees.firebaseapp.com/)

GitHub のリポジトリはこちらです。
[ryamakuchi/growing-trees: 木を育てる](https://github.com/ryamakuchi/growing-trees)

**※ 2019/10/20 追記**
もうメンテする気がないため、コードの書き方やバージョンは古い状態です。
仕組みだけさらっと知りたい、という人のために記事を残したままにしています。

## TL;DR

- Nuxt.js で作成した静的サイトを Firebase にホスティングする
- Vuex で状態管理する
- Google 認証機能と Firestore 連携機能は Nuxt.js で書くと 1日で実装できる

## どんなものが出来上がったか

### 最初の想定

本当は、[アビスリウム](https://twitter.com/abyssrium)みたいなタップして木を育てる育成ゲームが作りたかったです。
（今回は時間がなかったのでサンプル程度のアプリケーションとなりました）

### 1日で作れた範囲

![Nuxt.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/236385/948fa84d-5300-4ee9-3935-b44e53ba55a3.gif)

出来上がったサンプルは、

- Firebase Authentication を使って Google アカウントでログインする
- クリックすると戦闘力がカウントされ Firestore に保存される
- ログアウトしてからまたログインすると戦闘力が保持されている
- 「はじめから育てる」を押して戦闘力をリセットできる

といった部分が実装されています。

## 実装について

Nuxt.js で作ったアプリケーションをホスティングさせるとすると、AWS や Heroku なども候補に上がりますが、今回は Google での認証を簡単に行いたかったため、Firebase を選定しました。

まずは普通に Nuxt.js のアプリケーションを Firebase にホスティングさせます。
ホスティングするまでの流れについては [Nuxt.js x Firebase 事始め](https://omiend.hatenablog.jp/entry/2018/12/01/121105)が参考になったので、そちらをどうぞ。
ちなみに CSS は [Tailwind CSS](https://tailwindcss.com/) を使ってみました。
（Tailwind CSS をまだ触ったことがなく、「Utility first な CSS フレームワークってどんなもの？」と思いキャッチアップをしておきたかったため選択しました）

Nuxt.js を使っていますが SSR はしておらず単純な SPA 構成となっており、ログインしているかどうかでコンポーネントを出し分けています。

ちなみに今回したかったことの要件としては、

- Firebase と Firestore を使ってみる
- ログイン機能を Google 認証を使ってみる
- Vuex で状態管理する

という部分についてやってみたかったので、SSR する必要は特にない（SEO や初期ロードの時間などは気にしない）という前提のもと SPA なアプリケーションを作ることにしました。

また、今回のリポジトリでは API キーなどの情報が入っている `src/plugins/firebase.js` はバージョン管理対象から
から除外しています。
(JS で使われる API で、API キーが見えてしまうのは仕方がないとは思うのですが、、気休め程度にはなるかと思い）
[JavaScript - WEBAPI利用時のアプリケーションID隠匿について｜teratail](https://teratail.com/questions/48603)

firebase.js の中身はこのようなコードです。

```javascript:src/plugins/firebase.js
import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: "XXXXXXXXXXXXXXXXXXXXXXXX",
    authDomain: "XXXXXXXXXX.firebaseapp.com",
    databaseURL: "https://XXXXXXXXXX.firebaseio.com",
    projectId: "XXXXXXXXXX",
    storageBucket: "XXXXXXXXXX.appspot.com",
    messagingSenderId: "XXXXXXXXXX"
  })
}

const db = firebase.firestore()
const settings = { timestampsInSnapshots: true }
db.settings(settings)
export { db }
```

### Vuex で戦闘力を上げる流れの説明

今回作ったアプリケーションは、認証と Firestore への連携をすべて Vuex を使って行いました。

#### state で値を定義する

まずは state で状態管理したいものの値を定義します。
counter はクリックした数をカウントする値（戦闘力）です。
<https://github.com/ryamakuchi/growing-trees/blob/master/src/store/index.js>

```javascript:src/store/index.js
export const state = () => ({
  counter: 0,
  userEmail: null,
  userName: null,
  userPhoto: null,
  noAccount: false
})
```

#### コンポーネントで store の値を出力する

`{{ $store.state.counter }}` というように書くと state の値を出力できます。
<https://github.com/ryamakuchi/growing-trees/blob/master/src/components/MyTree.vue>

```vue:src/components/MyTree.vue
<template>
  <div class="max-w-full max-h-full my-8 p-12 rounded overflow-hidden shadow-lg">
    <div class="flex justify-center">
      <button
        @click="increment()"
        class="bounce"
      >
        <Logo/>
      </button>
    </div>
    <div class="mt-8 px-2 py-4 text-center">
      <div class="font-bold sm:text-lg lg:text-2xl my-4">
        クリックして Nuxt を育てよう
      </div>
      <div class="m-8 sm:text-lg lg:text-2xl">
        戦闘力 {{ $store.state.counter }}
      </div>
    </div>
    <button @click="reset()"
            class="bg-orange lg:text-2xl hover:bg-orange-dark text-white font-bold py-2 px-4 rounded">
      はじめから育てる
    </button>
  </div>
</template>
```

#### クリックされたら dispatch で actions の処理を呼ぶ

actions の処理を呼ぶときは dispatch を使います。

```vue:src/components/MyTree.vue
<template>
// ...
      <button
        @click="increment()"
        class="bounce"
      >
// ...
</template>

<script>
// ...

export default {
// ...
  methods: {
    increment () {
      this.$store.dispatch('increment')
    },
// ...
  }
}
</script>
```

#### actions で Firestore 側の値を更新して mutations に commit する

`{ commit, state }` と書くことで、commit と state を束縛することができます。
[アクション | Vuex](https://vuex.vuejs.org/ja/guide/actions.html)

`state.counter + 1` の値を Firestore に格納し、格納が成功したら
`commit('increment')` で mutations に commit しています。

```javascript:src/store/index.js
export const actions = {
// ...
  increment ({ commit, state }) {
    db.collection('users').doc(state.userEmail).update({
      counter: state.counter + 1
    }).then(() => {
      console.log("Document successfully updated!")
      commit('increment')
    }).catch((error) => {
      console.error("Error updating document: ", error)
    })
  },
// ...
}
```

**※ 2019/10/20 追記**
`async / await` をここで使うべきでは？と思われた方は正解です。
今思うと色々突っ込みどころの多いコードになっていますが、もうメンテする気がないのでご了承ください :pray:

#### mutations で state の情報を更新する

state の情報を更新できるのは mutations だけなので、最終的にはここで状態を変更します。

```javascript:src/store/index.js
export const mutations = {
  increment (state) {
    state.counter++
  },
// ...
}
```

これで一通り Vuex の流れが分かりました！
こうして見るとこの図の流れの通りで分かりやすいですね。

![vuex.png](https://qiita-image-store.s3.amazonaws.com/0/236385/6d7250fe-e8b7-d25e-31ba-68ea8ea5275f.png)

### Google アカウントを使った認証と Firestore へのデータ保存について

今回は Firebase Authentication を使って認証しましたが、思っていたよりもとても簡単に認証ができました。
認証するまでの流れについては以下の記事が分かりやすかったです。
[Nuxt.js と Firebase Authentication でログイン認証を作る](https://www.non-standardworld.co.jp/nuxt-js-firebase/20548/)

本当は Twitter アカウントを使った認証にしたかったのですが、Twitter developer account を取得するのに時間がかかりそうで1日ではできそうになかったので、Google アカウント認証にしました。

認証ロジックとしては、公式リファレンスのとおりにやっていく感じです。
<https://firebase.google.com/docs/auth/web/manage-users?hl=ja>

```javascript::src/store/index.js
// ...
export const actions = {
  // ログインするときの処理
  googleSignIn ({ dispatch }) {
    firebase.auth().signInWithRedirect(new firebase.auth.GoogleAuthProvider())
    // ログインしているかどうかのチェック
    dispatch('googleAuthStateChanged')
  },

  // ログアウトするときの処理
  googleSignOut ({ dispatch }) {
    firebase.auth().signOut()
    // ログインしているかどうかのチェック
    dispatch('googleAuthStateChanged')
  },

  // ログインしているかどうかチェックする処理
  googleAuthStateChanged ({ dispatch, commit }) {

    // 現在ログインしているユーザーを取得する
    firebase.auth().onAuthStateChanged(user => {
      // ログインしているユーザーがあったら
      if (user) {
        let { email, displayName, photoURL } = user
        // 取得した情報を mutations に commit する
        commit('storeUser', { userEmail: email, userName: displayName, userPhoto: photoURL })
        // 初回ログインかどうかをチェックする処理
        dispatch('userCheck')

      // ログインしていなかったら mutations に commit する（ログインしていないときの画面を表示させる処理）
      } else {
        commit('deleteUser')
      }
    })
  },

  // 初回ログインかどうかをチェックする処理
  userCheck ({ dispatch, commit, state }) {

    // Firestore に ユーザー情報があるか
    db.collection('users').doc(state.userEmail).get().then((doc) => {

      // ユーザー情報があったら
      if (doc.exists) {
        console.log("Document data:", doc.data())
        // counter の値を mutations に commit する
        commit('saveUser', { number: doc.data().counter })

      // ユーザー情報がなかったら
      } else {
        console.log("No such document!")
        // ユーザー情報を Firestore に登録する処理
        dispatch('createUser')
      }

    // エラーになったら
    }).catch((error) => {
      console.error("Error getting document:", error)
    })
  },

  // ユーザー情報を Firestore に登録する処理
  createUser ({ state }) {
    db.collection('users').doc(state.userEmail).set({
      counter: state.counter
    }).then(() => {
      console.log("Document successfully written!");
    }).catch((error) => {
      console.error("Error writing document: ", error);
    })
  },
// ...
```

正直このコードではエラー処理などを書いていないので色々と問題がありそうですが、サンプルとしてあたたかい目で見ていただけるとうれしいです。

## おわりに

初のアドベントカレンダーでしたが、誰かのお役に立てれば幸いです。