Firebase の Google 認証情報を Vuex で状態管理しよう


## はじめに

Vue.js や Nuxt.js への Firebase Authentication の導入は終わっている前提で進めます。
導入方法は次の過去記事を参考にしてみてください。

:link: [Firebase を Vue.js で使ってみよう](https://qiita.com/kurararara/items/2eb5baeacf5ec83843d6)

:link: [Firebase を Nuxt.js で使ってみよう](https://qiita.com/kurararara/items/844abc7b50c5795164df)



以前、『 [Firebase の Google 認証を Vue.js（Nuxt.js） で実装してみよう](https://qiita.com/kurararara/items/2845de493e52421e1d7f) 』という記事を投稿し、 Firebase を利用した Google 認証をする方法を紹介しました。

今回は、それをより実用的な形に実装する方法を紹介したいと思います。



### アウトプットをイメージする

Web アプリケーションを開発することを想像してみてください。
サインイン画面で認証し、以降の画面では認証情報を持ち回ります。

また、前回の認証情報を再利用したり、認証がないユーザーが認証が必要な画面に遷移した際のハンドリング。
そのあたりを Vuex ストアや、ミドルウェアを用いて実装していきます。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135956/42cc128a-eb24-94f6-a67c-afcf82d319a7.gif)


※画面見切れててごめんなさいですが、ポップアップで Google 認証が走っています



## Firebase の認証まわりのラッパーを作成する

さっそくストアを！……といきたいところですが、 Firebase で Google 認証させる部分のラッパーを作成しておくと何かと便利です。

認証情報を管理するストアでも利用しますので、先にそちらを作成しましょう。



### Firebase へのアクセス部分

前回の記事では `.vue` ファイルに直接記載していましたが、この様に別ファイルでエクスポートしておけば、どこでも利用可能です。

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

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: 'AIzaSyD32vSszxnkAO57Tm2i9rf-cVMf8XKYSis',
    authDomain: 'my-project-95530.firebaseapp.com',
    databaseURL: 'https://my-project-95530.firebaseapp.com',
    projectId: 'my-project-95530',
    storageBucket: 'my-project-95530.appspot.com',
    messagingSenderId: '800278616293'
  })
}

export default firebase
```

※記載している『プロジェクトID』などは削除済みのプロジェクトのものです



### Google 認証部分

先ほど作成した Firebase のラッパーを利用して、認証部分も別ファイルで定義、エクスポートしておきます。

```javascript:~/plugins/auth.js
import firebase from '~/plugins/firebase'

function auth() {
  return new Promise((resolve, reject) => {
    firebase.auth().onAuthStateChanged(user => {
      resolve(user || false)
    })
  })
}
export default auth
```

この様に実装しておけば、 Promise を使って `auth().then(user => /* any */)` のように利用できて便利です。



### 寄り道： Firestore を利用する場合

Firestore を利用して、データベースの情報をストアで管理したいケースがあるとします。

そういうときは、Google 認証と同様に、

```javascript:~/plugins/db.js
import firebase from '~/plugins/firebase'

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

