寝て起きて寝て

プログラミングが出来ない情報系のブログ

Fire baseの認証とデータベース機能を使ったアプリケーション制作のメモ

Fire baseとは

Google が提供しているモバイルおよび Web アプリケーションのバックエンドサービス
Baas(Backend as a Service)の一種。
自分でサーバーを構築する必要がなく、Fire baseのサービスとして認証機能や、
データベースの機能を利用できる。

公式

無料で認証やデータベースの機能を簡単に実装できる

セットアップ

※私は既にGoogleアナリティクスを利用しているのでフローが違うかも

まずはコンソール画面へアクセスする

プロジェクト作成を押下

f:id:krs1:20200426152511p:plain

適当なプロジェクト名を入力して、利用規約にチェックを入れて続行を押下

f:id:krs1:20200426152641p:plain

続行を押下

f:id:krs1:20200426152851p:plain

使用するアカウントを設定してプロジェクト作成を押下

f:id:krs1:20200426153013p:plain

続行を押下

f:id:krs1:20200426153243p:plain

画像の</>をクリック

f:id:krs1:20200426153358p:plain

プロジェクト名をつけてアプリを登録を押下

f:id:krs1:20200426153531p:plain

下記スクリプトをhtmlに貼り付けると使用できるようになる
(Vueならpublic/index.htmlのbodyの閉じタグの手前に貼り付ける)

f:id:krs1:20200426153936p:plain

Vueで使う場合

ライブラリがあるので下記コマンドを実行しインストールする

npm install firebase

public/index.htmlのbodyの閉じタグの手前に貼り付けたスクリプトを、
切り取り、src/main.jsのVueインスタンス生成前に貼り付ける
このとき下記のスクリプトタグは削除する。

<script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js"></script>

<script src="https://www.gstatic.com/firebasejs/7.14.2/firebase-analytics.js"></script>

また先程インストールしたライブラリをimportする

import firebase from 'firebase'

完成形は下記

import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import router from './router'
import store from './store'
import firebase from 'firebase'

Vue.config.productionTip = false

