GAミント至上主義

Web Monomaniacal Developer.

Firebase Functionsで関数ごとにファイルを分割し高速化とメンテナンス性向上も目指す

Firebase Functionsでは、基本的にはindex.jsにすべての関数を書くことになるので、数が増えるといろいろつらくなってきますが、2018秋アニメはAmazonでSAO新作とゴブリンスレイヤーを見ています。

ファイル分割については、ググればこんな感じのが見つかります。
javascript - How do I structure Cloud Functions for Firebase to deploy multiple functions from multiple files? - Stack Overflow

index.js:

const functions = require('firebase-functions');
const fooModule = require('./foo');
const barModule = require('./bar');

exports.foo = functions.database.ref('/foo').onWrite(fooModule.handler);
exports.bar = functions.database.ref('/bar').onWrite(barModule.handler);

普通にrequireして、exportsに追加する形です。

しかし、これだとどの関数が呼ばれてもindex.jsをすべて実行してしまい、無駄なモジュールをロードして起動が遅くなることがあるようです。
そのため下記記事ではCloud Functionsで設定される環境変数を使用して、読み込み分けを紹介されていました。

tech.ginco.io

index.js

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'Func1') {
    exports.Func1 = require('./funcs/func1');
}
if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'Func2') {
    exports.Func2 = require('./funcs/func2');
}

デプロイ時にはprocess.env.FUNCTION_NAMEが設定されないため、それを利用して全部読み込まれるようにしているのが上手い。

動作はこれで問題ないですが、関数が増えてきた場合、if分とexportsを繰り返して書くためちょっとすっきりしません。
そこで下記のように関数名とファイルパスをオブジェクトとし、forで回してみました。

index.js

const funcs = {
  sendPostCard2Chat: './src/send-postcard-to-chat',
  replyPostCard2Yagish: './src/reply-postcard-to-yagish',
  notFound: './src/not-found',
  updateOmikujiStatistics: './src/update-omikuji-statistics'
}

loadFunctions = (funcsObj) => {
  for(let name in funcsObj){
    if(! process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
      exports[name] = require(funcsObj[name])
    }
  }
}

console.log('process.env.FUNCTION_NAME:', process.env.FUNCTION_NAME)
loadFunctions(funcs)
console.log('exports:', exports)

デプロイも問題なく終了します。

