Nuxt.js + Firebase Authentication + FireStoreでwebアプリケーションハンズオン

本記事は以下に引っ越しました。
今後Qiita上での更新はありません。

https://zenn.dev/ririli47/articles/f8d12ad890ca4d9c615b

-----------------------------

## この記事のターゲット
- Nuxt.js + Firebaseで何か作りたい
- Firebase AuthenticationとCloud FireStore使いたい


## Chapter1 firebaseの準備
まずはfirebase側を準備します。
今回用意するのは
- プロジェクト
- Authentication
- FireStore
の三つ。

### firebase consoleにログイン
1.https://firebase.google.com/?hl=ja へアクセス
2.右上の「ログイン」ボタンからログイン。
3.ログインできたら「コンソールへ移動」をクリック。

### firebaseプロジェクトの作成
1.「プロジェクトを追加」からプロジェクトを追加します。
2.プロジェクト名は適当に設定。
3.アナリティクスの地域を「日本」にして「次へ」。（日本にすると料金報告などが円になる...らしい）
4.「新しいプロジェクトのデータ共有のカスタマイズ」とか聞かれますが今回はお試し用なので関係なし、「プロジェクトを作成」をクリック。
5.プロジェクトのコンソールに入れたらOK。


### Authentication設定
1.左のメニューから「Authentication」を選択。
2.タブから「ログイン方法」を選択。
3.今回はお手軽な「Google認証」を追加します。
「Google」を選択して「有効にする」ボタンをONに変更。
4.「プロジェクトのサポートメール」が必須項目なので連絡が着くメールアドレスを入れておきましょう。
5.「保存」をクリック。
6.ステータスが「有効」になればOK

### FireStore設定
1.左のメニューから「Database」を選択。
2.Cloud FireStoreに紐づいている「データベースの作成」をクリック。
3.「ロックモード」と「テストモード」の選択を迫られますが、 読み取られて困るものは突っ込まないので「テストモード」にして「次へ」。
4.ロケーションの設定は近そうな所にして「完了」。（asia-northeast1が東京らしいです）
5.Databaseのコンソールに入れていたらOK。


## Chapter2 Nuxt.jsの準備
続いてアプリケーション側としてNuxt.jsでプロジェクトを作成します。
リポジトリは各自適用に用意で。
Nuxtそのものについては日本語の情報もたくさんある公式に聞いてくださいな。
https://ja.nuxtjs.org/

### プロジェクト作成
便利なコマンドがあるのでこれでいきましょう。

```
yarn create nuxt-app <project-name>
```

色々聞かれますがそんなに深く考えなくて良いです。
とりあえず↓な感じで。

```
? Project name hoge-nuxt
? Project description My tiptop Nuxt.js project
? Author name ririli
? Choose the package manager Yarn
? Choose UI framework Bulma
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework None
? Choose rendering mode Universal (SSR)
```

| 質問 | 回答 | 備考 |
|---|---|---|
| Project name | <project-name> | |
| Project description | デフォルト | | 
| Author name | 好きな名前で | |
| Choose the package manager | お好み | |
| Choose UI framework | Bulma | 趣味 |
| Choose custom server framework | None (Recommended) | 特にバックエンドが必要なければnoneで |
| Choose Nuxt.js modules | (Press <space> to select, <a> to toggle all, <i> to invert selection) | 今回はfirebaseの公式のやり方でアクセスするのでaxiosも不要 |
| Choose linting tools | (Press <space> to select, <a> to toggle all, <i> to invert selection) | ポイ捨てプロジェクトなので何も入れませんでした（好みで） |
| Choose test framework | None | 上に同じく |
| Choose rendering mode | Universal (SSR) | 特に深い理由はないです（たまにライブラリによってはSPAだと動かないものがあるので注意） |


指示にしたがって開発環境を立ち上げてみましょう。

```
cd hoge-nuxt
yarn dev
```

ローカルホストが立ち上がるのでコンソールにでたURLにアクセス。
ちゃんと動いてればOKです。

### firebase接続準備

プロジェクトに「Firebase JavaScript SDK」を入れます。
（参考：https://firebase.google.com/docs/web/setup?authuser=0）

yarnならこっち。

```
yarn add firebase save
```

npmならこっち。

```
npm install --save firebase
```


### firebaseと繋ぐ

このアプリケーションがfirebaseのどのプロジェクトと紐づくのかを設定します。

1.firebaseコンソールの「プロジェクトの設定」からプロジェクトにアプリを追加します。
2.今回はwebを選択。
3.適当な名前をつけてHostingにチェックを入れ「次へ」。
4.Firebase SDKはもう入れたのでスルー。
5.最後にHostingするときに必要なので言われた通りに導入します。

