Firebase Functionsにデプロイする関数を動的に生成したい

#nodejs
#firebase

Firebase Functions は index.js で exports.func_name = ... といった感じで export した関数が Firebase にデプロイされる。

例えばデプロイ時に DB から読み込んだ設定に応じて関数をセットアップしたい場合を考える。以下のように書けばいい。

// index.ts interface CloudFunction { functionName: string; someParameter: string; } module.exports = (() => { const functions: CloudFunction[] = await getFunctions(); return functions.reduce( (array, f) => ({ ...array, [f.functionName]: doSomething(f.someParameter), }), {}, ); })();

Firebase CLI は index.js を require して関数をデプロイするので、require 時に { 関数名: 関数本体 } の形式となるようオブジェクトを渡せればよい。

firebase-tools/functionsEmulatorRuntime.ts at cd737c91b790b58555d4bf65320232b0b0a207fe · firebase/firebase-tools · GitHub


ここでオブジェクトを module.exports ではなく exports に代入すると期待通りに動かず悩まされた。exports.functionName = ... のような書き方であれば問題無い。

そもそも両者の違いは何だっけ、と調べてみると exports とは module.exports へのエイリアスのようなもので同義であるらしいとわかる。

さらによく分からなくなったので掘り下げていく。調べてみると以下のような記事にも助けられた。

Node.js : exports と module.exports の違い(解説編) - ぼちぼち日記

読んでいくとなんだか簡単な話じゃないということが分かってくるが、要点はこの部分だと解釈した。

よって exports には module.exports = {} の空のオブジェクトが渡されているので、そのオブジェクトのプロパティを変更した時のみ module.exports に反映されて require() の戻り値として扱うことができるということです。(関数へのオブジェクト渡し Call by sharing のため)

exports は初期化時に module.exports へセットされた空オブジェクトへの参照を持っている単純な変数のイメージ。なので以下のような挙動になるのは納得感がある。C 言語の授業のポインタクイズみたいな話だった。

exports.foo = ...; // module.exportsの{}にfooがセットされる
exports = { bar: ... }; // module.exportsへの参照が別のオブジェクトに向く。module.exportsの中身は変わらない
module.exports = { baz: ... }; // module.exportsの参照先が新しいオブジェクトに向く。require側はこれを参照することになる

Firebase Functions の話から逸れたが理解が深まった。どうしても shorthand な書き方をしたい場合でなければ module.exports を使う方が変な間違いを起こしにくいように思う。

Tweet