肥大化するCloud Functionsのファイルを複数のTsファイルに分ければメンテナンスもしやすい

2019年12月26日

FirebaseにおけるCloud Functions

Cloud Functionsはアクセスが有ったときだけ動くサーバのようなものだと勝手に解釈しています。通常のサーバと違い、稼働しっぱなしではなく機能が呼ばれたときだけ課金の対象になる点がとてもユニークです.
サーバから実行されるので、厳重に管理しなければならない処理はCloud Functionsにおまかせです。例えばそうですね、Nipoの例であれば、

  • クレジットカード決済
  • 新着日報お知らせメールの送信
  • Firestoreに書き込まれたデータをTypeSenseに動悸させる処理

などをクラウドファンクションで実行しています。

見逃しちゃいけないポイント!Cloud Functionsはセキュリティルールを素通りできます

クライアントから実行するとセキュリティルールの制限を受けます。しかしクラウドファンクションではセキュリティルールを通り越して処理ができます。
もちろんメリットも大きいですが悪用されないように開発には細心の注意を払う必要があります。

Cloud Functionsはindex.tsに書きましょう

そんな便利なクラウドファンクションは、index.tsに処理を記述していきます。記述するとこんな感じ

import * as functions from 'firebase-functions'
const admin = require('firebase-admin')

const firebaseKey = {
  'type': 'service_account',
  'project_id': 'さんぷる',
  'private_key_id': '長い桁の鍵',
  'private_key': '長い桁の鍵',
  'client_email': 'さんぷる',
  'client_id': 'さんぷる',
  'auth_uri': 'さんぷる',
  'token_uri': 'さんぷる',
  'auth_provider_x509_cert_url': 'さんぷる',
  'client_x509_cert_url': 'さんぷる'
}

admin.initializeApp({
  credential: admin.credential.cert(firebaseKey),
  databaseURL: 'https://salonkarte-87b6a.firebaseio.com'
})
// ↑ここまではほぼ定型文

// 例えばデータが追加されたらカウントアップするサンプルクラウドファンクション
// カウンターみたいな処理もクラウドファンクションに任せたほうがいいかも
exports.addDataCounter = functions.firestore.document('user/{userId}/data/{dataId}').onCreate(async (snap, context) => {
  const userId = context.params.userId
  const ref = await admin.firestore().collection('user').doc(userId).collection('lock').doc('state').get()
  if (ref.exists) {
    let currentCnt = ref.data().dataCnt
    currentCnt = currentCnt + 1
    admin.firestore().collection('user').doc(userId).collection('lock').doc('state').update({ dataCnt: currentCnt})
  } else {
    admin.firestore().collection('user').doc(userId).collection('lock').doc('state').set({ dataCnt: 1})
  }
})

上は簡単なクラウドファンクションのサンプルコードです(TypeScript) addDataCounterの行が実際の処理となる部分です。このサンプルはデータが書き込まれたらカウントと+1するというものです。

あとはやりたい処理を書き連ねていくだけです。

exports.関数名 = functions.firestore.document(‘監視対象Path’) .onCreate({ 処理 })

仕組みは非常にシンプルです。exportsで指定された関数の数だけクラウドファンクションに登録されます。しかしシステムは次から次へと新たしい要望が出てきます。するとクラウドファンクションもあっという間に膨大な行数へ豹変します。 300行を過ぎたくらいから、スクロールするのがだるくなります。600行を超えたあたりから危機感を覚えます。そう、「ファイル分割」しないとまずいぞ。と

クラウドファンクションのファイルを関数単位で分けよう

普通に考えれば1つのファイルにすべての処理を書くのは得策ではありませんが、公式サイトにはなぜかクラウドファンクションのファイルを分割する方法が書かれていません。Nipoの開発を始めた当時はこの辺の情報が結構少なく、さらにクラウドファンクション自体がβ版でした。(今は正式版です)

さて、クラウドファンクションの関数を1つのファイルに切り出してみましょう。注意点としては、

admin.initializeApp

この処理は1回しか呼び出せません。複数回呼び出すとエラーで落ちます。

先程のサンプルコードを例に、addDataCounterを別のファイル(myCounter.ts)に切り分けてみましょう。

// 【index.ts】
import * as functions from 'firebase-functions'
import * as myCounter from './myCounter' // 追記
const admin = require('firebase-admin')

const firebaseKey = {
  'type': 'service_account',
  'project_id': 'さんぷる',
  'private_key_id': '長い桁の鍵',
  'private_key': '長い桁の鍵',
  'client_email': 'さんぷる',
  'client_id': 'さんぷる',
  'auth_uri': 'さんぷる',
  'token_uri': 'さんぷる',
  'auth_provider_x509_cert_url': 'さんぷる',
  'client_x509_cert_url': 'さんぷる'
}

admin.initializeApp({
  credential: admin.credential.cert(firebaseKey),
  databaseURL: 'https://salonkarte-87b6a.firebaseio.com'
})
// ↑ここまではほぼ定型文
// ↓ここが追記されます
module.exports = {
  myCounter
}

続いて分割された側のファイルコードはこちら

// 【myCounter.ts】
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

// 例えばデータが追加されたらカウントアップするサンプルクラウドファンクション
// カウンターみたいな処理もクラウドファンクションに任せたほうがいいかも
export default functions.firestore.document('user/{userId}/data/{dataId}').onCreate(async (snap, context) => {
  const userId = context.params.userId
  const ref = await admin.firestore().collection('user').doc(userId).collection('lock').doc('state').get()
  if (ref.exists) {
    let currentCnt = ref.data().dataCnt
    currentCnt = currentCnt + 1
    admin.firestore().collection('user').doc(userId).collection('lock').doc('state').update({ dataCnt: currentCnt})
  } else {
    admin.firestore().collection('user').doc(userId).collection('lock').doc('state').set({ dataCnt: 1})
  }
})

このように無事ファイルを分けることができました。めでたしめでたし。

アイコン
今すぐ無料で始められます

同じタグのついた記事一覧です