なゆめも

途切れない気持ちが私の道しるべ。

コンベンションセンターもどきをFirebaseで作った所感

先日、グリマスライクのコンベンションセンターもどきをリリースしました!

多くの反響をいただき嬉しい限りです。作ったものを使ってもらえるってのは嬉しいですね。
作るぞー!と思いついてからちょうど1週間、Firebaseのおかげで結構簡単に作れました!

コンセプト

  • グリマスライク
    デザインを考えなくていい(超重要)
    既存ユーザーは使い勝手が分かっているのですんなり受け入れてくれそう
  • スマホファースト
    レスポンシブめんどい、デザイン(略)
  • 気軽に参加できる
    さすがに完全匿名はまずそうなので、SNS認証でサインインの敷居を下げる
  • サクサクと実装できる
    自前でサーバ運用はしてるけどMySQLみたいにファットなRDBはいらんし…そうだ、いま流行りのサーバレスを使ってみよう!

Firebaseを使ってみた感想

  • Firebaseを使えばバックエンドのことをある程度忘れてサービスをサクサク立ち上げられる!
  • 痒いところにはあまり手が届かないので、ある程度の割り切りは必要
  • 無料枠で運用したいけど、課金の仕組みを理解しておかないとぷちバズった時にサービスが止まる😱
  • 環境構築楽すぎ
  • UIライブラリとの相性良すぎ
  • でもぶっちゃけ掲示板システムとFirebaseのDBは相性がよくなかった😅(後述)

基本構成

  • バックエンド
    • Firebase (Authentication、Database、Hosting、Functions)
  • フロントエンド
    • Riot.js (UIライブラリ)
    • ほかHTMLとかCSS3とかJavaScriptとか
    • ES6の構文でゴリゴリ書いてるので、IEでは動かないサイトになってしまった。 今時IE使ってる人いないでしょEdgeを使ってください。

Firebase

  • Googleが運営しているBaaS
  • Googleアカウントを持ってればすぐ始められる
  • 初期設定は無料プラン(Spark)で、Firebaseコンソールからをプラン変えない限りは勝手に課金されることはない
    • GCPのようにクレカ登録せずに始められるのがいいですね!
  • 無料枠を使い切るとサービスが止まる😱
  • ご利用は計画的に→ 料金 - Firebase
  • 公式のガイドやリファレンスが結構充実してるので困ったら読もう!

初期導入がすごく楽

WebブラウザでFirebaseコンソールにアクセスし、まずはプロジェクトを追加します。
https://firebase.google.com/

環境構築が楽

作成したプロジェクトをローカル環境に紐付けます。

$ yarn add global firebase-tools
$ firebase login
$ mkdir ~/hogehoge_prj
$ cd ~/hogehoge_prj
$ firebase init

Nodeとyarn(またはnpm)が入ってればこれでおわり。

$ firebase serve

これでローカルの開発環境が立ち上がる!
http://localhost:5000/ で開発ができるようになります。

デプロイも楽

$ firebase deploy

以上!
簡単すぎて本番環境にデプロイする時は慎重さが求められるかも

環境切り替えも楽

同じFirebaseアカウントに本番用と開発用のプロジェクトを追加しておくと、デプロイ先のプロジェクトを切り替えることができます。

$ firebase use --add
  hogehoge-prj
❯ hogehoge-prj-dev 
? What alias do you want to use for this project? (e.g. staging) dev
$ firebase use dev

エイリアス名を付けとくこともできます。
簡単に環境が切り替えてしまえるので、deployする時はちゃんと環境を確認してからやらないと大変なことに・・・
※ちゃんとしたサービス作るなら本番との紐付けはCIだけとか限定的にしておきましょう。

参考: FirebaseをStaging環境とかDebug環境とかRelease環境で切り替えをする(Webアプリ編)

Hosting

  • 無料枠はストレージ1GBと月転送量10GBまで
  • デフォルトドメインは[プロジェクトID].firebaseapp.com
  • 独自ドメイン設定無料
    • TXTレコードによる所有権確認があるので自分でDNS設定できるドメインじゃないとダメ
  • SSL証明書無料