export default db
```

みたいなものを用意しておくと便利かもしれません。



### 参考にさせて頂いたサイト

:link: [【v2対応】Nuxt.jsとFirebaseを組み合わせて爆速でWebアプリケーションを構築する](https://qiita.com/potato4d/items/cfddeb8732fec63cb29c)

こちらの記事のほうが圧倒的にためになります！

Nuxt.js を扱う場合は、ぜひ読んでみてください！



## ストアを作成する

それでは認証情報を管理するストアを作成していきます。



**:warning: 注意 :warning:**

このあと出てくるコードやディレクトリ構造は Nuxt.js の Vuex ストアのモジュールモードを利用する際のものです。
クラシックモードでストア管理する場合や、「モジュールモードって？」という方は [こちら](https://ja.nuxtjs.org/guide/vuex-store/) を一読することをおすすめします。



### ストアオブジェクトの決定と初期化

まずは、認証情報を管理するストアを `user.js` として、管理する状態 `state` にストアオブジェクトを用意しましょう。

```javascript:~/store/user.js
export const state = () => ({
  isAuth: false,
  displayName: '',
  email: '',
  photoURL: ''
})
```

ストアオブジェクトは利用するアプリケーションによって異なるので、今回は例として `displayName` 、 `email` 、 `photoURL` あたりを使ってみます。

また、認証済みかどうかを判断できるように `isAuth` というオブジェクトも追加しました。



### ミューテーションの用意

ミューテーションというのは、ストアの状態を変化させるためのものです。
ミューテーションに定義されたハンドラ関数でストアの状態を変更し、コミットすることでストアを変更する事ができます。

具体的な例を見てみましょう。

```javascript:~/store/user.js
export const mutations = {
  setSignInState(state, user) {
    state.isAuth = !!user
  }
}
```

ミューテーション `setSignInState` は、第1引数に `state` （変更する対象である状態）と、第2引数に追加のパラメーターを持ちます。

今回の場合は Google 認証情報を扱いますので、 Firebase から返却された `user` 情報としましょう。
この `setSignInState` では、ハンドラ関数が呼ばれると、 `user` の有無を評価して `state` の `isAuth` に代入しています。

つまりストアの状態が変化しているわけです。



#### :warning: ミューテーションハンドラの起動方法 :warning:

Vuex では **全てのミューテーションは同期的に行う** というお作法のため、ミューテションを呼び出す側では次のように記載します。

```javascript
// 上のミューテーション `setSignInState` で行う状態変更をコミットする場合
store.commit('user/setSignInState', user)
```

<br>

また、コンポーネントからミューテーションをコミットする場合は `mapMutations` ヘルパーを利用することもできます。

```javascript
import { mapMutations } from 'vuex'

export default {
  mounted: function() {
      
      // `this.$store.commit('user/setSignInState', user)` と書かなくてもコミット
      this.setSignInState(user)
  },
  methods: {
    ...mapMutations('user', ['setSignInState'])
  }
}
```

`mapMutations` がコンポーネントのメソッドを `store.commit` にマッピングしてくれるため、 `this.$store.commit('xxx')` と書くことなくミューテーションをコミットできます。



#### :warning: ミューテーションの引数の追加について :warning:

これは僕がやった失敗ですが、「 `setSignInState` で `email` とか `displayName` とかも更新したいな」と思って、次のように実装しました。

```javascript
export const mutations = {
  setSignInState(state, email, displayName) {
    state.email = email || ''
    state.displayName = displayName || ''
  }
}
```

「いい感じじゃないっすか！」

……って思ってたんですが、このミューテーションをコミットすると `displayName` が常に空になってしまいます。

正しくは、

```javascript
export const mutations = {
  setSignInState(state, user) {
    state.email = user && user.email || ''
    state.displayName = user && user.displayName || ''
  }
}
```

のように、 **追加パラメーターは第2引数にまとめる** 必要があります。

認証情報のようにまとまっているものはいいですが、そうでないものは、ストアオブジェクトごとにミューテーションを用意するほうがいいかもしれません。



### ミューテーションを作り切る

上述した内容をふまえて、 Google 認証情報をストアにコミットするためのミューテーションを作りきりましょう。

```javascript:~/store/user.js
export const mutations = {
  setSignInState(state, user) {
    state.isAuth = !!user
    state.email = user && user.email || ''
    state.displayName = user && user.displayName || ''
    state.photoURL = user && user.photoURL || ''
  }
}
```

こんな感じになります。

<br>

もし認証情報を複数（ Twitter とか Facebook とか）扱うようなアプリケーションで、 **Google の認証情報を優先的に扱いたい……** という場合は、認証プロバイダを指定してやるといい感じです。

（ごめんなさい `lodash` 使ってます）

```javascript
export const mutations = {
  setSignInState(state, user) {
    const providerData = !user.providerData
      ? {}
      : _(user.providerData)
          .filter(['providerId', 'google.com'])
          .head()
    state.isAuth = !!user
    state.email = _.get(providerData, 'email') || _.get(user, 'email') || ''
    state.displayName = _.get(providerData, 'displayName') || _.get(user, 'displayName') || ''
    state.photoURL = _.get(providerData, 'photoURL') || _.get(user, 'photoURL') || ''
  }
}
```



### アクションを用意する

認証情報を変更するためのミューテーションも用意できて、あとはコンポーネントからコミットするだけ……と言いたいところですが、 Vuex にはアクションと呼ばれる機能があります。

> - アクションは、状態を変更するのではなく、ミューテーションをコミットします。
> - アクションは任意の非同期処理を含むことができます。

と、ミューテーションとの違いが公式には書いてあります。

**認証機能などの非同期性を求められる機能では、このアクションを利用する** ほうが良さそうです。



#### サインイン処理を書いていく

認証処理を実装する場合、大雑把に『サインイン』『サインアウト』『認証状態の確認』があります。
（細かいことをいうと、サインアップ処理などもありますが、今回は触れないことにします）

まずは『サインイン』処理を実装してみましょう。

```javascript:~/store/user.js
import firebase from '~/plugins/firebase'

