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 プロジェクトに「friendEats」という名前を付けます。

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

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

これから作成するアプリケーションでは、ウェブで利用できる以下の Firebase サービスを使用します。

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

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

フレンドリー 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. ブラウザで friendEats アプリに戻り、アプリを更新します。
  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 フィルタと 1 つの 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] ビューに移動します。

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

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

お疲れさまでした。アプリで App Check が機能するようになりました。