1。概要
目標
このコードラボでは、 Cloud Firestoreを利用したレストラン レコメンデーション ウェブ アプリを構築します。
学べること
- ウェブアプリから Cloud Firestore へのデータの読み取りと書き込み
- Cloud Firestore データの変更をリアルタイムでリッスンする
- Firebase Authentication とセキュリティ ルールを使用して Cloud Firestore データを保護する
- 複雑な Cloud Firestore クエリを作成する
必要なもの
このコードラボを開始する前に、以下がインストールされていることを確認してください。
2. Firebase プロジェクトを作成して設定する
Firebaseプロジェクトを作成する
- Firebase コンソールで、 [プロジェクトの追加]をクリックし、Firebase プロジェクトにFriendlyEatsという名前を付けます。
Firebase プロジェクトのプロジェクト ID を覚えておいてください。
- 「プロジェクトの作成」をクリックします。
これから構築するアプリケーションは、ウェブ上で利用可能ないくつかの Firebase サービスを使用します。
- ユーザーを簡単に識別するためのFirebase Authentication
- Cloud Firestore は構造化データをクラウド上に保存し、データが更新されたときに即座に通知を受け取ります
- 静的アセットをホストして提供するFirebase Hosting
この特定のコードラボでは、Firebase Hosting がすでに構成されています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスを構成し有効にする手順を説明します。
匿名認証を有効にする
このコードラボの焦点は認証ではありませんが、アプリに何らかの形式の認証を導入することが重要です。匿名ログインを使用します。つまり、ユーザーはプロンプトが表示されずにサイレントサインインされます。
匿名ログインを有効にする必要があります。
- Firebase コンソールの左側のナビゲーションで[ビルド]セクションを見つけます。
- [認証]をクリックし、 [サインイン方法]タブをクリックします (または、ここをクリックして直接そこに移動します)。
- 匿名サインイン プロバイダーを有効にして、 [保存]をクリックします。
これにより、アプリケーションはユーザーが Web アプリにアクセスするときにサイレント サインインできるようになります。詳細については、匿名認証のドキュメントを参照してください。
Cloud Firestoreを有効にする
このアプリは Cloud Firestore を使用して、レストランの情報と評価を保存および受信します。
Cloud Firestore を有効にする必要があります。 Firebase コンソールの[ビルド]セクションで、 [Firestore データベース]をクリックします。 [Cloud Firestore] ペインで[データベースの作成]をクリックします。
Cloud Firestore のデータへのアクセスは、セキュリティ ルールによって制御されます。ルールについてはこのコードラボの後半で詳しく説明しますが、始めるにはまず、データにいくつかの基本的なルールを設定する必要があります。 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; } } }
上記のルールにより、サインインしているユーザーへのデータ アクセスが制限され、認証されていないユーザーは読み取りまたは書き込みができなくなります。これはパブリック アクセスを許可するよりは優れていますが、安全とは程遠いため、コードラボの後半でこれらのルールを改善する予定です。
3. サンプルコードを入手する
コマンドラインからGitHub リポジトリのクローンを作成します。
git clone https://github.com/firebase/friendlyeats-web
サンプル コードは 📁 friendlyeats-web
ディレクトリに複製されているはずです。今後は、すべてのコマンドを必ずこのディレクトリから実行してください。
cd friendlyeats-web
スターターアプリをインポートする
IDE (WebStorm、Atom、Sublime、Visual Studio Code...) を使用して、📁 friendlyeats-web
ディレクトリを開くかインポートします。このディレクトリには、まだ機能していないレストラン推奨アプリで構成されるコードラボの開始コードが含まれています。このコードラボ全体で機能するようにするので、すぐにそのディレクトリ内のコードを編集する必要があります。
4. Firebaseコマンドラインインターフェースをインストールする
Firebase コマンド ライン インターフェイス (CLI) を使用すると、Web アプリをローカルで提供し、Web アプリを 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 プロジェクトにエイリアスを付けます。
エイリアスは、複数の環境 (本番環境、ステージング環境など) がある場合に便利です。ただし、このコードラボでは、 default
のエイリアスを使用することにします。
- コマンドラインの残りの指示に従います。
5. ローカルサーバーを実行する
実際にアプリの作業を開始する準備ができました。アプリをローカルで実行してみましょう。
- 次の Firebase CLI コマンドを実行します。
firebase emulators:start --only hosting
- コマンドラインには次の応答が表示されるはずです。
hosting: Local server: http://localhost:5000
アプリをローカルで提供するためにFirebase Hostingエミュレーターを使用しています。 Web アプリは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 アプリに戻り、更新します。
- 「モックデータの追加」をクリックします。
アプリはレストラン オブジェクトのランダムなセットを自動的に生成し、 addRestaurant
関数を呼び出します。ただし、データの取得を実装する必要があるため (コードラボの次のセクション) 、実際の Web アプリではデータはまだ表示されません。
ただし、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 件のレストランを平均評価順に取得するクエリを構築します (現在はすべて 0)。このクエリを宣言した後、データのロードとレンダリングを担当する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(); };
このメソッドを実装すると、各レストランのページを表示できるようになります。リスト内のレストランをクリックするだけで、レストランの詳細ページが表示されます。
現時点では、後ほどコードラボで評価の追加を実装する必要があるため、評価を追加することはできません。
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 を大規模に高速に保つことができます。
エラー メッセージからリンクを開くと、正しいパラメータが入力されたインデックス作成 UI が Firebase コンソールで自動的に開きます。次のセクションでは、このアプリケーションに必要なインデックスを作成してデプロイします。
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. データを保護する
このコードラボの開始時に、データベースを完全に読み取りまたは書き込みできるようにアプリのセキュリティ ルールを設定しました。実際のアプリケーションでは、望ましくないデータ アクセスや変更を防ぐために、より詳細なルールを設定する必要があります。
- 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 について詳しくは、次のリソースをご覧ください。