// Your web app's Firebase configuration
var firebaseConfig = {
  apiKey: "AIzaSyCGTqL7NG3JMurGIkzQosYdtsD75n2gmNk",
  authDomain: "my-address-pj-c683f.firebaseapp.com",
  databaseURL: "https://my-address-pj-c683f.firebaseio.com",
  projectId: "my-address-pj-c683f",
  storageBucket: "my-address-pj-c683f.appspot.com",
  messagingSenderId: "852390100399",
  appId: "1:852390100399:web:2b5faa892d84b52d50835a",
  measurementId: "G-GVNJ2D1NLW"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

Firebaseの認証機能(Authentication)の設定

ここでの認証は Googleアカウントでログインすること

まずは、Firebaseのコンソールへ遷移
サイドメニューの「Authentication」を押下

f:id:krs1:20200426161544p:plain

ログイン方法を設定を押下 f:id:krs1:20200426161736p:plain

今回はGoogleアカウントで認証を行うのでGoogleを押下

f:id:krs1:20200426161905p:plain

右上の有効にするを押下し、
プロジェクトの公開名を適当に決める
プロジェクトのサポートメールは自分のメールアドレスにしておく
その後保存を押下

f:id:krs1:20200426161949p:plain

Googleの認証が有効になっていることを確認

f:id:krs1:20200426162256p:plain

承認済みドメインlocalhostが存在していることが確認できたら設定は完了

f:id:krs1:20200426162535p:plain

Vue内でログイン機能を実装

ここではログインボタン押下後Googleの認証画面へ遷移し、
再度アプリケーションへ戻ってくる機能を実装する

まず、storeにログイン機能を実装する
src/store/index.jsを開く

firebaseの機能を使えるようにimportしておく。

import firebase from 'firebase'

actionsに下記メソッドを追加
GoogleAuthProviderのインスタンスを作成し、signInWithRedirectの引数にGoogleAuthProvider
インスタンスを渡すとGoogleの認証画面にリダイレクトする。

    login(){
      const google_auth_provider = new firebase.auth.GoogleAuthProvider()
      firebase.auth().signInWithRedirect(google_auth_provider)
    },

認証後、再度このアプリケーションに戻ってくるので、
そのときにログインユーザーの情報を受け取ることができる

src/views/Home.vueを下記のように変更する
ここではGoogleアカウントでログインを押下するとGoogleの認証画面へ遷移し、
認証後再度アプリケーションに戻ってくる。

<template>
  <v-container text-xs-center justify-center>
    <v-layout row wrap>
      <v-flex xs12>
        <h1>マイアドレス帳</h1>
        <p>マイアドレス帳をご利用の方は、Googleアカウントでログインしてください。</p>
      </v-flex>

      <v-flex xs12 mt-5>
        <v-btn color='info' @click="login">Googleアカウントでログイン</v-btn>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
// @ is an alias to /src
import {mapActions} from 'vuex'

export default {
  methods:{
    ...mapActions(['login'])
  }
}
</script>

ログインユーザーの取得機能

ここでは、Firebaseからユーザー情報を取得して、storeに格納する機能を実装する

まずはsrc/store/index.jsを編集

stateにlogin_user:null,というプロパティを作成。
これにユーザー情報を格納する。

そして、mutationsにユーザー情報の格納、
actionsに発火用の記載を追記

 mutations: {
    setLoginUser(state, user){
      state.login_user = user
    },
----------------------

  actions: {
    setLoginUser({commit},user){
      commit('setLoginUser', user)
    },

これで遷移時setLoginUserを呼び出せばログイン情報を格納できる

次にFirebaseの機能でログイン情報を取得できる機能を追加する

どこのページにいてもログイン情報を取得できるようにする場合は、

src/App.vueにユーザー取得の処理を追加する。
App.vueは全ページ共通のコンポーネントを記載する場所なので、
createdメソッドにログイン情報の取得処理を記載する。

ログイン状態の確認はonAuthStateChangedというメソッドを使って確認できる。
このメソッドは、引数に状態が変わったときに呼び出されるコールバック関数を指定できる。
状態が変わったときとは、ログイン時、ログアウト時のこと。
ログイン時はユーザー情報を。ログアウト時はnullが渡ってくる。

created(){
    firebase.auth().onAuthStateChanged(user => {
      if(user){
        //ログイン状態
        this.setLoginUser(user)
      }
    })

完成形

<template>
  <v-app>
    <v-toolbar app>

    <v-toolbar-side-icon @click="toggleSideMenu"></v-toolbar-side-icon>
      <v-toolbar-title class="headline text-uppercase">
        <span>マイアドレス帳</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
    </v-toolbar>
    <SideNav/>>

    <v-content>
      <router-view/>
    </v-content>
  </v-app>
</template>

<script>
import firebase from 'firebase'
import SideNav from './components/SideNav'
import { mapActions } from 'vuex'
export default {
  name: 'App',
  components:{
    SideNav
  },
  created(){
    firebase.auth().onAuthStateChanged(user => {
      if(user){
        //ログイン状態
        this.setLoginUser(user)
      }
    })
  },
  data () {
    return {
      //
    }
  },
  methods:{
    ...mapActions(['toggleSideMenu', 'setLoginUser'])
  }
}
</script>

ログアウト機能を実装

現状ログアウトできないのでログアウト機能を実装する。

まず、src/store/index.jsから編集する

mutationsにメソッドを追加

deleteLoginUser(state){
      state.login_user  = null
    },

actionsにメソッドを追加
firebase.auth().signOut()でログアウトできる。

    deleteLoginUser({commit}){
      commit('deleteLoginUser')
    },
    logout(){
      firebase.auth().signOut()
    },

次に追加した機能を使えるように'src/App.vue`を編集する

新たにログアウト用のボタンを作成し、押下されたときにログアウトするようにする

      <v-tllobar-items>
        <v-btn @click="logout">ログアウト</v-btn>
      </v-tllobar-items>

ログアウト時、状態が変わった事により下記コールバック関数が呼ばれるため、
このときにuser情報の削除を行う。

  created(){
    firebase.auth().onAuthStateChanged(user => {
      if(user){
        //ログイン状態
        this.setLoginUser(user)
      }
      else{
        //ログアウト状態
        this.deleteLoginUser()
      }
    })
  },

ログイン状態による表示の切り替え

src/App.vueを編集する

ログアウトのボタンは下記条件式で消す

<v-tllobar-items v-if="$store.state.login_user">

次にsrc/store/index.jsを編集
ここではログイン時、ユーザー名と画像を取得できるようにするgettersを定義する。
gettersが呼ばれた場合、stateが引数に入ってくる。
戻り値には加工した値を戻している。

getters:{
    userName: state => state.login_user ? state.login_user.displayName : '',
    photoURL: state => state.login_user ? state.login_user.photoURL:''
  },

次にsrc/components/SideNav.vueを編集

mapGettersmapActionsのGetter版
Actionsとは違いcomputedに記載する点に注意

import { mapGetters } from 'vuex'
export default {
  data () {
    return {
      items: [
        { title: 'ホーム', icon:'home', link:{name:'home'}},
        { title: '連絡先一覧', icon: 'list', link:{name:'addresses'}}
      ]
    }
  },
  computed:{
    ...mapGetters(['userName','photoURL'])
  }
}

ログイン状態による遷移の制御

ここではログイン状態に応じたログイン制御を行う。
ユーザーログイン時は、連絡先一覧の画面へ、
ログアウト時は、Homeのログインボタンのあるページへ自動遷移するようにする

src/App.vueを編集する

createdを下記のように修正

created(){
    firebase.auth().onAuthStateChanged(user => {
      if(user){
        //ログイン状態
        this.setLoginUser(user)
        //現在homeにいる場合はアドレス帳一覧へ遷移
        if (this.$router.currentRoute.name === 'home') this.$router.push({name:'addresses'})
      }
      else{
        //ログアウト状態
        this.deleteLoginUser()
        //ログアウトしたらホームへ遷移
        this.$router.push({name:'home'})
      }
    })
  },

次にログイン時サイドメニューからホームボタンを非表示にする。

<v-toolbar-side-icon v-show="$store.state.login_user" @click="toggleSideMenu"></v-toolbar-side-icon>

次にログインしている場合のみサイドメニューを表示させる処理を入れる。

src/components/SideNav.vueを編集 (ホームの配列を削除)

  data () {
    return {
      items: [
        { title: '連絡先一覧', icon: 'list', link:{name:'addresses'}}
      ]
    }
  },

Cloud Firestoreを利用したデータベースを作成する

まず、Firebaseのコンソールを表示し作成したプロジェクトを押下 次にDatabaseを押下

f:id:krs1:20200426200055p:plain

CloudFirestoreのデータベースの作成を押下

テストモードで開始にチェックを入れ有効にするを押下

f:id:krs1:20200427130436p:plain

ロケーションは下記を選択
asia-northeast1→東京

f:id:krs1:20200427130609p:plain

ルールタブをクリック

f:id:krs1:20200426235551p:plain

この画面で読み書き可能なルールを設定できる。
Cloud Firestoreではパスの形式でデータの保存ができる

/users/{userId}/addresses/{addressId}

みたいなパスのデータを保持できる
保持データのイメージは下記の感じ

user1:情報1
user1:情報2...etc

今回のルールで定義すると下記のようになる

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId}/addresses/{addressId} {
      allow read, update, delete: if request.auth.uid == userId;
      allow create: if request.auth.uid != null;
    }
  }
}

データベースを使う場合下記は固定っぽい

service cloud.firestore {
  match /databases/{database}/documents {

3行目のmatchの部分がルールの設定対象になるデータベースのパスを表している
userId,addressIdの部分にはデータベースに保持されているuserIdとaddressIdが入る

match /users/{userId}/addresses/{addressId} {

read, update, deleteの権限を与えている文
条件としてユーザーIDが一致したときのみデータベースの更新ができる
request.auth.uidとはリクエストしてきた認証済みのユーザーのuidのこと

allow read, update, delete: if request.auth.uid == userId;

これは認証済みのuidであれば誰でもデータを挿入できるという文

allow create: if request.auth.uid != null;

公開を押下

f:id:krs1:20200426235720p:plain

アプリケーションからCloud Firestoreへのデータの保存

src/store/index.js

保存先は下記の様にアドレス形式で保存先を指定する

/users/{userId}/addresses/

まずGetterでグーグルのログインIDを取得できるようにする

uid:      state => state.login_user ? state.login_user.uid : null

次にActionsのアドレス追加メソッドにfirebase側へ格納処理を追加する
これはfirebaseのcollectionメソッドで、格納先のユーザーidを紐付けて、addメソッドでアドレスを追加している
また引数のcontextオブジェクトにはgettersもあるので指定している。

   addAddress({ getters, commit }, address){
      if(getters.uid) firebase.firestore().collection(`users/${getters.uid}/addresses`).add(address)
      commit('addAddress', address)
    }

その後アプリケーション側で次のようにテストデータを入れてみる

f:id:krs1:20200427123109p:plain

データベースに格納されたかどうかの確認をするため
FirebaseコンソールのDatabaseのデータタブをクリック

f:id:krs1:20200427131255p:plain

すると1レコード格納されていることがわかる

f:id:krs1:20200427131527p:plain

これをクリックしてたどっていくと最終的に先程フォームから送信したデータが入っていることがわかる

f:id:krs1:20200427131604p:plain

アプリケーション側からCloudFirestoreからデータを取り出す

現状データベースからデータの取得を行っていないので、リロードしたときに
アドレス一覧から入力した値が消えてしまう。
これを防ぐために画面表示時データベースから保存したデータを取り出す処理を入れる

src/store/index.jsを開く

firestoreからデータを取ってくるアクションを追加する

fetchAddresses({getters, commit}){
      firebase.firestore().collection(`users/${getters.uid}/address`).get().then(
        snapshot => {
          snapshot.forEach( doc => commit('addAddress', doc.data()))
        }
      )
    },

これはfirebase.firestore().collection(users/${getters.uid}/address)でどのパラメーターからデータを取得するかを設定している
その後getメソッドで非同期にデータの取得が行われ、thenメソッドで結果のデータを受け取っている。
snapshotにDBに保存されているユーザーIDが一致しているデータが入っている
そしてcommitでアプリケーション側のアドレスの配列に追加していっている。

src/App.vueを編集

this.fetchAddresses()をcreatedに追加する。 すると、リロード時にも保存したデータが表示される

f:id:krs1:20200427141008p:plain

保存したデータの編集

保存したデータを編集するためにはデータのユニークなIDが必要になるので取得する処理を書いていく

src/store/index.jsを開く

    addAddress({ getters, commit }, address){
    if(getters.uid) firebase.firestore().collection(`users/${getters.uid}/addresses`).add(address)
      commit('addAddress', address)
    }

この部分を下記のように修正

    addAddress({ getters, commit }, address){
      if(getters.uid){
        firebase.firestore().collection(`users/${getters.uid}/addresses`).add(address).then(
          doc => {
            //doc.idがユニークID
            commit('addAddress',{id: doc.id, address})
          }
        )
      }
    }

.addメソッドの戻り値にユニークのIDが含まれているオブジェクトが入っている。

これに伴いmutationsaddAddressも変更する

変更前

    addAddress(state, address){
      state.addresses.push(address)
    }

変更後

    addAddress(state, {id, address}){
      address.id = id
      state.addresses.push(address)
    }

これでaddressesの配列にもユニークなIDも含める事ができるようになった

次にactionsのfetchAddressesでもidを取得できる用に変更する

変更前

    fetchAddresses({getters, commit}){
      firebase.firestore().collection(`users/${getters.uid}/addresses`).get().then(
        snapshot => {
          snapshot.forEach( doc => commit('addAddress', doc.data()))
        }
      )
    },

変更後

    fetchAddresses({getters, commit}){
      firebase.firestore().collection(`users/${getters.uid}/addresses`).get().then(
        snapshot => {
          snapshot.forEach( doc => commit('addAddress', {id:doc.id, address:doc.data() }))
        }
      )
    },

get()で呼び出した場合は、idプロパティに格納されている

次に編集画面へ遷移できるようにリンクを追加する

src/views/Addresses.vue

操作の列には各連絡先を編集するためのボタンを配置するので、ソート機能を塞いでいる

headers: [
        { text: '名前', value: 'name' },
        { text: '電話番号', value: 'tel' },
        { text: 'メールアドレス', value: 'email' },
        { text: '住所', value: 'address' },
        { text: '操作', sortable: false}
      ],

HTML側も変更する

        <v-data-table :headers='headers' :items='addresses'>
          <template v-slot:items="props">
            <td class="text-xs-left">{{ props.item.name }}</td>
            <td class="text-xs-left">{{ props.item.tel }}</td>
            <td class="text-xs-left">{{ props.item.email }}</td>
            <td class="text-xs-left">{{ props.item.address }}</td>
            <td class="text-xs-left">
              <span>
                <router-link :to="{name :'address_edit', params:{address_id: props.item.id}}">
                  <v-icon small class="mr-2">edit</v-icon>
                </router-link>
              </span>
            </td>
          </template>
        </v-data-table>

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

</script>

<style scoped lang="scss">
a{
  text-decoration:none;
}
</style>

連絡先編集フォームに値をセットする

現状編集すると中身が入っていない状態で表示されるので、
編集の場合はデータがセットされている状態で表示するようにする。

f:id:krs1:20200427151833p:plain

src/store/index.jsを編集する

対象のIDからデータを取得できるようにgetterメソッドを追加

getAddressById: state => id => state.addresses.find( address => address.id === id )

これはidにマッチしたアドレスデータを戻り値とする関数を定義している。

次にsrc/views/AddressForm.vueを編集する

今回は連絡先情報を非同期にfirestoreから取得しているので、
直接このアドレスに遷移した場合は、データの取得ができていない場合がある。
そのため取得できた場合のみデータを反映、それ以外は連絡先一覧を表示する作りにする

  created(){
    if(!this.$route.params.address_id) return
    const address = this.$store.getters.getAddressById(this.$route.params.address_id)
    if(address){
      this.address = address
    }
    else{
      this.$router.push({ name:'addresses'})
    }
  },

if(!this.$route.params.address_id) return
ここではrouteのパラメーターにaddress_idが含まれているかをチェックしている。

連絡先情報の更新をfirebaseに反映する

src/store/index.jsを編集する

まずactionsメソッドを更新する。

updateAddress ({ getters, commit}, {id, address}){
      if(getters.uid){
        firebase.firestore().collection(`users/${getters.uid}/addresses`).doc(id).update(address).then(
          () => {
            commit('updateAddress', { id, address })
          }
        )
      }
    }

doc(id).update(address)の部分は、idを取得してからアドレスを更新という流れで更新している。

次にmutationsを更新する

    updateAddress(state, {id, address}){
      const index = state.addresses.findIndex(address => address.id === id)
      state.addresses[index] = address
    },

ここでは同じIdの配列番号を取得し、その配列に新しい文字列を追加する処理を書いている。

src/views/AddressForm.vueを編集

 methods:{
    submit(){
      if(this.$route.params.address_id){
        this.updateAddress({id: this.$route.params.address_id, address:this.address})
      }
      else{
        this.addAddress(this.address)
      }
      //addressesへ画面遷移
      this.$router.push({name:'addresses'}),
      this.address = {}
    },
    ...mapActions(['addAddress', 'updateAddress'])
  }

ここではIdが入っていればアップデート処理、
入っていなければ新規作成処理の出し分けを行っている

連絡の削除機能追加

src/store/index.js

actionにdeleteAddressを追加する(addAddressに似ているので比較する)

    deleteAddress({ getters, commit }, {id}){
      if(getters.uid){
        firebase.firestore().collection(`users/${getters.uid}/addresses`).doc(id).delete().then(
          () => {
            //doc.idがユニークID
            commit('deleteAddress',{id})
          }
        )
      }
    },

deleteの場合はaddressはいらないので削除対象のIDだけ引数で受け取る。
そしてそのIDを使って.doc(id).delete()で削除
最後にcommit('deleteAddress',{id})でstateから削除している。

次にmutationsを更新

    deleteAddress(state, { id }){
      const index = state.addresses.findIndex(address => address.id === id)
      state.addresses.splice(index, 1)
    }

これは同じアドレスのインデックスを算出しspliceメソッドでデータを抜き出している。

src/views/Addresses.vueを編集する

methodsを作成

  methods:{
    deleteConfirm(id){
      if( confirm('削除してよろしいですか?')){
        this.deketeAddress({ id })
      }
    },
    ...mapActions(['deleteAddress'])
  }

confirm('削除してよろしいですか?')ここでOKを押すと削除の処理が走る

deleteと書いたv-iconを使用するとゴミ箱のアイコンが生成されるようになる

              <span>
                <v-icon small class="mr-2" @click="deleteConfirm(props.item.id)">delete</v-icon>
              </span>