Firebase Hosting + Cloud Run で、Cloud Run が実行されない

Firebase Hosting で、特定のURLを Cloud Run で実行させるように rewrites の設定をした。
一回目は期待通りに Cloud Run が実行されるが、二回目以降は Cloud Run が実行されない(Response は、一回目と同じになる)。

調べてみると、Firebase Hosting の手前のCDNがキャッシュして応答を返すみたいです。
ここ参照。

Cloud Run 側のプログラムでも対応出来るようですが、Response に毎回設定するのも面倒なので、
Hosting の設定(firebase.json)に↓のように記載しました。

    {
      ・・・
      "rewrites": [
        {
          "source": "/**",
          "run": {
            "region": "xxxxx",
            "serviceId": "xxxxx"
          }
        }
      ],
      "headers": [{
        "source": "/**",
        "headers": [{
          "key": "Cache-Control",
          "value": "no-store"
        }]
      }]
    }

Firebase Functions ではキャッシュされないので少しハマりました。

Firestore でデータ削除

Webクライアント側でFirestoreのドキュメントを削除するときに、ドキュメントに紐づくコレクションが存在すると、コレクションが残る。
FirebaseのWebコンソールから確認すると削除したドキュメントは斜体になった状態で、下層のコレクションは普通に存在する。

Webクライアント側で丁寧に紐づくコレクションを削除するのも手ですが、データが多い場合などで完全に削除して良ければ、Firebase の Functions で削除可能です。
Functions で削除すると下層のコレクションもまるっと削除できます。

Functions は WebAPI として作るか、あるいは Firestore を拡張すると良いと思います。
私は後者の方が好みです。こんな感じでしょうか?

import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import * as tools from "firebase-tools";

const deleteXxxx = async (
  snap: FirebaseFirestore.DocumentSnapshot,
  context: functions.EventContext
) => {
  await tools.firestore.delete(snap.ref.path, {
    project: process.env.GCLOUD_PROJECT,
    recursive: true,
    yes: true
  });

  return "OK";
};

export const firestoreDeleteXxxx = functions.firestore
  .document("xxxx/{deleteId}")
  .onDelete((snap, context) => {
    return deleteXxxx(snap, context);
  });

Monaca + Vue.js + Firebase で匿名認証

スマホアプリ開発でiOS、Androidの両方を行ってみて思うのが、やっぱり2つ開発はシンドイと思います。
で、考えるのはハイブリッド開発でしょうか?
Web、Xamarin、React nativeなどいくつか選択肢がありますが、まずはMonacaというHTML5ベースのプラットフォームを試します。

今回は試しにFirebaseと連携し匿名認証を行います。
ユーザーをこちらから特定する必要はないですが、ユーザーの属性情報だけ欲しいというような使い方です。
試して思ったのですが、FirebaseではSNSアカウント、メールアドレスなどで認証するのも簡単そうです。

Monacaの開発環境はクラウド上ではなくローカルで行いました。
MonacaでVue.jsのテンプレートから作成しています。
また、Firebaseで匿名認証を許可する設定まで完了してる前提でプログラムのみ紹介。

src/public/index.html.ejs

まずはFirebaseの設定です。最初 www/index.html に書いたのですが、何かのタイミングでファイルが戻ってしまう?現象があったので、index.html.ejs に書きました。

<body>
  <div id="app"></div>
  <script src="https://www.gstatic.com/firebasejs/5.5.5/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: "{Firebaseから}",
      authDomain: "{Firebaseから}",
      databaseURL: "{Firebaseから}",
      projectId: "{Firebaseから}",
      storageBucket: "{Firebaseから}",
      messagingSenderId: "{Firebaseから}"
    };
    firebase.initializeApp(config);
  </script>
</body>

ここの設定は Firebase console の “ウェブアプリに Firebase を追加” から取得できます。

src/Home.vue

