Vue と Firebase / Firestore で CRUD 操作を行う Web アプリを作る最短のコード

# はじめに

すいません、最短は煽りですw

簡単な連絡帳みたいなものを作るサンプルを通して、Vue と Firebase / Firestore の組み合わせの開発コストが低いかを紹介したいと思います。

最初は、アクセス制御をかけずに作成し、次に Firebase の auth を使って認証をかけてみます。

最終形を以下でホスティングしてあります。適当にサインアップして試してみてもらってかまいません。

ロジックに集中するために、デザインはほぼあてていません。見た目悪いのは気にしないでください。

https://gce-experimental.firebaseapp.com

![SU1.png](https://qiita-image-store.s3.amazonaws.com/0/73493/189e00c8-005f-91ff-784d-f7cbe12055ab.png)

# 準備

- Node.js と npm をインストールしてください。
https://nodejs.org/en/
この記事は、8.10.0 LTS で検証しています。

- vue-cli をインストールしてください。

```
$ npm i vue-cli -g
```

## Firebase / Firestore をセッティング

### 公式ドキュメントにのっとればプロジェクトのセットアップまでは簡単です。
https://firebase.google.com/docs/firestore/quickstart?hl=ja

### Authentication のログイン方法で、メール／パスワードを有効にします。
![SS1.png](https://qiita-image-store.s3.amazonaws.com/0/73493/17b7458e-88f6-b570-a204-1628a15492fd.png)

### データベースを Firestore を選択して、ルールをとりあえずテストモードにしておきます。
![SS2.png](https://qiita-image-store.s3.amazonaws.com/0/73493/0455e1a6-53e9-8bdb-4b56-c4a608670bd4.png)

- Firebase CLI をインストールしてください。

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

- Firebase のアカウントと紐付けてください。

```
$ firebase login
```

# Vue アプリを作成する

```
vue init webpack ＜フォルダ名＞
```

いくつか選択肢が出て答えを求められます。下のように答えたとして話をすすめます。

```
? Project name 適当
? Project description A Vue.js project
? Author 内緒 <内緒>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm
```

firebase モジュールをインストールします。

```
cd ＜フォルダ名＞
npm i firebase --save
```

# CRUD 画面

このコンポーネントは、認証有り／無しで共通です。

```src/components/Main.vue
<template>
    <div>
        <table style="border:1px #808080 solid" v-for="( doc, i ) in docs" :key="i">
            <tr v-for="( v, k ) in doc.data()" :key="k"><td>{{ k }}</td><td>{{ v }}</td></tr>
            <tr><td><button @click="edit( doc )">編集</button></td><td><button @click="remove( doc )">削除</button></td></tr>
        </table>
        <hr>
        <p v-if="id"><input readonly v-model="id" />を編集中<button @click="id=''">中止</button></p>
        <p v-if="!id">新規登録</p>
        <div v-for="( v, k ) in draft" :key="k">
            <label>{{ k }}</label><input v-model="draft[ k ]" />
        </div>
        <button @click="submit">登録</button>
    </div>
</template>


<script>

import  firebase from 'firebase'
import  'firebase/firestore'

export default {
    methods: {
        edit( p ) {
            this.draft = p.data()
            this.id = p.id
        }
    ,   remove( p ) {
            this.col.doc( p.id ).delete().catch( e => alert( e ) )
        }
    ,   submit() {
            if ( this.id ) {
                this.col.doc( this.id ).set( this.draft ).catch( e => alert( e ) )
            } else {
                this.col.add( this.draft ).catch( e => alert( e ) )
            }
        }
    }
,   data() {
        return {
            col     : firebase.firestore().collection( "clients" )
        ,   id      : ''
        ,   draft   : { 名前: '', 携帯: '', メール: '' }
        ,   docs    : []
        }
    }
,   created() {
        this.unsubscribe = this.col.onSnapshot( p => this.docs = p.docs )
    }
,   beforeDestroy() {
        this.unsubscribe()
    }
}

</script>
```

# 認証無し


```src/main.js
import firebase from 'firebase'

firebase.initializeApp(
    {   apiKey              : 'ここには'
    ,   authDomain          : '自分の'
    ,   databaseURL         : 'やつを'
    ,   projectId           : 'はりつけて'
    ,   storageBucket       : 'ください'
    ,   messagingSenderId   : 'ね！'
    }
)

import Vue from 'vue'
Vue.config.productionTip = false

import Main from '@/components/Main'

new Vue(
    {   el: '#app'
    ,   template: '<Main />'
    ,   components: { Main }
    }
)
```

わずかこれだけのソースで書けてしまします。

```
npm run dev
```

で走らせてみます。

![SU2.png](https://qiita-image-store.s3.amazonaws.com/0/73493/f58b3c92-7ec8-2d0a-beca-ebcf7899e5db.png)


# 認証あり

認証を入れると、ちょっと複雑になります。

認証画面と CRUD 画面の遷移が発生するので、vue-router を使っています。また、ログイン情報をグローバルに使いたいので、Vuex を使っています。以下のコマンドでインストールしてください。

```
npm i vue-router --save
npm i vuex --save
```

```src/main.js
import firebase from 'firebase'

firebase.initializeApp(
    {   apiKey              : 'ここには'
    ,   authDomain          : '自分の'
    ,   databaseURL         : 'やつを'
    ,   projectId           : 'はりつけて'
    ,   storageBucket       : 'ください'
    ,   messagingSenderId   : 'ね！'
    }
)

import Vue from 'vue'
Vue.config.productionTip = false

import Router from 'vue-router'
Vue.use( Router )

import  Vuex from 'vuex'
Vue.use( Vuex )

const
Login = {
    template: `
        <div>
            <label>e-mail__:</label><input  type="email"    v-model="email" />      <br />
            <label>password:</label><input  type="password" v-model="password" />   <br />
            <button type="button"   @click="login">     Login</button>              <br />
            <label>nickname:</label><input  type="text"     v-model="nickname" />   <br />
            <button type="button"   @click="signup">    Signup</button>             <br />
        </div>
    `
,   data() {
        return {
            email   : ''
        ,   password: ''
        ,   nickname: ''
        }
    }
,   methods: {
        login() {
            firebase.auth().signInWithEmailAndPassword( this.email, this.password ).then(
                p => this.$router.push( this.$route.query.redirect ? this.$route.query.redirect : '/' )
            ).catch( e => alert( e ) )
        }
    ,   signup() {
            if ( this.nickname ) {
                firebase.auth().createUserWithEmailAndPassword( this.email, this.password ).then(
                    user => user.updateProfile( { displayName: this.nickname } )
                ).then(
                    () => {
                        this.$store.commit( 'user', firebase.auth().currentUser )
                        this.$router.push( this.$route.query.redirect ? this.$route.query.redirect : '/' )
                    }
                ).catch( e => alert( e ) )
            }
        }
    }
}

import Main from '@/components/Main'

const
App = {
    template: `
        <div>
            <template v-if="$store.state.user">
                <span>You're {{ $store.state.user.displayName }}</span>
                <button @click="$router.push( '/login' )">Logout</button>
                <hr>
            </template>
            <Main />
        </div>
    `
,   components: { Main }
}

let app
firebase.auth().onAuthStateChanged(
    p => {
        if ( app ) {
            app.$store.commit( 'user', p )
        } else {
            const
            router = new Router(
                {   routes:
                    [   {   path: '/'           ,   component: App }
                    ,   {   path: '/login'      ,   component: Login }
                    ,   {   path: '/*'          ,   component: { template: '<div>NotFound</div>' } }
                    ]
                }
            )
            router.beforeEach(
                async ( to, from, next ) => {
                    if ( firebase.auth().currentUser ) {
                        if ( to.path == '/login' ) await firebase.auth().signOut()
                        next()
                    } else {
                        if ( to.path == '/' ) {
                            next( { path: 'login', query: { redirect: to.path } } )
                        } else {
                            next()
                        }
                    }
                }
            )
            app = new Vue(
                {   el      : '#app'
                ,   store   : new Vuex.Store(
                        {   state       : { user: p }
                        ,   mutations   : { user: ( state, payload ) => state.user = payload }
                        }
                    )
                ,   router
                ,   template: `<router-view />`
                }
            )
        }
    }
)
```

# おわりに

Vue.js を使うとビジネスロジックと認証のロジックをソース上で綺麗にわけることができます。
ほんとにいい時代になりました。
それではー