export const actions = {
  async signIn({ commit }) {
    await firebase
      .auth()
      .signInWithPopup(new firebase.auth.GoogleAuthProvider())
      .then(res => commit('setSignInState', res.user))
      .catch(error => {
        if (error.code === 'auth/popup-closed-by-user') {
          // Do nothing.
        } else {
          // any
        }
      })
  }
}
```

Firebase のラッパーをインポートして、 Google 認証を行っています。

`async/await` が使われており、非同期処理で認証処理を行い、成功した場合に `commit('setSignInState', res.user)` でミューテーションをコミット、失敗した場合はエラーハンドリングしています。

エラーの際、 `if (error.code === 'auth/popup-closed-by-user')` を無視していますが、これはユーザーがポップアップウインドウを閉じてしまった場合なのでそうしました。
（このエラーも拾いたい機能はよしなに……）



#### サインアウト処理を書いていく

どんどんいきましょう！
先程の要領で、『サインアウト』処理です。

```javascript:~/store/user.js
import firebase from '~/plugins/firebase'

export const actions = {
  async signIn({ commit }) {
    await firebase
      .auth()
      .signInWithPopup(new firebase.auth.GoogleAuthProvider())
      .then(res => commit('setSignInState', res.user))
      .catch(error => {
        if (error.code === 'auth/popup-closed-by-user') {
          // Do nothing.
        } else {
          // any
        }
      })
  },
  async signOut({ commit }) {
    await firebase
      .auth()
      .signOut()
      .then(res => {
        commit('setSignInState', false)
      })
  }
}
```

スッキリしていますね。

サインアウトが成功したらミューテーションに `false` を渡すことで認証情報をクリアさせています。
（サインインとサインアウトで共通のミューテーションを利用していますが、サインアウト用のミューテーションを用意するほうが良いかもしれません）



#### 認証状態の確認処理を書いていく

『サインイン』『サインアウト』が書けました。
あとは再ログイン時などに再認証しなくていいよう、認証状態を確認していく処理を書いていきましょう。

```javascript:~/store/user.js
import firebase from '~/plugins/firebase'
import auth from '~/plugins/auth'

export const actions = {
  async signIn({ commit }) {
    await firebase
      .auth()
      .signInWithPopup(new firebase.auth.GoogleAuthProvider())
      .then(res => commit('setSignInState', res.user))
      .catch(error => {
        if (error.code === 'auth/popup-closed-by-user') {
          // Do nothing.
        } else {
          // any
        }
      })
  },
  async signOut({ commit }) {
    await firebase
      .auth()
      .signOut()
      .then(res => {
        commit('setSignInState', false)
      })
  },
  async checkAuth({ commit }) {
    await auth().then(user => commit('setSignInState', user))
  },
}
```

Google 認証用に用意した `auth` プラグインをインポートして、認証状態を確認しています。

プラグイン側で認証情報があればその情報を、なければ `fales` を返すようにしていますので、ミューテーションにはその返却値をそのまま渡してコミットすれば OK です！



#### :warning: アクションの使い方 :warning:

```javascript
store.dispatch('user/signIn')
// コンポーネントからは `this.$store.dispatch('user/signIn')`
```

のように利用できます。

また、ミューテーションのときと同じく、コンポーネントからは `mapActions` マッパーを利用して利用することもできます。

```javascript
import { mapMutations } from 'vuex'