Monacaのテンプレートに入っているHome.vueを変更しています。
イメージは起動時に匿名認証を行っているか判定し、行っていなければダイアログでユーザー属性を入力させ、Firebaseで匿名認証させます。
認証時の属性情報は Firebase の Cloud Firestore に保存します。
あとで気づいたのですが Cloud Firestore は現在ベータ版で、Webで”Firebase database”で検索すると Realtime Database の情報も出てくるので混乱しました。

<template id="main">
  <v-ons-page>
    <div style="display: table; height: 100%; width: 100%;">
      <div style="display: table-cell;text-align: center;vertical-align: middle;">
        <p>
          Hi!
        </p>
        <p>
          <v-ons-button modifier="quiet" style="background-color: transparent;" @click="signOut">
            登録解除
          </v-ons-button>
        </p>
      </div>
    </div>

    <v-ons-modal style="background-color: #FFFFFF;" :visible="showSignInModal">
      <div style="text-align: center; color: #333333;">
        <p>
          あなたのことを教えてください。
        </p>
        <p>
          <v-ons-select class="signInSelect" v-model="gender">
            <option disabled="disabled" value="">性別</option>
            <option v-for="(item, index) in genders" :key="index" :value="item.value">
              {{ item.text }}
            </option>
          </v-ons-select>
        </p>
        <p>
          <v-ons-select class="signInSelect" v-model="generation">
            <option disabled="disabled" value="">年齢</option>
            <option v-for="(item, index) in generations" :key="index" :value="item.value">
              {{ item.text }}
            </option>
          </v-ons-select>
        </p>
        <v-ons-button modifier="outline" class="signInBtn" @click="signIn">次へ</v-ons-button>
      </div>
    </v-ons-modal>

    <v-ons-modal :visible="showLoading">
      <p style="text-align: center">
        Loading <v-ons-icon icon="fa-spinner" spin></v-ons-icon>
      </p>
    </v-ons-modal>
  </v-ons-page>
</template>

<script>
export default {
  data() {
    return {
      showLoading: false,
      showSignInModal: false,
      gender: "",
      genders: [
        { text: "男性", value: "MALE" },
        { text: "女性", value: "FEMALE" },
        { text: "その他", value: "OTHER" }
      ],
      generation: "",
      generations: [
        { text: "10歳未満", value: "_10" },
        { text: "10-19歳", value: "10_19" },
        { text: "20-29歳", value: "20_29" },
        { text: "30-39歳", value: "30_39" },
        { text: "40-49歳", value: "40_49" },
        { text: "50-59歳", value: "50_59" },
        { text: "60-69歳", value: "60_69" },
        { text: "70-79歳", value: "70_79" },
        { text: "80歳以上", value: "80_" }
      ]
    };
  },
  methods: {
    signIn(event) {
      this.showLoading = true;
      // 匿名ユーザサインイン
      firebase
        .auth()
        .signInAnonymously()
        .catch(function(error) {
          alert(error.message);
        });
    },
    signOut(event) {
      firebase.auth().signOut();
    }
  created() {
    var self = this;
    firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        const firestore = firebase.firestore();
        firestore.settings({ timestampsInSnapshots: true });
        firestore
          .collection("users")
          .doc(user.uid)
          .set({
            gender: self.gender,
            generation: self.generation
          })
          .then(function() {
            self.showLoading = false;
            self.showSignInModal = false;
          })
          .catch(function(error) {
            console.error("Error writing document: ", error);
            alert("登録に失敗しました");
          });
      } else {
        self.showSignInModal = true;
      }
    });
  }
};
</script>

こんな感じでユーザー情報がFirebase の Authentication と Database(Cloud Firestore) に登録されます。

user.uid をキーにアプリデータをFirebaseのDatabaseに保存すれば、アンケートみたいなものは簡単に作れそうです。
もちろん要件によってはiOS、Androidを別々に開発すべきですが、要件を満たせるならハイブリッドで開発するのは十分にアリだと思います。
ただ、バックグラウンド処理やBLE周りとかどうなんでしょう?