Nuxt.jsで味わうFirebaseハンズオン


## 0. 下準備
- Firebaseプロジェクトの作成  
https://firebase.google.com/

- firebaseツールのインストール（global）

```sh
$ npm install -g firebase-tools
```

 - npxのインストール(global)

```sh
$ npm install -g npx
```

---

## 1. Nuxt.jsのインストールと起動確認
-  Nuxt.jsのインストール

```sh
npx create-nuxt-app firenuxt
# 設定内容
# > Generating Nuxt.js project in /Users/keisuke/workspace/nuxtjs/firenuxt
# ? Project name firenuxt
# ? Project description My marvelous Nuxt.js project
# ? Use a custom server framework none
# ? Use a custom UI framework none
# ? Choose rendering mode Universal
# ? Use axios module yes
# ? Use eslint no
# ? Use prettier no
# ? Author name keisuke shingaki
# ? Choose a package manager npm
```

---

- 開発サーバーの起動

```sh
$ cd firenuxt
$ npm run dev
# http://localhost:3000で表示が確認できる
```

<img width="1392" alt="スクリーンショット 2018-12-03 16.32.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/2469/9668bdc6-c586-d9be-954e-0fed2e498ffc.png">

---

## 1. Firebaseのセットアップ

```sh
$ firebase init
# 設定内容
# ? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites
# ? Select a default Firebase project for this directory: firenuxt-30550 (firenuxt)
# ? What do you want to use as your public directory? public
# ? Configure as a single-page app (rewrite all urls to /index.html)? No
```

- 下記のような構成になる

```sh
.
├── .editorconfig
├── .firebaserc # firebaseのプロジェクトファイル
├── .git
├── .gitignore
├── .nuxt
├── README.md
├── assets
├── components
├── firebase.json # firebaseの設定ファイル
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── public
├── static
└── store
```

---

### 2. Nuxt.jsのビルドとFirebase Hostingへのデプロイ
- Nuxt.jsのビルド

```sh
$ npm run generate
```

- firebaseのpublicルートの変更（firebase.json）

```diff:firebase.json
{
  "hosting": {
-   "public": "public",
+   "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

```

---

- デプロイ

```sh
$ firebase deploy
```

<img width="1834" alt="スクリーンショット 2018-12-03 15.27.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/2469/a0f1fefc-5435-45db-365d-e088b7533e94.png">

---

# 2. Firebase Authenticationで認証
## 2.1 Loginテンプレートを作成

```vue:pages/index.vue
<template>
  <section class="container">
    <div>
      
      <!-- 新規登録フォーム -->
      <section>
        <h5>新規登録</h5>
        <p><input type="text" placeholder="メールアドレス"></p>
        <p><input type="password" placeholder="パスワード"></p>
        <div class="links">
          <button>新規登録</button>
        </div>
      </section>

      <!-- ログインフォーム -->
      <section>
        <h5>ログイン</h5>
        <p><input type="text" placeholder="メールアドレス"></p>
        <p><input type="password" placeholder="パスワード"></p>
        <div class="links">
          <button>ログイン</button>
        </div>
      </section>

      <!-- マイページ -->
      <section>
        <h5>ログイン中です</h5>
        <!-- ログイン中ユーザーのメールアドレスを表示 -->
        <p>メールアドレス：</p>
        <div class="links">
          <button>ログアウト</button>
        </div>
        <div>
          <ul><li>めっせーじ</li></ul>
        </div>
        <div class="links">
          <p><input type="text" placeholder="メッセージ"></p>
          <button>メッセージ追加</button>
        </div>
      </section>

    </div>
  </section>
</template>

<style>
.container {
  padding: 100px;
}
</style>

```

---

### 2.2 Firebase Authenticationの設定

- ライブラリのインストール

```sh
$ npm install firebase
```