export default {
  mounted: function() {
      
      // `this.$store.dispatch('user/signIn')` と書かなくてもよい
      this.signIn()
  },
  methods: {
    ...mapActions('user', ['signIn', 'signOut', 'checkAuth'])
  }
}
```



## ミドルウェアを使った事前認証

（ Nuxt.js 向けの情報です。 Vue CLI でミドルウェア利用したことないので、もし使うことがあれば追記するかも？）

<br>

ストアができたので、 Vue インスタンスやコンポーネントから `checkAuth` してやれば、やりたかったことが実現できます。

少し欲をだすと、
- **画面に到達する前に認状態の確認をしてしまいたい** な～
- **認証がない人が認証が必要な画面にアクセスしてきたらサインイン画面に飛ばしたい** な～
など、やりたいことは絶えません。
（ ~~ユーザーってこんな感じで、どんどん追加要望入れてくるんでしょうね~~ ）

そこで、ミドルウェアを導入してみましょう。

```javascript:~/middleware/authenticated.js
export default function({ route, store }) {
  if (store.state.user.isAuth) {
    // Do nothing if authenticated...
  } else {
    if (route.path === '/') {
      store.dispatch('user/checkAuth')
    } else {
      // @see https://www.yo1000.com/nuxt-spa-redirect/
      window.location.href = '/'
      return new Promise(resolve => {
        // Wait for broswer to redirect...
      })
    }
  }
}
```

ミドルウエアはレンダリング前に処理を行うことができます。

この `authenticated.js` では、

- 認証があれば何もしない
- 認証がなければ……
  - インデックスページ（サインインページ）であればストアのアクション `checkAuth` を利用して認証状態の確認を行う
  - それ以外のページであればインデックスページにリダイレクトする

という処理を行っています。

ミドルウェアの実装については、作成するアプリケーションに合わせて行うといいですね！
（ブログであればポストを取得しておくなど）



#### :warning: リダイレクトの方法について :warning:

上記コードのコメントにもありますが、少し特殊なリダイレクトを行っています。

僕も元々は公式の記述に従って、 `redirect('/redirect-ex-mod')` と書いていました。
すると、リダイレクトするまえに遷移元画面がチラつく現象が起こってしまったのです。

偶然みつけた [こちら](https://www.yo1000.com/nuxt-spa-redirect/) の記事を参考にプロミスを使って無理やりチラつきを抑えています。



## まとめ

Google 認証の処理や状態の管理を Vuex ストアを使って行う方法を説明していきました。

各インスタンスからそれぞれ処理を実装するよりも、だいぶスッキリしたのではないでしょうか？

今までの記事に比べて少し難しくなっていますが、使うことができると **複数の画面を持つような複雑なアプリケーションでも、状態の管理を簡単に行える** と思いますので、ぜひ使ってみてくださいね！



## インデックスページから来た人向け

:link: [Vue.js 初心者がなにか作りきってみる（願望）](https://qiita.com/kurararara/items/9c4f78eb42e657221c06)

という記事で Vue.js を学び始めてそろそろ1年が経とうとしています（2018/10/31に書いた記事）。

ぜんぜん進捗ないじゃん！と思うかもしれませんが、まさにそのとおりです（笑 ）[^1]

ゆっくりでも進んではいますので、気長にお付き合いいただけると嬉しいです :yum:

[^1]: 実際は個人的に Nuxt.js で Web サービスを公開したりしているとか、いないとか。