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: 静的アセットをホストして提供します。
この Codelab では、すでに Firebase Hosting を構成しています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスを構成し、有効にする手順を説明します。
匿名認証を有効にする
この Codelab では認証に焦点を当てませんが、アプリでなんらかの認証方法を用意することが重要です。ここでは匿名ログインを使用します。つまり、ユーザーはプロンプトを表示せずにサイレント ログインを行います。
匿名ログインを有効にする必要があります。
- Firebase コンソールの左側のナビゲーションで [ビルド] セクションを見つけます。
- [Authentication] をクリックし、[Sign-in method] タブをクリックします(またはこちらをクリックして、直接移動します)。
- [匿名 ログイン プロバイダ] を有効にして [保存] をクリックします。
これにより、ユーザーがウェブアプリにアクセスするときに、暗黙のログインが可能になります。詳しくは、匿名認証に関するドキュメントをご覧ください。
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 にデプロイできます。
- 次の npm コマンドを実行して CLI をインストールします。
npm -g install firebase-tools
- 次のコマンドを実行して、CLI が正しくインストールされていることを確認します。
firebase --version
Firebase CLI のバージョンが v7.4.0 以降であることを確認します。
- 次のコマンドを実行して、Firebase CLI を承認します。
firebase login
アプリのローカル ディレクトリとファイルから Firebase Hosting の構成を pull するためのウェブアプリ テンプレートを設定しました。そのためには、アプリを 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 プロジェクトに接続されている FriendlyEats のコピーが表示されます。
アプリは 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
を使用してリアルタイムで更新を取得する方法を紹介してきましたが、必ずしもそれが必要なわけではありません。場合によっては、データを 1 回だけ取得することをおすすめします。
ユーザーがアプリ内の特定のレストランをクリックしたときにトリガーされるメソッドを実装します。
- ファイル
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
を更新する必要があります。そのうちの 1 つが失敗し、もう 1 つが失敗すると、データベースのある部分のデータが別の部分のデータと一致しない、一貫性のない状態になります。
幸いなことに、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 の冒頭で、あらゆる読み取りや書き込みに対してデータベースを完全に開くように、アプリのセキュリティ ルールを設定しました。実際のアプリケーションでは、望ましくないデータアクセスや変更を防ぐために、より詳細なルールを設定する必要があります。
- Firebase コンソールの [構築] セクションで、[Firestore データベース] をクリックします
- Cloud Firestore セクションの [ルール] タブをクリックします(またはここをクリックすると、ルールに直接移動できます)。
- デフォルトを以下のルールに置き換え、[公開] をクリックします。
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 コンソールで、左側のパネルにある [ビルド] セクションを見つけます。
- [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 の [アプリビュー] に移動します。
オーバーフロー メニューをクリックして、[デバッグ トークンを管理] を選択します。
次に、[デバッグ トークンを追加] をクリックし、プロンプトに従ってコンソールからデバッグ トークンを貼り付けます。
これで完了です。これで、アプリで App Check が機能するようになりました。