Udemy 「Vue.js + Firebaseで作るSPA」(Section11-12)をやってみた

# 前書き
自分はバックエンドエンジニアであるが一度フロントエンドの思考に触れたいと考えていたため、冬休みを利用してUdemyのコースを受講した。本投稿ではその後半(全12章中1−10章)の個人的記録である。後半はVue+Firebaseを使ったアドレス帳アプリケーションの実装とデプロイハンズオンレクチャーである。

# 環境
特筆すべき点として、動画に合わせてvuetifyは1.5.5を使った。 


# 11. アドレス帳アプリケーションの作成

## VuetifyによるUIコンポーネントの作成

* UIコンポーネントのライブラリはVuetifyを使う(ElementUI, Bootstrapなどが他のvue.jsに特化したライブラリ)。導入はvue cliで`$ vue add vuetify`

    * ツールバー:`<v-appbar></v-appbar>`を利用(vuetifyより)
    * サイドメニュー:(vuelidateのNavigationDrawerのスクリプトをコピーして引っ張ってきた)

* コンポーネント間で値を共有するのに使えるのが`Vuexのストア`。VuexとはVue.jsで状態の管理を行うためのモジュール。( 今まで見てきたのは親子間でのデータの共有)。`src/store/index.js`で管理する。`Vue.user(Vuex)`で有効化。


    * actions内のメソッドの引数には自動的にcontextオブジェクトが渡される。この中でcommit(`toggleSideMenu`)でmutationのメソッドを呼び出す。
    * mutations内のメソッドの引数には自動的にstateが渡される。ここでstateの値を書き換える。
    * stateの値にはtemplateから`$store.state`でアクセスできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/734a7718-8c0f-fa69-28e3-2c436dfb415e.png)

* ヘッダー左のmenuボタンで開閉するサイドメニューの作成。Vuexを用いる。まずSideNav.vueで`<v-navigation-drawer v-model="$store.state.drawer" absolute temporary>`このv-modelとはmenuの開閉の状態を表すbooleanだ。
    1. **dispatcherメソッド**を使う方法。つまり、App.vueで`<v-app-bar-nav-icon @click="openSideMenu">`し、このopenSideMenuメソッド内で`this.$store.dispatch('toggleSideMenu')`
    2. **mapActionsメソッド**を使うよりスマートな方法。App.vueで`import { mapActions } from 'vuex'`し、これをmethods内で分割代入。`methods: { ...mapActions(['toggleSideMenu']) }`これをクリック時に`<v-app-bar-nav-icon @click="toggleSideMenu">`で呼び出す。

* 連絡先一覧テーブルおよびそのページ作成。Vuetifyの`<v-data-table :headers='headerの配列' :items='データの配列'>`を使う。
* サイドバーからhomeページ、連絡先一覧ページそれぞれへのリンクを有効にした。`<v-list-item v-for="(item, index) in items" :key="index" :to="item.link">`を使った。
* 連絡先追加ページの作成。連絡先一覧ページからのジャンプボタンと連絡先一覧ページへのキャンセルボタンを設置した。

    * このページのpathはrouterにおいて`path: '/addresses/:address_id?/edit'`で指定した。?はこのaddress_idがオピションの値であることを表す。address_idなしへのリンクは`<router-link :to="{ name: 'address_edit' }">`で実装可能。
    * キャンセルボタンでのページジャンプは`<v-btn @click="$router.push({ name: 'addresses' })">キャンセル</v-btn>`で実装可能。


## Firebaseによるユーザ認証(ログイン、ログアウト機能)

### 下準備
* Firebase指定のスクリプトを`public/index.html`のbodyタグの下部、他のjsを読み込む前にコピペする。
* これをvue.jsの利用に適した形にいじる。すなわち`$npm install firebase`し、`src/main.js`でインポートかつ先ほどのスクリプトでInitialize Firebaseする。(`public/index.html`のコピペは消す)
* FirebaseのwebコンソールのAuthenticationでgoogle認証を有効化する。

### ログイン、ログアウトの仕組み
Googleアカウントでログイン認証を実装する。これは`loginアクション`が呼ばれたタイミングでgoogleのGoogleの認証ページにリダイレクトされ、認証成功したらまたアプリケーションに自動的に戻ってくる。そこでログインユーザの情報をGoogleからFirebase経由で(?)受け取る。


* AppVueのcreatedメソッド: `firebase.auth().onAuthStateChanged(~~~)`というオブザーバーを置いている。これはfirebaseと通信し、そのuserのログイン/ログアウト状態が切り替わったら発動されるメソッドである。
* vuex-storeの
login(), logout()メソッドはこのfirebaseと通信し、そのユーザのlogin/logoutの状態変更をする。
* vuex-storeのsetLoginUser, setLogoutUserは動作中フロントエンドにおけるstate.login_userの切り替えにつかわれる。

