Firebaseの機能で、いいねボタンを作ってみる。

Firebaseの機能をつかっていいねボタンを作ってみました。
ただの「いいねボタン」です。記事とか投稿に「いいね」をするのではなく、ただただ「いいね」を押すだけの、ただそれだけのアプリです。
あくまでFirebase各機能を練習がてら利用するためにつくったものなので、実用的でないコードも多々あります。

# 仕様
## ボタンの状態は3種類
![disabled.png](https://qiita-image-store.s3.amazonaws.com/0/73833/aa0560cb-4ce9-7ca2-51dd-2839bb6f252d.png)

`<a id="btn1" class="btn disabled" href="">いいね<span class="count"></span></a>`
- ユーザーログインしていないとき。
- クリックできません。
- class `.disabled` がついています。

![normal.png](https://qiita-image-store.s3.amazonaws.com/0/73833/9cbe6592-f28d-2b2c-5389-c59e9cd16e32.png)
`<a id="btn1" class="btn" href="">いいね<span class="count">99</span></a>`
- ユーザーがログインしているとき
- クリックできます。

![liked.png](https://qiita-image-store.s3.amazonaws.com/0/73833/7755edb4-01bc-9b0b-bed4-9f34fd29fe77.png)
`<a id="btn1" class="btn liked" href="">いいね<span class="count">99</span></a>`
- ユーザーがログインしていて、クリックされているとき
- 「いいね」が押された状態です。
- class `.liked` がついています。

## 画面

こんな感じでログインができて、いいねが押せるだけ。
それだけの画面。
![view.png](https://qiita-image-store.s3.amazonaws.com/0/73833/2d555d05-c015-774c-aa01-8501ff61d688.png)

# 前準備

1. Firebaseのコンソールを開きプロジェクトを作る
2. Authenticationを開き、ログイン方法からメール/パスワードを有効にする。
3. 適当にユーザーを登録しておく。

## データベース構造

RealtimeDatabaseを使います。

```json:RaltimeDatabase
{
  "like" : {
    "count" : {
      "ボタンID" : 2,
      "ボタンID" : 0,
      "ボタンID" : 1
    },
    "liked" : {
      "ボタンID" : {
        "ユーザーID" : 1,
        "ユーザーID" : 1,
      },
      "ボタンID" : {
        "ユーザーID" : 0,
      },
      "ボタンID" : {
        "ユーザーID" : 1
      }
    }
  }
}

```
- likeという親キーの下に必要なデータを保存します。
- likedの下に「ボタンのID」をキーとして、「押したユーザーのID：（いいね:１、いいね解除:2）を作成
- Liked下が更新されたら、その値によって、countが増減します。

## データベースルール

```json:database-rules
{
  "rules": {
    "like": {
      "count": {
        ".read": "null !== auth.uid",
        ".write": false
      },
      "liked": {
        "$button_id": {
          "$user_id": {
            ".write": "$user_id === auth.uid",
            ".read": "$user_id === auth.uid"
          }
        }
      }
    }
  }
}
```
こんなかんじで考えました。
ユーザーから変更できるのは、`Liked/ボタンID/ユーザーID`のみです。
ここを更新すると、それに応じてシステムでcountを増減させるようにします。

# ホスティング（HTMLのコード）

ボタンなどのHTMLを作成してアップロードします。
- FirebaseのHostingを利用すると、ライブラリなどの読み込みも`/__`という予約URLでアクセスできるので簡単に各機能が利用できました。
- あくまでFirebaseの各機能を試すための練習アプリなので、HTML,CSS,Javascriptもフレームワークなどを使わず素のまま、ひとつのファイルにまとめて書きました。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Login</title>
    <link rel="stylesheet" href="style.css">
    <style>
        
    </style>
</head>
<body>
    <!-- ログイン状態を表示。ログインしているときにユーザー名とログアウトボタンを表示します -->
    <div id="loginStatus" class="loginStatus">
        <span id="loginName"></span>
        <a href="" id="logout" class="logout">ログアウト</a>
    </div>

    <!-- ログインフォームのエラーがあったときに表示します。（パスワード違いなど） -->
    <ul id="errors" class="errors">
        
    </ul>

    <!-- ログインフォームです -->
    <form action="" id="login-form">
        <div><label for="user_id">User ID</label><input type="email" name="user_id" id="user_id"></div>
        <div><label for="password">Password</label><input type="password" name="password" id="password"></div>
        <div><input type="submit" value="ログイン"></div>
    </form>

    <!-- いいねボタンです -->
    <section id="like-content" class="like-content">
        <div id="like1" class="like-box">
            <a id="btn1" class="btn disabled" href="">いいね<span class="count"></span></a>
        </div>
        <div id="like2" class="like-box">
            <a id="btn2" class="btn disabled" href="">いいね<span class="count"></span></a>
        </div>
        <div id="like3" class="like-box">
            <a id="btn3" class="btn disabled" href="">いいね<span class="count"></span></a>
        </div>
    </section>




<!-- ライブラリ読み込み -->
<script src="/__/firebase/5.8.2/firebase.js"></script>
<script src="/__/firebase/5.8.2/firebase-app.js"></script>

<!-- 認証、データベース、functionsを使います -->
<script src="/__/firebase/5.8.2/firebase-auth.js"></script>
<script src="/__/firebase/5.8.2/firebase-database.js"></script>
<script src="/__/firebase/5.8.2/firebase-functions.js"></script>

<!-- APIキーなど初期化。Firebaseホスティングならこれを書くだけでAPI情報などを読み込めます -->
<script src="/__/firebase/init.js"></script>

<!-- 実際のコード -->
<script>

    // form関連要素を取得する
    var el = document.getElementById('login-form'); // フォーム
    var errors = document.getElementById('errors'); // エラー表示エリア

    // ボタン
    var btnList = document.getElementsByClassName( 'btn' ); // 各ボタンのリスト


    // ログイン処理 ログインフォームから送信されたときに実行
    el.addEventListener('submit', function(event){
        var user_id = el.user_id.value; // 入力されたユーザー名
        var password = el.password.value; // 入力されたパスワード
        while(errors.firstChild) errors.removeChild(errors.firstChild); // エラー表示領域を初期化

        // ユーザーID（email）とパスワードでのログイン処理
        firebase.auth().signInWithEmailAndPassword(user_id, password).catch(function(error) {
            // ログインエラーのとき
            var errorCode = error.code;
            var errorMessage = error.message;
            var errorItem = document.createElement('li');
            errorItem.textContent = errorMessage;
            errors.appendChild( errorItem );
        });

        // イベントキャンセル
        event.preventDefault();
    });

    // ログアウト処理
    document.getElementById('logout').addEventListener('click', function(event){
        firebase.auth().signOut();
    });


    // ログインしたときの処理
    // ログイン状態を監視して、変化があったらトリガー
    firebase.auth().onAuthStateChanged( function( user ) {
        // ログイン成功したら
        if ( user ) {

            // ボタンをアクティブにする
            for(var i = 0; i < btnList.length; i++){ // ボタンリスト全部に処理
                let btn = btnList[i]; // ボタン要素取得
                btn.classList.remove( 'disabled' ); // ボタンの見た目を有効にする disabledクラスを除去

                // いいね数取得
                let btnId = btn.id; // ボタンのid属性を取得
                let likeCountRef = firebase.database().ref('/like/count/' + btnId );
                // データベースからいいね数を取得
                likeCountRef.on('value', (snapshot) => {
                    // いいね数を表示する領域を取得
                    let countTags = btn.getElementsByClassName('count'); 
                    // いいね数を取得
                    let count = snapshot.val();
                    // 値がなときは0
                    if( count === null ) count = 0;
                    // いいね数の表示
                    countTags[0].textContent = count;
                });
                // ユーザーが対象のボタンを「いいね」しているか判定
                let likedRef = firebase.database().ref('/like/liked/' + btnId + '/' + user.uid );
                likedRef.on('value', (snapshot) => {
                    // /like/liked の下に ボタンID/ユーザーID:1 の値があればいいね済み
                    if(snapshot.val() === 1){
                        btn.classList.add('liked'); // いいね済み
                    }else{
                        btn.classList.remove('liked'); // 未いいね
                    }
                });
            }

            // ログイン状態の表示　ログイン名とログアウトボタンの表示
            document.getElementById('loginStatus').style.display = 'block';
            document.getElementById('loginName').textContent = user.email + 'でログイン中';

            
        // ログアウトしたら
        } else {
            // ボタンを無効にする
            for(var i = 0; i < btnList.length; i++){
                let btn = btnList[i];
                btn.classList.add( 'disabled' ); // .disabledクラスを追加
                btn.classList.remove('liked'); // .likedクラスを除去
                let countTags = btn.getElementsByClassName('count'); // いいね数も削除
                countTags[0].textContent = '';
            }
            // ログイン状態を隠す
            document.getElementById('loginStatus').style.display = 'none'; // ログイン名　ログアアウトボタン　を非表示
        }
    });


    // いいねボタンを押したときの処理
    for(var i = 0; i < btnList.length; i++){
        let btn = btnList[i];

        // いいねクリック時のイベント設定
        btn.addEventListener('click', (e) => {
            // aタグなのでデフォルト動作を無効
            e.preventDefault();

            // いいねボタンが無効状態のときは処理しない
            if( btn.classList.contains("disabled") ) return false;
            
            // いいねボタン
            let value; // データベースに送る値（※1）
            if( btn.classList.contains('liked') ){
                value = 0; // いいね済みの場合は解除（解除は0）
            }else{
                value = 1; // 「いいね」を送信（値は1）
            }

            // 書き込み処理
            let user_id = firebase.auth().currentUser.uid; // ユーザーID取得
            let updates = {};
            // /like/liked/ボタンID/ユーザーID に値（※1）を書き込み
            updates['/like/liked/' + btn.id + '/' + user_id] = value; 
            firebase.database().ref().update(updates);

            return false
        });

    }

</script>
</body>
</html>
```

スタイルシートはほぼボタンの分だけなので省略。

# Functions

- Cloud Functions for Firebaseを使います。
- Functionでは、上記の画面から「いいねボタン」のクリックで送信された値を受け取り、その結果に基づいてボタンのいいねカウント数を増減させるようにしてみました。
- 上記の「データベースルール」により、ユーザーが書き込めるのは、`like/liked/ボタンID/ユーザーID`の値だけで、これが1のときは「いいね済み」ということにしました。
- 1:いいね、2:いいね解除、が書き込まれると、それをトリガーにFunctionが実行されていいね数のカウントを増減させる仕組みです。

```javascript:functions/index.js
const functions = require('firebase-functions');

// いいねボタンのクリックをトリガーにして実行
exports.likeCounter = functions.database.ref('/like/liked/{buttonId}/{userName}')
    .onWrite(( change, context ) => {
        // 送信されてきた値を取得
        const input = change.after.val();
        // ボタンIDを取得
        const buttonId = context.params.buttonId;
        // 増減させるデータベースのパス
        const path = '/like/count/' + buttonId;
        // 増減させるパスへの参照を作成
        const ref = change.after.ref.root.child(path);
        // 増減させるパスの値（いいねカウント数）を取得してから処理
        return ref.once('value').then( snap => {
            var val = snap.val(); // 現在のカウント数
            if( !val ) val = 0; // カウント数が存在しないときは0
            // inputにはDBに書き込まれた値が入っている
            if( input === 1 ) {
                // 1が書き込まれた（いいね）ときは増やす
                return ref.set( val + 1 );
            }else{
                // 0が書き込まれた（解除）ときは減らす
                return ref.set( val - 1 );
            }
        });

    });
```

以上。

# 動作について

- FirebaseのRealtime Databaseを利用しているので、いいね数は非同期で更新されます。
- 画面を2つ開き、それぞれ別のユーザーでログインしたうえで、一つの画面でいいねを押すと他の画面でもカウントされます。

![gamen2.gif](https://qiita-image-store.s3.amazonaws.com/0/73833/b743f0df-cda2-6081-69c7-2d22eb70b831.gif)


- こんなリッチな機能があっというまに実装できるのでFirebase面白いですね。

# 参考サイト

Firebase を JavaScript プロジェクトに追加する
https://firebase.google.com/docs/web/setup

作成したコードをGitHubにアップしてみました。
https://github.com/shin1kt/firebase-like-sample