1. 概要
目標
この Codelab では、Cloud Firestore を利用して、おすすめレストラン ウェブアプリを構築します。
学習内容
- ウェブアプリから Cloud Firestore へのデータの読み取りと書き込みを行う
- Cloud Firestore データの変更をリアルタイムでリッスンする
- Firebase Authentication とセキュリティ ルールを使用して Cloud Firestore データを保護する
- 複雑な Cloud Firestore クエリを作成する
必要なもの
この Codelab を開始する前に、次のものがインストールされていることを確認してください。
2. Firebase プロジェクトを作成して設定する
Firebase プロジェクトを作成する
- Google アカウントを使用して Firebase コンソールにログインします。
- ボタンをクリックして新しいプロジェクトを作成し、プロジェクト名(例:
FriendlyEats
)を入力します。
- [続行] をクリックします。
- Firebase の利用規約が表示されたら、内容を読み、同意して [続行] をクリックします。
- (省略可)Firebase コンソールで AI アシスタンス(「Gemini in Firebase」)を有効にします。
- この Codelab では Google アナリティクスは必要ないため、Google アナリティクスのオプションをオフに切り替えます。
- [プロジェクトを作成] をクリックし、プロジェクトのプロビジョニングが完了するまで待ってから、[続行] をクリックします。
Firebase プロダクトを設定する
これから構築するアプリでは、ウェブで利用できる以下の Firebase サービスを使用します。
- Firebase Authentication: ユーザーを簡単に識別できます。
- Cloud Firestore: 構造化データをクラウドに保存し、データが更新されたらすぐに通知を受け取れます。
- Firebase Hosting: 静的アセットをホストして配信できます。
このコードラボでは、Firebase Hosting はすでに構成されています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスを構成し、有効にする手順を説明します。
匿名認証を有効にする
認証はこの Codelab の主要なトピックではありませんが、アプリではなんらかの形の認証を行うことが重要です。ここでは、匿名ログイン(プロンプトが表示されず、ユーザーが自動的にログインする)を使用します。
匿名ログインを有効にする必要があります。
- Firebase コンソールの左側のナビゲーションで、[構築] セクションを見つけます。
- [Authentication] をクリックし、[Sign-in method] タブをクリックします(または、ここをクリックして直接移動します)。
- [匿名] ログイン プロバイダを有効にして、[保存] をクリックします。
これにより、ユーザーはウェブアプリにアクセスしたときに、アプリに自動的にログインできるようになります。詳しくは、匿名認証のドキュメントをご覧ください。
Cloud Firestore の有効化
このアプリは、Cloud Firestore を使用してレストランの情報と評価を受信し、保存します。
Cloud Firestore を有効にする必要があります。Firebase コンソールの [構築] セクションで、[Firestore データベース] をクリックします。[Cloud Firestore] ペインで [データベースを作成] をクリックします。
Cloud Firestore 内のデータへのアクセスは、セキュリティ ルールによって制御されます。ルールについてはこの Codelab の後半で詳しく説明しますが、まず、データに基本的なルールを設定して始めましょう。Firebase コンソールの [ルール] タブで次のルールを追加し、[公開] をクリックします。
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; } } } }
これらのルールとその仕組みについては、この 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 にデプロイできます。
- 次の npm コマンドを実行して、CLI をインストールします。
npm -g install firebase-tools
- 次のコマンドを実行して、CLI が正しくインストールされたことを確認します。
firebase --version
Firebase CLI のバージョンが v7.4.0 以降であることを確認します。
- 次のコマンドを実行して、Firebase CLI を承認します。
firebase login
ウェブアプリ テンプレートは、アプリのローカル ディレクトリとファイルから Firebase Hosting のアプリの構成を取得するように設定されています。ただし、そのためには、アプリを Firebase プロジェクトに関連付ける必要があります。
- コマンドラインがアプリのローカル ディレクトリにアクセスしていることを確認します。
- 次のコマンドを実行して、アプリを Firebase プロジェクトに関連付けます。
firebase use --add
- プロンプトが表示されたら、プロジェクト ID を選択して、Firebase プロジェクトにエイリアスを指定します。
エイリアスは、複数の環境(本番環境、ステージング環境など)を使用する場合に役立ちます。ただし、この Codelab では、default
というエイリアスのみを使用します。
- コマンドラインで指示される手順に沿って操作します。
5. ローカル サーバーを実行する
アプリで実際に作業を開始する準備が整いました。アプリをローカルで実行してみましょう。
- 次の Firebase CLI コマンドを実行します。
firebase emulators:start --only hosting
- コマンドラインに次のレスポンスが表示されます。
hosting: Local server: http://localhost:5000
Firebase Hosting エミュレータを使用して、アプリをローカルで提供します。これで、ウェブアプリは http://localhost:5000 から利用できます。
- http://localhost:5000 でアプリを開きます。
Firebase プロジェクトに接続された FriendlyEat のコピーが表示されるはずです。
アプリは自動的に Firebase プロジェクトに接続され、ユーザーは匿名ユーザーとして自動的にログインします。
6. Cloud Firestore にデータを書き込む
このセクションでは、アプリの UI にデータを入力できるように、Cloud Firestore にデータを書き込みます。これは Firebase コンソールから手動で実行できますが、ここでは、アプリ自体で実行して、基本的な Cloud Firestore の書き込みを例示します。
データモデル
Firestore データは、コレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。各レストランは、restaurants
という名前の最上位のコレクションにドキュメントとして保存されます。
後で、各レストランの ratings
という名前のサブコレクションに各レビューを格納します。
レストランを Firestore に追加する
このアプリのメインのモデル オブジェクトは、レストランです。レストランのドキュメントを restaurants
コレクションに追加するコードを作成してみましょう。
- ダウンロードしたファイルから
scripts/FriendlyEats.Data.js
を開きます。 - 関数
FriendlyEats.prototype.addRestaurant
を見つけます。 - 関数全体を次のコードに置き換えます。
FriendlyEats.Data.js
FriendlyEats.prototype.addRestaurant = function(data) { var collection = firebase.firestore().collection('restaurants'); return collection.add(data); };
上記のコードは、restaurants
コレクションに新しいドキュメントを追加します。ドキュメント データは、プレーンな JavaScript オブジェクトから取得されます。そのためには、まず Cloud Firestore コレクション restaurants
への参照を取得し、次にデータを add
します。
レストランを追加しましょう。
- ブラウザで FriendlyEats アプリに戻り、更新します。
- [Add Mock Data] をクリックします。
アプリはレストラン オブジェクトのランダムなセットを自動的に生成し、addRestaurant
関数を呼び出します。ただし、データの「取得」を実装する必要があるため(Codelab の次のセクションで行います)、実際のウェブアプリにデータは表示されません。
ただし、Firebase コンソールで [Cloud Firestore] タブに移動すると、restaurants
コレクションに新しいドキュメントが表示されるはずです。
おつかれさまです。これで、ウェブアプリから Cloud Firestore にデータが書き込まれました。
次のセクションでは、Cloud Firestore からデータを取得してアプリに表示する方法を学びます。
7. Cloud Firestore からのデータを表示する
このセクションでは、Cloud Firestore からデータを取得してアプリに表示する方法を学びます。主要なステップは、クエリの作成とスナップショット リスナーの追加の 2 つです。このリスナーは、クエリに一致するすべての既存データについて通知され、リアルタイムで更新を受信します。
まず、フィルタリングされていないデフォルトのレストラン リストを提供するクエリを作成してみましょう。
- ファイル
scripts/FriendlyEats.Data.js
に戻ります。 - 関数
FriendlyEats.prototype.getAllRestaurants
を見つけます。 - 関数全体を次のコードに置き換えます。
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()
メソッドにこのクエリを渡します。
これを行うには、スナップショット リスナーを追加します。
- ファイル
scripts/FriendlyEats.Data.js
に戻ります。 - 関数
FriendlyEats.prototype.getDocumentsInQuery
を見つけます。 - 関数全体を次のコードに置き換えます。
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 コンソールに移動して、手動でレストランを削除したり、名前を変更したりしてみてください。変更内容がすぐにサイトに反映されるはずです。
8. Get() データ
ここまでは、onSnapshot
を使用してリアルタイムで更新を取得する方法を学びました。しかし、いつでもそうするのが望ましいわけではありません。一度だけデータをフェッチすればよい場合もあります。
ユーザーがアプリで特定のレストランをクリックしたときにトリガーされるメソッドを実装します。
- ファイル
scripts/FriendlyEats.Data.js
に戻ります。 - 関数
FriendlyEats.prototype.getRestaurant
を見つけます。 - 関数全体を次のコードに置き換えます。
FriendlyEats.Data.js
FriendlyEats.prototype.getRestaurant = function(id) { return firebase.firestore().collection('restaurants').doc(id).get(); };
このメソッドを実装すると、各レストランのページを表示できるようになります。リスト内のレストランをクリックすると、そのレストランの詳細ページが表示されます。
現時点では、評価を追加することはできません。評価の追加は、この Codelab の後半で実装する必要があります。
9. データの並べ替えとフィルタ
現在、アプリにはレストランのリストが表示されますが、ユーザーが自分のニーズに基づいてフィルタすることはできません。このセクションでは、Cloud Firestore の高度なクエリを使用して、フィルタリングを有効にします。
Dim Sum
のレストランをすべてフェッチするシンプルなクエリの例を次に示します。
var filteredQuery = query.where('category', '==', 'Dim Sum')
where()
メソッドは、その名が示すように、設定した制限を満たすフィールドを持つコレクションのメンバーのみをクエリしてダウンロードします。この例では、category
が Dim Sum
のレストランのみをダウンロードします。
アプリでは、複数のフィルタを連結して、「サンフランシスコのピザ」や「ロサンゼルスのシーフードを人気順に表示」などの特定のクエリを作成できます。
ユーザーが選択した複数の条件に基づいてレストランをフィルタするクエリを構築するメソッドを作成します。
- ファイル
scripts/FriendlyEats.Data.js
に戻ります。 - 関数
FriendlyEats.prototype.getFilteredRestaurants
を見つけます。 - 関数全体を次のコードに置き換えます。
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 を使用して、一度に多数のインデックスを簡単にデプロイできます。
- アプリのダウンロードされたローカル ディレクトリに
firestore.indexes.json
ファイルがあります。
このファイルには、フィルタのすべての可能な組み合わせに対して必要なすべてのインデックスが記述されています。
firestore.indexes.json
{ "indexes": [ { "collectionGroup": "restaurants", "queryScope": "COLLECTION", "fields": [ { "fieldPath": "city", "order": "ASCENDING" }, { "fieldPath": "avgRating", "order": "DESCENDING" } ] }, ... ] }
- 次のコマンドを使用して、これらのインデックスをデプロイします。
firebase deploy --only firestore:indexes
数分後、インデックスが有効になり、エラー メッセージが表示されなくなります。
11. トランザクションでデータを書き込む
このセクションでは、ユーザーがレストランに対するレビューを投稿する機能を追加します。これまでのところ、すべての書き込みはアトミックで、比較的単純でした。いずれかの書き込みがエラーになった場合は、単にユーザーに再試行を求める方法が考えられます。ユーザーがそうしなかった場合、アプリは自動的に書き込みを再試行します。
アプリにはレストランのレビューを書き込みたいユーザーが多数いるはずなので、複数の読み取りと書き込みを調整する必要があります。最初に、レビュー自体を送信する必要があります。次に、レストランの評価の count
と average rating
を更新する必要があります。一方が失敗して他方が成功した場合、データベースのある部分のデータが別の部分のデータと一致しなくなるという矛盾した状態に陥ります。
幸いなことに、Cloud Firestore は単一のアトミック オペレーションで複数の読み取りと書き込みを実行できるトランザクション機能を備えているため、データの整合性を保証できます。
- ファイル
scripts/FriendlyEats.Data.js
に戻ります。 - 関数
FriendlyEats.prototype.addRating
を見つけます。 - 関数全体を次のコードに置き換えます。
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); }); }); };
上記のブロックでは、トランザクションをトリガーして、レストラン ドキュメント内の avgRating
と numRatings
の数値を更新します。同時に、新しい rating
を ratings
サブコレクションに追加します。
12. データをセキュリティで保護する
この Codelab の冒頭では、アプリのセキュリティ ルールを設定して、アプリへのアクセスを制限しました。
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 を有効にする
- Cloud コンソールで、[セキュリティ] の [reCaptcha Enterprise] を見つけて選択します。
- プロンプトが表示されたらサービスを有効にして、[キーを作成] をクリックします。
- 表示名を入力し、プラットフォーム タイプとして [ウェブサイト] を選択します。
- デプロイした URL を [ドメインリスト] に追加し、[チェックボックスによる本人確認を使用する] オプションがオフになっていることを確認します。
- [Create Key] をクリックし、生成されたキーを安全な場所に保存します。この値は、この手順の後半で必要になります。
App Check を有効にする
- Firebase コンソールの左側のパネルで、[構築] セクションを見つけます。
- [App Check] をクリックし、[開始] ボタンをクリックします(または、 コンソールに直接リダイレクトします)。
- [登録] をクリックし、メッセージが表示されたら reCaptcha Enterprise キーを入力して、[保存] をクリックします。
- [API] ビューで [ストレージ] を選択し、[適用] をクリックします。Cloud Firestore についても同様に操作します。
これで App Check が適用されるようになりました。アプリを更新して、レストランの作成または表示を試します。次のエラー メッセージが表示されます。
Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
つまり、App Check はデフォルトで未検証のリクエストをブロックします。それでは、アプリに検証を追加しましょう。
FriendlyEats.View.js ファイルに移動し、initAppCheck
関数を更新して、App Check を初期化するための reCaptcha キーを追加します。
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;
}
これにより、ローカル ウェブアプリのコンソールに次のようなデバッグ トークンが記録されます。
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 が動作するようになります。