Firebaseの小ネタ集

Firebase で開発する上で便利なテクニックをいくつか紹介します。

私は Firebase を Web でしか使っていないので，Android or iOS で使っている人に有用な情報はほとんどありません。予めご了承下さい。

## Firebase JavaScript SDK の初期化用 API key をハードコーディングしない方法

Firebase JavaScript SDKを初期化するときに以下のようなコードを書くと思います。

```js
// https://firebase.google.com/docs/web/setup?hl=ja#add_firebase_to_your_app
var config = {
  apiKey: "<API_KEY>",
  authDomain: "<PROJECT_ID>.firebaseapp.com",
  databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
  storageBucket: "<BUCKET>.appspot.com",
  messagingSenderId: "<SENDER_ID>",
};
firebase.initializeApp(config);
```

開発用と本番用で2つのFirebaseプロジェクトを使っている場合などで，`API_KEY` などをハードコーディングしたくない場合があります。このような場合にはwebpackの機能を使って `API_KEY` などを環境変数から流し込む方法もありますが，Firebase Hostingを使っている場合には，もっと簡単な方法があります。それは `/__/firebase/init.json` に設置されているJSONを使うことです。

このJSONはFirebase Hostingの本番環境と `firebase serve` で提供されています。たとえば
 [FirebaseUI Demo](https://fir-ui-demo-84a6c.firebaseapp.com/) のページの場合だと https://fir-ui-demo-84a6c.firebaseapp.com/__/firebase/init.json にアクセスすることでSDKの初期化に必要な情報を取得することができます。例として，以下のように初期化することができます。

```js
const resp = await fetch('/__/firebase/init.json');
const config = await resp.json();
firebase.initializeApp(config);
```

注意しなければならないのは，`API_KEY` をハードコーディングしていた場合には `firebase.initializeApp(config)` までに非同期的な処理が含まれなかったのに対し，fetchで `/__/firebase/init.json` を取ってくる場合には非同期的な処理となってしまうことです。SDKの初期化が必要な処理を `firebase.initializeApp(config)` を呼び出す前に行わないような設計にする必要があります。

ところで， `API_KEY` が他の人に見えないようにするために工夫している人をたまに見かけるのですが，Firebase Hostingを使っている限りは `/__/firebase/init.json` にアクセスするだけで手軽に見えてしまうので意味が無いです。

この情報は以下のページで紹介されています。

https://firebase.google.com/docs/hosting/reserved-urls?hl=ja#sdk_auto-configuration

## Firebase JavaScript SDK で必要な機能だけインクルードする

Firebase JavaScript SDKは非常にサイズが大きいので，全機能をインクルードしてwebpackやbrowserifyを使うと生成後のJavaScriptが1MBを超えてしまうこともあります。以下のように書くと，必要な機能だけインクルードすることができ，生成後のJavaScriptのサイズを小さくすることができるので活用しましょう。

```js
import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
```

この情報は以下のページで紹介されています。

https://firebase.google.com/docs/web/setup?hl=ja#add_firebase_to_your_app

## Cloud Functions から Cloud Storage にバケット名を省略して接続する

Cloud FunctionsからCloud Storageを使う場合には `gcs.bucket(bucketName)` のようにバケット名を指定しなければならないと思ってしまいがちですが（[サンプル](https://github.com/firebase/functions-samples) がそうなっているので），Cloud Functionsから使う場合にはAdmin SDKを初期化した時点でプロジェクトに紐付けられているバケットに簡単にアクセスできます。

```js
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
admin.initializeApp(functions.config().firebase);
const bucket = admin.storage().bucket();
```

Firebase Admin SDKのリファレンスをよく読むと，バケット名がoptionalになっています。

https://firebase.google.com/docs/reference/admin/node/admin.storage.Storage?hl=ja#bucket

## （非推奨）Cloud FunctionsからCloud Storage上のファイルの公開用URLをgetSignedUrlを使わずに取得する

Firebase のクライアント用の SDK では，Cloud Storage 上のパスを与えることで `getDownloadURL()` によって簡単に公開用URLを取得することができます。

```js
// https://firebase.google.com/docs/storage/web/download-files?hl=ja#download_data_via_url
const url = await storageRef.child('images/stars.jpg').getDownloadURL()
document.querySelector('#myimg').src = url;
```

しかしながら，Cloud Functions用のAdmin SDKではクライアント用SDKほど簡単にURLを取得することができません。[File#getSignedUrl](https://cloud.google.com/nodejs/docs/reference/storage/1.6.x/File#getSignedUrl) というメソッドを呼ぶ必要があって，[サービスアカウントの設定](https://github.com/firebase/functions-samples/blob/3d779a6c26727e0f78c762c917a20ba92490da42/generate-thumbnail/functions/index.js#L20) と [公開用URLの有効期限の設定](https://github.com/firebase/functions-samples/blob/3d779a6c26727e0f78c762c917a20ba92490da42/generate-thumbnail/functions/index.js#L93) が必要です（チュートリアルとしては [公式のFirecasts](https://www.youtube.com/watch?v=3lzEgwiSs-M) がおすすめです）。

これはクライアント用SDKと比べてかなり面倒だし，サービスアカウントの管理も避けたいところですが，裏技として次のような方法があります。Cloud Storage for Firebase上のpublicなファイルのURLは以下のコードで取得することができます。

```js
`https://firebasestorage.googleapis.com/v0/b/${bucketName}/o/${encodeURIComponent(filePath)}?alt=media`
```

この裏技はドキュメントに載っていないのであまりおすすめできませんが，便利なのは確かです。この裏技はStackOverflowの以下の投稿で知りました。

* [node\.js \- Get Download URL from file uploaded with Cloud Functions for Firebase \- Stack Overflow](https://stackoverflow.com/questions/42956250/get-download-url-from-file-uploaded-with-cloud-functions-for-firebase)
* [google cloud storage \- Get Public URL from file uploaded with firebase\-admin \- Stack Overflow](https://stackoverflow.com/questions/47072883/get-public-url-from-file-uploaded-with-firebase-admin)


## Cloud Storage にアップロードするときはメタデータを適切に設定しよう

Cloud Storageにアップロードしたファイルは，何もしないと 'Cache-Control' と 'Content-Type' が設定されていません。そのため，画像ファイルが全くキャッシュされずにスマホの通信料が増えてしまったり，画像ファイルのURLをクリックしたら 'Content-Type' がデフォルトの 'application/octet-stream' になっていてブラウザで表示されずにダウンロードされてしまったりします。

ファイルのメタデータは，クライアントから [ref.put(file)](https://firebase.google.com/docs/reference/js/firebase.storage.Reference?hl=ja#put) するときに設定できたり，Cloud Functionsから [bucket.upload](https://cloud.google.com/nodejs/docs/reference/storage/1.6.x/Bucket#upload) するときに設定できたりします。

設定可能・取得可能なメタデータの一覧は以下のページで確認できます。

* https://firebase.google.com/docs/storage/web/file-metadata?hl=ja
* https://firebase.google.com/docs/reference/js/firebase.storage.UploadMetadata?hl=ja

Cache-Control の適切な設定は，少し時間を確保してしっかり学習することをおすすめします。ISUCON7予選の解説記事なども参考になります。

* [HTTP キャッシュ | Web | Google Developers](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=ja)

## Cloud Storage でファイルの変更・削除を禁止する

セキュリティルールを記述するときに，Firestore では `allow XXX` の部分に read, get, list, write, create, update, delete を指定することができる ([Cloud Firestore Security Rules Reference](https://firebase.google.com/docs/firestore/reference/security/#request_methods)) のに対し，Cloud Storage では read と write というざっくりとした指定しか用意されていません ([Firebase Security Rules for Cloud Storage Reference](
Firebase Security Rules for Cloud Storage Reference))。

Cloud Storageのセキュリティルールでdeleteを禁止するには以下のように指定すれば良いようです。

```
allow write: if resource == null
```

https://stackoverflow.com/questions/38995011/firebase-3-storage-dont-allow-file-updating-overwriting

## Cloud Functions で multipart/form-data のリクエストを扱う

Cloud Functions の HTTP トリガーでは[リクエストはExpressのミドルウェア (body-parser) で処理されます](https://firebase.google.com/docs/functions/http-events#read_values_from_the_request)が，`multipart/form-data` だけは処理してくれません。どうしても `multipart/form-data` を扱いたい場合には [busboy](https://www.npmjs.com/package/busboy) というモジュールを使う方法があります。

やり方は [Google Cloud Platform のほうの Cloud Functions のドキュメント](https://cloud.google.com/functions/docs/writing/http#handling_multipart_form_uploads) に載っています。ほとんど Firebase のドキュメントと同じなのに，なぜかこの内容だけ Google Cloud Platform のほうのドキュメントにしか載っていないという罠にはまりました。

私の実験では数MB以上のファイルを Cloud Functions の HTTP トリガーで処理すると非常に不安定な状況になりました。ドキュメントにもあるように，このような場合にはCloud Storage経由でデータをアップロードするほうが適切なようです。