Edited at

【Firebase】Cloud Functionsで学ぶPromiseとasync/await

More than 1 year has passed since last update.


はじめに

Cloud Functions for Firebase の学習(動画シリーズ)が、Cloud Functions入門としてだけでなく、

Promiseasync/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を返す



  • 対応言語は、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
}
}

function getRank() {
return Promise.resolve(1)
}


async/awaitの特徴


  1. 常にPromiseを返す


  2. asyncを付けた関数は、Promiseを返す処理でなかったとしても、元々の返り値をPromise型にラップして、返す


  3. awaitを付けた関数は、必ずPromiseが完了(resolve)した値を返す


  4. try~catch構文を使用して、途中で失敗したawait関数のエラーを補足できる


  5. 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)
- })
})

またボストンだけでなく、他の地域も並列で取得し、全地域の天気情報が取得できたら、何かしらのレスポンスを返す関数も作成していました。

現状の関数は、以下のとおりです。

複数のthenPromise.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式が不要になり、同期的なシンタックスが実現でき、可読性が上がります。