【Firebase】Cloud Functionsで学ぶPromiseとasync/await
はじめに
Cloud Functions for Firebase の学習(動画シリーズ)が、Cloud Functions入門としてだけでなく、Promiseやasync/await構文の解説としても、とてもわかりやすかったので、メモとして残すことにしました。
セットアップ
Firebaseコンソールからプロジェクトを事前に作成
- https://console.firebase.google.com/ にアクセスして、「プロジェクトの追加」をクリックして、プロジェクトを作成
- 今回の記事で使用するプロジェクト名は、
cloud_functions_sample
にします
CLIからの操作
- Node.js v6以上
- npm(v5.6.0以上)
mkdir cloud_functions_sample cd cloud_functions_sample npm i -g firebase-tools # Googleアカウントでの認証が要求されるので、許可しましょう firebase login # プロジェクトの選択やランタイム # 途中、どの機能を使用するか聞かれるので、Cloud Functionsのみを矢印キーとスペースキーで選択して、Enter firebase init
Cloud Functions
のランタイムは、今回は、TypeScript
を使用します。
理由としては、async/await
構文を使用したいからですが、TypeScript以外のランタイムは、Node.jsのみで、そのバージョンが、 v6.11.5 のため、async/await
が使用できません。
コマンド処理が完了すると、プロジェクトのルートディレクトリ以下に、functions
というディレクトリができているので、functions
ディレクトリ内にあるindex.ts
デプロイ
- 実行したい関数ができたとき、
Cloud Functions
のみデプロイしたい場合は、以下コマンドを実行
firebase deploy --only functions
今回は、TypeScriptを使用しているため、通常なら素のJavaScriptにコンパイルする必要がありますが、Firebaseは、デプロイ時にそのへんもよしなにやってくれます。やったぜ😉
主な特徴
- イベントトリガーで、バックグラウンドに実行
- HTTPによるトリガー
- レスポンスを返す
- バックグラウンドによるトリガー
- Promiseを返す
- HTTPによるトリガー
- 対応言語は、JavaScript, TypeScript, Node.js
- TypeScriptとVS Codeにすると、FirebaseのAPIのレスポンスの型が補完で出てくるので、便利
- レスポンスの型は基本的にPromise
- HTTPSでのみ動作可能
イベントトリガーの種類
HTTPトリガー
functions.https.onRequest
の部分
例:とあるエンドポイントURLにアクセスした時に実行
import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' export const getBostonWeather = functions.https.onRequest((req, res) => { // something action })
バックグラウンドトリガー
- データベース(
Firestore
,Realtime Database
)のいずれかのCRUD処理をトリガーに自動実行 - 他にも、
Cloud Storage
へのアップロードをトリガーに実行できるなど、イベントトリガーの種類はいくつかある- 詳細は、こちら
例:Firestoreの更新時に実行
export const onBostonWeatherUpdate = functions.firestore.document("cities-weather/boston-ma-us").onUpdate(change => { // something action })
Cloud FunctionsにおけるPromiseによる逐次処理と並列処理
ユースケース
- ボストンだけでなく、他の地域の天気を取得したり、更新したいとき
- Firestoreのデータ構造
- areas - greater-boston - cities - bostom-ma-us: true - cambridge-ma-us: true - somerville-ma-us: true
- それぞれの地域のデータ取得・更新は、並列処理したい
- HTTPトリガーなので、並列処理で全ての処理を終えてから1つのレスポンスを返すようにしたい
- 並列処理を実現するためには、
Promise.all
を使う
export const getBostonAreaWeather = functions.https.onRequest((request, response) => { admin.firestore().doc("area/greater-boston").get() .then(areaSnapshot => { const cities = areaSnapshot.data().cities const promises = [] for (const city in cities) { const p = admin.firestore().doc(`cities-weather/${city}`).get() promises.push(p) } return Promise.all(promises) }) .then(citySnapshots => { const results = [] citySnapshots.forEach(citySnap => { const data = citySnap.data() data.city = citySnap.id results.push(data) }) response.send(results) }) .catch(error => { console.log(error) response.status(500).send(error) }) })
結果
[ { "city": "somerville-ma-us", "conditions": "partly-cloudy", "temp": 8 }, { "city": "boston-ma-us", "conditions": "sunny", "temp": 10 }, { "city": "cambridge-ma-us", "conditions": "sunny", "temp": 9 } ]
ここまでが、Promiseを使った非同期処理となる
これを後述するasync/await
を使った処理だと、より同期的な見た目で非同期処理を書くことができる
イメージ
const p1 = doSomeWork(1) const p2 = doSomeWork(2) const p3 = doSomeWork(3) const pMany = [p1, p2, p3] const finalPromise = Promise.all(pMany) finalPromise.then(results => { results.forEach(result => {...}) }) .catch(error => {...})
async/await
によるリファクタリング
例
async function myFunction(): Promise<any> { try { // const rankPromise = getRank() const rank = await getRank() return "firebase" } catch (err) { return "Error: " + err } } functiongetRank() { return Promise.resolve(1) }
async/await
の特徴
- 常にPromiseを返す
async
を付けた関数は、Promiseを返す処理でなかったとしても、元々の返り値をPromise型にラップして、返すawait
を付けた関数は、必ずPromiseが完了(resolve)した値を返すtry~catch
構文を使用して、途中で失敗したawait
関数のエラーを補足できるawait
キーワードは、async
関数内でしか、使用できない
async/await
で、Cloud Functions内のコードを書き換える
先程、ボストンの天気を取得する関数を作成したので、それを書き換えます。 今の実装は以下の通り。
import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' admin.initializeApp() export const getBostonWeather = functions.https.onRequest((request, response) => { admin.firestore().doc("cities-weather/boston-ma-us").get() .then(snapshot => { const data = snapshot.data() response.send(data) }) .catch(error => { console.log(error) response.status(500).send(error) }) })
これをasync/await
構文をを使用して、以下のように変更
import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' admin.initializeApp() export const getBostonWeather = + functions.https.onRequest(async (request, response) => { + try { + const snapshot = await admin.firestore().doc("cities-weather/boston-ma-us").get() + const data = snapshot + response.send(data) + } + catch (error) { + console.log(error) + response.status(500).send(error) + } - admin.firestore().doc("cities-weather/boston-ma-us").get() - .then(snapshot => { - const data = snapshot.data() - response.send(data) - }) - .catch(error => { - console.log(error) - response.status(500).send(error) - }) })
またボストンだけでなく、他の地域も並列で取得し、全地域の天気情報が取得できたら、何かしらのレスポンスを返す関数も作成していました。
現状の関数は、以下のとおりです。
複数のthen
とPromise.all
を使って、それぞれの地域の天気情報を並列で取得し、更新していました。
export const getBostonAreaWeather = functions.https.onRequest((request, response) => { admin.firestore().doc("area/greater-boston").get() .then(areaSnapshot => { const cities = areaSnapshot.data().cities const promises = [] for (const city in cities) { const p = admin.firestore().doc(`cities-weather/${city}`).get() promises.push(p) } return Promise.all(promises) }) .then(citySnapshots => { const results = [] citySnapshots.forEach(citySnap => { const data = citySnap.data() data.city = citySnap.id results.push(data) }) response.send(results) }) .catch(error => { console.log(error) response.status(500).send(error) }) })
async/await
に書き換えたのが以下のとおりです。
export const getBostonAreaWeather = + functions.https.onRequest(async (request, response) => { + try { + admin.firestore().doc("area/greater-boston").get() + const cities = areaSnapshot.data().cities + const promises = [] + cities.forEach(city => { + const p = admin.firestore().doc(`cities-weather/${city}`).get() + promises.push(p) + }) + const snapshots = await Promise.all(promises) + + const results = [] + citySnapshots.forEach(citySnap => { + const data = citySnap.data() + data.city = citySnap.id + results.push(data) + }) + response.send(results) + } + catch (error) { + console.log(error) + response.status(500).send(error) + } - admin.firestore().doc("area/greater-boston").get() - .then(areaSnapshot => { - const cities = areaSnapshot.data().cities - const promises = [] - for (const city in cities) { - const p = admin.firestore().doc(`cities-weather/${city}`).get() - promises.push(p) - } - return Promise.all(promises) - }) - .then(citySnapshots => { - const results = [] - citySnapshots.forEach(citySnap => { - const data = citySnap.data() - data.city = citySnap.id - results.push(data) - }) - response.send(results) - }) - .catch(error => { - console.log(error) - response.status(500).send(error) - }) - })
const snapshots = await Promise.all(promises)
に変更することによぅて、Promise.all
として完了した値を担保してくれます。
async/await
を使うことで、then
式が不要になり、同期的なシンタックスが実現でき、可読性が上がります。
一応、Qiita記事の方にも書きました。