SDKの連携が楽

WebでFirebaseを利用するためにAPIキーなどの設定を読み込ませる必要があるのですが、以下のJSファイルを追加するだけで、ローカル環境も含め全て自動的にやってくれます!

本来だったら以下のように環境ごとに用意しないといけないAPIキーを

<script>
  // Initialize Firebase
  const config = {
    apiKey: "<API_KEY>",
    authDomain: "<PROJECT_ID>.firebaseapp.com",
    databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
    projectId: "<PROJECT_ID>",
    storageBucket: "<BUCKET>.appspot.com",
    messagingSenderId: "<SENDER_ID>",
  }
  firebase.initializeApp(config)
</script>

Hosting使えばこの1行でおわり。

<script src="/__/firebase/init.js"></script>

⚠️ ローカル環境に複数プロジェクトある場合、firebase serveを実行した時に選択されているプロジェクトの設定が読み込まれるので注意

Authentication

  • 完全無料で使える(電話番号認証のみ無料枠設定あり)
  • メールアドレス/パスワード認証、Googleアカウント認証、SNS認証(Facebook,Twitter)、匿名認証などの各種認証に対応
  • Firebaseコンソールから利用したい認証を有効にするだけで使えるようになる
  • 認証が成功するとUID(Firebase固有のユーザID)が発行されるので、Firebaseのユーザ識別はUIDを使うことになる
    • 同じユーザでもTwitterGoogleなど別の認証方法でログインした場合は、別々のUIDとなる
  • これだけのためにFirebaseを使ってもいいレベル

数行のコードで認証処理が書ける

通常SNS認証する場合、バックエンドでOAuthのリクエスト処理やアクセストークン、リフレッシュトークンの管理をしなければなりませんが、それをFirebaseが全て引き受けてくれます。

Twitter認証の場合

FirebaseConsoleのAuthenticationのログイン方法からTwitterを「有効」に設定後、APIなどの必要な情報を設定します。

ログインの導線は以下のような感じです。(Riot.jsですが適時読み替えてください)

<sign-in>
<a href="#" onclick={sign_in_twitter}>Twitterでサインインする</a>
sign_in_twitter(e) {
  e.preventDefault()
  firebase.auth().signInWithRedirect(new firebase.auth.TwitterAuthProvider())
}
</sign-in>

リンクをクリックするとTwitterの認証ページにリダイレクトし、ユーザによって承認や拒否が行われると、同じページにリダイレクトされて戻ってきますので、以下のコードでログイン後の受け取ります。

firebase.auth().getRedirectResult()
.then(result => {
  if (result.user) {
    // ログイン成功時の処理
    alert('(o・∇・o)ログイン成功だよ〜')
  }
})
.catch(error => {
  // ログイン失敗時の処理
  alert('(*>△<)<ナーンナーンっっ')
})

これでおしまいです!

Google認証の場合

さらにGoogle認証を追加したい場合は、FirebaseConsoleでGoogle認証を「有効」にして以下のコードを追加するだけで終わりです。

<a href="#" onclick={sign_in_google}>Googleでサインインする</a>
sign_in_google(e) {
  e.preventDefault()
  firebase.auth().signInWithRedirect(new firebase.auth.GoogleAuthProvider())
}

ログイン後処理は上記Twitterのやつと同じものを使います。

か、簡単すぎる・・・!

認証状態を受け取る場合は以下のように取得します。

firebase.auth().onAuthStateChanged(user => {
  if (user) {
    // ログイン済み
  }
})

Firebase でユーザーを管理する - 現在ログインしているユーザーを取得する | Firebase
認証状態が変わるたびにコールバックが呼ばれるため、SPAとも相性バッチリ。

データベース

Firebaseには2種類のデータベースが用意されています。 Realtime Databaseと現在β版で提供されているCloud Firestoreです。

