Cloud Firestore ウェブ コードラボ

1。概要

目標

このコードラボでは、 Cloud Firestoreを利用したレストラン レコメンデーション ウェブ アプリを構築します。

img5.png

学べること

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

必要なもの

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

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

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

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

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

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

これから構築するアプリケーションは、ウェブ上で利用可能ないくつかの Firebase サービスを使用します。

  • ユーザーを簡単に識別するためのFirebase Authentication
  • Cloud Firestore は構造化データをクラウド上に保存し、データが更新されたときに即座に通知を受け取ります
  • 静的アセットをホストして提供するFirebase Hosting

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

匿名認証を有効にする

このコードラボの焦点は認証ではありませんが、アプリに何らかの形式の認証を導入することが重要です。匿名ログインを使用します。つまり、ユーザーはプロンプトが表示されずにサイレントサインインされます。

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

  1. Firebase コンソールの左側のナビゲーションで[ビルド]セクションを見つけます。
  2. [認証]をクリックし、 [サインイン方法]タブをクリックします (または、ここをクリックして直接そこに移動します)。
  3. 匿名サインイン プロバイダーを有効にして、 [保存]をクリックします。

img7.png

これにより、アプリケーションはユーザーが Web アプリにアクセスするときにサイレント サインインできるようになります。詳細については、匿名認証のドキュメントを参照してください。

Cloud Firestoreを有効にする

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

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

Cloud Firestore のデータへのアクセスは、セキュリティ ルールによって制御されます。ルールについてはこのコードラボの後半で詳しく説明しますが、始めるにはまず、データにいくつかの基本的なルールを設定する必要があります。 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;
    }
  }
}

上記のルールにより、サインインしているユーザーへのデータ アクセスが制限され、認証されていないユーザーは読み取りまたは書き込みができなくなります。これはパブリック アクセスを許可するよりは優れていますが、安全とは程遠いため、コードラボの後半でこれらのルールを改善する予定です。

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ディレクトリを開くかインポートします。このディレクトリには、まだ機能していないレストラン推奨アプリで構成されるコードラボの開始コードが含まれています。このコードラボ全体で機能するようにするので、すぐにそのディレクトリ内のコードを編集する必要があります。

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

Firebase コマンド ライン インターフェイス (CLI) を使用すると、Web アプリをローカルで提供し、Web アプリを 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 用のアプリの設定を取得するようにウェブアプリ テンプレートをセットアップしました。ただし、これを行うには、アプリを Firebase プロジェクトに関連付ける必要があります。

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

エイリアスは、複数の環境 (本番環境、ステージング環境など) がある場合に便利です。ただし、このコードラボでは、 defaultのエイリアスを使用することにします。

  1. コマンドラインの残りの指示に従います。

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

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

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

アプリをローカルで提供するためにFirebase Hostingエミュレーターを使用しています。 Web アプリはhttp://localhost:5000から利用できるようになります。

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

Firebase プロジェクトに接続された FriendlyEats のコピーが表示されるはずです。

アプリは 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. ブラウザで FriendlyEats アプリに戻り、更新します。
  2. 「モックデータの追加」をクリックします。

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

ただし、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. 関数全体を次のコードに置き換えます。

FriendlyEats.Data.js

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

  this.getDocumentsInQuery(query, renderer);
};

上記のコードでは、 restaurantsという名前のトップレベルのコレクションから最大 50 件のレストランを平均評価順に取得するクエリを構築します (現在はすべて 0)。このクエリを宣言した後、データのロードとレンダリングを担当する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.type removedと等しくなります。したがって、この場合は、UI からレストランを削除する関数を呼び出します。

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

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

img5.png

8. Get() データ

これまで、 onSnapshot使用してリアルタイムで更新を取得する方法を説明してきました。ただし、それが常に私たちが望むものであるとは限りません。データを 1 回だけフェッチする方が合理的な場合もあります。

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

  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

現時点では、後ほどコードラボで評価の追加を実装する必要があるため、評価を追加することはできません。

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

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

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

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

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

私たちのアプリでは、ユーザーは複数のフィルターを連鎖させて、「サンフランシスコのピザ」や「人気順に注文したロサンゼルスのシーフード」などの特定のクエリを作成できます。

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

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

FriendlyEats.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 を大規模に高速に保つことができます。

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

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. トランザクションでのデータの書き込み

このセクションでは、ユーザーがレストランにレビューを送信する機能を追加します。これまでの書き込みはすべてアトミックで比較的単純でした。いずれかにエラーが発生した場合は、ユーザーに再試行するよう促すだけか、アプリが自動的に書き込みを再試行する可能性があります。

このアプリにはレストランの評価を追加したいユーザーが多数いるため、複数の読み取りと書き込みを調整する必要があります。まずレビュー自体を送信し、次にレストランの評価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の数値を更新するトランザクションをトリガーします。同時に、新しいrating ratingsサブコレクションに追加します。

12. データを保護する

このコードラボの開始時に、データベースを完全に読み取りまたは書き込みできるようにアプリのセキュリティ ルールを設定しました。実際のアプリケーションでは、望ましくないデータ アクセスや変更を防ぐために、より詳細なルールを設定する必要があります。

  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. プロンプトに従って表示名を入力し、プラットフォームの種類として[Web サイト]を選択します。
  4. デプロイした URL をドメイン リストに追加し、[チェックボックス チャレンジを使用する] オプションが選択されていないことを確認します。
  5. [キーの作成]をクリックし、生成されたキーを安全な場所に保存します。このステップの後半で必要になります。

アプリチェックを有効にする

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

アプリチェックが強制されるようになりました。アプリを更新して、レストランを作成/表示してみてください。次のエラー メッセージが表示されるはずです。

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

これは、App Check がデフォルトで未検証のリクエストをブロックしていることを意味します。次に、アプリに検証を追加しましょう。

FriendlyEats.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;
}

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

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 のアプリ ビューに移動します。

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

次に、 [デバッグ トークンの追加]をクリックし、プロンプトに従ってコンソールからデバッグ トークンを貼り付けます。

おめでとう!これで、アプリ内で App Check が機能するはずです。