Cloud Firestore Web Codelab

1. 概要

目標

この Codelab では、Cloud Firestore を利用しておすすめレストラン ウェブアプリを構築します。

img5.png

ラボの内容

  • ウェブアプリから Cloud Firestore へのデータの読み取りと書き込み
  • Cloud Firestore データの変更をリアルタイムでリッスンする
  • Firebase Authentication とセキュリティ ルールを使用して Cloud Firestore データを保護する
  • 複雑な Cloud Firestore クエリを作成する

必要なもの

この Codelab を開始する前に、以下がインストールされていることを確認してください。

  • npm(通常は Node.js に付属しています)- Node 16 以降をおすすめします
  • 任意の IDE/テキスト エディタ(WebStormVS CodeSublime など)

2. Firebase プロジェクトを作成して設定する

Firebase プロジェクトを作成する

  1. Firebase コンソールで [プロジェクトを追加] をクリックし、Firebase プロジェクトに FriendlyEats という名前を付けます。

Firebase プロジェクトのプロジェクト ID を覚えておいてください。

  1. [プロジェクトの作成] をクリックします。

これから構築するアプリでは、ウェブで利用できる以下の Firebase サービスを使用します。

  • ユーザーを簡単に識別できる Firebase Authentication
  • Cloud Firestore: 構造化データを Cloud に保存し、データが更新されるとすぐに通知を受け取る
  • Firebase Hosting: 静的アセットをホストして提供します。

このコードラボでは、Firebase Hosting はすでに構成されています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスの構成と有効化の手順を説明します。

匿名認証を有効にする

認証はこの Codelab の主要なトピックではありませんが、アプリではなんらかの形の認証を行うことが重要です。ここでは、匿名ログイン(プロンプトが表示されず、ユーザーが自動的にログインする)を使用します。

匿名ログインを有効にする必要があります。

  1. Firebase コンソールの左側のナビゲーションで、[ビルド] セクションを見つけます。
  2. [Authentication] をクリックしてから、[Sign-in method] タブをクリックします(直接移動する場合は、こちらをクリックします)。
  3. [匿名] ログイン プロバイダを有効にして、[保存] をクリックします。

img7.png

これにより、ユーザーがウェブアプリにアクセスしたときに、アプリケーションが通知なくログインできるようになります。詳しくは、匿名認証に関するドキュメントをご覧ください。

Cloud Firestore の有効化

このアプリは、Cloud Firestore を使用して、レストランの情報と評価を保存し、受信します。

Cloud Firestore を有効にする必要があります。Firebase コンソールの [構築] セクションで、[Firestore Database] をクリックします。[Cloud Firestore] ペインで [データベースを作成] をクリックします。

Cloud Firestore 内のデータへのアクセスは、セキュリティ ルールによって制御されます。ルールについてはこの Codelab の後半で詳しく説明しますが、その前に、データに基本的なルールを設定しておく必要があります。Firebase コンソールの [ルールタブ] で次のルールを追加し、[公開] をクリックします。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

上記のルールでは、データアクセスをログインしているユーザーに制限し、認証されていないユーザーによる読み取りや書き込みを禁止しています。これは一般公開するよりも安全ですが、まだ安全とは言えません。これらのルールは、Codelab の後半で改善します。

3. サンプルコードを取得する

コマンドラインから GitHub リポジトリのクローンを作成します。

git clone https://github.com/firebase/friendlyeats-web

サンプルコードのクローンは、📁friendlyeats-web ディレクトリに作成されているはずです。これ以降は、次のディレクトリからすべてのコマンドを実行してください。

cd friendlyeats-web/vanilla-js

スターター アプリをインポートする

IDE(WebStorm、Atom、Sublime、Visual Studio Code など)を使用して、🏏?friendlyeats-web ディレクトリを開くかインポートします。このディレクトリには、この Codelab の開始用コードが含まれています。このコードは、まだ機能していないレストランのおすすめアプリで構成されています。この Codelab 全体で機能するようにするため、このディレクトリ内のコードはすぐに編集する必要があります。

4. Firebase コマンドライン インターフェースをインストールする

Firebase コマンドライン インターフェース(CLI)を使用すると、ウェブアプリをローカルで提供し Firebase Hosting にデプロイできます。

  1. 次の npm コマンドを実行して、CLI をインストールします。
npm -g install firebase-tools
  1. 次のコマンドを実行して、CLI が正しくインストールされたことを確認します。