違いは各項目で書くとして、大体の共通事項は

  • NoSQLデータベース
  • リアルタイム更新
    • Readをバインドしておけば追加/変更/削除と同時に全ユーザに配信される
  • オフライン対応
  • 無料枠はストレージ容量は1GBまで、ダウンロード転送量が月10GBまで
  • 管理画面が貧弱
    • PCはもちろんのこと、スマホは一応レスポンシブっぽいのにiOSだとまともには動かず、Androidならなんとか使えるレベル。さすがGoogle様…
  • 連番がつかえない
    一般的なRDBにあるオートインクリメントのような連番の仕組みがありません。
    掲示板の書き込み番号のような連番や、カウンタのように排他的に連番を振る機能を実装したければ、トランザクションを利用するなどして工夫する必要あり。

データベースを直接叩く感じ

  • SQLのSELECT句のように取得フィールドを選択する術がないので、ユーザに見せたくない情報は含めないようなデータ構造で設計すべき
    • いろんなところで言われてますが、正規化せずビューのためのデータベースと割り切る
    • 転送量もカウントされるので取得データは必要最小限に

掲示板の書き込みデータはboards配下にアイドルちゃんのキー毎に書き込み情報である「名前」「ID」「本文」「書き込み日時」をオブジェクトで保存するデータ構造にしてます。

{
 "boards": {
    "arisa": {
      "-IDXXXXXXX": {
        name: "亜利沙P",
        ID: "XXXXXXX",
        text: "⌒(*>∨<)b⌒",
        createdAt: "2018/11/11 11:11:11"
      },
      "-IDXXXXXXY": {
        name: "亜利沙P",
        ID: "XXXXXXXX",
        text: "A・R・I・S・A!!",
        createdAt: "2018/11/11 11:11:11"
      }
    }
  },
  "mirai": {
    "-IDXXXXXXZ": {
      name: "未来P",
      ID: "ZZZZZZZZ",
      text: "未来ちゃんかわいい",
      createdAt: "2018/11/11 11:11:11"
    }
  }
}

以下のコードでAPIを叩いてデータを取得し、UIライブラリに渡せばそのまま表示される!すごい簡単!

<board>
<div each={messages}>
  <div>{no} 名前:{name} 日時:{createdAt}</div>
  <div>{text}</div>
</div>
firebase.database().ref(`boards/arisa`) // 亜利沙の書き込み一覧を取得する
.orderByChild('createdAt')
.once('value')
.then(snap => {
    const messages = []
    let no = 1
    snap.forEach(doc => {
        const message = {
          ...doc.val(),
          no,
        }
        messages.push(message)
        no++
    })
    this.update({messages})
})
</board>

ルールはしっかり書こう!

Firebase Realtime Database ルールについて | Firebase
Cloud Firestore セキュリティ ルールを使ってみる | Firebase

  • 悪意を持ったユーザに好き放題されないためにルールが用意されている
    • かなり細かく指定できるのでしっかり書きましょう
  • Firestoreの方が新しいこともあってか、ルールは書きやすい
    • 共通処理の切り出しとか、他コレクションの相関チェックが容易
  • いわゆるバリデーション処理もルールでやる
    • ルールがあるからといって全部Firebase側で処理するとコスト(特にFirestore)がかかるので、クライアント側でもしっかりバリデーション処理をしましょう

Realtime Database

  • 無料枠はダウンロード転送量が月10GBまで
  • 無料枠だと同時接続が100まで
    • それ以上の接続がくるとエラーになるみたい
  • データ構造はJSONツリー
  • Firestoreに比べて使える型が少ない
  • 参照回数が多くデータサイズが小さい場合はこっちの方がいいかも

Cloud Firestore

  • ドキュメント指向のNoSQLデータベース
  • スケーラビリティが自動的に行われる
  • 型が豊富
    • 特に別のドキュメントを参照できるリファレンス型などは使いどころによっては便利そう
  • 無料枠はRead:5万回/day、Write:2万回/day、Delete:2万回/day
    • 太平洋時間0時(日本時間16時)ごろにリセットされる

読み込み回数カウントがかなりシビア!

1日あたり読み込み50,000回もあれば余裕だと思うじゃん?

f:id:nayuneko:20181113003239p:plain

