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,可將結構化資料儲存在 Cloud 中,並在資料更新時立即傳送通知
- Firebase 代管:用於代管和提供靜態資產
我們已針對這個特定的程式碼研究室設定 Firebase 託管。不過,如果是 Firebase Auth 和 Cloud Firestore,則會逐步說明使用 Firebase 控制台設定及啟用服務。
啟用匿名驗證
雖然驗證並非本程式碼研究室的重點,但在應用程式中設定某種形式的驗證非常重要。我們會使用匿名登入,意味著使用者會自動登入,系統不會提示使用者。
您必須啟用匿名登入。
- 在 Firebase 控制台中,找到左側導覽列的「Build」部分。
- 依序點選「驗證」和「登入方式」分頁標籤 (或按這裡直接前往)。
- 啟用匿名登入提供者,然後按一下「儲存」。
這可讓應用程式在使用者存取網頁應用程式時,在不發出通知的情況下登入使用者。詳情請參閱匿名驗證說明文件。
啟用 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/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
函式。 - 使用下列程式碼取代整個函式。
Internetgoats.Data.js
FriendlyEats.prototype.addRestaurant = function(data) { var collection = firebase.firestore().collection('restaurants'); return collection.add(data); };
以上程式碼會將新文件新增到 restaurants
集合。文件資料來自純文字的 JavaScript 物件。方法是先取得 Cloud Firestore 集合 restaurants
的參照,再取得資料add
。
我們來新增餐廳吧!
- 在瀏覽器中返回 LoyaltyEats 應用程式,並重新整理應用程式。
- 按一下「Add Mock Data」。
應用程式會自動產生一組隨機的餐廳物件,然後呼叫 addRestaurant
函式。不過,您無法在實際網頁應用程式中查看資料,因為我們仍然需要導入擷取資料 (請參閱程式碼研究室的下一節)。
如果您前往 Firebase 控制台的「Cloud Firestore」分頁,應該會看到 restaurants
集合中的新文件!
恭喜!您已成功從網頁應用程式將資料寫入 Cloud Firestore!
下一節將說明如何從 Cloud Firestore 擷取資料,並在應用程式中顯示資料。
7. 顯示 Cloud Firestore 中的資料
本節將說明如何從 Cloud Firestore 擷取資料,並在應用程式中顯示資料。建立查詢和新增快照事件監聽器是兩個重要步驟。這個事件監聽器會收到符合查詢的所有現有資料,並即時接收更新。
首先,我們要建構查詢,以便提供未篩選的預設餐廳清單。
- 返回
scripts/FriendlyEats.Data.js
檔案。 - 找出
FriendlyEats.prototype.getAllRestaurants
函式。 - 使用下列程式碼取代整個函式。
Internetgoats.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
函式。 - 使用下列程式碼取代整個函式。
Internetgoats.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
。在這個案例中,我們會呼叫函式,將餐廳從使用者介面中移除。
我們已經導入這兩種方法,接著請重新整理應用程式,確認先前在 Firebase 控制台中看到的餐廳現在都會顯示在應用程式中。如果您已成功完成這個部分,表示應用程式現在可以透過 Cloud Firestore 讀取及寫入資料!
隨著餐廳清單變更,這個事件監聽器也會持續自動更新。您可以前往 Firebase 控制台手動刪除餐廳或變更餐廳名稱,這些變更就會立即顯示在您的網站上。
8. Get() 資料
到目前為止,我們已說明如何使用 onSnapshot
即時擷取更新;但這並非總是如此有時候,只擷取資料是較合適的做法。
我們希望導入一種方法,會在使用者點選應用程式中的特定餐廳時觸發。
- 返回「
scripts/FriendlyEats.Data.js
」檔案。 - 找出
FriendlyEats.prototype.getRestaurant
函式。 - 使用下列程式碼取代整個函式。
Internetgoats.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
函式。 - 使用下列程式碼取代整個函式。
Internetgoats.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
函式。 - 使用下列程式碼取代整個函式。
Internetgoats.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 CLI,透過 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」。
- 按照系統提示啟用服務,然後按一下「Create Key」。
- 按照系統提示輸入顯示名稱,然後選取「網站」做為平台類型。
- 將已部署的網址新增至網域清單,並確認「使用核取方塊驗證方式」已取消選取選項。
- 按一下「建立金鑰」,並將產生的金鑰儲存在其他位置來妥善保存。後續步驟會用到這項資訊。
啟用 App Check
- 在 Firebase 控制台中,找到左側面板中的「Build」區段。
- 按一下「App Check」,然後點選「Get Started」按鈕 (或直接重新導向 控制台)。
- 按一下 [註冊],並在系統提示時輸入您的 reCAPTCHA Enterprise 金鑰,然後按一下「儲存」。
- 在 API 檢視畫面中選取「Storage」,然後點選「強制執行」。對 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 現在應該可以在你的應用程式中運作。