firebase --version

Firebase CLI のバージョンが v7.4.0 以降であることを確認します。

  1. 次のコマンドを実行して、Firebase CLI を承認します。
firebase login

アプリのローカル ディレクトリとファイルから Firebase Hosting 用のアプリの構成を pull するようにウェブアプリ テンプレートをセットアップしました。ただし、これを行うには、アプリを Firebase プロジェクトに関連付ける必要があります。

  1. コマンドラインがアプリのローカル ディレクトリにアクセスしていることを確認します。
  2. 次のコマンドを実行して、アプリを Firebase プロジェクトに関連付けます。
firebase use --add
  1. プロンプトが表示されたら、プロジェクト ID を選択して、Firebase プロジェクトにエイリアスを指定します。

エイリアスは、複数の環境(本番環境、ステージング環境など)を使用する場合に役立ちます。ただし、この Codelab では、default というエイリアスのみを使用します。

  1. コマンドラインの残りの手順に沿って操作します。

5. ローカル サーバーを実行する

アプリで実際に作業を開始する準備が整いました。アプリをローカルで実行しましょう。

  1. 次の Firebase CLI コマンドを実行します。
firebase emulators:start --only hosting
  1. コマンドラインに次のレスポンスが表示されます。
hosting: Local server: http://localhost:5000

Firebase Hosting エミュレータを使用して、アプリをローカルで配信します。これで http://localhost:5000 からウェブアプリを利用できるようになります。

  1. http://localhost:5000 でアプリを開きます。

Firebase プロジェクトに接続されている FRIENDEats のコピーが表示されます。

アプリが自動的に Firebase プロジェクトに接続され、匿名ユーザーとしてサイレント ログインされます。

img2.png

6. Cloud Firestore にデータを書き込む

このセクションでは、アプリの UI に入力されるデータを作成して、Cloud Firestore に書き込みます。これは Firebase コンソールから手動で行うことができますが、ここでは基本的な Cloud Firestore の書き込みのデモを行うためにアプリ自体で行います。

データモデル

Firestore データは、コレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。各レストランを、restaurants という最上位のコレクションにドキュメントとして保存します。

img3.png

後で各レビューを、各レストランの ratings というサブコレクションに保存します。

img4.png

Firestore にレストランを追加する

アプリ内のメインのモデル オブジェクトは、レストランです。レストランのドキュメントを restaurants コレクションに追加するコードを記述しましょう。

  1. ダウンロードしたファイルから scripts/FriendlyEats.Data.js を開きます。
  2. 関数 FriendlyEats.prototype.addRestaurant を見つけます。
  3. 関数全体を次のコードに置き換えます。

FriendlyEats.Data.js

FriendlyEats.prototype.addRestaurant = function(data) {
  var collection = firebase.firestore().collection('restaurants');
  return collection.add(data);
};

上記のコードは、restaurants コレクションに新しいドキュメントを追加します。ドキュメント データは、単純な JavaScript オブジェクトから取得されます。そのためには、まず Cloud Firestore コレクション restaurants への参照を取得し、次にデータを add します。

レストランを追加しましょう。

  1. ブラウザで FlexibleEats アプリに戻り、アプリを更新します。
  2. [Add Mock Data] をクリックします。

アプリは、レストラン オブジェクトのランダムなセットを自動的に生成し、addRestaurant 関数を呼び出します。ただし、データの「取得」を実装する必要があるため(Codelab の次のセクションで行います)、実際のウェブアプリにはまだデータは表示されません

ただし、Firebase コンソールで [Cloud Firestore] タブに移動すると、restaurants コレクションに新しいドキュメントが表示されるはずです。

img6.png

おつかれさまです。これで、ウェブアプリから Cloud Firestore にデータが書き込まれました。

次のセクションでは、Cloud Firestore からデータを取得してアプリに表示する方法を説明します。

7. Cloud Firestore からのデータを表示する

このセクションでは、Cloud Firestore からデータを取得してアプリに表示する方法について説明します。主なステップは、クエリの作成とスナップショット リスナーの追加の 2 つです。このリスナーは、クエリに一致するすべての既存のデータについて通知され、リアルタイムで更新を受信します。

まず、フィルタされていないデフォルトのレストランリストを返すクエリを作成します。

  1. scripts/FriendlyEats.Data.js ファイルに戻ります。
  2. 関数 FriendlyEats.prototype.getAllRestaurants を見つけます。
  3. 関数全体を次のコードに置き換えます。

