Edited at

Cloud Functions for Firebase をつかってみる。ついでにBabelとかTypeScriptのトランスパイルも。

Cloud Functions for Firebaseはいわゆる AWS Lambdaの Firebase 版です。サーバレスで、サーバ側にロジックを構築できるわけですが、FirebaseやFirestoreが便利すぎる件にくわえて、さらにそれらの処理をトリガーにロジックがうごかせるわけなので、、ちゃんと勉強しておいた方がよさそうです。。

ちなみに、サイトにあるユースケースを見ると


  • Realtime Database の書き込みをトリガーに、ユーザに通知を投げるとか、

  • 画像のアップロードをトリガーに、画像の加工を行うとか

  • GitHub webhook APIをつかって、commitのpushをトリガーにSlackに通知を投げる

なんて事ができそうです。HTTPSの受信をトリガーに出来るので、簡単なRESTサーバとかも構築できますね。。

という事で作業メモ。


やってみる


Hello World

firebase-toolsのインストール

$ npm install -g firebase-tools

$ source ~/.bash_profile
$ firebase --version
6.3.0

プロジェクトのディレクトリ作成

$ mkdir fb_function_samples  && cd $_

Firebaseにログインしておく

$ firebase login

? Allow Firebase to collect anonymous CLI usage and error reporting information?
No

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=... // OAuthでアクセスの認可まち

Waiting for authentication...

✔ Success! Logged in as masatomix@example.com

プロジェクト作成

$  firebase init functions

######## #### ######## ######## ######## ### ###### ########
## ## ## ## ## ## ## ## ## ## ##
###### ## ######## ###### ######## ######### ###### ######
## ## ## ## ## ## ## ## ## ## ##
## #### ## ## ######## ######## ## ## ###### ########

You're about to initialize a Firebase project in this directory:

質問には基本デフォルトでよさげ

? Select a default Firebase project for this directory: [create a new project]

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
? Do you want to install dependencies with npm now? Yes

✔ Firebase initialization complete!

Project creation is only available from the Firebase Console
Please visit https://console.firebase.google.com to create a new project, then run firebase use --add

いわれるままに、コマンドを実行(どのFirebase上のプロジェクトにデプロイする?とかを指定してるぽい)

$ firebase use --add

? Which project do you want to add? sandbox-xxxxxxx
? What alias do you want to use for this project? (e.g. staging) test_env

Created alias test_env for sandbox-xxxxxxx.
Now using alias test_env (sandbox-xxxxxxx)

結果、プロジェクト構成はこんな感じ

$ pwd

/..../fb_function_samples
$ tree -a
.
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
├── .gitignore
├── index.js
├── node_modules/.... 割愛
├── package-lock.json
└── package.json

さあ index.jsをいじってみます。


functions/index.js(以下のように修正)

$ cat functions/index.js

const functions = require('firebase-functions');

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
exports.helloWorld = functions.https.onRequest((request, response) => {
response.send("Hello from Firebase!");
});

exports.echo = functions.https.onRequest((request, response) => {
const task = request.body
console.log(JSON.stringify(task))
// const firestore = admin.firestore()
// const ref = firestore.collection("todos")
response.send(JSON.stringify(task));
});


HTTPのリクエストの受信をトリガーに動く、メソッド helloworldとechoを作成しました。

サーバへデプロイ

$ firebase deploy --only functions

=== Deploying to 'sandbox-xxxxxxx'...

i deploying functions
i functions: ensuring necessary APIs are enabled...
✔ functions: all necessary APIs are enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (42.39 KB) for uploading
✔ functions: functions folder uploaded successfully
i functions: creating Node.js 6 function helloWorld(us-central1)...
i functions: creating Node.js 6 function echo(us-central1)...
✔ functions[echo(us-central1)]: Successful create operation.
Function URL (echo): https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/echo
✔ functions[helloWorld(us-central1)]: Successful create operation.
Function URL (helloWorld): https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/helloWorld

✔ Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/sandbox-xxxxxxx/overview

さあ、デプロイが完了したので、 firebase loginしたコンソールで、curlで呼び出してみます

$ curl https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/helloWorld

Hello from Firebase!

$ cat request.json
{
"id": "001",
"name": "こんにちは",
"isDone": true
}

$ curl -X POST -H "Content-Type:application/json" \
--data-binary @request.json \
https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/echo | jq
{
"id": "001",
"name": "こんにちは",
"isDone": true
}
$

よさそうです。


requireでなくてimportをつかいたい

vue-cliで vue.jsのアプリを書いていると、ES2015形式(?)でコードを書いてますよね。外部モジュールをconst functions = require('firebase-functions'); ではなく import * as functions from 'firebase-functions' って書きたい、ってヤツです。

ってことで、babelを入れてトランスパイルすることで、ES2015形式でかけるようにしてみます。


babel インストール他

$ cd functions

$ npm install --save-dev babel-cli babel-preset-es2015
$ cd ../

firebase.json にpredeployを追加(ココに書いておくと、deploy前に実行されます)

$ cat firebase.json

{
"functions": {
"predeploy": "npm --prefix \"$RESOURCE_DIR\" run build" // buildはこのあと定義します
}
}

package.json にbuildのスクリプトの定義を追加

$ cat  functions/package.json

{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"build": "mkdir -p dist && ./node_modules/.bin/babel ./*.js --out-file ./dist/index.js", // buildの定義を追加
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"main": "dist/index.js", // デプロイするファイルをココで指定
"engines": {
"node": "8" // ついでにnodeを8にしてみる
},
"dependencies": {
"firebase-admin": "~6.0.0",
"firebase-functions": "^2.1.0"
},
"private": true,
"devDependencies": { // これらは自動で追加されたはず
"babel-cli": "^6.26.0",
"babel-preset-es2015": "^6.24.1"
}
}

