Nuxt.js + Firebaseでプロジェクト作る

# 開発環境
node v10.11.0
yarn 1.10.1

# はじめに
この記事の内容は

[Vue.js＋Firebaseで認証付きチャット](https://cr-vue.mio3io.com/tutorials/firebase.html)
[Nuxt.jsの開発環境構築の手順（最初の一歩）](https://qiita.com/buchiya4th/items/44cfb43e96a9158a029b)
[Web開発が捗るFirebase入門！JavaScriptで「Webユーザー認証」機能を超お手軽に作るチュートリアル大公開！](https://paiza.hatenablog.com/entry/2016/10/03/Web%E9%96%8B%E7%99%BA%E3%81%8C%E6%8D%97%E3%82%8BFirebase%E5%85%A5%E9%96%80%EF%BC%81JavaScript%E3%81%A7%E3%80%8CWeb%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E8%AA%8D%E8%A8%BC%E3%80%8D%E6%A9%9F%E8%83%BD%E3%82%92)

を自分なりに掛け合わせて、使えるようにした記録です。

# 手順

##0. firebase登録・RealtimeDatabase設定

「Vue.js＋Firebaseで認証付きチャット | 基礎から学ぶ Vue.js」
https://cr-vue.mio3io.com/tutorials/firebase.html
参照。

##1. nuxtアプリ作成・各種インストール

```sh
$ yarn create nuxt-app <my-project>
$ yarn add -D pug@2.0.3 pug-plain-loader node-sass sass-loader @nuxtjs/style-resources
$ yarn add firebase
$ yarn add vue-nl2br #改行をbrタグに変換する
```

##2. nuxt.config.js編集

```js:nuxt.config.js
const pkg = require('./package')

module.exports = {
  mode: 'universal',
  head: {
    title: pkg.name,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: pkg.description }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  loading: { color: '#fff' },
  plugins: [
    { src: '~/plugins/global-components.js', ssr: true },
  ],
  css: [
  ],
  webfontloader: {
    google: {
      families: ['Noto+Sans+JP']
    }
  },
  modules: ['@nuxtjs/style-resources', '@nuxtjs/axios', '@nuxtjs/pwa'],
  generate: {
  },
  stykeResources: {
    sass:[],
  },
  build: {
    extend(config, ctx) {
    }
  }
}
```
firebaseの情報はグローバルコンポーネントとして切り分ける。

```sh
$ yarn generate #ページ生成
$ yarn dev #サーバ起動
```

##3. global-component.jsにfirebase情報切り分け

```js:global-component.js
import Vue from 'vue'

// firebase
import firebase from 'firebase'
Vue.config.productionTip = false
const config = {
  apiKey: "AIza....",
  authDomain: "YOUR_APP.firebaseapp.com",
  databaseURL: "https://YOUR_APP.firebaseio.com",
  projectId: "YOUR_APP",
  storageBucket: "YOUR_APP.appspot.com",
  messagingSenderId: "123456789"
}
firebase.initializeApp(config)

//default components
import LoginForm from '~/components/LoginForm.vue'
Vue.component('LoginForm',LoginForm)

```
##4. chat.vue作成

```vuejs:chat.vue
<template lang="pug">
  #app
    header.header
      h1 Chat
      div(v-if="user.uid" key="login")
        |[ {{ user.email }} ]
        button(type="button" @click="doLogout") ログアウト
      div(v-else key="logout")
        LoginForm
        // button(type="button" @click="doLogin") ログイン
    transition-group(name="chat" tag="div" class="list content")
      section(v-for="{ key, name, image, message } in chat" :key="key" class="item")
        .item-image
          img(:src="image" width="40" height="40")
        .item-detail
          .item-name {{ name }}
          .item-message
            nl2br(tag="div" :text="message")
    form(action="" @submit.prevent="doSend" class="form")
      textarea(
        v-model="input"
        :disabled="!user.uid"
        @keydown.enter.exact.prevent="doSend"
      )
      button(type="submit" :disabled="!user.uid" class="send-button") Send
</template>

<script>
  // firebase モジュール
  import firebase from 'firebase'
  // 改行を <br> タグに変換するモジュール
  import Nl2br from 'vue-nl2br'
  export default {
    components: { Nl2br },
    data() {
      return {
        user: {},  // ユーザー情報
        chat: [],  // 取得したメッセージを入れる配列
        input: ''  // 入力したメッセージ
      }
    },
    created() {
      firebase.auth().onAuthStateChanged(user => {
        this.user = user ? user : {}
        const ref_message = firebase.database().ref('message')
        if (user) {
          this.chat = []
          // message に変更があったときのハンドラを登録
          ref_message.limitToLast(10).on('child_added', this.childAdded)
        } else {
          // message に変更があったときのハンドラを解除
          ref_message.limitToLast(10).off('child_added', this.childAdded)
        }
      })
    },
    methods: {
      // ログアウト処理
      doLogout() {
        firebase.auth().signOut()
      },
      // スクロール位置を一番下に移動
      scrollBottom() {
        this.$nextTick(() => {
          window.scrollTo(0, document.body.clientHeight)
        })
      },
      // 受け取ったメッセージをchatに追加
      // データベースに新しい要素が追加されると随時呼び出される
      childAdded(snap) {
        const message = snap.val()
        this.chat.push({
          key: snap.key,
          name: message.name,
          image: message.image,
          message: message.message
        })
        this.scrollBottom()
      },
      doSend() {
        if (this.user.uid && this.input.length) {
          // firebase にメッセージを追加
          firebase.database().ref('message').push({
            message: this.input,
            name: this.user.email,
            image: this.user.photoURL
          }, () => {
            this.input = '' // フォームを空にする
          })
        }
      }
    }
  }
</script>

<style lang="scss">
  * {
    margin: 0;
    box-sizing: border-box;
  }
  .header {
    background: #3ab383;
    margin-bottom: 1em;
    padding: 0.4em 0.8em;
    color: #fff;
  }
  .content {
    margin: 0 auto;
    padding: 0 10px;
    max-width: 600px;
  }
  .form {
    position: fixed;
    display: flex;
    justify-content: center;
    align-items: center;
    bottom: 0;
    height: 80px;
    width: 100%;
    background: #f5f5f5;
  }
  .form textarea {
    border: 1px solid #ccc;
    border-radius: 2px;
    height: 4em;
    width: calc(100% - 6em);
    resize: none;
  }
  .list {
    margin-bottom: 100px;
  }
  .item {
    position: relative;
    display: flex;
    align-items: flex-end;
    margin-bottom: 0.8em;
  }
  .item-image img {
    border-radius: 20px;
    vertical-align: top;
  }
  .item-detail {
    margin: 0 0 0 1.4em;
  }
  .item-name {
    font-size: 75%;
  }
  .item-message {
    position: relative;
    display: inline-block;
    padding: 0.8em;
    background: #deefe8;
    border-radius: 4px;
    line-height: 1.2em;
  }
  .item-message::before {
    position: absolute;
    content: " ";
    display: block;
    left: -16px;
    bottom: 12px;
    border: 4px solid transparent;
    border-right: 12px solid #deefe8;
  }
  .send-button {
    height: 4em;
  }
  /* トランジション用スタイル */
  .chat-enter-active {
    transition: all 1s;
  }
  .chat-enter {
    opacity: 0;
    transform: translateX(-1em);
  }
</style>
```
ほぼコピペ。
ログインフォームのみ、LoginForm.vueにコンポーネントとして切り分ける。

##5. LoginForm.vue（component）作成

```vuejs:LoginForm.vue
<template lang="pug">
  #input-area
    input#email(type="email" placeholder="メールアドレスを入力" required)
    input#password(type="password" placeholder="パスワードを入力" required)
    button(@click="doLogin") ログイン
    button(@click="userCreate") 新規登録
</template>

<script>

  import firebase from 'firebase'
  export default {
    methods: {
      userCreate(){
        const email = document.getElementById('email').value;
        const password = document.getElementById('password').value;
        firebase.auth().createUserWithEmailAndPassword(email, password)
        .catch(function(error) {
          alert('登録できません（' + error.message + '）');
        });
      },
      doLogin(){
        const email = document.getElementById('email').value;
        const password = document.getElementById('password').value;
        firebase.auth().signInWithEmailAndPassword(email, password)
        .catch(function(error) {
          alert('ログインできません（' + error.message + '）');
        });
      }
    }
  }
</script>

<style lang="scss">
</style>
```
これまたほぼコピペ。