フレンドリー Eats.Data.js

FriendlyEats.prototype.getAllRestaurants = function(renderer) {
  var query = firebase.firestore()
      .collection('restaurants')
      .orderBy('avgRating', 'desc')
      .limit(50);

  this.getDocumentsInQuery(query, renderer);
};

上記のコードでは、restaurants という名前の最上位のコレクションから最大 50 件のレストランを取得し、平均評価(現在はすべてゼロ)の順に並べ替えるクエリを作成します。このクエリを宣言したら、データの読み込みとレンダリングを行う getDocumentsInQuery() メソッドに渡します。

スナップショット リスナーを追加して実現します。

  1. scripts/FriendlyEats.Data.js ファイルに戻ります。
  2. 関数 FriendlyEats.prototype.getDocumentsInQuery を見つけます。
  3. 関数全体を次のコードに置き換えます。

FriendlyEats.Data.js

FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) {
  query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants".

    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        renderer.remove(change.doc);
      } else {
        renderer.display(change.doc);
      }
    });
  });
};

上記のコードでは、query.onSnapshot は、クエリの結果に変更があるたびにコールバックをトリガーします。

  • 初回では、クエリの結果セット全体(Cloud Firestore の restaurants コレクション全体)でコールバックがトリガーされます。次に、すべての個々のドキュメントを renderer.display 関数に渡します。
  • ドキュメントが削除されると、change.typeremoved になります。この場合、レストランを UI から削除する関数を呼び出します。

両方のメソッドを実装したので、アプリを更新し、先ほど Firebase コンソールで見たレストランがアプリに表示されることを確認します。このセクションが正常に完了していれば、アプリは Cloud Firestore を使用してデータの読み取りと書き込みを行えるようになりました。

レストランのリストが変更されると、このリスナーは自動的に更新され続けます。Firebase コンソールに移動して、手動でレストランを削除したり、名前を変更したりしてみてください。変更内容がすぐにサイトに反映されるはずです。

img5.png

8. Get() データ

ここまでは、onSnapshot を使用してリアルタイムで更新を取得する方法を学びました。しかし、いつでもそうするのが望ましいわけではありません。一度だけデータをフェッチすればよい場合もあります。

ユーザーがアプリ内の特定のレストランをクリックしたときにトリガーされるメソッドを実装します。

  1. ファイル scripts/FriendlyEats.Data.js に戻ります。
  2. 関数 FriendlyEats.prototype.getRestaurant を見つけます。
  3. 関数全体を次のコードに置き換えます。

FriendlyEats.Data.js

FriendlyEats.prototype.getRestaurant = function(id) {
  return firebase.firestore().collection('restaurants').doc(id).get();
};

このメソッドを実装すると、各レストランのページを表示できるようになります。リスト内のレストランをクリックすると、そのレストランの詳細ページが表示されます。

img1.png

現時点では、評価を追加することはできません。評価の追加は、この Codelab の後半で実装する必要があります。

9. データの並べ替えとフィルタ

現在、アプリにはレストランのリストが表示されますが、ユーザーが自分のニーズに基づいてフィルタすることはできません。このセクションでは、Cloud Firestore の高度なクエリを使用してフィルタリングを有効にします。

すべての Dim Sum レストランを取得する簡単なクエリの例を次に示します。

var filteredQuery = query.where('category', '==', 'Dim Sum')

where() メソッドは、その名が示すように、設定した制限を満たすフィールドを持つコレクションのメンバーのみをクエリしてダウンロードします。この例では、categoryDim Sum のレストランのみをダウンロードします。

このアプリでは、ユーザーが複数のフィルタを連結して特定のクエリ(「サンフランシスコのピザ屋」など)を作成できます。または「ロサンゼルスのシーフード 人気度順」など

ユーザーが選択した複数の条件に基づいてレストランをフィルタするクエリを作成するメソッドを作成します。

  1. ファイル scripts/FriendlyEats.Data.js に戻ります。
  2. 関数 FriendlyEats.prototype.getFilteredRestaurants を見つけます。
  3. 関数全体を次のコードに置き換えます。

フレンドリー Eats.Data.js

FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) {
  var query = firebase.firestore().collection('restaurants');

  if (filters.category !== 'Any') {
    query = query.where('category', '==', filters.category);
  }

  if (filters.city !== 'Any') {
    query = query.where('city', '==', filters.city);
  }

  if (filters.price !== 'Any') {
    query = query.where('price', '==', filters.price.length);
  }

  if (filters.sort === 'Rating') {
    query = query.orderBy('avgRating', 'desc');
  } else if (filters.sort === 'Reviews') {
    query = query.orderBy('numRatings', 'desc');
  }

  this.getDocumentsInQuery(query, renderer);
};

上記のコードは、複数の where フィルタと単一の orderBy 句を追加して、ユーザー入力に基づく複合クエリを作成します。これで、クエリはユーザーの要件を満たすレストランのみを返すようになりました。

ブラウザで FriendlyEats アプリを更新し、料金、都市、カテゴリでフィルタできることを確認します。テスト中、ブラウザの JavaScript コンソールに次のようなエラーが表示されます。

The query requires an index. You can create it here: https://console.firebase.google.com/project/project-id/database/firestore/indexes?create_composite=...

このようなエラーが発生するのは、Cloud Firestore はほとんどの複合クエリについてインデックスを必要とするためです。クエリでインデックスを必須にすると、Cloud Firestore は大規模なデータを高速で処理できます。

エラー メッセージのリンクを開くと、Firebase コンソールのインデックス作成 UI が自動的に開き、正しいパラメータが入力されます。次のセクションでは、このアプリに必要なインデックスを作成してデプロイします。

10. インデックスをデプロイする

アプリ内のすべてのパスを調査して個々のインデックス作成リンクをたどりたくない場合は、Firebase CLI を使用して、一度に多数のインデックスを簡単にデプロイできます。

  1. アプリのダウンロードされたローカル ディレクトリに firestore.indexes.json ファイルがあります。

このファイルには、フィルタのすべての可能な組み合わせに対して必要なすべてのインデックスが記述されています。

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. 次のコマンドを使用して、これらのインデックスをデプロイします。
firebase deploy --only firestore:indexes

数分後、インデックスが有効になり、エラー メッセージが表示されなくなります。

11. トランザクションでデータを書き込む

このセクションでは、ユーザーがレストランにレビューを送信できる機能を追加します。これまでのところ、すべての書き込みはアトミックで、比較的シンプルです。いずれか 1 つでもエラーになった場合は、ユーザーに再試行するよう促すか、アプリが書き込みを自動的に再試行します。

アプリにはレストランのレビューを書き込みたいユーザーが多数いるはずなので、複数の読み取りと書き込みを調整する必要があります。まずレビュー自体を送信し、次にレストランの評価 countaverage rating を更新する必要があります。一方が失敗して他方が成功した場合、データベースのある部分のデータが別の部分のデータと一致しない不整合状態になります。

幸いなことに、Cloud Firestore は単一のアトミック オペレーションで複数の読み取りと書き込みを実行できるトランザクション機能を備えているため、データの整合性を保証できます。

  1. scripts/FriendlyEats.Data.js ファイルに戻ります。
  2. 関数 FriendlyEats.prototype.addRating を見つけます。
  3. 関数全体を次のコードに置き換えます。

FriendlyEats.Data.js

FriendlyEats.prototype.addRating = function(restaurantID, rating) {
  var collection = firebase.firestore().collection('restaurants');
  var document = collection.doc(restaurantID);
  var newRatingDocument = document.collection('ratings').doc();

  return firebase.firestore().runTransaction(function(transaction) {
    return transaction.get(document).then(function(doc) {
      var data = doc.data();

      var newAverage =
          (data.numRatings * data.avgRating + rating.rating) /
          (data.numRatings + 1);

      transaction.update(document, {
        numRatings: data.numRatings + 1,
        avgRating: newAverage
      });
      return transaction.set(newRatingDocument, rating);
    });
  });
};

上記のブロックでは、トランザクションをトリガーして、レストラン ドキュメント内の avgRatingnumRatings の数値を更新します。同時に、新しい ratingratings サブコレクションに追加します。

12. データを保護する

この Codelab の冒頭では、データベースをあらゆる読み取りまたは書き込みに対して完全にオープンにするアプリのセキュリティ ルールを設定しました。実際のアプリケーションでは、よりきめ細かいルールを設定して、望ましくないデータアクセスや変更を防止する必要があります。

  1. Firebase コンソールの [構築] セクションで、[Firestore データベース] をクリックします
  2. Cloud Firestore セクションの [ルール] タブをクリックします(直接移動するには、こちらをクリックします)。
  3. デフォルト値を次のルールに置き換えて、[公開] をクリックします。

