1. 總覽
目標
在本程式碼研究室中,您將建構由 Cloud Firestore 支援的餐廳推薦網頁應用程式。
課程內容
- 從網頁應用程式讀取及寫入 Cloud Firestore 資料
- 即時監聽 Cloud Firestore 資料的變更
- 使用 Firebase 驗證和安全規則保護 Cloud Firestore 資料
- 編寫複雜的 Cloud Firestore 查詢
軟硬體需求
開始本程式碼研究室之前,請確認您已安裝下列項目:
2. 建立及設定 Firebase 專案
建立 Firebase 專案
- 使用 Google 帳戶登入 Firebase 控制台。
- 按一下按鈕建立新專案,然後輸入專案名稱 (例如
FriendlyEats
)。
- 按一下「繼續」。
- 如果系統提示,請詳閱並接受 Firebase 條款,然後按一下「繼續」。
- (選用) 在 Firebase 控制台中啟用 AI 輔助功能 (稱為「Gemini in Firebase」)。
- 本程式碼研究室不需要 Google Analytics,因此請關閉 Google Analytics 選項。
- 按一下「建立專案」,等待專案佈建完成,然後按一下「繼續」。
設定 Firebase 產品
我們要建構的應用程式會使用幾項 Firebase 網路服務:
- Firebase 驗證,輕鬆識別使用者
- Cloud Firestore:在雲端儲存結構化資料,並在資料更新時立即收到通知
- Firebase Hosting,用於託管及提供靜態資產
在本程式碼研究室中,我們已設定 Firebase 託管。不過,我們會逐步說明如何使用 Firebase 控制台設定及啟用 Firebase 驗證和 Cloud Firestore 服務。
啟用匿名驗證
雖然驗證並非本程式碼研究室的重點,但應用程式中必須有某種形式的驗證。我們將使用匿名登入,也就是使用者會自動登入,不會收到提示。
您必須啟用匿名登入。
- 在 Firebase 控制台中,找出左側導覽列的「Build」部分。
- 依序點選「Authentication」和「Sign-in method」分頁標籤 (或按這裡直接前往該分頁)。
- 啟用「Anonymous」登入供應商,然後按一下「Save」。
這樣一來,使用者存取網頁應用程式時,應用程式就能自動登入。歡迎參閱匿名驗證說明文件,瞭解詳情。
啟用 Cloud Firestore
應用程式會使用 Cloud Firestore 儲存及接收餐廳資訊和評分。
您需要啟用 Cloud Firestore。在 Firebase 控制台的「Build」專區中,按一下「Firestore Database」。在 Cloud Firestore 窗格中,按一下「建立資料庫」。
Cloud Firestore 中的資料存取權由安全性規則控管。我們稍後會在本程式碼研究室中詳細說明規則,但首先需要為資料設定一些基本規則,才能開始使用。在 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; } } } }
我們會在後續章節中討論這些規則及其運作方式。
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
目錄。這個目錄包含本程式碼研究室的起始程式碼,其中包含尚未運作的餐廳推薦應用程式。我們會在整個程式碼研究室中讓應用程式運作,因此您很快就需要編輯該目錄中的程式碼。
4. 安裝 Firebase 指令列介面
Firebase 指令列介面 (CLI) 可讓您在本地提供網頁應用程式,並將網頁應用程式部署至 Firebase 託管。
- 執行下列 npm 指令來安裝 CLI:
npm -g install firebase-tools
- 執行下列指令,確認 CLI 是否已正確安裝:
firebase --version
確認 Firebase CLI 版本為 7.4.0 以上。
- 執行下列指令,授權 Firebase CLI:
firebase login
我們已設定網頁應用程式範本,從應用程式的本機目錄和檔案中,提取 Firebase 代管服務的應用程式設定。但為此,我們需要將應用程式與 Firebase 專案建立關聯。
- 請確認指令列正在存取應用程式的本機目錄。
- 執行下列指令,將應用程式與 Firebase 專案建立關聯:
firebase use --add
- 系統顯示提示訊息時,請選取「專案 ID」,然後為 Firebase 專案提供別名。
如果您有多個環境 (正式版、預先發布版等),別名就非常實用。不過,在本程式碼研究室中,我們只會使用 default
的別名。
- 按照指令列中的後續指示操作。
5. 執行本機伺服器
我們已準備好實際開始開發應用程式!我們在本機執行應用程式!
- 執行下列 Firebase CLI 指令:
firebase emulators:start --only hosting
- 指令列應會顯示下列回應:
hosting: Local server: http://localhost:5000
我們使用 Firebase Hosting 模擬器在本機提供應用程式。現在應該可以透過 http://localhost:5000 存取網路應用程式。
- 在 http://localhost:5000 開啟應用程式。
您應該會看到 FriendlyEats 的副本,且該副本已連結至 Firebase 專案。
應用程式已自動連結至 Firebase 專案,並以匿名使用者身分登入。
6. 將資料寫入 Cloud Firestore
在本節中,我們將一些資料寫入 Cloud Firestore,以便填入應用程式的 UI。您可以透過 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
函式。不過,您還不會在實際的網頁應用程式中看到資料,因為我們仍需實作擷取資料 (程式碼研究室的下一節)。
不過,如果您前往 Firebase 控制台的「Cloud Firestore」分頁,現在應該會在 restaurants
集合中看到新文件!
恭喜!您剛才已從網頁應用程式將資料寫入 Cloud Firestore!
下一節將說明如何從 Cloud Firestore 擷取資料,並在應用程式中顯示。
7. 顯示 Cloud Firestore 中的資料
在本節中,您將瞭解如何從 Cloud Firestore 擷取資料,並在應用程式中顯示。主要步驟有兩個:建立查詢和新增快照監聽器。這個事件監聽器會收到所有符合查詢條件的現有資料通知,並即時接收更新。
首先,請建構查詢,提供未經過濾的預設餐廳清單。
- 返回
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(); };
實作這個方法後,您就能查看每間餐廳的頁面。只要按一下清單中的餐廳,就會看到餐廳的詳細資料頁面:
目前您無法新增評分,因為我們還需要在程式碼研究室中實作新增評分的功能。
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 控制台會自動開啟索引建立使用者介面,並填入正確的參數。在下一節中,我們將編寫並部署這個應用程式所需的索引。
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. 保護資料安全
在本程式碼研究室的開頭,我們已設定應用程式的安全規則,限制應用程式的存取權。
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. 結論
在本程式碼研究室中,您已瞭解如何使用 Cloud Firestore 執行基本和進階的讀取和寫入作業,以及如何透過安全規則保護資料存取權。您可以在 quickstarts-js 存放區中找到完整解決方案。
如要進一步瞭解 Cloud Firestore,請參閱下列資源:
14. [選用] 使用 App Check 強制執行
Firebase App Check 可協助驗證及防範應用程式的不當流量,提供保護機制。在這個步驟中,您將透過 reCAPTCHA Enterprise 新增 App Check,確保服務存取安全。
首先,您需要啟用 App Check 和 reCaptcha。
啟用 reCAPTCHA Enterprise
- 在 Cloud 控制台中,找出並選取「安全性」下方的「reCAPTCHA Enterprise」。
- 按照提示啟用服務,然後按一下「建立金鑰」。
- 按照提示輸入顯示名稱,然後選取「網站」做為平台類型。
- 將已部署的網址新增至「網域清單」,並確認「使用核取方塊驗證」選項未選取。
- 按一下「建立金鑰」,然後將產生的金鑰存放在安全的地方。您稍後會用到這項資訊。
啟用 App Check
- 在 Firebase 控制台中,找出左側面板的「Build」部分。
- 點選「應用程式檢查」,然後點選「開始使用」按鈕 (或直接重新導向至 控制台)。
- 按一下「註冊」,在系統提示時輸入 reCAPTCHA Enterprise 金鑰,然後按一下「儲存」。
- 在「API 檢視畫面」中,選取「Storage」,然後按一下「Enforce」。對 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。