Nuxt.js で Firebase SDK の自動構成を使おうとしたら死ぬほど苦労した話

## はじめに

Nuxt.js + Firebase の自動構成 + ローカルでは固定のFirebaseConfig をやろうとしたらすんごい苦労した & どこにも記事がなかったので、自分への備忘録も含め記事にしてみました。

### お詫び

まず最初にお断りしておきますが、本記事では__明確な解決法は出ておりません。__一応、動くところまでは行き着いたのですが、絶対もっといい方法があるよなーというコードになっています。
なので、解決方法を知りたくてこの記事をご覧になった方は残念ながら、あまりお役には立てません。
もし解決方法をご存知の方がいらっしゃいましたら、是非コメントに書いていただけると幸いです。

### 序

今、JavaScript界隈で[Vue.jsがアツい](https://risingstars.js.org/2019/en/)らしいです。そして、Vue.js で Webをやるには [Nuxt.js](https://ja.nuxtjs.org/) というフレームワークがとても人気らしく、これさえ使えればナウいヤングの間でも一目置かれる存在になれるそうです。アラサーになり、そろそろ__学園物ドラマを見ていると疲れが出てくるようになった私__には朗報です。

さらに、[Firebase](https://firebase.google.com/)というプラットフォームを使いこなせれば[人生が変わった](https://qiita.com/owen/items/c51c6ec27c9e2f5804b0)という証言まであります。これは億万長者・酒池肉林も夢じゃないです。

「…やらないという手はないやろ」

と言うわけで、この2つを使って勉強がてら Web アプリケーションを作ってみました。

## 第0形態:一般的な Firebase SDK の呼び出し

Nuxtの構築 や Firebase の初期設定については、検索すれば星の数…とまでは行かなくても__鳥取県の人口くらいの数__の記事が出てくるので割愛します。

今回作ったのは Firebase Auth を使ってユーザー認証を行うページです。
様々なサンプルにあるように、 plugins 配下で以下のように firebase を初期化して

```plugins/firebase.js
import firebase from 'firebase'

const firebaseConfig = {
     apiKey: "api-key",
     authDomain: "project-id.firebaseapp.com",
     databaseURL: "https://project-id.firebaseio.com",
     projectId: "project-id",
     storageBucket: "project-id.appspot.com",
     messagingSenderId: "sender-id",
     appID: "app-id",
  };

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig)
}

export default firebase

```

pages 以下の .vue ファイルで import して使います。

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from '~/plugins/firebase'

export default {
  mounted() {
    let auth = firebase.auth();
	(略)
  }
}
</script>
```

さて、ここまでは全然難しくありません。
問題はここからです。

## 第0.5形態: SDK の自動構成を使う

練習がてら作っているこのアプリですが、最終的にはサービスとしてリリースをする予定です。なので、dev 環境と production 環境の2種類の Firebase プロジェクトを作り、それぞれの Firebase Hosting にデプロイすることを考えました。

すると、上記 `firebase.js` のような API Key べた書きなコードだと問題が発生します。デプロイした環境に応じて適切な設定で初期化をしたいです。
幸いにも、Firebase はその様な機能を提供しており[公式ドキュメントの SDK の自動構成](https://firebase.google.com/docs/hosting/reserved-urls?hl=ja#sdk_auto-configuration)という項目にやり方が書いてあります。
`/__/firebase/init.json` というスクリプトを Hosting 環境で実行すると、適切な設定ファイルが入手できるそうです。

ドキュメントのやり方に従って `firebase.js` を書き換えます。

```plugins/firebase.js
import firebase from 'firebase'

fetch('/__/firebase/init.json').then(async response => {
  firebase.initializeApp(await response.json());
});

export default firebase

```

めでたしめでたし。
…と思いきや、今度は別の問題が発生します。これでは `nuxt` コマンドを使った開発ができません。

ローカルでデプロイのテストを行うには `firebase serve` コマンドで行うことが出来ます。(詳しくは[こちら](https://firebase.google.com/docs/hosting/deploying?hl=ja#test-locally))
つまり、`nuxt generate; fireabase serve` とやれば一応ローカルでも上記コードで動かすことが出来るのですが、ちょっとの修正でも毎回確認に1分ほど待つ必要があり、まるで Java で大型プロジェクトを開発しているような気分になります。

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/c983092b-21df-498c-1ef7-d6c64a1699d7.jpeg">

と言うわけで、__ローカルの時はconfigを指定し、Firebase Hosting 環境では自動構成を使う__にはどうしたらいいかを考えました。

## 第1形態: とりあえず context を呼んでみる

この手のフレームワークには必ずと言っていいほど `isDevelop` の様な定数が定義されています。
Nuxt にもない訳がありません。

検索してみると、案の定 `context` と言うオブジェクト内に `isDev` と言う定数があることが分かりました。
[ドキュメント](https://ja.nuxtjs.org/api/context/#-code-isdev-code-em-boolean-em-)によると…

> 開発モードであるかどうかを知らせます。

随分ざっくりとした説明ですが、多分、`nuxt` の時は `true` になって、`nuxt build` や `nuxt generate` の場合は `false` になる、んだよね？私は信じます。

また、[公式ドキュメントの Context のページ](https://ja.nuxtjs.org/api/context/)にはサンプルコードが載っているのですが

```
function (context) {
  // Universal keys
  const {
    app,
    store,
    route,
    params,
    query,
    env,
    isDev,
    isHMR,
    redirect,
    error
  } = context
  // Server-side
  if (process.server) {
    const { req, res, beforeNuxtRender } = context
  }
  // Client-side
  if (process.client) {
    const { from, nuxtState } = context
  }
}
```

__ちょっと何言ってるか分からないです。__
![134293794799413201156.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/b962d909-ce7b-2d0d-0042-6d08d90593bf.jpeg)


唐突に出てくる関数定義、全く見えないコードの全体像、サンプルコードの次の項目を見てもそれぞれの定数の意味の解説しかありません。
中々に玄人志向なドキュメントですね。

分かんないときは__とりあえず動かします。__
`context.isDev` に必要な値が入っていることは分かったので、それを使って分岐させてみましょう。

```firebase.js
import firebase from 'firebase'

if (context.isDev) {
  const firebaseConfig = {
     apiKey: "api-key",
     authDomain: "project-id.firebaseapp.com",
     databaseURL: "https://project-id.firebaseio.com",
     projectId: "project-id",
     storageBucket: "project-id.appspot.com",
     messagingSenderId: "sender-id",
     appID: "app-id",
   };
  
  if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig)
  }
} else {
  fetch('/__/firebase/init.json').then(async response => {
    firebase.initializeApp(await response.json());
  });
}

