ABEJA Tech Blog

中の人の興味のある情報を発信していきます

Vue.jsのバックエンドとしてのFirebase

このポストは英語版を日本語に直したものです。

Vue.jsのバックエンドとしてのFirebase

こんにちは、株式会社ABEJAのカンとです。AIの会社として知られているABEJAですが、フロントエンドをメインとするチームもあります。私が所属しているSaaS Development チームです。私達は(最近のフロントエンドチームは皆そうですが…)最新技術が大好きで、以下のような技術を使っています。

f:id:abeja:20170726184558p:plain

最近、社内ツールを作っています。元々使っていた「AWS Lambda+Vue(S3)」の代わりに「Firebase + Cloud functions for firebase」を利用していますが、とても便利で楽しいです。ぜひ皆さんにも使って頂きたく、このブログを書くことになりました。

以下のような流れで書きたいと思います。

  • なぜFirebaseなのか
  • Firebase as the database
    • Real-time
    • Authentication
    • Deploy and hosting
    • Vue.js and vuefire
  • Firebase with Cloud Functions
    • So good.
    • Not so good.
  • 学んだこと
    • Databaseの構造
    • 集計
  • さいごに

より理解を高めるためにABEJA totalizerというサンプルサービスを用意しました。このサービスは簡単に投票用課題を作り、リアルタイムで集計するサービスです。このサービスはVuejs, firebase, cloud function for firebaseを使って作りました。

なぜFirebaseなのか

理由は簡単です。我々フロントエンドチームはサーバーを運用したくないからです。小規模から中規模までカバーできるBaaSをどれにするか検討した結果、FirebaseとGraphcoolに辿り着きました。最近、GCPの勢いがいいこともありFirebaseに決めました。

Firebase as the database

Real-time

FirebaseはWeb用のSDKを入れるだけでRealTime Web systemが実現できます。WebSocketを入れる必要もEventをListenする必要もありません。DBの値が変わったら自動にブラウザ上の値が変更されます。さらに、DBを直接LoadしているのでAPIを書く必要もそれ用のサーバーを持つ必要もありません。

Authentication

認証。どのシステムにも必須で大事な仕組みであり、当然開発するのに時間もかかります。Firebaseは Emailはもちろん、Facebook、GoogleなどSocial Authenticationをサービスとして提供していて簡単に追加できます。これにはPassword変更やEmail Verificationなど開発・管理が面倒な機能も全て含まれています。

Deploy and hosting

ホスティング。新しいサービスができたらどうやってホスティングしますか?SSLは?Firebaseはfirebase deployコマンド一発でデプロイでき、すぐにあなたのサービスを提供することができます。

// abeja-totalizer/package.json
"scripts": {
  "prepackage-functions": "rimraf functions",
  "package-functions": "babel 'functionsES6' --out-dir 'functions' --presets=es2015 --copy-files --ignore 'node_modules'",
  "postpackage-functions": "cd functions && yarn",
  "deploy": "yarn run package-functions && firebase deploy"
}

実際私達が使ってるDeployコマンドです。イケてるフロントエンド開発者ならprepackagepostpackageでいったい何をやってるんだと思うかも知れません。簡単に説明しますと、Google Cloud FunctionsがES6に対応していないため、トランスパイルしています。Linkこのリンクを参考にしました。

Vue.js and vuefire

このイケてるDBとVue.jsをどうやって繋げるのか。Vue.jsの詳細については最近の人気からたくさん情報がありますのでふれないようにしますが、Vue.jsを使えば簡単かつ恐ろしいスピートでコンポネント化されたWebサービスを作ることができます! FirebaseとVue.jsを繋げるところに話を戻すと、使用するのはvuefireです。そして私達はそれをより簡単にするため、以下のようなPluginを書きました。

// abeja-totalizer/src/libs/FirebaseDBPlugin.js
import firebase from '../helper/firebaseDB'

const db = firebase.firebaseApp.database()
const DATABASE_NAME = process.env.DB_NAME
const fbRef = ref => {
  return db.ref(`${DATABASE_NAME}/${ref}`)
}
const questions = db.ref(`${DATABASE_NAME}/questions`)
const answers = db.ref(`${DATABASE_NAME}/answers`)
const FB_MAPPING = {
  'questions': function () {
    this.$bindAsArray('questions', questions)
  },
  'question': function (param) {
    if (!param.questionKey) return
    this.$bindAsObject('question', questions.child(param.questionKey))
  },
  'answers': function (param) {
    if (!param.questionKey) return
    this.$bindAsArray('answers', answers.child(param.questionKey))
  },
  'myAnswer': function (param) {
    if (!param.questionKey) return
    if (!param.myId) return
    this.$bindAsObject('myAnswer', answers.child(param.questionKey).child(param.myId))
  }
}

const fbBinding = function () {
  let fbBind = this.$options['fbBind']
  if (typeof fbBind === 'function') fbBind = fbBind.call(this)
  if (!fbBind) return
  Object.keys(fbBind).forEach(k => {
    const b = FB_MAPPING[k]
    b.call(this, fbBind[k])
  })
}
const init = function () {
  fbBinding.call(this)
}

