Firebase CLI の init 後の初期状態では Cloud Functions 関連のファイルは以下となっているはずです。(TypeScript の場合)

└ functions/
   ├ その他ファイルたち
   ├ lib/
   └ src/
     └ index.ts

あとは index.ts にトリガー関数を書いていけばそれで完成ではありますが、トリガーの数をそれなりに作成する場合は、ファイルを分割したい気持ちに駆られることは間違いないですよね。

ということで、私なりに分割した例を載せます、というのが本記事です。

TL;DR

完成形を先に示して、ポイントだけ後ほど補足します。

ファイルの配置

(ソースコード以外のファイルは記載を省略します。)

└ functions/
   └ src/
     ├ firebase.ts
     ├ index.ts
     └ triggers/
        ├ sampleCreate.ts
        ├ sampleDelete.ts
        ├ sampleUpdate.ts
        └ sampleHttps.ts

firebase.ts

import * as functions from 'firebase-functions';

export type Snapshot = functions.firestore.QueryDocumentSnapshot;
export type Change = functions.Change<functions.firestore.QueryDocumentSnapshot>;
export type Context = functions.EventContext;
export type Request = functions.https.Request;
export type Response = functions.Response;

type SnapshotHandler = { trigger: (snapshot: Snapshot, context: Context) => Promise<unknown> };
type ChangeHandler = { trigger: (change: Change, context: Context) => Promise<unknown> };
type RequestHandler = { trigger: (req: Request, resp: Response) => Promise<unknown> };

const getHandler = async (handlerFileName: string) => {
  const handlerFilePath = `./triggers/${handlerFileName}`;
  return await import(handlerFilePath);
};

const db = functions.region('asia-northeast1').firestore;
const https = functions.region('asia-northeast1').https;

/**
 * @param documentPath - トリガー起点の Firestore ドキュメントのパス
 * @param handlerFileName - "./triggers"直下のファイル名(拡張子除く)
 */
export const onCreate = (documentPath: string, handlerFileName: string) => {
  return db.document(documentPath).onCreate(async (snapshot, context) => {
    const handler: SnapshotHandler = await getHandler(handlerFileName);
    return handler.trigger(snapshot, context);
  });
};

/**
 * @param documentPath - トリガー起点の Firestore ドキュメントのパス
 * @param handlerFileName - "./triggers"直下のファイル名(拡張子除く)
 */
export const onDelete = (documentPath: string, handlerFileName: string) => {
  return db.document(documentPath).onDelete(async (snapshot, context) => {
    const handler: SnapshotHandler = await getHandler(handlerFileName);
    return handler.trigger(snapshot, context);
  });
};

/**
 * @param documentPath - トリガー起点の Firestore ドキュメントのパス
 * @param handlerFileName - "./triggers"直下のファイル名(拡張子除く)
 */
export const onUpdate = (documentPath: string, handlerFileName: string) => {
  return db.document(documentPath).onUpdate(async (change, context) => {
    const handler: ChangeHandler = await getHandler(handlerFileName);
    return handler.trigger(change, context);
  });
};

/**
 * @param handlerFileName - "./triggers"直下のファイル名(拡張子除く)
 */
export const onHttps = (handlerFileName: string) => {
  return https.onRequest(async (req, resp) => {
    const handler: RequestHandler = await getHandler(handlerFileName);
    return handler.trigger(req, resp);
  });
};

index.ts

import { onCreate, onDelete, onUpdate, onHttps } from './firebase';

export const sampleCreate = onCreate('/collection/{id}', 'sampleCreate');
export const sampleDelete = onDelete('/collection/{id}', 'sampleDelete');
export const sampleUpdate = onUpdate('/collection/{id}', 'sampleUpdate');
export const sampleHttps = onHttps('sampleHttps');

sampleCreate.ts

import type { Snapshot, Context } from '../firebase';

export const trigger = async (snapshot: Snapshot, context: Context) => {
  // Do something ...
};

sampleDelete.ts

import type { Snapshot, Context } from '../firebase';

export const trigger = async (snapshot: Snapshot, context: Context) => {
  // Do something ...
};

sampleUpdate.ts

import type { Change, Context } from '../firebase';

export const trigger = async (change: Change, context: Context) => {
  // Do something ...
};

sampleHttps.ts

import type { Request, Response } from '../firebase';

export const trigger = async (request: Request, response: Response) => {
  // Do something ...
};

解説

流れとしては次のような感じになっています。

triggers/ 配下の各関数を firebase.ts を介して index.ts に渡している。逆から説明すると、 index.ts から firebase.ts の関数を実行して、その内部で triggers/ の関数を呼び出している。

コード量的にわかると思いますが、ポイントは firebase.ts です。必要な記述をここで一通り揃えておき、 index.ts や各トリガー関数を薄くできるようにしています。僅かではありますが firebase.ts にコメント(JSDoc)も書いておいたのでご参考としてください。(記載方法が適当ですみません。。。)

トリガーを追加する際は、triggers/ 配下にファイルを作成し、 index.ts に1行記載を追加するだけです。

なお、triggers/ というディレクトリ名を変えたい場合は、firebase.tsgetHandler() 内のディレクトリ名指定も合わせて修正することをお忘れないようご注意ください。

その他ポイント:呼び出されたトリガーファイルのみインポート

ファイルを分割する以外にも気を配った点として、ダイナミックインポートを利用して、呼び出されたトリガーファイルのみをインポートするようにしています。

firebase.tsgetHandler() がそれです。

このようにしておくと、例えば sampleCreate が動く時には、他の sampleDeletesampleUpdatesampleHttps のトリガーファイルはインポートされません。

こうすることで全てをインポートするときと比較して、僅かではありますが、起動時間の短縮とコンピュータリソースの削減(つまり使用料の節約)につながります。

以上

この記事がお役に立てば幸いです!