```
npm install -g firebase-tools
```

```
yarn add firebase-tools
```

6.これまた指示通りにデプロイしましょう。
デプロイするファイルは静的なものでなければいけないのでnuxtの機能で静的に書き出します。
package.jsonにコマンドが書かれてるのでこれで。

```
yarn generate
```

するとdistというディレクトリに静的ファイルが吐き出されます。
firebaseにログインしたあとinitでデプロイするディレクトリをデフォルトではなくdistとしてください。

```
firebase login
firebase init
firebase deply
```

デプロイ先URLが表示されるのでアクセスして確認。

### アプリケーションに設定

以下ファイルを作成。
firebaseコンソールの設定から取得できる以下の値を貼り付けます。

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

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
        apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        authDomain: "xxxxxxxxxxxxxxxx.firebaseapp.com",
        databaseURL: "https://xxxxxxxxxxxxxxxx.firebaseio.com",
        projectId: "xxxxxxxxxxxxxxxx",
        storageBucket: "xxxxxxxxxxxxxxxx.appspot.com",
        messagingSenderId: "xxxxxxxxxxxxx",
        appId: "x:xxxxxxxxxxxxx:web:xxxxxxxxxxxxxxxx"
    }
  )
}

export default firebase
```

一応隠してますがこのパラメータはgithubなどに公開しても問題ないようにできています。
ただしDBのruleが甘かったりすると危なかったりするので隠しておいて損はないかと。
詳しくはこちら:[Firebase apiKey ってさらしていいの? ほんとに?](https://qiita.com/odistagon/items/e9c14ed157200b36eaa5)


## Chapter3 ログインしてみよう

一通り準備が済んだので早速Firebaseに繋いでみましょう。
まずはAuthenticationでログインするところから。

### ログイン画面準備
ログイン画面もなしにログインとか無理なので適当に作っちゃいましょう。
pages/index.vueを書き換えます。
以下はBULMAを使った例。
デザインはお好みです。

```index.vue
<template>
  <div class="container">
      <button class="button is-primary is-rounded">
        ログイン
      </button>
  </div>
</template>

<script>
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>
```


### ログイン処理
これでは何も起きないボタンがあるだけなのでログイン機能を付け足していきます。
まずはクリックされたらconsoleに「login」と出るところから

```index.vue
<template>
  <div class="container">
-     <button class="button is-primary is-rounded">
+     <button class="button is-primary is-rounded" @click="login">
        ログイン
      </button>
  </div>
</template>

<script>
+export default {
+  methods: {
+    login() {
+      console.log('login')
+    }
+  }
+}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>
```

無事にloginと出たらログイン処理をかいていきます。
実際にログイン処理を行うのはストア側の責務としましょう。
以下ファイルを作成します。

```store/index.js
import firebase from '~/plugins/firebase'

export const state = () => ({
})
  
export const mutations = {
}

export const actions = {
  login() {
    console.log('login action')
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(function(result) {
      const user = result.user;
      console.log('success : ' + user)
    }).catch(function(error) {
      var errorCode = error.code;
      console.log('error : ' + errorCode)
    });
  }
}

export const getters = {
}
```

actionにログイン処理を作ったのでログイン画面からこれを呼び出します。

```pages/index.vue
<template>
  <div class="container">
      <button class="button is-primary is-rounded" @click="login">
        ログイン
      </button>
  </div>
</template>