これらを利用して、以下の手順でログイン、ログアウト機能が作動する。

```
login() ==> firebase.auth().onAuthState()[オブザーバが検知] =>setLoginUserでstate.login_userを設定する。
logout() ==> firebase.auth().onAuthState()[オブザーバが検知] => setLogoutUserでstate.login_userをnullにする。
```

### ログイン状態のルートの制御
* ユーザログイン時に連絡先一覧ページに飛ばす。ログアウト時にはホームのログインのページに飛ばす。
* ログインしている場合のみサイドメニューを表示するためのアイコンを表示する。

## Firebaseによるデータの永続化

firebaseのルールは

```
rules_version = '2';
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;
    }
  }
}
```

* ユーザが連絡先を新規登録時にfirebaseのデータベースにデータを保存する。すでにvuex-storeに格納するアクションは用意できているので、そこにcloud firestoreに保存する処理**if (getters.uid) firebase.firestore().collection(\`users/${getters.uid}/addresses\`).add(address)**を追加する。ここで使うuser-idはログインユーザのオブジェクト内にuidという名前で持っている。

* ユーザーがログイン時に連絡先一覧データをデータベースから取ってくる。これはvuex-storesの`fetchAddressesアクション:`**firebase.firestore().collection(\`users/${getters.uid}/addresses\`).get().then(snapshot =>{
        snapshot.forEach(doc => commit('addAddress', doc.data()))**を定義し、これを`firebase.auth().onAuthStateChanged()`から呼び出す。

* 個々の連絡先の編集ページを作る。
    * まずは個々の連絡先オブジェクトに固有のidを与える。これは`addAdresssアクション`で**if (getters.uid) firebase.firestore().collection(\`users/${getters.uid}/addresses\`).add(address).then(doc => { commit('addAddress', {id: doc.id, address}) })**かつ`addAddressミューテーション`でstate.addressesへのpush前に`address.id = id`により実現可能。これに伴い、`fetchAddressesアクション`でも同様にidを含めた処理を実装する。
    
* 個々の連絡先編集ページへの遷移ボタンを連絡先一覧ページに仕込む。Addresses.vueを改造し、ボタンは`<router-link :to="{ name: 'address_edit', params: { address_id: item.id }}">`でよし。
    
* 個々の連絡先編集ページではすでに登録されている対応する連絡先のデータをfirebaseから取得し、フォームに反映している状態にする。このために、store/index.jsにおいて`getAddressById: state => id => state.addresses.find(address => address.id === id)`という、連絡先のidに一致する連絡先をstate.addressから取ってこれる関数を返す関数を定義する。これを、AddressFormコンポーネントのcreatedメソッドにおいて呼び出して、フォームにv-modelで反映する()。
    * 現在のページが示すaddressは`this.$route.params.address_id`で取得する。
    * addressesの中に該当するaddressがなかった場合は連絡先一覧ページに遷移する。

* 個々の連絡先編集ページはupdate, 新規連絡先追加はaddにする。実装するのは以下。
    * stores/index.jsのupdateAddressミューテーション
    * stores/index.jsのupdateAddressアクション
    * AddressForm.vueでmapActionsを分割代入する。連絡先追加ボタンが押された時、`this.$route.params.address_id`がnullではない場合には、updateAddressミューテーションを呼び出し(引数はアドレスidとaddressオブジェクト)、nullの時はaddAddressミューテーションを呼び出す。
* 連絡先一覧ページに、それぞれの連絡先の削除ボタンを入れる。削除ボタンを押した際には確認のconfirmダイアログが出るようにする。実装は以下。
    * stores/index.jsのdeleteAddressミューテーション
    * stores/index.jsのdeleteAddressアクション
    * Address.vueでmapActionsを分割代入し、deleteAddressミューテーションを呼び出せるようにする(引数はアドレスid)。連絡先削除ボタンが押された時、deleteConfirmメソッドを介して(confirmダイアログでの確認)、これにユーザがYesを押すと、deleteAddressミューテーションを呼び出す。

# 12: Firebaseのホスティングを利用したアプリのデプロイ

```
$ npm install -g firebase-tools
$ firebase login
$ firebase init
# 設定(distを公開する設定など)
$ npm run build
$ firebase deploy

# ホスティングを停止するには、、、
$firebase hosting:disable

```





# その他のメモ

`cp -a dir newdir`でdirのコピーディレクトリnewdirができる。