> functions@ lint /shanyang/app/functions
> eslint .

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (95.95 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: updating Node.js 8 function sendPostCard2Chat(us-central1)...
i  functions: updating Node.js 8 function replyPostCard2Yagish(us-central1)...
i  functions: updating Node.js 8 function notFound(us-central1)...
i  functions: updating Node.js 8 function updateOmikujiStatistics(us-central1)...
✔  functions[updateOmikujiStatistics(us-central1)]: Successful update operation.
✔  functions[sendPostCard2Chat(us-central1)]: Successful update operation.
✔  functions[replyPostCard2Yagish(us-central1)]: Successful update operation.
✔  functions[notFound(us-central1)]: Successful update operation.

実行時のconsole.log('exports:', exports)のログも一つだけになりました。

10:31:29.014 午前 sendPostCard2Chat exports: { sendPostCard2Chat: { [Function: cloudFunctionNewSignature] run: [AsyncFunction] } }

動作も影響なさそうです。
まだいろいろ確認中なのでlogなども残ってますが、exportsを関数内で実行しても問題なく動くようです。
更にこの設定のオブジェクトとloadFunctions関数を別ファイルにすることも考えましたが、index.jsという名前的にここに一覧を書くのがよさそうだし、loadFunctionsをモジュール化して読み込むとindex.jsでは読まれないようで動きませんでした。もっと美しくできそうですが現時点はこんなところ。

あと設定をObjectではなく、Mapでやってもいいかなと思ったけどいいのかわからない。

変数をグローバルにキャッシュしておく

関数ごとにすべて分けるのが絶対に良いかというとそんな単純ではなく、下記の通り起動した実行環境(コンテナ?)は再利用されることがあるようです。
そのためメンテナンス性だけを考えるとファイルを分けた方がいいですが、速度を重視するようになるとこのグローバル変数も考慮する必要がありそうです。
ヒントとアドバイス  |  Firebase

Cloud Function の状態は、将来の呼び出しのために必ずしも保持されるわけではありません。しかし、Cloud Functions が以前の呼び出しの実行環境をリサイクルすることはよくあります。変数をグローバル スコープで宣言すると、その値は再計算せずに後続の呼び出しで再利用できるようになります。

まだ実際のコードは書いてませんが、絶対使うfirebase-funcstionsみたいなものはglobalオブジェクトに突っ込んでいいけど、関数によって使うか使わないか違うやつは、遅延評価をした方がいいとなると呼び出し時に上記のような書き方をする必要があり、コードが煩雑になるつらみもある。
開発初期はメンテナンス性重視で良く使うものはglobalに、開発が進んでパフォーマンスが必要になった箇所で遅延評価、みたいな感じが現実的だろうか。



改訂新版 Vue.jsとFirebaseで作るミニWebサービス (技術書典シリーズ(NextPublishing))

改訂新版 Vue.jsとFirebaseで作るミニWebサービス (技術書典シリーズ(NextPublishing))

WEB+DB PRESS Vol.105

WEB+DB PRESS Vol.105

  • 作者: 小笠原みつき,西村公宏,柳佳音,志甫侑紀,池田友洋,木村涼平,?橋優介,大塚雅和,飯塚直,吉川竜太,末永恭正,久保田祐史,浜田真成,穴井宏幸,大島一将,桑原仁雄,牧大輔,池田拓司,はまちや2,竹原,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/06/23
  • メディア: 単行本
  • この商品を含むブログを見る

Firestoreのパス管理に悩む

FirestoreFilestoreはぱっと見違いが分からないですが、Firestoreでアプリケーションを作っていてデータが増えてくるとドキュメントとコレクションのパスの管理に困ります。

yagish履歴書では、ユーザーごとに/userdir/{ユーザーID}/というドキュメントを作り、それ以下にサブコレクションやドキュメントを追加していくという形で管理してます。
シンプルですが、それでもこのユーザーのバックアップがあるコレクションのパスが欲しい、となったときに毎回 /userdir/{uid}/backupsをコーディングすることはしたくありません。
なので一か所で管理し、foobar.backups(uid) みたいな感じでパスが返ってくると、使いやすいし、変更の際も1か所変えれば済むので便利です。

いろいろ試行錯誤したけど、現状はこんな感じで関数を作って対応してます。

const createFirestorePaths = function (uid, docId) {
  if (!uid) {
    console.error('uid not set')
    return {}
  }
  let paths = {
    userdir: `userdir`,
    userDoc: `userdir/${uid}`,
    backups: `userdir/${uid}/backups`,
    configs: `userdir/${uid}/configs`,
    omikujis: `userdir/${uid}/omikujis`,
    postcards: `userdir/${uid}/postcards`,
    statistics: `userdir/${uid}/statistics`,
    statisticsOmikuji: `userdir/${uid}/statistics/omikuji`
  }
  if (docId) {
    const docPaths = {
      omikujiDoc: `userdir/${uid}/omikujis/${docId}`
    }
    paths = Object.assign(paths, docPaths)
  }
  return paths
}

これで、

console.log(createFirestorePaths('123'))
{userdata: "userdata", userDoc: "userdata/123", backups: "userdata/123/backups", configs: "userdata/123/configs", omikujis: "userdata/123/omikujis", …}
backups: "userdata/123/backups"
configs: "userdata/123/configs"
omikujis: "userdata/123/omikujis"
postcards: "userdata/123/postcards"
statistics: "userdata/123/statistics"
statisticsOmikuji: "userdata/123/statistics/omikuji"
userDoc: "userdata/123"
userdata: "userdata"

とりあえずこんな感じでObjectで返ってくるので使ってます。

Functionsでイベントを取りたいときは{uId}のような文字列を渡してしまえば同じように使えます。

exports.updateOmikujiStatistics = functions
  .runWith(runtimeOptions)
  .firestore
  .document(createFirestorePaths('{uId}', '{omikujiId}').omikujiDoc)
  .onCreate( async (snap, context) => {
    const uId = context.params.uId
    const omikujiData = snap.data().content

4階層目のドキュメントをID指定で取得したいとき(omikujiDocのところ)は苦肉感あふれてるがパスの生成を一か所でやることを優先すると他の方法が思いつかなかった。

関数と一覧のObjectを分けて管理したくなるけど、置換にテンプレート文字列を使って楽なので外に出せない。

developer.mozilla.org

みんなどうしているんだろう。

WEB+DB PRESS Vol.105

WEB+DB PRESS Vol.105

  • 作者: 小笠原みつき,西村公宏,柳佳音,志甫侑紀,池田友洋,木村涼平,?橋優介,大塚雅和,飯塚直,吉川竜太,末永恭正,久保田祐史,浜田真成,穴井宏幸,大島一将,桑原仁雄,牧大輔,池田拓司,はまちや2,竹原,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/06/23
  • メディア: 単行本
  • この商品を含むブログを見る

Google Pixelbookでdockerを動かす

Linuxを入れた前回に引き続き。

機種はこれ

docker公式通り行くと下記エラーで止まる

$ sudo docker run python:3.7
docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:402: container init caused \"could not create session key: function not implemented\"": unknown.
ERRO[0000] error waiting for container: context canceled

ちょうどいい記事があった。
[https//qiita.com/azumag/items/a834f8cce08a65570033:embed:cite]

ニュースで見覚えのあるGoogle謹製のコンテナランタイムgVisorにすれば動くらしい。
github.com

この記事にあったデフォルト設定まで入れて再起動

$ sudo vim /etc/docker/daemon.json
{
    "runtimes": {
        "runsc": {
            "path": "/usr/local/bin/runsc"
        }
    },
    "default-runtime": "runsc"
}
$  sudo systemctl restart docker

動いた

$ sudo docker run -it node:10-slim bash
root@f65f2e70b47a:/# 

次はVue CLI3とか入れる