firestore.rules

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  function unchanged(key) {
    return (key in resource.data) 
      && (key in request.resource.data) 
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys()) 
                    && unchanged("name");
      
      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}

これらのルールによりアクセスが制限され、クライアントは安全に変更を加えることができます。例:

  • レストラン ドキュメントの更新では、評価のみが変更されます。名前およびその他の不変データは変更されません。
  • 評価は、ユーザー ID がログインしているユーザーと一致する場合にのみ作成できるため、なりすましを防ぎます。

Firebase コンソールを使用する代わりに、Firebase CLI を使用して Firebase プロジェクトにルールをデプロイすることもできます。作業ディレクトリにある firestore.rules ファイルには、上記のルールがすでに含まれています。このルールを(Firebase コンソールを使用せずに)ローカル ファイルシステムからデプロイするには、次のコマンドを実行します。

firebase deploy --only firestore:rules

13. まとめ

この Codelab では、Cloud Firestore で基本的な読み取りと高度な読み取りを実行する方法と、セキュリティ ルールを使用してデータアクセスを保護する方法について学習しました。ソリューション全体については、quickstarts-js リポジトリをご覧ください。

Cloud Firestore について詳しくは、以下のリソースをご覧ください。

14. [省略可] App Check で適用

Firebase App Check は、アプリへの不要なトラフィックを検証および防止することで保護します。このステップでは、reCAPTCHA Enterprise を使用して App Check を追加して、サービスへのアクセスを保護します。

まず、App Check と reCAPTCHA を有効にする必要があります。

reCaptcha Enterprise の有効化

  1. Cloud コンソールで、[セキュリティ] で [reCaptcha Enterprise] を見つけて選択します。
  2. プロンプトが表示されたらサービスを有効にし、[鍵を作成] をクリックします。
  3. メッセージに沿って表示名を入力し、プラットフォームの種類として [ウェブサイト] を選択します。
  4. デプロイした URL を [ドメインリスト] に追加し、[チェックボックスによる本人確認を使用する] オプションが選択されていないことを確認します。
  5. [鍵を作成] をクリックし、生成された鍵を安全な場所に保存します。これは、この手順の後半で必要になります。

App Check を有効にする

  1. Firebase コンソールの左側のパネルで、[ビルド] セクションを見つけます。
  2. [App Check] をクリックし、[使ってみる] ボタンをクリックします(または、コンソールに直接リダイレクトします)。
  3. [登録] をクリックし、プロンプトが表示されたら reCaptcha Enterprise キーを入力し、[保存] をクリックします。
  4. [API] ビューで [ストレージ] を選択し、[適用] をクリックします。Cloud Firestore についても同じことを行います。

これで App Check が適用されるようになりました。アプリを更新して、レストランを作成/表示してみます。次のエラー メッセージが表示されます。

Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

つまり、App Check はデフォルトで未検証のリクエストをブロックします。それでは、アプリに検証を追加しましょう。

friendEats.View.js ファイルに移動し、initAppCheck 関数を更新して reCaptcha キーを追加して App Check を初期化します。

FriendlyEats.prototype.initAppCheck = function() {
    var appCheck = firebase.appCheck();
    appCheck.activate(
    new firebase.appCheck.ReCaptchaEnterpriseProvider(
      /* reCAPTCHA Enterprise site key */
    ),
    true // Set to true to allow auto-refresh.
  );
};

appCheck インスタンスは、キーを含む ReCaptchaEnterpriseProvider で初期化されます。isTokenAutoRefreshEnabled を使用すると、アプリでトークンを自動更新できます。

ローカルテストを有効にするには、FriendlyEats.js ファイルでアプリが初期化されるセクションを見つけ、FriendlyEats.prototype.initAppCheck 関数に次の行を追加します。

if(isLocalhost) {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

これにより、ローカル ウェブアプリのコンソールに次のようなデバッグ トークンが記録されます。

App Check debug token: 8DBDF614-649D-4D22-B0A3-6D489412838B. You will need to add it to your app's App Check settings in the Firebase console for it to work.

Firebase コンソールで App Check の [アプリビュー] に移動します。

オーバーフロー メニューをクリックし、[デバッグ トークンを管理] を選択します。

次に、[Add debug token] をクリックし、プロンプトが表示されたらコンソールからデバッグ トークンを貼り付けます。

これで完了です。アプリで App Check が機能するようになりました。