Cloud Firestore Web Codelab

1。概要

目標

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

img5.png

学習内容

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

必要なもの

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

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

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

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

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

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

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

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

この特定の Codelab では、すでに Firebase Hosting を構成しています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスの構成と有効化について説明します。

匿名認証を有効にする

認証はこの Codelab の焦点では​​ありませんが、アプリでなんらかの形式の認証を行うことが重要です。匿名ログインを使用します。つまり、ユーザーはプロンプトが表示されずにサイレントにサインインします。

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

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

img7.png

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

Cloud Firestore を有効にする

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

Cloud Firestore を有効にする必要があります。 Firebase コンソールの[ビルド]セクションで、 [Firestore データベース]をクリックします。 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

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

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

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 プロジェクトにエイリアスを付与します。

エイリアスは、複数の環境 (本番、ステージングなど) がある場合に役立ちます。ただし、この Codelab では、 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関数を呼び出します。ただし、データの取得を実装する必要があるため (Codelab の次のセクション) 、実際のウェブアプリにはまだデータが表示されません

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

img6.png

これで、Web アプリから 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 軒のレストランを取得するクエリを作成します。これらのレストランは平均評価 (現在はすべてゼロ) で並べられています。このクエリを宣言した後、データのロードとレンダリングを担当する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 、クエリの結果が変更されるたびにコールバックをトリガーします。

  • 1 回目は、クエリの結果セット全体、つまり 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

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

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

エラー メッセージからリンクを開くと、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. トランザクションにデータを書き込む

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

このアプリには、レストランの評価を追加したいユーザーが多数いるため、複数の読み取りと書き込みを調整する必要があります。最初にレビュー自体を送信する必要があり、次にレストランの評価countaverage ratingを更新する必要があります。これらのいずれかが失敗し、他の部分が失敗しない場合、データベースの一部のデータが別の部分のデータと一致しないという矛盾した状態になります。

幸いなことに、Cloud Firestore は、1 回のアトミック オペレーションで複数の読み取りと書き込みを実行できるトランザクション機能を提供し、データの一貫性を維持します。

  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. データを保護する

この 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 の詳細については、次のリソースにアクセスしてください。