<script>
export default {
  methods: {
    login() {
      console.log('login')
+     this.$store.dispatch('login')
    }
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
</style>
```

### いざログイン！
といきたいところですがおそらく失敗します。
（失敗しなかった方は飛ばしてください。）

以下のエラーが出たのではないでしょうか？

```
承認エラー
エラー 403: restricted_client
```

これはログイン先のサイトが信用にたるサイトかどうかわからないために発生しています。
googleの設定画面で利用規約に同意することで解決します。
そのためにはまずエラー画面にある「詳細」でgoogleの設定画面へ移動。
サポート先のメールアドレスを設定して「保存」。
ここまでやったらログインをやり直してみましょう。
ログインして元の画面に帰ってこれたら成功！

### ログイン情報を表示してみよう
ログインできたユーザーのIDを画面に出してみましょう。
まずはvuex側でstateを宣言、ログイン時にuserのUidとdisplayNameを格納、state取得用のgettersを宣言します。

```store/index.js
import firebase from '~/plugins/firebase'

export const state = () => ({
+ userUid: '',
+ userName: ''
})
  
export const mutations = {
+ setUserUid(state,userUid) {
+   state.userUid = userUid
+ },
+ setUserName(state,userName) {
+   state.userName = userName
+ }
}

export const actions = {
- login() {
+ login({ commit }) {
    console.log('login action')
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(function(result) {
      const user = result.user;
-     console.log('success : ' + user)
+     console.log('success : ' + user.uid + ' : ' + user.displayName)
+     commit('setUserUid', user.uid)
+     commit('setUserName', user.displayName)
    }).catch(function(error) {
      var errorCode = error.code;
      console.log('error : ' + errorCode)
    });
  }
}

export const getters = {
+ getUserUid(state) {
+   return state.userUid
+ },
+ getUserName(state) {
+   return state.userName
+ }
}
```


次に表示側。

```pages/index.vue
<<template>
  <div class="container">
+     <p class="title is-1 is-spaced">user: {{ $store.getters.getUserName }}</p>
      <button class="button is-primary is-rounded" @click="login">
        ログイン
      </button>
  </div>
</template>

<script>
export default {
  methods: {
    login() {
      console.log('login')
      this.$store.dispatch('login')
    }
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  flex-direction: column;
}
</style>
```

ログインして名前が表示されれば成功！


## Chapter4 Firestoreへの読み出し/書き込み

無事にGoogle Authでログインできたところで、今度はFirestoreを使っていきましょう。
大きく分けて読み出し、書き込みを試してみます。

### 読み出し
先に読み出しから。
firestoreのコンソールから手動でデータを追加できるのでまずは読みだすためのデータを作りましょう。

1.コレクションを開始をクリック
2.コレクション名を「todos」にして次へ
3.ドキュメントIDは自動IDに、フィールドには「todo」として適当に値を入れて保存。
4.フィールドを追加してlimitというフィールド名で適当に値を入れて保存
5.ドキュメントを追加から3,4の手順をもう一度

これで読み出すべきデータが二つ作られたのでアプリケーション側から読み出しましょう。
firestoreを利用するには、pluginで宣言したfirebaseオブジェクトからfirestoreメソッドを利用します。
ついでなんども宣言することになるのでコレクションへのパスを宣言しておきます。

```
const db = firebase.firestore();

const todoRef = db.collection('todos')
```

本来ならstoreをtodos用に切るところですが簡略化のためにstore/index.jsにstateと取得する処理を追加します。

```store/index.js
import firebase from '~/plugins/firebase'
+const db = firebase.firestore();

+const todoRef = db.collection('todos')


export const state = () => ({
  userUid: '',
  userName: '',
+ todos: []
})
  
export const mutations = {
  setUserUid(state,userUid) {
    state.userUid = userUid
  },
  setUserName(state,userName) {
    state.userName = userName
  },
+ addTodo(state, todo) {
+   state.todos.push(todo)
+ }
}

export const actions = {
  login({ commit }) {
    console.log('login action')
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(function(result) {
      const user = result.user;
      console.log('success : ' + user.uid + ' : ' + user.displayName)
      commit('setUserUid', user.uid)
      commit('setUserName', user.displayName)
    }).catch(function(error) {
      var errorCode = error.code;
      console.log('error : ' + errorCode)
    });
  },
+ fetchTodos({ commit }) {
+   todoRef
+   .get()
+   .then(res => {
+     res.forEach((doc) => {
+       console.log('success : ' + `${doc.id} => ${doc.data()}`);
+       commit('addTodo', doc.data())
+     })
+   })
+   .catch(error => {
+     console.log('error : ' + error)
+   })
+ }
}

export const getters = {
  getUserUid(state) {
    return state.userUid
  },
  getUserName(state) {
    return state.userName
  },
+ getTodos(state) {
+   return state.todos
+ }
}
```

次に表示側テーブルを作ってv-forで取得したtodosを回します。

```pages/index.vue
<template>
  <div class="container">
      <p class="title is-1 is-spaced">user: {{ $store.getters.getUserName }}</p>
      <button class="button is-primary is-rounded" @click="login">
        ログイン
      </button>

+     <table class="table is-narrow">
+       <thead>
+         <tr>
+           <th>todo</th>
+           <th>limit</th>
+         </tr>
+       </thead>
+       <tbody>
+         <tr v-for="todo in $store.getters.getTodos" :key="todo.todo">
+           <td>{{todo.todo}}</td>
+           <td>{{todo.limit}}</td>
+         </tr>
+       </tbody>
+     </table>
  </div>
</template>

<script>
export default {
  methods: {
    login() {
      console.log('login')
      this.$store.dispatch('login')
    }
  },
+ created() {
+   this.$store.dispatch('fetchTodos')
+ }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  flex-direction: column;
}
</style>
```

無事に表示されれば取得処理は成功！

### Firestoreへの書き込み
ついに最後、書き込みです。

先ほど作ったtodosに新しく追加してみましょう。
まずはstore側。
addTodoというacitonを切ってfirestoreへ送信するとともにstateのtodosにも追加します。

```store/index.js
import firebase from '~/plugins/firebase'
const db = firebase.firestore();

const todoRef = db.collection('todos')


export const state = () => ({
  userUid: '',
  userName: '',
  todos: []
})
  
export const mutations = {
  setUserUid(state,userUid) {
    state.userUid = userUid
  },
  setUserName(state,userName) {
    state.userName = userName
  },
  addTodo(state, todo) {
    state.todos.push(todo)
  },
+ clearTodo(state) {
+   state.todos = []
+ }
}

export const actions = {
  login({ commit }) {
    console.log('login action')
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).then(function(result) {
      const user = result.user;
      console.log('success : ' + user.uid + ' : ' + user.displayName)
      commit('setUserUid', user.uid)
      commit('setUserName', user.displayName)
    }).catch(function(error) {
      var errorCode = error.code;
      console.log('error : ' + errorCode)
    });
  },
  fetchTodos({ commit }) {
    commit('clearTodo')
    todoRef
    .get()
    .then(res => {
      res.forEach((doc) => {
        console.log('success : ' + `${doc.id} => ${doc.data()}`);
        commit('addTodo', doc.data())
      })
    })
    .catch(error => {
      console.log('error : ' + error)
    })
  },
+ addTodo({commit}, todo) {
+   console.log(todo)
+   todoRef
+   .add({
+     todo: todo.todo,
+     limit: todo.limit,
+   })
+   .then(function(docRef) {
+     console.log("Document written with ID: ", docRef.id);
+     commit('addTodo', todo)
+   })
+   .catch(function(error) {
+     console.error("Error adding document: ", error);
+   });
  }
}

export const getters = {
  getUserUid(state) {
    return state.userUid
  },
  getUserName(state) {
    return state.userName
  },
  getTodos(state) {
    return state.todos
  }
}
```

次に表示側。
firestoreに送信する値のフォームを作成。

```pages/index.vue
<template>
  <div class="container">
    <p class="title is-1 is-spaced">user: {{ $store.getters.getUserName }}</p>
    <button class="button is-primary is-rounded" @click="login">
      ログイン
    </button>

    <table class="table is-narrow">
      <tbody>
        <tr>
          <th>todo</th>
          <th>limit</th>
        </tr>
      </tbody>
      <tbody>
        <tr v-for="todo in $store.getters.getTodos" :key="todo.todo">
          <td>{{todo.todo}}</td>
          <td>{{todo.limit}}</td>
        </tr>
      </tbody>
    </table>
+   <div class="field is-grouped">
+     <p class="control is-expanded">
+       <input v-model="newTodo" class="input" type="text" placeholder="todo">
+     </p>
+     <p class="control is-expanded">
+       <input v-model="newLimit" class="input" type="text" placeholder="limit">
+     </p>
+     <p class="control">
+       <a class="button is-primary" @click="addTodo">
+         add
+       </a>
+     </p>
+   </div>
  </div>
</template>

<script>
export default {
+ data() {
+   return {
+     newTodo: '',
+     newLimit: ''
+   }
+ },
  methods: {
    login() {
      console.log('login')
      this.$store.dispatch('login')
    },
+   addTodo() {
+     const todo = this.newTodo
+     const limit = this.newLimit
+     
+     this.$store.dispatch('addTodo', {todo, limit})
+     this.newTodo = ''
+     this.newLimit = ''
+   }
+ },
  created() {
    this.$store.dispatch('fetchTodos')
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  flex-direction: column;
}
</style>
```

送信した値がfirestoreコンソールから確認できれば成功！
画面をリロードしてもfetchTodosで最初から取得できているはずです。

最後にfirebase deployでhostingして動きもみてみましょう。

## まとめ
お疲れ様でした。
nuxt.jsからfirebase AuthenticationとCloud firestoreを利用することができました。
実際にはfirestoreのruleやstateの持ち方、更新の方法などもっと気にしなければいけない部分や今後のことを考えた実装も必要かもしれません。
ですが、とりあえず使ってみたという経験がいざという時に役に立つことも多いはず。
みなさまの参考になれば幸いです。

ちなみにこの説明の中では自分が詰まった部分を書くことを目的の一つとしていました。
具体的には
- authenticationの403エラー
- firestoreとauth両方使う場合の宣言
のあたりです。

同じ問題で詰まってしまう人が減るといいなと思います。