babel設定ファイル作成

$ cat functions/.babelrc

{
"presets": [
[
"es2015",
{
"targets": {
"node": "current"
}
}
]
]
}

ディレクトリ構成は以下のように。

$ tree -a

.
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
├── .babelrc
├── .gitignore
├── index.js
├── node_modules/.... 割愛
├── package-lock.json
└── package.json

デプロイ

$ firebase deploy --only functions

=== Deploying to 'sandbox-xxxxxxx'...

i deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /..../fb_function_samples/functions
> mkdir -p dist && ./node_modules/.bin/babel ./*.js --out-file ./dist/index.js

✔ 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 (55.53 KB) for uploading
✔ functions: functions folder uploaded successfully
i functions: updating Node.js 8 function helloWorld(us-central1)...
i functions: updating Node.js 8 function echo(us-central1)...
✔ functions[helloWorld(us-central1)]: Successful update operation.
✔ functions[echo(us-central1)]: Successful update operation.

✔ Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/sandbox-xxxxxxx/overview
$

ログを見ると、babelのトランスパイルが動いてからデプロイされていますね。。

curlで実行出来ることを確認してください。


ちゃんとES2015の文法に書き直して、再ラン

さっきはindex.jsをもとのファイルそのままつかいましたが、以下のようにトランスパイルが必要なコードにしてみます。


functions/index.js

import * as functions from 'firebase-functions'

// // Create and Deploy Your First Cloud Functions
// // https://firebase.google.com/docs/functions/write-firebase-functions
//
export const helloWorld = functions.https.onRequest((request, response) => {
response.send("Hello from Firebase!")
})

export const echo = functions.https.onRequest((request, response) => {
const task = request.body
console.log(JSON.stringify(task))
// const firestore = admin.firestore()
// const ref = firestore.collection("todos")
response.send(JSON.stringify(task))
})


も一度デプロイしてみましょう。ちなみに dist/index.js をみても、ちゃんと変換されてそうですね。。


っていうかTypeScriptつかえば?ってことかな

いまいち JavaScriptのES2015とTypeScriptの関係がよく分かってないのですが、ES2015でbabelで前提でコードを書くのと、Gradual Typing なTypeScript で書くのって、構文おなじっぽいですよね?ってことで、そもそも初めのプロジェクト作成で、JavaScriptでなくてTypeScriptを選べばいいんじゃんね?っておもいました。。

ということで、別のTypeScriptプロジェクトを作成します。index.jsは後でつかうのでどっかに取っておきましょう。


TypeScriptをつかってみる

プロジェクト作成(こんどはTypeScriptをえらびますよ)

$ mkdir fb_function_samples  && cd $_

$ firebase init functions

? Select a default Firebase project for this directory: [create a new project]
? What language would you like to use to write Cloud Functions? TypeScript ← TypeScriptをえらぶ
? Do you want to use TSLint to catch probable bugs and enforce style? No ← いったんNo
? Do you want to install dependencies with npm now? Yes

✔ Firebase initialization complete!

Project creation is only available from the Firebase Console
Please visit https://console.firebase.google.com to create a new project, then run firebase use --add

$ firebase use --add

? Which project do you want to add? sandbox-xxxxxxx
? What alias do you want to use for this project? (e.g. staging) test_env

Created alias test_env for sandbox-xxxxxxx.
Now using alias test_env (sandbox-xxxxxxx)
$

ディレクトリ構成

$ tree -a

.
├── .firebaserc
├── .gitignore
├── firebase.json
└── functions
├── .gitignore
├── node_modules/.... 割愛
├── package-lock.json
├── package.json
├── src
│ └── index.ts
└── tsconfig.json

さて、待避しておいた functions/index.js の内容を functions/src/index.ts へ上書きして、firebase deploy します

$ firebase deploy --only functions

=== Deploying to 'sandbox-xxxxxxx'...

i deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /..../fb_function_samples_ts/functions
> tsc

✔ 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 (43.74 KB) for uploading
✔ functions: functions folder uploaded successfully
i functions: updating Node.js 8 function helloWorld(us-central1)...
i functions: updating Node.js 8 function echo(us-central1)...
✔ functions[helloWorld(us-central1)]: Successful update operation.
✔ functions[echo(us-central1)]: Successful update operation.

✔ Deploy complete!

Please note that it can take up to 30 seconds for your updated functions to propagate.
Project Console: https://console.firebase.google.com/project/sandbox-xxxxxxx/overview

さきほどのbabelではpredeployにbabelの処理を手動で追加しましたが、TypeScriptで作成したプロジェクトにはあらかじめ tsc(TypeScriptのトランスパイラ?)が事前に動くように設定されていますね。

(っていうかさっきbabelの処理を入れる際に、TypeScriptのプロジェクトを参考にしたので当たり前なのですが :-))

ちなみにトランスパイルするときのチェック処理などは、functions/tsconfig.json の定義に則って行われるみたいなので、必要に応じて設定ファイルを見直してください。

参考:

http://js.studio-kingdom.com/typescript/project_configuration/tsconfig_json

さて、curlでやってみて、おなじ結果が得られることを確認しましょう。

$ curl https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/helloWorld

Hello from Firebase!

$ cat request.json
{
"id": "001",
"name": "こんにちは",
"isDone": true
}

$ curl -X POST -H "Content-Type:application/json" \
--data-binary @request.json \
https://us-central1-sandbox-xxxxxxx.cloudfunctions.net/echo | jq
{
"id": "001",
"name": "こんにちは",
"isDone": true
}
$

なんだか遠回りした気がしますが、Vue.jsでつかってるJavaScriptとおなじ文法で、Functionsを記述することができるようになりました!

おつかれさまでした。