- 設定ファイルを追加  
[コンソール](https://console.firebase.google.com/u/0/)から情報を確認

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

if (!firebase.apps.length) {
    firebase.initializeApp({
        apiKey: "xxxxx",
        authDomain: "xxxxx",
        databaseURL: "xxxxx",
        projectId: "xxxxx",
        storageBucket: "xxxxx",
        messagingSenderId: "xxxxx"
    })
}

export default firebase
```

---

- FirebaseコンソールでAuthenticatioでメール／パスワードの認証を有効にする
<img width="1691" alt="スクリーンショット 2018-12-03 17.55.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/2469/40f25a22-34a4-5dbd-06c6-8df562489135.png">

---

### 2.3 Firebase Authenticationで新規登録

- APIアクセス部分を作成

```js:api/UserAPI.js

import firebase from '@/plugins/firebase'

export default class UserAPI {

    /**
     * メールアドレスとパスワードで新規ユーザー登録を実施します。
     * @param {string} mailAddress 
     * @param {string} password 
     */
    static regist(mailAddress, password) {
        return firebase.auth().createUserWithEmailAndPassword(mailAddress, password);
    }

    /**
     * メールアドレスとパスワードでログインを実施します。
     * @param {string} mailAddress 
     * @param {string} password 
     */
    static login(mailAddress, password) {
        return firebase.auth().signInWithEmailAndPassword(mailAddress, password);
    }

    /**
     * ログアウトします。
     */
    static logout() {
        return firebase.auth().signOut();
    }

    /**
     * ユーザー情報を取得します。
     */
    static getUser() {
        return firebase.auth().currentUser;
    }
}
```

---

- Store部分を作成

```js:store/user.js

// UserAPIのインポート
import UserAPI from "@/api/UserAPI"

export const state = () => ({
    isLogin: false,
    emailAddress: ""
})

export const mutations = {
    // ユーザー情報を設定します。
    setEntity(state, user) {
        state.emailAddress = user.email;
    },
    // stateを初期化します。
    clear(state) {
        state.emailAddress = "";
        state.isLogin = false;
    },
    // ログイン状態を設定します。
    setIsLogin(state, isLogin) {
        state.isLogin = isLogin;
    }
}

export const actions = {
    // ユーザー情報を取得します。
    load: ({ commit }) => {
        var currentUser = UserAPI.getUser();
        if (currentUser) {
            // ログイン済み
            commit('setIsLogin', true);
            commit('setEntity', currentUser);
        } else {
            // 未ログイン
            commit('setIsLogin', false);
            commit('clear');
        }
    },
    // 新規ユーザー登録を行います。
    regist: ({ commit, dispatch }, payload) => {
        return UserAPI.regist(payload.mailAddress, payload.password)
            .then((res) => {
                commit('setIsLogin', true);
                dispatch('setEntity', res.user);
            })
            .catch(function(error) {
                commit('setIsLogin', false);
                commit('clear');
            });
    },
    // ユーザーログインを行います。
    login: ({ commit, dispatch }, payload) => {
        return UserAPI.login(payload.mailAddress, payload.password)
            .then((res) => {
                commit('setIsLogin', true);
                dispatch('setEntity', res.user);
            })
            .catch(function(error) {
                commit('setIsLogin', false);
                commit('clear');
            });
    },
    // ログアウトを行います。
    logout: ({ commit }) => {
        return UserAPI.logout()
            .then((res) => {
                commit('setIsLogin', false);
                commit('clear');
            })
            .catch(function(error) {
                console.log(error);
            });
    },
    // 新規ユーザー登録を行います。
    setEntity: ({ commit }, user) => {
        commit('setEntity', user);
    }
}
```

---

- viewファイルへ組み込み

```vue:pages/index.vue
<template>
  <section class="container">
    <div>
      
      <!-- 新規登録フォーム -->
      <section v-if="!isLogin">
        <h5>新規登録</h5>
        <p><input type="text" v-model="mailAddress" placeholder="メールアドレス"></p>
        <p><input type="password" v-model="password" placeholder="パスワード"></p>
        <div class="links">
          <button @click="regist()">新規登録</button>
        </div>
      </section>

      <!-- ログインフォーム -->
      <section v-if="!isLogin">
        <h5>ログイン</h5>
        <p><input type="text" v-model="mailAddress" placeholder="メールアドレス"></p>
        <p><input type="password" v-model="password" placeholder="パスワード"></p>
        <div class="links">
          <button @click="login()">ログイン</button>
        </div>
      </section>

      <!-- マイページ -->
      <section v-if="isLogin">
        <h5>ログイン中です</h5>
        <!-- ログイン中ユーザーのメールアドレスを表示 -->
        <p>メールアドレス：{{$store.state.user.emailAddress}}</p>
        <div class="links">
          <button @click="logout()">ログアウト</button>
        </div>
      </section>

    </div>
  </section>
</template>

<script>
export default {
  data: function(){
    return {
        mailAddress: '',
        password: '',
        content: '',
    }
  },
  computed: {
    isLogin(){
        return this.$store.state.user.isLogin;
    }
  },
  methods: {
    init: function() {
        this.password = "";
        this.mailAddress = "";
        this.content = "";
    },
    regist: function () {
        console.log("regist")
        this.$store.dispatch('user/regist', {mailAddress:this.mailAddress, password:this.password});
        this.init();
    },
    login: function () {
        this.$store.dispatch('user/login', {mailAddress:this.mailAddress, password:this.password});
        this.init();
        this.getMessages()
    },
    logout : function() {
        this.$store.dispatch('user/logout');
    }
  },
}
</script>

<style>
.container {
  padding: 100px;
}
</style>

```

---

# 3. Firebase Firestoreでデータを保存
## 3.1 Firestoreの設定

<img width="1056" alt="firenuxt_–_Database_–_Firebase_console.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/2469/d60f85ed-1ded-da9e-74c0-6bc42b1b7401.jpeg">

## 3.2 設定ファイルの編集

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

if (!firebase.apps.length) {
    firebase.initializeApp({
        apiKey: "xxxxx",
        authDomain: "xxxxx",
        databaseURL: "xxxxx",
        projectId: "xxxxx",
        storageBucket: "xxxxx",
        messagingSenderId: "xxxxx"
    })
}

// DB設定を追加
const firestore = firebase.firestore();
const settings = { timestampsInSnapshots: true };
firestore.settings(settings);

export default firebase
```

---

- API部分の実装

```js:api/MessageAPI.js
import firebase from '@/plugins/firebase'

export default class MessageAPI {

    /**
     * メッセージのリストを取得
     */
    static getMessages() {
        const messagesRef = firebase.firestore().collection('messages')
        return messagesRef.orderBy('created', 'desc').get()
    }

    /**
     * メッセージを追加
     * @param {string} message
     */
    static addMessage(messageContent) {
        let message = {
            content: messageContent,
            created: Date.now()
        }
        return firebase.firestore().collection('messages').add(message)
    }
}
```

---

- Store部分を作成

```js:store/message.js
// MessageAPIのインポート
import MessageAPI from "@/api/MessageAPI"

export const state = () => ({
    items: []
})

export const mutations = {
    setEntity(state, items) {
        state.items = items;
    },
    clear(state) {
        state.items = [];
    }
}

export const actions = {
    load: ({ commit }) => {
        return MessageAPI.getMessages()
            .then((res) => {
                let messages = []
                res.forEach(doc => {
                    let data = {
                        id: doc.id,
                        content: doc.data().content
                    }
                    messages.push(data)
                })
                commit('setEntity', messages)
            })
            .catch(function(error) {
                console.log(error)
                commit('clear');
            });
    },
    add: ({ commit, dispatch }, payload) => {
        return MessageAPI.addMessage(payload.messageContent)
            .then((res) => {
                dispatch('load');
            })
            .catch(function(error) {
                commit('clear');
            });
    },
}
```

- viewファイルへ組み込み

```vue:pages/index.vue
<template>
  <section class="container">
    <div>
      
      <!-- 新規登録フォーム -->
      <section v-if="!isLogin">
        <h5>新規登録</h5>
        <p><input type="text" v-model="mailAddress" placeholder="メールアドレス"></p>
        <p><input type="password" v-model="password" placeholder="パスワード"></p>
        <div class="links">
          <button @click="regist()">新規登録</button>
        </div>
      </section>

      <!-- ログインフォーム -->
      <section v-if="!isLogin">
        <h5>ログイン</h5>
        <p><input type="text" v-model="mailAddress" placeholder="メールアドレス"></p>
        <p><input type="password" v-model="password" placeholder="パスワード"></p>
        <div class="links">
          <button @click="login()">ログイン</button>
        </div>
      </section>

      <!-- マイページ -->
      <section v-if="isLogin">
        <h5>ログイン中です</h5>
        <!-- ログイン中ユーザーのメールアドレスを表示 -->
        <p>メールアドレス：{{$store.state.user.emailAddress}}</p>
        <div class="links">
          <button @click="logout()">ログアウト</button>
        </div>
        <div>
          <ul v-for="(item,index) in $store.state.message.items" :key="index">
　　　　　　　<li>{{ item.content }}</li>
          </ul>
        </div>
        <div class="links">
          <p><input type="text" v-model="content" placeholder="メッセージ"></p>
          <button @click="addMessage()">メッセージ追加</button>
        </div>
      </section>

    </div>
  </section>
</template>

<script>
export default {
  data: function(){
    return {
        mailAddress: '',
        password: '',
        content: '',
    }
  },
  computed: {
    isLogin(){
        return this.$store.state.user.isLogin;
    }
  },
  methods: {
    init: function() {
        this.password = "";
        this.mailAddress = "";
        this.content = "";
    },
    regist: function () {
        console.log("regist")
        this.$store.dispatch('user/regist', {mailAddress:this.mailAddress, password:this.password});
        this.init();
    },
    login: function () {
        this.$store.dispatch('user/login', {mailAddress:this.mailAddress, password:this.password});
        this.init();
        this.getMessages()
    },
    getMessages: function() {
      this.$store.dispatch('message/load');
    },
    addMessage: function() {
      this.$store.dispatch('message/add', { messageContent: this.content });
      this.init();
    },
    logout : function() {
        this.$store.dispatch('user/logout');
    }
  },
}
</script>

<style>
.container {
  padding: 100px;
}
</style>

```

---

- 実装画面

![スクリーンショット 2018-12-19 18.51.53.png](https://qiita-image-store.s3.amazonaws.com/0/2469/68b05347-35d4-a82a-6f24-7bf124ff7c41.png)

![スクリーンショット 2018-12-19 18.52.12.png](https://qiita-image-store.s3.amazonaws.com/0/2469/09480a30-b300-fda1-eb7a-d97196bf95e0.png)

参考
Nuxt.jsとFirebaseでWebアプリケーション開発 - Speaker Deck
https://speakerdeck.com/takec24/nuxt-dot-jstofirebasedewebapurikesiyonkai-fa?slide=20

Firebaseの各機能を3行で説明する - Qiita
https://qiita.com/shibukk/items/4a015c5b3296563ac19d

Nuxt v2とFirebase(CloudFirestore)でPWA対応Webアプリ開発 - Qiita
https://qiita.com/_takeshi_24/items/3ee051e1db1b3e8da106