Cloud Firestore ウェブ 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: 構造化データを Cloud に保存し、データが更新されるとすぐに通知を受け取る
  • Firebase Hosting: 静的アセットをホストして提供します。

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

匿名認証を有効にする

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

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

  1. Firebase コンソールの左側のナビゲーション バーで、[Build] セクションを見つけます。
  2. [認証] をクリックし、[ログイン方法] タブをクリックします(または、ここをクリックして [ログイン方法] タブに直接移動します)。
  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 プロジェクトに接続された FriendlyEat のコピーが表示されます。

アプリが自動的に 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. 関数全体を次のコードに置き換えます。

フレンドリー Eats.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. 関数全体を次のコードに置き換えます。

フレンドリー Eats.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 回だけ取得する方が合理的です。

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

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

フレンドリー Eats.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 句を追加して、ユーザー入力に基づく複合クエリを作成します。これで、クエリはユーザーの要件を満たすレストランのみを返すようになりました。

ブラウザで FRIENDS アプリを更新し、価格、都市、カテゴリでフィルタできることを確認します。テスト中は、ブラウザの 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 を更新する必要があります。いずれか 1 つが失敗し、もう 1 つでは失敗すると、データベースのある部分のデータが別の部分のデータと一致しないという不整合な状態になります。

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

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

フレンドリー Eats.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 ビューで [Storage] を選択し、[適用] をクリックします。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 を使用すると、アプリ内でトークンを自動更新できます。

ローカルテストを有効にするには、friendEats.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 の [Apps] ビューに移動します。

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

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

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