Electron + nuxt.js + Firebase v9 でFirebase Authenticationのログイン状態をVuexに保存したい。

# はじめに
こんにちは。久しぶりのQiitaですが、UIとか変わってておーって感じです。せっかくのクリスマスなので何か書いてみようと書いたのが、「Electron + nuxt.js + Firebase v9 でFirebase Authenticationのログイン状態をVuexに保存したい。」ということで、要素盛りすぎだろ！な感じでいきたいと思います。というのも、ここ半年は文化祭の準備で色々忙しく、ゆっくり考えて開発する時間もなかったわけで、「ここはいっちょでっかいProjectやってみるか！」ということで「TecTask」なるタスク管理ソフトの開発を画策しています。公開してサービスとして展開することは全く考えておらず、自分用として企業並みの機能を兼ね備えたものを作ろうとしている次第です。文化祭についてやったことはおいおい書いていくとしてGithubやGitを使ったり、Nuxt.jsでの開発の経験をぶつけようというわけです。

今回はNode.jsでアプリが開発できちゃう`Electron`を`Nuxt.js`と併用した環境で`Firebase Authentication`を使いたいなという感じです。`Electron`が`Firebase`に関係してくるところは少ないので複数の記事をごっちゃにした感じになってしまいました。以下はこの記事でやっていくことです。

- `Electron + nuxt.js`環境を作りたいよ
- `Firebase SDK v9`をnuxtで使用したいよ
- `Firebase Authentication`をnuxtで使用したいよ
- ログインの状態を`vuex`で保存したいよ

# 第1章 導入
ここではElectronとNuxtの導入、そしてFirebaseSDKv9の導入をおこなっていきます。

## プロジェクトの生成
```:コマンドライン
npm install -g vue-cli
vue init michalzaq12/electron-nuxt <project-name>

cd <project-name>
yarn install
yarn dev 
```

