1. 製作內容
在本程式碼研究室中,您將使用 Angular 程式庫的最新版本 AngularFire,建構具有即時協作地圖的旅遊網誌。最終的網路應用程式會包含旅遊網誌,您可以在網誌中上傳每個旅遊地點的圖片。
我們將使用 AngularFire 建構網頁應用程式、使用模擬器套件進行本機測試、使用驗證追蹤使用者資料、使用 Firestore 和儲存空間保存資料和媒體、使用 Cloud Functions 提供支援,最後使用 Firebase 託管部署應用程式。
課程內容
- 如何使用模擬器套件在本機開發 Firebase 產品
- 如何使用 AngularFire 強化網頁應用程式
- 如何在 Firestore 中保留資料
- 如何將媒體保留在儲存空間中
- 如何將應用程式部署至 Firebase 託管
- 如何使用 Cloud Functions 與資料庫和 API 互動
事前準備
- Node.js 10 以上版本
- 用於建立及管理 Firebase 專案的 Google 帳戶
- Firebase CLI 11.14.2 以上版本
- 你選擇的瀏覽器,例如 Chrome
- 對 Angular 和 JavaScript 有基本瞭解
2. 取得程式碼範例
從指令列複製程式碼研究室的 GitHub 存放區:
git clone https://github.com/firebase/codelab-friendlychat-web
或者,如果您未安裝 Git,可以將存放區下載為 ZIP 檔案。
Github 存放區包含多個平台的範例專案。
本程式碼研究室只會使用 webframework 存放區:
- 📁 webframework:您將在本程式碼研究室中以此為基礎建構程式碼。
安裝依附元件
複製完成後,請在根目錄和 functions 資料夾中安裝依附元件,再建構網頁應用程式。
cd webframework && npm install
cd functions && npm install
安裝 Firebase CLI
在終端機中使用下列指令安裝 Firebase CLI:
npm install -g firebase-tools
使用下列指令,再次確認 Firebase CLI 版本是否高於 11.14.2:
firebase --version
如果版本低於 11.14.2,請使用下列指令更新:
npm update firebase-tools
3. 建立及設定 Firebase 專案
建立 Firebase 專案
- 使用 Google 帳戶登入 Firebase 控制台。
- 按一下按鈕建立新專案,然後輸入專案名稱 (例如
FriendlyChat)。
- 按一下「繼續」。
- 如果系統提示,請詳閱並接受 Firebase 條款,然後按一下「繼續」。
- (選用) 在 Firebase 控制台中啟用 AI 輔助功能 (稱為「Gemini in Firebase」)。
- 本程式碼研究室不需要 Google Analytics,因此請關閉 Google Analytics 選項。
- 按一下「建立專案」,等待專案佈建完成,然後按一下「繼續」。
將 Firebase 網頁應用程式新增至專案
- 按一下網頁圖示,建立新的 Firebase 網頁應用程式。
- 在下一個步驟中,您會看到設定物件。將這個物件的內容複製到
environments/environment.ts檔案。
設定 Firebase 產品
我們即將建構的應用程式會使用幾項 Firebase 產品,這些產品都可用於網頁應用程式:
- Firebase 驗證:可讓使用者輕鬆登入您的應用程式。
- Cloud Firestore:在雲端儲存結構化資料,並在資料變更時收到即時通知。
- Cloud Storage for Firebase,將檔案儲存到雲端。
- Firebase 託管,用於代管及提供資產。
- 函式:與內部和外部 API 互動。
部分產品需要特殊設定或透過 Firebase 控制台啟用。
啟用 Firebase 驗證的 Google 登入
如要允許使用者透過 Google 帳戶登入網頁應用程式,請使用 Google 登入方式。
如要啟用 Google 登入功能,請按照下列步驟操作:
- 在 Firebase 控制台中,找出左側面板的「Build」部分。
- 依序點選「Authentication」,然後點選「登入方式」分頁標籤 (或按這裡直接前往該分頁)。
- 啟用「Google」登入供應商,然後按一下「儲存」。
- 將應用程式的公開名稱設為「<您的專案名稱>」,然後從下拉式選單中選擇「專案支援電子郵件」。
啟用 Cloud Firestore
- 在 Firebase 控制台的「Build」專區中,按一下「Firestore Database」。
- 在 Cloud Firestore 窗格中,按一下「建立資料庫」。
- 設定 Cloud Firestore 資料的儲存位置。您可以保留預設值,或選擇您附近的區域。
啟用 Cloud Storage
網頁應用程式會使用 Cloud Storage for Firebase 儲存、上傳及分享圖片。
- 在 Firebase 控制台的「Build」專區中,按一下「Storage」。
- 如果沒有「開始使用」按鈕,表示 Cloud Storage 已
如果已啟用,則不必按照下列步驟操作。
- 點選「開始使用」。
- 請詳閱 Firebase 專案的安全性規則免責事項,然後點選「下一步」。
- 系統會預先選取與 Cloud Firestore 資料庫相同的地區做為 Cloud Storage 位置。按一下「完成」即可完成設定。
根據預設安全性規則,通過驗證的使用者可以將任何內容寫入 Cloud Storage。我們會在後續章節中,進一步提升儲存空間的安全性。
4. 連結至 Firebase 專案
透過 Firebase 指令列介面 (CLI),您可以使用 Firebase 託管功能在本地提供網頁應用程式,以及將網頁應用程式部署至 Firebase 專案。
請確認指令列正在存取應用程式的本機 webframework 目錄。
將網頁應用程式程式碼連結至 Firebase 專案。首先,請在指令列中登入 Firebase CLI:
firebase login
接著執行下列指令,建立專案別名。將 $YOUR_PROJECT_ID 替換為 Firebase 專案 ID。
firebase use $YOUR_PROJECT_ID
新增 AngularFire
如要將 AngularFire 新增至應用程式,請執行下列指令:
ng add @angular/fire
然後按照指令列的指示操作,並選取 Firebase 專案中現有的功能。
初始化 Firebase
如要初始化 Firebase 專案,請執行下列指令:
firebase init
然後按照指令列提示,選取 Firebase 專案中使用的功能和模擬器。
啟動模擬器
在 webframework 目錄中執行下列指令,啟動模擬器:
firebase emulators:start
最終您應該會看到如下內容:
$ firebase emulators:start
i emulators: Starting emulators: auth, functions, firestore, hosting, functions
i firestore: Firestore Emulator logging to firestore-debug.log
i hosting: Serving hosting files from: public
✔ hosting: Local server: http://localhost:5000
i ui: Emulator UI logging to ui-debug.log
i functions: Watching "/functions" for Cloud Functions...
✔ functions[updateMap]: firestore function initialized.
┌─────────────────────────────────────────────────────────────┐
│ ✔ All emulators ready! It is now safe to connect your app. │
│ i View Emulator UI at http://localhost:4000 │
└─────────────────────────────────────────────────────────────┘
┌────────────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Authentication │ localhost:9099 │ http://localhost:4000/auth │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Functions │ localhost:5001 │ http://localhost:4000/functions │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080 │ http://localhost:4000/firestore │
├────────────────┼────────────────┼─────────────────────────────────┤
│ Hosting │ localhost:5000 │ n/a │
└────────────────┴────────────────┴─────────────────────────────────┘
Emulator Hub running at localhost:4400
Other reserved ports: 4500
Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
看到 ✔All emulators ready! 訊息後,即可開始使用模擬器。
您應該會看到旅遊應用程式的 UI,但目前還無法運作:
現在就來建構儲存空間吧!
5. 將網頁應用程式連線至模擬器
根據模擬器記錄中的表格,Cloud Firestore 模擬器監聽通訊埠 8080,而 Authentication 模擬器監聽通訊埠 9099。
開啟 EmulatorUI
在網路瀏覽器中,前往 http://127.0.0.1:4000/。您應該會看到模擬器套件 UI。
將應用程式路徑設為使用模擬器
在 src/app/app.module.ts 中,將下列程式碼新增至 AppModule 的匯入清單:
@NgModule({
declarations: [...],
imports: [
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => {
const auth = getAuth();
if (location.hostname === 'localhost') {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
}
return auth;
}),
provideFirestore(() => {
const firestore = getFirestore();
if (location.hostname === 'localhost') {
connectFirestoreEmulator(firestore, '127.0.0.1', 8080);
}
return firestore;
}),
provideFunctions(() => {
const functions = getFunctions();
if (location.hostname === 'localhost') {
connectFunctionsEmulator(functions, '127.0.0.1', 5001);
}
return functions;
}),
provideStorage(() => {
const storage = getStorage();
if (location.hostname === 'localhost') {
connectStorageEmulator(storage, '127.0.0.1', 5001);
}
return storage;
}),
...
]
應用程式現在已設定為使用本機模擬器,因此您可以在本機進行測試和開發。
6. 新增驗證
現在已為應用程式設定模擬器,我們可以新增驗證功能,確保每位使用者都已登入,才能發布訊息。
為此,我們可以從 AngularFire 直接匯入 signin 函式,並使用 authState 函式追蹤使用者的驗證狀態。修改登入頁面函式,讓頁面在載入時檢查使用者授權狀態。
注入 AngularFire Auth
在 src/app/pages/login-page/login-page.component.ts 中,從 @angular/fire/auth 匯入 Auth,並將其注入 LoginPageComponent。您也可以直接從相同套件匯入驗證供應商 (例如 Google) 和函式 (例如 signin、signout),並在應用程式中使用。
import { Auth, GoogleAuthProvider, signInWithPopup, signOut, user } from '@angular/fire/auth';
export class LoginPageComponent implements OnInit {
private auth: Auth = inject(Auth);
private provider = new GoogleAuthProvider();
user$ = user(this.auth);
constructor() {}
ngOnInit(): void {}
login() {
signInWithPopup(this.auth, this.provider).then((result) => {
const credential = GoogleAuthProvider.credentialFromResult(result);
return credential;
})
}
logout() {
signOut(this.auth).then(() => {
console.log('signed out');}).catch((error) => {
console.log('sign out error: ' + error);
})
}
}
現在登入頁面已可正常運作!嘗試登入,並在驗證模擬器中查看結果。
7. 設定 Firestore
在這個步驟中,您將新增功能,發布及更新儲存在 Firestore 中的旅遊網誌文章。
與 Authentication 類似,Firestore 函式是從 AngularFire 預先封裝而來。每份文件都屬於一個集合,且每份文件也可以有巢狀集合。如要建立及更新旅遊網誌文章,必須知道 Firestore 中文件的 path。
導入 TravelService
由於許多不同網頁都需要在 Web 應用程式中讀取及更新 Firestore 文件,因此我們可以在 src/app/services/travel.service.ts 中實作函式,避免在每個網頁重複注入相同的 AngularFire 函式。
首先,請將 Auth (與上一個步驟類似) 和 Firestore 注入服務。定義可觀察的 user$ 物件,監聽目前的驗證狀態,也很有幫助。
import { doc, docData, DocumentReference, Firestore, getDoc, setDoc, updateDoc, collection, addDoc, deleteDoc, collectionData, Timestamp } from "@angular/fire/firestore";
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
user$ = authState(this.auth).pipe(filter(user => user !== null), map(user => user!));
router: Router = inject(Router);
新增旅遊貼文
旅遊貼文會以文件的形式儲存在 Firestore 中,而文件必須存在於集合中,因此包含所有旅遊貼文的集合會命名為 travels。因此,任何旅遊貼文的路徑都會是 travels/
使用 AngularFire 的 addDoc 函式,即可將物件插入集合:
async addEmptyTravel(userId: String) {
...
addDoc(collection(this.firestore, 'travels'), travelData).then((travelRef) => {
collection(this.firestore, `travels/${travelRef.id}/stops`);
setDoc(travelRef, {... travelData, id: travelRef.id})
this.router.navigate(['edit', `${travelRef.id}`]);
return travelRef;
})
}
更新及刪除資料
只要有任何旅遊貼文的 UID,就能推斷儲存在 Firestore 中的文件路徑,然後使用 AngularFire 的 updateFoc 和 deleteDoc 函式讀取、更新或刪除該文件:
async updateData(path: string, data: Partial<Travel | Stop>) {
await updateDoc(doc(this.firestore, path), data)
}
async deleteData(path: string) {
const ref = doc(this.firestore, path);
await deleteDoc(ref)
}
以可觀測項目的形式讀取資料
由於旅遊貼文和沿途停靠站都可以在建立後修改,因此將文件物件做為可觀測物件會更有用,可訂閱所做的任何變更。這項功能是由 @angular/fire/firestore 的 docData 和 collectionData 函式提供。
getDocData(path: string) {
return docData(doc(this.firestore, path), {idField: 'id'}) as Observable<Travel | Stop>
}
getCollectionData(path: string) {
return collectionData(collection(this.firestore, path), {idField: 'id'}) as Observable<Travel[] | Stop[]>
}
在旅遊貼文中新增停靠點
旅遊貼文作業設定完成後,接下來請考慮停靠站,停靠站會位於旅遊貼文的子集合下方,如下所示:travels/
這幾乎與建立旅遊貼文相同,因此請自行實作,或查看下方的實作方式:
async addStop(travelId: string) {
...
const ref = await addDoc(collection(this.firestore, `travels/${travelId}/stops`), stopData)
setDoc(ref, {...stopData, id: ref.id})
}
太好了!Firestore 函式已在 Travel 服務中實作,現在您可以查看這些函式的運作情形。
在應用程式中使用 Firestore 函式
前往 src/app/pages/my-travels/my-travels.component.ts 並注入 TravelService,即可使用其函式。
travelService = inject(TravelService);
travelsData$: Observable<Travel[]>;
stopsList$!: Observable<Stop[]>;
constructor() {
this.travelsData$ = this.travelService.getCollectionData(`travels`) as Observable<Travel[]>
}
建構函式中會呼叫 TravelService,取得所有行程的 Observable 陣列。
如果只需要目前使用者的行程,請使用 query function。
確保安全的其他方法包括實作安全規則,或搭配使用 Cloud Functions 和 Firestore,如下方選用步驟所述
然後,只要呼叫 TravelService 中實作的函式即可。
async createTravel(userId: String) {
this.travelService.addEmptyTravel(userId);
}
deleteTravel(travelId: String) {
this.travelService.deleteData(`travels/${travelId}`)
}
現在「我的行程」頁面應該可以正常運作了!建立新的旅遊貼文時,請查看 Firestore 模擬器中發生的情況。
接著,針對 /src/app/pages/edit-travels/edit-travels.component.ts 中的更新函式重複上述步驟:
travelService: TravelService = inject(TravelService)
travelId = this.activatedRoute.snapshot.paramMap.get('travelId');
travelData$: Observable<Travel>;
stopsData$: Observable<Stop[]>;
constructor() {
this.travelData$ = this.travelService.getDocData(`travels/${this.travelId}`) as Observable<Travel>
this.stopsData$ = this.travelService.getCollectionData(`travels/${this.travelId}/stops`) as Observable<Stop[]>
}
updateCurrentTravel(travel: Partial<Travel>) {
this.travelService.updateData(`travels${this.travelId}`, travel)
}
updateCurrentStop(stop: Partial<Stop>) {
stop.type = stop.type?.toString();
this.travelService.updateData(`travels${this.travelId}/stops/${stop.id}`, stop)
}
addStop() {
if (!this.travelId) return;
this.travelService.addStop(this.travelId);
}
deleteStop(stopId: string) {
if (!this.travelId || !stopId) {
return;
}
this.travelService.deleteData(`travels${this.travelId}/stops/${stopId}`)
this.stopsData$ = this.travelService.getCollectionData(`travels${this.travelId}/stops`) as Observable<Stop[]>
}
8. 設定儲存空間
現在請實作 Storage,儲存圖片和其他類型的媒體。
Cloud Firestore 最適合用於儲存結構化資料,例如 JSON 物件。Cloud Storage 的設計用途是儲存檔案或 Blob。在本應用程式中,您將使用這項功能,讓使用者分享旅遊相片。
同樣地,使用 Storage 儲存及更新檔案時,每個檔案都需要專屬 ID。
我們來實作 TraveService 中的函式:
上傳檔案
前往 src/app/services/travel.service.ts,然後從 AngularFire 插入 Storage:
export class TravelService {
firestore: Firestore = inject(Firestore);
auth: Auth = inject(Auth);
storage: Storage = inject(Storage);
並實作上傳函式:
async uploadToStorage(path: string, input: HTMLInputElement, contentType: any) {
if (!input.files) return null
const files: FileList = input.files;
for (let i = 0; i < files.length; i++) {
const file = files.item(i);
if (file) {
const imagePath = `${path}/${file.name}`
const storageRef = ref(this.storage, imagePath);
await uploadBytesResumable(storageRef, file, contentType);
return await getDownloadURL(storageRef);
}
}
return null;
}
從 Firestore 存取文件和從 Cloud Storage 存取檔案的主要差異在於,雖然兩者都遵循資料夾結構路徑,但基本網址和路徑組合是透過 getDownloadURL 取得,然後可以儲存並用於 檔案。
在應用程式中使用這項功能
前往 src/app/components/edit-stop/edit-stop.component.ts,然後使用下列程式碼呼叫上傳函式:
async uploadFile(file: HTMLInputElement, stop: Partial<Stop>) {
const path = `/travels/${this.travelId}/stops/${stop.id}`
const url = await this.travelService.uploadToStorage(path, file, {contentType: 'image/png'});
stop.image = url ? url : '';
this.travelService.updateData(path, stop);
}
上傳圖片時,媒體檔案本身會上傳至儲存空間,而網址則會相應地儲存在 Firestore 的文件中。
9. 部署應用程式
現在可以開始部署應用程式了!
將 src/environments/environment.ts 中的 firebase 設定複製到 src/environments/environment.prod.ts,然後執行:
firebase deploy
畫面應如下所示:
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.
=== Deploying to 'friendly-travels-b6a4b'...
i deploying storage, firestore, hosting
i firebase.storage: checking storage.rules for compilation errors...
✔ firebase.storage: rules file storage.rules compiled successfully
i firestore: reading indexes from firestore.indexes.json...
i cloud.firestore: checking firestore.rules for compilation errors...
✔ cloud.firestore: rules file firestore.rules compiled successfully
i storage: latest version of storage.rules already up to date, skipping upload...
i firestore: deploying indexes...
i firestore: latest version of firestore.rules already up to date, skipping upload...
✔ firestore: deployed indexes in firestore.indexes.json successfully for (default) database
i hosting[friendly-travels-b6a4b]: beginning deploy...
i hosting[friendly-travels-b6a4b]: found 6 files in .firebase/friendly-travels-b6a4b/hosting
✔ hosting[friendly-travels-b6a4b]: file upload complete
✔ storage: released rules storage.rules to firebase.storage
✔ firestore: released rules firestore.rules to cloud.firestore
i hosting[friendly-travels-b6a4b]: finalizing version...
✔ hosting[friendly-travels-b6a4b]: version finalized
i hosting[friendly-travels-b6a4b]: releasing new version...
✔ hosting[friendly-travels-b6a4b]: release complete
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/friendly-travels-b6a4b/overview
Hosting URL: https://friendly-travels-b6a4b.web.app
10. 恭喜!
現在應用程式應該已完成,並部署至 Firebase 託管!現在您可以在 Firebase 控制台中存取所有資料和數據分析。
如要瞭解 AngularFire、Functions 和安全性規則的更多功能,別忘了查看下方的選用步驟,以及其他 Firebase 程式碼研究室!
11. 選用:AngularFire 驗證防護措施
除了 Firebase 驗證,AngularFire 也提供以驗證為基礎的路徑防護措施,因此存取權不足的使用者可以重新導向。這有助於保護應用程式,避免使用者存取受保護的資料。
在 src/app/app-routing.module.ts 中,匯入
import {AuthGuard, redirectLoggedInTo, redirectUnauthorizedTo} from '@angular/fire/auth-guard'
接著,您可以定義函式,說明在特定網頁上,使用者應在何時重新導向至何處:
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['signin']);
const redirectLoggedInToTravels = () => redirectLoggedInTo(['my-travels']);
然後將這些地點加入路線:
const routes: Routes = [
{path: '', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'signin', component: LoginPageComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectLoggedInToTravels}},
{path: 'my-travels', component: MyTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
{path: 'edit/:travelId', component: EditTravelsComponent, canActivate: [AuthGuard], data: {authGuardPipe: redirectUnauthorizedToLogin}},
];
12. 選用:安全性規則
Firestore 和 Cloud Storage 都會使用安全性規則 (分別為 firestore.rules 和 security.rules) 來強制執行安全性措施及驗證資料。
目前,Firestore 和 Storage 資料的讀取和寫入權限皆為開放存取,但您不希望使用者隨意變更他人的貼文!您可以使用安全性規則,限制集合和文件的存取權。
Firestore 規則
如要只允許通過驗證的使用者查看旅遊貼文,請前往 firestore.rules 檔案並新增:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/travels {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
安全性規則也可用於驗證資料:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/posts {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
&& "author" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
儲存規則
同樣地,我們可以使用安全性規則,強制執行 storage.rules 中儲存空間資料庫的存取權。請注意,我們也可以使用函式進行更複雜的檢查:
rules_version = '2';
function isImageBelowMaxSize(maxSizeMB) {
return request.resource.size < maxSizeMB * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
service firebase.storage {
match /b/{bucket}/o {
match /{userId}/{postId}/{filename} {
allow write: if request.auth != null
&& request.auth.uid == userId && isImageBelowMaxSize(5);
allow read;
}
}
}