ほげえええええwwwwwwwwww ※カウントリセットされてから4時間後です
Twitterでぷちバズっただけでこれよ・・・

ちゃんとカウントの仕組みをよく読みましょう
Cloud Firestore の料金

  • 1ドキュメントの読み込みで1回消費
  • コレクションIDリスト1つ取得で1回消費
  • Firebaseコンソールのデータ参照でも消費!
    • レコード数が多いと表示するだけで100単位でごっそりカウントされる😰
  • ルールの検証でコレクション/ドキュメントを読み込む
    • exists(),get(),getAfter()を使うと呼び出しドキュメント数に応じて消費
    • クエリがキャッシュされるらしいので、呼ばれたから必ず1回消費されるわけではない模様
    • exists()は存在しなかった場合でも1回カウントされるっぽい
  • レコード数を取得するためにはすべてのドキュメントを読み込んでsize()で取得するしか方法がないっぽい
    • もちろん読み込み件数分消費します
  • オフセット取得するとスキップ分件数消費
    • 先頭11レコード目から10件読みたい場合は、スキップしたレコード件数10件分の読み込み件数と実際データを読み込みした10件分、合計読み込み回数20回消費する
      Cloud Firestore の料金

こんなにカウントが複雑なのに回数の取得がヒジョーにめんどい・・・

ちなみに、Readの料金は安い(0.06$/100k)ので多少のバズぐらいなら課金で解決するのも手。
というかバズったら無料枠で収めるのは無理なので、サービス止めるのが嫌だったら課金するしかなさそう。

ちなみに無料プランのSparkと従量制のBlazeプランの切り替えは日毎に可能なので、バズって無料枠を超えそうになったらBlazeに切り替え→16時ぐらいになって回数がリセットされたのを確認したらSparkに戻す、と運用するのが無難そう。
ちなみに2日ほど枠超えて運用していましたが、請求額はこのぐらい。

f:id:nayuneko:20181113220812p:plain

やよいの給食費の利子ぐらいなので安心ですね!

掲示板(コンベ)がFirebaseのDBに向いていない理由(ワケ)

  • コンベのログは編集・削除できないので過去ログは静的データになるため、DBで管理する必要性が薄い
    • Readするたびに課金が発生するので、ぷちバズったら速攻で無料枠を食いつぶす恐れ
  • 普通のDBとして使うのがしんどい
    • 基本的にオフライン時はエラーとならず、オンラインになった瞬間にデータを取得したり書き込みをする仕組みになっているので、あえてオフライン制御しようとするとすごーく面倒。
  • ページング処理が大変
    一般的な1ページからのページングならさほど制御は難しくないけど、コンベのように最新ページを初期表示する仕様だとしんどい

最初Firestoreを使っていたのですが、掲示板のようなシステムの場合、書き込みより読み込みが圧倒的に多いシステムの場合、ぷちバズるとFirestoreの読み込み回数の無料枠が一瞬で蒸発するため、Realtime Databaseに移行しました。

Cloud Functions

  • 完全にバックエンドで動くのでユーザに見せたくない処理をさせるのに最適
  • 既存の機能だけだと痒いところに手が届かないので、ちょっとだけ掻いてくれる
    • それなりに制約があるので万能ではない
  • 無料枠は呼び出し月12.5万回
  • 初回起動は遅い
  • 管理者権限で動くので、データベースルールが適用されなくなる
    • 自前でルールと同等の処理を書く必要がある
  • onRequestなどUIDが取得できないトリガーもあるので注意

トリガーは便利だけと使い所は限られる

  • データベースの書き込み系のトリガーはリアルタイム性を重視するなら使わない方が無難
    • 上述の通り、初回起動が遅い
    • RealtimeDatabaseのonCreateトリガーは一旦書き込み後、呼ばれるので微妙
  • β版のFirestoreのトリガーはたまに起動しない
    • トリガーで表示データを加工処理してアップデートなどしてると痛い目を見る→みましたorz

まとまらない、まとめ。

初めて使うので色々調べながらやりましたが、公式のドキュメント・リファレンスは結構充実してますし、これだけサクサクかけるのはすごい・・・!