1. 總覽
目標
在本程式碼研究室中,您將建構以 Cloud Firestore 為後端的餐廳推薦網頁應用程式。
課程內容
- 從網頁應用程式讀取資料並寫入 Cloud Firestore
- 即時監聽 Cloud Firestore 資料的變更
- 使用 Firebase 驗證和安全性規則保護 Cloud Firestore 資料
- 編寫複雜的 Cloud Firestore 查詢
軟硬體需求
開始本程式碼研究室之前,請確認您已安裝:
2. 建立及設定 Firebase 專案
建立 Firebase 專案
- 在 Firebase 控制台中,按一下「新增專案」,然後將 Firebase 專案命名為「FriendlyEats」。
請記下 Firebase 專案的專案 ID。
- 按一下 [Create Project]。
我們要建構的應用程式會使用幾項可在網路上使用的 Firebase 服務:
- Firebase 驗證:輕鬆識別使用者
- Cloud Firestore:在雲端儲存結構化資料,並在資料更新時立即收到通知
- Firebase 代管:託管及提供靜態素材資源
在這個程式碼研究室中,我們已設定 Firebase 託管。不過,我們會逐步說明如何使用 Firebase 主控台設定及啟用 Firebase Auth 和 Cloud Firestore 服務。
啟用匿名驗證功能
雖然本程式碼研究室的焦點並非驗證,但在應用程式中加入某種驗證機制相當重要。我們會使用「匿名登入」,也就是在未顯示提示的情況下,讓使用者靜默登入。
您必須啟用匿名登入功能。
- 在 Firebase 控制台中,找出左側導覽面板中的「建構」專區。
- 依序點選「驗證」和「登入方式」分頁標籤 (或按這裡直接前往)。
- 啟用「匿名」登入供應器,然後按一下「儲存」。
這樣一來,應用程式就能在使用者存取網路應用程式時,自動登入使用者。如需更多資訊,請參閱匿名驗證說明文件。
啟用 Cloud Firestore
應用程式會使用 Cloud Firestore 儲存及接收餐廳資訊和評分。
您必須啟用 Cloud Firestore。在 Firebase 控制台的「建構」專區中,按一下「Firestore 資料庫」。按一下 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 託管模擬器在本機上提供應用程式。網路應用程式現在應該可透過 http://localhost:5000 使用。
- 前往 http://localhost:5000 開啟應用程式。
您應該會看到已連結至 Firebase 專案的 FriendlyEats 副本。
應用程式已自動連結至您的 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 控制台中開啟索引建立 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. 保護資料安全
在本程式碼研究室的開頭,我們設定應用程式的安全性規則,以便限制應用程式的存取權。
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 控制台中,找出並選取「Security」(安全性) 下方的「reCaptcha Enterprise」。
- 依提示啟用服務,然後按一下「Create Key」(建立金鑰)。
- 依提示輸入顯示名稱,然後選取「網站」做為平台類型。
- 將已部署的網址加入網域清單,並確認「使用核取方塊驗證」選項未選取。
- 按一下「建立金鑰」,然後將產生的金鑰存放在安全的地方。稍後在本步驟中會用到這項資訊。
啟用 App Check
- 在 Firebase 控制台中,找出左側面板中的「建構」專區。
- 按一下「App Check」,然後點選「開始使用」按鈕 (或直接前往 控制台)。
- 按一下「註冊」,然後在系統顯示提示時輸入 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 應用程式檢視畫面。
按一下溢出選單,然後選取「管理偵錯符記」。
接著,按一下「Add debug token」,然後按照提示在控制台中貼上偵錯符記。
恭喜!應用程式現在應可正常執行 App Check。