ここでは、`michalzaq12`さんの`electron-nuxt`をつかって手っ取り早く環境構築をしていきます。（[Github](https://github.com/michalzaq12/electron-nuxt)）
`nuxt`のルートフォルダは`src`内の`renderer`になります。

## 必要なプラグインの導入
今回は`firebase`とAPIキーを保存するために`dotenv`を導入します。

```:コマンドライン
yarn add firebase
yarn add @nuxtjs/dotenv
```

僕の環境でのバージョンは以下のとおりです。

```json:package.json（抜粋）
"@nuxtjs/dotenv": "^1.4.1",
"firebase": "^9.6.1"
```
ここで注意してもらいたいのが`firebaseSDK`のバージョンが`v9`だと言うことです。`v8`から`v9`で大きな変更が加えられています。一応、互換性はありますが、`v9`の解説も兼ねているので`v9`で書いていきます。

```:コマンドライン
firebase init
```
最後にこのコマンドを実行することで`firestore`などの設定ファイルをローカルに持ってきます。これが果たして必要かどうかは未検証です。このコマンドの実行前に[Firebase Console](https://console.firebase.google.com/u/2/?hl=JA)でプロジェクトを生成しとくことをお勧めします。

なお今後、`Electron`の環境というより、`nuxt.js`の環境となるので、これでこの記事で`Electron`が出てくることは無くなります。ただ、`Electron`特有の要素があるかもしれないので詳しい方はぜひコメントください。


## firebaseの初期化
`firebase`はAPIキーなどを用いた初期化が最初に必要になります。それを`plugin`でおこなっていきます。なお、あらかじめ使用するFirebaseプロジェクトを生成し、`設定 > マイアプリ`からアプリ（今回ならWEBアプリ）を生成し、生成後の画面の`SDK の設定と構成`から`apiKey, authDomain, databaseURL, projectId, storageBucket, messagingSenderId, appId, measurementId`を取得してください。（いつでも設定からみれるのでメモする必要はない。）

直下に`.env`という名前のファイルを作りAPIキー周りを保存します。

```:.env
API_KEY=
AUTH_DOMAIN=
DATABASE_URL=
PROJECT_ID=
STORAGE_BUCKET=
MESSAGING_SENDER_ID=
APP_ID=
MEASUREMENT_ID=
```

また、`nuxt.config.js`に`dotenv`を使うことを明記しておきます。

```javascript:nuxt.config.js（抜粋）
modules: ["@nuxtjs/dotenv"],
```

つぎに、`plugins`フォルダに`firebase.js`を生成して以下を書き込みます。

```javascript:plugins/firebase.js
import { initializeApp } from "firebase/app";
import { initializeAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.API_KEY,
  authDomain: process.env.AUTH_DOMAIN,
  databaseURL: process.env.DATABASE_URL,
  projectId: process.env.PROJECT_ID,
  storageBucket: process.env.STORAGE_BUCKET,
  messagingSenderId: process.env.MESSAGING_SENDER_ID,
  appId: process.env.APP_ID,
  measurementId: process.env.MEASUREMENT_ID,
};

const app = initializeApp(firebaseConfig);

// Initialize Auth
export const auth = initializeAuth(app);
```
これでFirebaseはInitialize出来ました。なお、最新の`nuxt`では`dotenv`の代わりに`nuxt`標準の`RuntimeConfig`機能で同じことができます。が、`electron`のせいなのかなんなのか僕の環境では`.env`を認識してくれなかったので`dotenv`をやむなく使いました。なにかわかる方いらっしゃいますでしょうか。

`RuntimeConfig`については[この記事](https://qiita.com/taai/items/cbc61b9b4a18aacad57e)がわかりやすいです。

最後に`plugin`を使うことを明記してここは完成です。

```javascript:nuxt.config.js（抜粋）
module.exports = {
  // 中略...
  plugins: ["@/plugins/firebase.js"]
}
```
# 第2章 実装
## ログイン処理を書く
`Vuex`にログイン処理を書いていきます。

`store`フォルダ内に`auth.js`を生成して以下を書き込みます。

```javascript:store/auth.js
import { auth } from "@/plugins/firebase.js";
import { onAuthStateChanged, signInWithEmailAndPassword, signOut } from "firebase/auth";

export const state = () => ({
  isLoggedIn: false, // ログイン状態
  user: {},          // ログイン情報
});

export const mutations = {};

// stateの内容を直接とるのではなくgetterを通して取得する方が良い。
export const getters = {
  isAuthenticated: (state) => !!state.isLoggedIn, // ログイン状態
  currentUserInfo: (state) => state.user,         // ログイン情報
};

export const actions = {
  firebaseAuthLogin({ state }, loginInfo) {
    if (!loginInfo.loginMailAddress || !loginInfo.loginPassword) return null;
    signInWithEmailAndPassword(
      auth,
      loginInfo.loginMailAddress,
      loginInfo.loginPassword
    )
      .then((userCredential) => {
        const user = userCredential.user;
        // ログイン成功
      })
      .catch((error) => {
        const errorCode = error.code;
        const errorMessage = error.message;
        // ログイン失敗
      });
  },
  firebaseAuthLogout({ state }) {
    signOut(auth)
      .then(() => {
        state.isLoggedIn = false;
        // ログアウト成功
      })
      .catch((error) => {
        // ログアウト失敗
      });
  },
  setAuthChangedListener({ commit }) {
    onAuthStateChanged(auth, (user) => {
      user = user ? user : {};
      state.user = user;
      state.isLoggedIn = user.uid ? true : false;
    });
  },
};


```
今回はメール/パスワードでログインすることを想定しました。`Firebase Authentication`の`Sign-in method`で色々なプロバイダ（例えばGoogleやFacebookなど）を利用することができます。その際、`firebaseAuthLogin`を改変する必要があるのでご注意ください（詳しくは[こちら](https://firebase.google.com/docs/auth/web/start?hl=ja#next_steps)）。

`onAuthStateChanged`でログイン情報の変化に対応しています。ログインしているか否かは`isLoggedIn`に格納され、 `isAuthenticated`を介して取得可能です。`firebaseAuthLogin`でログイン、`firebaseAuthLogout`でログアウトできます。度々出てくる`{ state }`は自動で与えられるもので、vuex内のstateを参照できます。

また、Vuex内の関数は第二引数までしか渡すことができません。（第一引数は`{ state }`）そこで、引数をオブジェクト型にし、階層をもって三つ以上の引数を渡しています（詳しくは[こちら](https://mseeeen.msen.jp/deal-with-multiple-arguments-with-action-or-mutation-in-vuex/)）。

ではどのように使用するのか見てみましょう。

## middlewareをつくる
middlewareはページ遷移時に比較的早く呼ばれる部分でページに進入できるかどうかを振り分けます。

`middleware`ディレクトリ内に`checkAuth.js`を生成し、以下のとおり記述します。

```javascript:middleware/checkAuth.js
const allowUnauthorizedPages = ["index", "login"];
const banAuthorizedPages = ["login"];

export default function ({ store, route, redirect }) {
  store.dispatch("auth/setAuthChangedListener");
  if (
    !store.getters["auth/isAuthenticated"] &&
    !allowUnauthorizedPages.includes(route.name)
  ) {
    // 未ログインかつ、未ログインで入ってはいけないページの場合はログインページへ
    redirect("/login");
  }
  if (
    store.getters["auth/isAuthenticated"] &&
    banAuthorizedPages.includes(route.name)
  ) {
    // ログイン済みかつ、ログイン済みで入ってはいけないページの場合はホームへ
    redirect("/");
  }
}
```

`allowUnauthorizedPages`は未ログインで入れるページを記録します。このとき、`route.name`と比較するのでホームは`index`となることに注意してください。また、`banAuthorizedPages`ではログイン済みでは入れないページ（例えばログインページなど）を記述してください。それぞれ条件が揃った時に`redirect`される仕組みです。

`store.dispatch()`で`store/auth.js`の`setAuthChangedListener`を呼び、ログイン情報に変化があったときのコールバックを登録しときます。なお、`auth/setAuthChangedListener`のように`ファイル名/Action名`を引数に取ります。`store.getters`も同様にとります。

最後にこの`middleware`を全ページに登録して終わりです。

```javascript:nuxt.config.js（抜粋）
router: {
  middleware: ["checkAuth"],
},
```

長期的なテストをおこなっていないため、`isAuthenticated`はfalseだが、firebase上では認証されていると言う可能性もありますが、ユーザ視点ではセッションが切れた程度にしか感じないためその場合でもログインページに飛ばしています。そもそもそのような状況があるかどうかもわからないのでもし深刻であれば`firebase/auth`に現在のユーザ情報をとる関数があるはずなのでそれを使うといいと思います。

# 第3章 使い方
ここまでpage本体には手を出してこなかったため、かなりきれいな記述ができ他と思います。ですが、どうしてもpageで書かないといけないことがあります。それはログイン状態やユーザ情報の取得とログインです。

## vuexからログイン状態やログイン情報を取得する
```vue:pages/index.vue
<template>
  <!-- 中略 -->
  <div>{{ isAuthenticated }}:{{ currentUserInfo }}</div>
  <!-- 中略 -->
</template>

<script>
// 中略...
import { mapGetters } from "vuex";

export default {
  // 中略...
  computed: {
    ...mapGetters("auth", ["isAuthenticated", "currentUserInfo"]),
  },
  // 中略...
}
</script>
```

`mapGetters`で`<template>`内にあるように普通の変数として扱えるようになります。

## vuexのactionsを叩いてログインする

```vue:pages/index.vue
<template>
  <!-- 中略 -->
  <div @click="login">ログイン</div>
  <!-- 中略 -->
</template>

<script>
// 中略...
import { mapActions } from "vuex";

export default {
  // 中略...
  data() {
    return {
      loginMailAddress: "",
      loginPassword: "",
    };
  },
  methods: {
    ...mapActions("auth", ["firebaseAuthLogin"]),
    login() {
      this.firebaseAuthLogin({
        loginMailAddress: this.loginMailAddress,
        loginPassword: this.loginPassword,
      });
    },
  },
  // 中略...
}
</script>
```
`mapActions`で指定した`actions`をこちらの`methods`として扱えるようになります。先述したとおり、メールアドレスとパスワードは第三引数を取れないため、Object型で渡していることがわかると思います。`v-model`などで`this.loginMailAddress`や`this.loginMailAddress`をどこかの`<input>`と紐づければログイン処理は完成です。なお、`null`除外処理はしっかり書いた方がいいと思いますが今回は省略しました。

# さいごに
今回はかなり急ぎ&雑に書いてしまったのでこのコードが完璧に動くかどうかは未検証です。もしかしたら、書いていない部分で弄り回しててそこが効いてる可能性もありますが...。その場合はご指摘ください。また、`electron`がいると`RuntimeConfig`使えない（？）問題については鋭意調査中ですが何か知っている方がいればコメントお願いします。

今回で一番戸惑ったのは`firebase v9`の圧倒的情報不足です...。公式リファレンスを読みまくって実装したのでもしかしたらもっといい実装方法があったかもしれません。それはおいおいみなさんが記事を出してくれることでしょう。そう祈って _**Merry Christmas**_ –––良いお年を。

<details><summary>p.s.今回参考にした記事様・公式リファレンスです...</summery>
[【公式リファレンス】ウェブサイトで Firebase Authentication を使ってみる](https://firebase.google.com/docs/auth/web/start?hl=ja)
[【公式リファレンス】JavaScript でメールリンクを使用して Firebase 認証を行う](https://firebase.google.com/docs/auth/web/email-link-auth?hl=ja)
[【公式リファレンス】認証状態の永続性](https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ja)
[【公式リファレンス】auth package](https://firebase.google.com/docs/reference/js/auth)
[【Qiita】【４】Firebase AuthenticationとNuxt.jsのVuexで認証機能を実現する(2)](https://qiita.com/kobori_akira/items/3e64d543023266051a8f)
[【Qiita】【nuxt】middlewareについて](https://qiita.com/yama-t/items/97871a4c2f1cd8325078)
[【Hatena Blog】Nuxtで画面に横断的にMiddlewareを適用する](https://su-kun1899.hatenablog.com/entry/2018/06/19/200000)
[【Hatena Blog】Nuxtで.envファイルを使って環境変数を設定する](https://firebase.google.com/docs/auth/web/start?hl=ja)
[【MSeeeeN】Vuex で Action, Mutation に第3引数を渡したくなったら](https://mseeeen.msen.jp/deal-with-multiple-arguments-with-action-or-mutation-in-vuex/)
[【Metrocode.co】Firebase Authentication と NUXT（Vue.Js）で簡単にユーザー登録＆認証を作成する](https://www.metrocode.co/blog/post/firebase-authentication-nuxt-vuejs)</details>
</details>