1. 概要
目標
この Codelab では、Cloud Firestore を利用しておすすめレストラン ウェブアプリを構築します。
ラボの内容
- ウェブアプリから Cloud Firestore へのデータの読み取りと書き込みを行う
- Cloud Firestore データの変更をリアルタイムでリッスンする
- Firebase Authentication とセキュリティ ルールを使用して Cloud Firestore データを保護する
- 複雑な Cloud Firestore クエリを作成する
必要なもの
この Codelab を開始する前に、次のものがインストールされていることを確認してください。
2. Firebase プロジェクトを作成して設定する
Firebase プロジェクトを作成する
- Firebase コンソールで [プロジェクトを追加] をクリックし、Firebase プロジェクトに FriendlyEats という名前を付けます。
Firebase プロジェクトのプロジェクト ID を覚えておいてください。
- [プロジェクトの作成] をクリックします。
これから構築するアプリでは、ウェブで利用できる次の Firebase サービスを使用します。
- Firebase Authentication: ユーザーを簡単に識別できます。
- Cloud Firestore: 構造化データをクラウドに保存し、データが更新されたらすぐに通知を受け取れます。
- Firebase Hosting: 静的アセットをホストして配信できます。
このコードラボでは、Firebase Hosting はすでに構成されています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスの構成と有効化を行う手順について説明します。
匿名認証を有効にする
認証はこの Codelab の主要なトピックではありませんが、アプリではなんらかの形の認証を行うことが重要です。ここでは、匿名ログイン(プロンプトが表示されず、ユーザーが自動的にログインする)を使用します。
匿名ログインを有効にする必要があります。
- Firebase コンソールの左側のナビゲーション バーで、[Build] セクションを見つけます。
- [認証] をクリックし、[ログイン方法] タブをクリックします(または、ここをクリックして [ログイン方法] タブに直接移動します)。
- [匿名] ログイン プロバイダを有効にして、[保存] をクリックします。
これにより、ユーザーはウェブアプリにアクセスしたときに、アプリに自動的にログインできるようになります。詳しくは、匿名認証のドキュメントをご覧ください。
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 を [ドメインリスト] に追加し、[チェックボックスによる本人確認を使用する] オプションが選択されていないことを確認します。
- [鍵を作成] をクリックし、生成された鍵を安全な場所に保存します。このステップの後半で必要になります。
App Check を有効にする
- Firebase コンソールの左側のパネルで、[Build] セクションを見つけます。
- [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
関数を更新して 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
を使用すると、アプリでトークンを自動更新できます。
ローカルテストを有効にするには、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 の [アプリビュー] に移動します。
オーバーフロー メニューをクリックし、[デバッグ トークンを管理] を選択します。
次に、[Add debug token] をクリックし、プロンプトが表示されたらコンソールからデバッグ トークンを貼り付けます。
これで完了です。これで、アプリで App Check が機能するようになります。