export default firebase
```

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from '~/plugins/firebase'

export default {
  mounted() {
    let auth = firebase.auth();
	(略)
  }
}
</script>
```

さて、実行させてみると…

![コメント 2020-02-14 225418.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/91e49353-d332-01c2-78fd-eabf02ceca8d.jpeg)


__ですよねー__
`context is not defined`  と言われてしまいました。

## 第2形態: サンプルコードに従って context を使う

ちゃんとドキュメントを読んでみましょう。
Nuxt公式ドキュメントのプラグインのページに[コンテキストを使ったサンプルコードが載っていました](https://ja.nuxtjs.org/guide/plugins/#%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%81%AB%E6%B3%A8%E5%85%A5%E3%81%99%E3%82%8B)

```plugins/ctx-inject.js
export default ({ app }, inject) => {
  // context.app オブジェクトへ関数を直接セットします
  app.myInjectedFunction = string => console.log('Okay, another function', string)
}
```

コレを見るに、plugin では `export default` の関数の場合、 context が引数として渡されるみたいです。
(他にも渡される条件があるのかもしれませんが、ドキュメントを読んでも分かりませんでした。。知ってる方がいましたら、教えてください！)

ちなみに、 `{ app }` というナウい書き方は__分割代入 (Destructuring assignment) 構文__というらしいです。詳しくは[MDNのこのページ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)を見てください。

```plugins/firebase.js
import firebase from 'firebase'

export default (context) => {
  if (context.isDev) {
    const firebaseConfig = {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",
    };
    
    if (!firebase.apps.length) {
      firebase.initializeApp(firebaseConfig)
    }
  } else {
    fetch('/__/firebase/init.json').then(async response => {
      firebase.initializeApp(await response.json());
    });
  }

  return firebase
}
```

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from '~/plugins/firebase'

export default {
  mounted() {
    let auth = firebase.auth();
	(略)
  }
}
</script>
```

実行してみます。

![コメント 2020-02-15 172801.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/2860bd96-8242-d94c-0dc1-15231e1d7ef9.jpeg)


`_plugins_firebase__WEBPACK_IMPORTED_MODULE_6__.default.auth is not a function` と言われてしまいました。
これは__想定内__です。
今までは `login.vue` の `firebase` 変数には値が入っていましたが、__今回の書き方では関数が入っています。__
なので、 `firebase.auth()` としても `firebase` の中に `auth()` なんてものがないと言われてしまうのです。

と言う訳で、`firebase` を実行してあげましょう。
`firebase().auth()` と書き換えます。

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from '~/plugins/firebase'

export default {
  mounted() {
    let auth = firebase().auth();
	(略)
  }
}
</script>
```

これで動くはずです。実行してみます。

![コメント 2020-02-15 173306.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/3d0e2a5d-b5b3-382e-f070-71eccf8abae6.jpeg)

__！？__

![コメント 2020-02-15 192612.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/ad80dc95-4f77-67a3-2e1d-a4aac67ea0f3.jpeg)


なんでや！！ `context` が `undefined` になっています。
この後、ドキュメントのサンプルコードにあった ctx-inject.js を実行してみたところ、やはり同じエラーが出ました。
Nuxt のバグでも踏んだか…？

…改めて、 [Nuxt のプラグインのドキュメント](https://ja.nuxtjs.org/guide/plugins/) を読んでみます。

> それから nuxt.config.js の plugins キー内にファイルパスを追加します:

__あ、これやってねぇ__

いや、知ってましたよ。自分、ドキュメントを上から下まで全部読まないと先に進まない派なんで、知ってましたよ。
__ドキュメントも禄に読まずにエラー起こしてパニックなってたのが俺なんやとしたら、俺なんで今こんな堂々としてる？__
<img width="500" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/f69274bb-1c9e-0eee-d93c-44115313946b.jpeg">

…ということで、 `nuxt.config.js` も書き直しまして

```nuxt.config.js
(略)
  plugins: [
    '@/plugins/firebase.js'
  ],
(略)
```

改めて実行してみます。

![コメント 2020-02-15 172801.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/2860bd96-8242-d94c-0dc1-15231e1d7ef9.jpeg)

__ッッッッッ！！！！！__

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/46ebf21f-82af-fdbf-3ecb-628d8ee80ad9.png)

…一旦冷静になりましょう。
あくまでも個人的な推測なのですが、plugins のファイルがロードされる最初のタイミング以外では、 context が undefined になってしまうのではないかと考えられます。
これは、`context` が asyncData、fetch、plugins、middleware、そして nuxtServerInit のような特別な nuxt ライフサイクル内でしか利用可能でないことから予想されます。

何より動かないのでこの方法は没です。

## 第2.5形態:即時関数で書いてみる

第2形態の反省を踏まえ、 `login.vue` 内で firebase を実行するのはやめます。
となると、JavaScript をやっているとこう書きたくなってしまいます。

```plugins/firebase.js
import firebase from 'firebase'

export default (context) => {
  if (context.isDev) {
    const firebaseConfig = {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",
    };
    
    if (!firebase.apps.length) {
      firebase.initializeApp(firebaseConfig)
    }
  } else {
    fetch('/__/firebase/init.json').then(async response => {
      firebase.initializeApp(await response.json());
    });
  }

  return firebase
}()
```

関数定義して最後に `()` 付ければ値が返ってくれるのではないかという希望にかけて実行してみます。

`nuxt` コマンドでビルドしてみると

```
ERROR  Failed to compile with 1 errors
ERROR  in ./plugins/firebase.js
Syntax Error: Unexpected token, expected ";" (26:1)
```

…シンタックスエラーになりました。残念。

## 第3形態: FirebaseConfig を Vue オブジェクトに渡す

今までは割と「こんな風に書いたら動くやろ」で書いて動いてくれてたんですが、今回はちっとも上手くいきません。
__どうしよう。俺、こんな状況 生まれて初めてだ__
![medium_thumb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/1435a919-81df-38cb-085d-2c24d15f75c2.png)

もう、若干コードが汚くなっても動く事だけを考えます。

今までは firebase.js で初期化した firebase オブジェクトを渡すことを考えていましたが、全く別のアプローチで行きます。
firebase.js では__設定だけを定義して__、それぞれの使用するファイル内で initializeApp するという方法です。

まず、 firebase .js で環境毎の設定を定義し、それを Vue オブジェクトに入れます。

```plugins/firebase.js
import Vue from 'vue'

export default (context) => {
  if (context.isDev) {
    Vue.prototype.$firebaseConfig = {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",
    }; 
  } else {
    fetch('/__/firebase/init.json').then(async response => {
      Vue.prototype.$firebaseConfig = await response.json();
    });
  }
}
```

そして、pages 以下のファイルでは `Vue.$firebaseConfig` を使って初期化を行います。

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from 'firebase'

export default {
  mounted() {
    firebase.initializeApp(this.$firebaseConfig);
    let auth = firebase().auth();
	(略)
  }
}
</script>
```

実行してみます。
![コメント 2020-02-15 183419.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/0b2a40d2-94e1-ae1a-248a-ce79d1684a9f.jpeg)

おぉー、やっと動きました。36時間ぶりに見るログイン画面です。

しかしながら、この方法だと firebase を使うページ(≑全ページ)で initializeApp する必要があり、非常に面倒です。
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/c983092b-21df-498c-1ef7-d6c64a1699d7.jpeg">
の再来です。本当にこれでいいんでしょうか…？


## 世界に助けを求めてみる

自分の知識量と発想力ではここが限界点だと感じました。
というか、こんな作業に既に1日半かけていました。こんなことしてる場合ではないのです。

と言う訳で、[StackOverflow で質問をしてみました。](https://stackoverflow.com/questions/60146562/firebase-initialize-in-nuxt-js-plugin-with-context-isdev)
第2形態や第3形態を書くと「何言ってんだコイツ」となりそうだったので、一番シンプルな第1形態の状態で質問を投げて、詳しい人がやり方をレクチャーしてくれることに期待します。
ちなみに、前に質問をしたときは__30分でCloseされました__。今回はどうでしょうか。

5日ほど寝かせて改めて見に行ってみました。

![コメント 2020-02-15 181337.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/a3a49b3b-6a20-2912-e5f8-6af48580be77.jpeg)



…



__お口チャックマンか！__
![a251cc55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/c3126927-d769-0bb8-0394-4ffcc5ed9801.jpeg)

母さん、インターネットは冷たいです。

## 第4形態: plugin では初期化だけを行う

数日寝かせてコードを改めて見ていると、ある仮説が頭をよぎりました。
__これ、 initializeApp した firebase オブジェクトを pages に渡す必要あるのか？__

つまり、plugins/firebase.js では初期化だけを行い、
pages/* では素の `import firebase from 'firebase'` を使うという方法です。

試しに書いてみました。

```plugins/firebase.js
import firebase from 'firebase'

export default (context) => {
  if (context.isDev) {
    const firebaseConfig = {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",
    };

    if (!firebase.apps.length) {
      firebase.initializeApp(firebaseConfig)
    }
  } else {
    fetch('/__/firebase/init.json').then(async response => {
      firebase.initializeApp(await response.json());
    });
  }
}
```

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from 'firebase'

export default {
  mounted() {
    let auth = firebase().auth();
	(略)
  }
}
</script>
```

実行してみます。
![コメント 2020-02-15 183419.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/0b2a40d2-94e1-ae1a-248a-ce79d1684a9f.jpeg)

__うわ、動いちゃったよ__

コードとしては今までで一番すっきりしているんですが、この時 `firebase` オブジェクトの状態がどうなってるのかちっとも分かりません。
これ、アレですね。
![Dkt3fnqUcAAVaUN.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88180/23dcdf56-d69d-62a2-14a4-2b91a869eefa.jpeg)

いや、ヨシじゃねぇ！これ本番で使うの怖すぎる。
まだ冗長でも、動く原理が分かる第3形態の方がましです。
この辺りに詳しい方いらっしゃいましたら、これでいいのか、それともこれじゃマズいのか教えていただけると有り難いです…。

## 暫定結論

Nuxt.js + Firebase の自動構成 + ローカルでは固定の設定 としたい場合、とりあえずこうやれば動きます。
…冗長ですが。

```nuxt.config.js
(略)
  plugins: [
    '@/plugins/firebase.js'
  ],
(略)
```

```plugins/firebase.js
import Vue from 'vue'

export default (context) => {
  if (context.isDev) {
    Vue.prototype.$firebaseConfig = {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",	
    }; 
  } else {
    fetch('/__/firebase/init.json').then(async response => {
      Vue.prototype.$firebaseConfig = await response.json();
    });
  }
}
```

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from 'firebase'

export default {
  mounted() {
    firebase.initializeApp(this.$firebaseConfig);
    let auth = firebase().auth();
	(略)
  }
}
</script>
```

こうやっても動きますが、本当にこれでいいのかは…

```nuxt.config.js
(略)
  plugins: [
    '@/plugins/firebase.js'
  ],
(略)
```

```plugins/firebase.js
import firebase from 'firebase'

export default (context) => {
  if (context.isDev) {
    const firebaseConfig = {
      apiKey: "api-key",
      authDomain: "project-id.firebaseapp.com",
      databaseURL: "https://project-id.firebaseio.com",
      projectId: "project-id",
      storageBucket: "project-id.appspot.com",
      messagingSenderId: "sender-id",
      appID: "app-id",
    };

    if (!firebase.apps.length) {
      firebase.initializeApp(firebaseConfig)
    }
  } else {
    fetch('/__/firebase/init.json').then(async response => {
      firebase.initializeApp(await response.json());
    });
  }
}
```

```pages/login.vue
<template>
<h1>Login Form</h1>
(略)
</template>

<script>
import firebase from 'firebase'

export default {
  mounted() {
    let auth = firebase().auth();
	(略)
  }
}
</script>
```

### おわりに

この記事を見てくださった方の中で、詳しい方がいらっしゃいましたら、どうか正解を教えてください(泣)

## 参考

- [Firebase: SDK の自動構成](https://firebase.google.com/docs/hosting/reserved-urls?hl=ja#sdk_auto-configuration)
- [Nuxt.js: API:コンテクスト](https://ja.nuxtjs.org/api/context/)
- [Nuxt.js: プラグイン](https://ja.nuxtjs.org/guide/plugins/)
- [MDN: export](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/export)
- [MDN: 分割代入](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)
- [Qiita: FirebaseとNuxt.jsを使ってユーザ認証関係を簡単に作ってみる+1ヶ月前の自分に教えたいリンク集](https://qiita.com/redshoga/items/da5c0e247e0df314a257)
- [Nuxt.js で Firebase SDK の自動構成を使おうとしたら死ぬほど苦労した話 (オリジナルのブログの記事)](https://at-428.blogspot.com/2020/02/nuxtjs-firebase-sdk.html)