const install = (Vue) => {
  Vue.prototype.$fbRef = fbRef
  Vue.prototype.$fbBinding = fbBinding
  Vue.mixin({
    created: init // 1.x and 2.x
  })
}

export default {
  install
}
    // abeja-totalizer/src/main.js
    import FirebaseDBPlugin from './libs/FirebaseDBPlugin'
    Vue.use(FirebaseDBPlugin)

このようにして使うことができます。

// abeja-totalizer/src/components/Main.vue
export default {
  fbBind: function () {
    return {
      'questions': {}
    }
  }
}

Firebase with Cloud Functions

So good

Cloud Functions for Firebase. これはFirebaseで開発を始めて以降一番興奮した機能です。FirebaseはRealtime DBを持つBaaSとしてだけでなく、様々なFunctionを持っています。このFunctionを使ってHTTP Request(普通のAPI)の処理はもちろん、DB、Storage、ユーザー登録変更イベントをキャッチし、ロジックを入れる(Event Sourcing)ことができます!Serverless Architectureに感謝です!functionを書いてDeployすれば、もうメンテナンスもスケーリングも気にする必要がありません。no worry be happy 😬

Not so good

完璧なシステムはないです。今のところHTTP triggered function以外のCloud Functions for FirebaseをLocal環境でDebugする術はありません。Firebase CLIを使えばHTTP triggered functionのみLocal環境でDebugすることができます。そしてGoogleは「cloud Functions Local Emulator」というツールをCloud Functions用に提供していますが、Cloud Functions for firebaseでは使うことができません!(なんと!)Cloud FunctionsとCloud Functions for firebaseはほぼ同じですが少し違います。では他にDeployしないでDebugする術はないでしょうか。答えはUnitTestを書くことです。このドキュメントを参考してください。

学んだこと

デバッグ以外にもFirebaseを使いながら学んだことがいくつかあります。

データの構造はFlatに!

可能な限りデータはフラットに持ちましょう。データをNestedして持つと2つ大きな問題に直面します。1つ目はレコードを1つ取得するQueryを書くことが難しいこと、2つ目はSingle RequestのPayloadが大きくなることです。それに対する対策は以下の例みたいにデータをフラットにすることです。

// customers has many shops, each shop has many devices, devices owned by customer
customers
  -KnrCVAqhTQ33fGkz50s
    shops
      -KnrCVBHeSztSd86_CVI: true
      ...
shops
  -KnrCVBHeSztSd86_CVI
    customerKey: -KnrCVAqhTQ33fGkz50s
    devices
      -KnXFP1dGQoLsu4dzran: true
      ...
devices
  -KnXFP1dGQoLsu4dzran
    customerKey: -KnrCVAqhTQ33fGkz50s
    shopKey: -KnrCVBHeSztSd86_CVI
    name: '

集計

Firebaseでは従来のRDBが提供するような集計機能を使うことができません。でもRealtimeDatabaseならではの方法でこれを実現することができます。それはCloud Functions for Firebaseを利用することです。集計ロジックをFunctionとして書いて、DBの値が変更された時集計することです。以下のサンプルを参考にしてください。

import 'babel-polyfill'
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
admin.initializeApp(functions.config().firebase)

const setFirebaseRef = async (ref, index, plusOrMinus) => {
  let countRef = ref.child(`${index}/count`)
  let countValue = (await countRef.once('value')).val()
  countRef.set(countValue + plusOrMinus)
}

export const countSelect = functions.database.ref('/totalizer/answers/{questionKey}/{userId}')
  .onWrite(async event => {
    console.log('on write')
    let selectionRef = admin.database().ref(`/totalizer/questions/${event.params.questionKey}/selections`)
    if (event.data.previous.exists()) {
      console.log('update')
      let prevIndex = event.data.previous.val()
      await setFirebaseRef(selectionRef, prevIndex, -1)
      let newIndex = event.data.val()
      await setFirebaseRef(selectionRef, newIndex, 1)
    } else {
      console.log('create')
      let index = event.data.val()
      await setFirebaseRef(selectionRef, index, 1)
    }
  })

※このFunctionにはBugがあります。答えの変更があった時元の答えを-1にし変更された答えを+1にしていますが、同時にたくさんの処理が走る場合正しく動作しません。Eventがあるタイミングで計算をし直す必要があると思います。

さいごに

数週間Firebaseを使って、FirebaseとVue.jsは一緒に使うことが非常に便利であることが分かりました。これからのProjectにもこの組み合わせでいくと思います。このPostに使ったコードを参考に皆さんもぜひ試してみてください😃

サンプルコードはこちらから利用できます。今度会う時まで、Happy coding!

ABEJA SaaS Dev

We are hiring!

ABEJAが発信する最新テクノロジーに興味がある方は、是非ともブログの読者に!

ABEJAという会社に興味が湧いてきた方はWantedlyで会社、事業、人の情報を発信しているので、是非ともフォローを!! www.wantedly.com

ABEJAの中の人と話ししたい!オフィス見学してみたいも随時受け付けておりますので、気軽にポチッとどうぞ↓↓