Firebaseについてはこちら
要件
- Webブラウザ + Firebaseのみで動作すること
- ログインが不要で投票ができること
- 1ユーザ1投票であること
- Cookie等クリアでの再投票は許容する
- 投票結果をリアルタイムで表示できること
- リロードしなくても最新の状態を表示する
DB設計
Firebaseのデータベースは所謂ドキュメント型DBなのでJSONでスキーマを定義します。
{
"sample" : {
"click" : {
"yaHDECcAfnXBofptYtXuO7Y793r2" : "2017-06-02T00:09:58+09:00",
...
},
"meta" : {
"end_date" : "2017-06-24T20:00:00+09:00",
"start_date" : "2017-06-01T19:00:00+09:00",
"target_count" : 1000
}
}
}
簡単に解説します。
sampleは投票の対象で親要素になります。-
sample/clickは投票数をカウントします。click配下はKey/ValueになっていてKeyがユーザID、Valueは投票した日時- Firebaseには認証機能がありセッションを確立するとUID(ユーザID)が振られます
-
sample/metaは投票に関するメタデータです- 今回は開始日/終了日と目標クリック数を設定
DBのセキュリティ
DBへアクセスできる権限を制御します。 こちらもJSON形式で記述します。
{
"rules": {
"sample": {
".read": true,
".write": false,
"click": {
".write": true
},
"meta": {
".write": false
}
}
}
}
rulesはルール(アクセス権限)の親要素です。rules/sample配下に データsampleへの権限を設定します。rules/sample/.readはsample配下への読み込み権限です。rules/sample/.writeはsample配下への書き込み権限です。
- 設定をしない場合は親要素のルールを引き継ぎます。
今回は
sample/click 配下のみWebクライアントから書き込みを許可するので、
それ以外のデータについては読み取りのみ許可設定を行いました。
(ユーザIDを指定して自分のID配下しか投票できないようにも記述できますが今回は簡単にしておきます)
投票してみる
sample/click 配下に「Key=ユーザID、Value=現在時刻」のデータを登録する処理は以下です。
FirebaseのSDKとmoment.jsを利用していますがその説明は割愛します。
firebase.database().ref('sample/click/' + uid).set(moment().format());
uid はFirebaseのSDKで認証(今回の場合は匿名)を行った際に取得できます。
投票の表示
投票の表示は初回に取得する場合と他の人が追加した結果を受け取って表示するパターンの2つ必要になります。
初回表示
firebase.database().ref('sample/click').once("value", function (snapshot) {
// sample/click配下に登録されているKey/Valueの数を取得
count = snapshot.numChildren();
});
firebase.database().ref('sample/click/' + uid).once("value", function (snapshot) {
// 投票をしたかチェックする(trueの場合は表示を変える)
is_voted = snapshot.val() !== null;
});
追加イベント
firebase.database().ref('sample/click').on('child_added', function (snapshot) {
// sample/click配下にKey/Valueが追加された場合に呼ばれる
});
firebase.database().ref('sample/click').on('child_removed', function (snapshot) {
// sample/click配下からKey/Valueが削除された場合に呼ばれる
});
投票数が多い場合に追加イベントが多発する問題
child_addedイベントは以下の仕様。
その場所にあるコンテンツ全体を返す value とは異なり、child_added は既存の子ごとに 1 回トリガーされます。さらに、指定されたパスに新しい子が追加されると、そのたびに再びトリガーされます。
「既存の子ごとに1回トリガー」というのはすでにデータベースに入っているデータ(今回はKey/Value)の数だけイベントが発行されるという意味で、 投票数が1000を超えてくると初期化時に1000回のイベントが発生することになります。 iOSのSafariだと途中で処理を止めてしまう動きをするなどパフォーマンス的にも問題がありますので以下のようにコードを修正します。
var now = moment().format();
firebase.database().ref('sample/click').orderByValue().startAt(now).on('child_added', function (snapshot) {
// 時刻「now」より後に、sample/click配下にKey/Valueが追加された場合に呼ばれる
});
nowに現在時刻をセットorderByValueでKey/ValueのValueでソートする- ソートしたKey/Valueを
startAt(now)で絞り込む
この絞り込みにより初期化時(now)以降に追加された要素に対してトリガーをするよう対象を絞り込むことができます。
これにより、初回時は
once('value') で更新時は .on('child_added') で無駄なく取得できるようになりました。
松木佑徒