Google は、黒人コミュニティのための人種的公平の促進に取り組んでいます。詳細をご覧ください。

Cloud Firestore Web Codelab

目標

このコードラボでは、あなたがで駆動レストラン勧告のWebアプリ構築しますクラウドFirestoreを

img5.png

あなたが学ぶこと

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

必要なもの

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

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

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

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

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

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

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

この特定のコードラボでは、FirebaseHostingを既に構成しています。ただし、FirebaseAuthとCloudFirestoreの場合は、Firebaseコンソールを使用したサービスの構成と有効化について説明します。

匿名認証を有効にする

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

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

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

img7.png

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

CloudFirestoreを有効にする

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

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

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

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

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

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

サンプルコードは、📁friendlyeats friendlyeats-webディレクトリに複製されているはずです。今後は、必ず次のディレクトリからすべてのコマンドを実行してください。

cd friendlyeats-web

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

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

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

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

FirebaseCLIのバージョンがv7.4.0以降であることを確認してください。

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

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

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

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

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

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

  1. 次のFirebaseCLIコマンドを実行します。
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

このセクションでは、アプリのUIにデータを入力できるように、CloudFirestoreにデータを書き込みます。これはFirebaseコンソールを介して手動で実行できますが、基本的なCloudFirestoreの書き込みを示すためにアプリ自体で実行します。

データ・モデル

Firestoreデータは、コレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。各レストランは、 restaurantsと呼ばれるトップレベルのコレクションにドキュメントとして保存され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コレクションに新しいドキュメントを追加しrestaurants 。ドキュメントデータは、プレーンなJavaScriptオブジェクトから取得されます。これを行うには、最初にCloud Firestoreコレクションrestaurantsへの参照を取得してから、データをaddします。

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

  1. ブラウザでFriendlyEatsアプリに戻り、更新します。
  2. [モックデータの追加]をクリックします。

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

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

img6.png

おめでとうございます。WebアプリからCloudFirestoreにデータを書き込んだところです。

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

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

  • 初めて、コールバックはクエリの結果セット全体、つまりCloudFirestoreのrestaurantsコレクション全体でトリガーされます。次に、すべての個々のドキュメントをrenderer.display関数に渡します。
  • 文書が削除されると、 change.typeに等しいremoved 。したがって、この場合、UIからレストランを削除する関数を呼び出します。

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

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

img5.png

これまで、 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

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

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

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

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

その名前が示すように、 where()メソッドは、フィールドが設定された制限を満たすコレクションのメンバーのみをクエリにダウンロードさせます。この場合、 categoryDim 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フィルターと1つのorderBy句を追加してwhereユーザー入力に基づいて複合クエリを作成します。クエリは、ユーザーの要件に一致するレストランのみを返すようになりました。

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

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

これらのエラーは、CloudFirestoreがほとんどの複合クエリにインデックスを必要とするためです。クエリにインデックスを要求することで、CloudFirestoreを大規模に高速に保つことができます。

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

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

  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

数分後、インデックスが有効になり、エラーメッセージが消えます。

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

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

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

  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コンソールを使用する代わりに、FirebaseCLIを使用してルールをFirebaseプロジェクトにデプロイできます。作業ディレクトリのfirestore.rulesファイルには、すでに上記のルールが含まれています。 (Firebaseコンソールを使用するのではなく)ローカルファイルシステムからこれらのルールをデプロイするには、次のコマンドを実行します。

firebase deploy --only firestore:rules

このコードラボでは、Cloud Firestoreを使用して基本および高度な読み取りと書き込みを実行する方法と、セキュリティルールを使用してデータアクセスを保護する方法を学習しました。完全なソリューションは、 quickstarts-jsリポジトリにあります

Cloud Firestoreの詳細については、次のリソースにアクセスしてください。