Firebase Angular 網頁架構程式碼研究室

1. 製作內容

在本程式碼研究室中,您將使用 Angular 程式庫的最新版本 AngularFire,建構具有即時協作地圖的旅遊網誌。最終的網路應用程式會是旅遊網誌,您可以為每個旅遊地點上傳圖片。

我們將使用 AngularFire 建構網頁應用程式、使用模擬器套件進行本機測試、使用 Authentication 追蹤使用者資料、使用 Firestore 和 Storage 持續保存資料和媒體、使用 Cloud Functions 支援應用程式,最後使用 Firebase Hosting 部署應用程式。

課程內容

  • 如何使用模擬器套件在本機開發 Firebase 產品
  • 如何使用 AngularFire 強化網頁應用程式
  • 如何在 Firestore 中保留資料
  • 如何將媒體保留在 Storage 中
  • 如何將應用程式部署至 Firebase Hosting
  • 如何使用 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 專案

  1. 使用 Google 帳戶登入 Firebase 控制台
  2. 按一下按鈕建立新專案,然後輸入專案名稱 (例如 FriendlyChat)。
  3. 按一下「繼續」
  4. 如果系統提示,請詳閱並接受 Firebase 條款,然後按一下「繼續」
  5. (選用) 在 Firebase 控制台中啟用 AI 輔助功能 (稱為「Gemini in Firebase」)。
  6. 本程式碼研究室不需要 Google Analytics,因此請關閉 Google Analytics 選項。
  7. 按一下「建立專案」,等待專案佈建完成,然後按一下「繼續」

將 Firebase 網頁應用程式新增至專案

  1. 按一下網頁圖示,建立新的 Firebase 網頁應用程式。
  2. 在下一個步驟中,您會看到設定物件。將這個物件的內容複製到 environments/environment.ts 檔案。

設定 Firebase 產品

我們要建構的應用程式會使用幾項 Firebase 產品,這些產品都可用於網頁應用程式:

  • Firebase 驗證:可讓使用者輕鬆登入您的應用程式。
  • Cloud Firestore:在雲端儲存結構化資料,並在資料變更時收到即時通知。
  • Cloud Storage for Firebase,可在雲端儲存檔案。
  • Firebase Hosting,用於代管及提供資產。
  • 函式:與內部和外部 API 互動。

部分產品需要特殊設定或透過 Firebase 控制台啟用。

啟用 Firebase 驗證的 Google 登入

如要允許使用者透過 Google 帳戶登入網頁應用程式,請使用 Google 登入方式。

如要啟用 Google 登入功能,請按照下列步驟操作:

  1. 在 Firebase 控制台中,找出左側面板的「Build」部分。
  2. 依序點選「Authentication」和「Sign-in method」分頁標籤 (或按這裡直接前往該分頁)。
  3. 啟用「Google」登入供應商,然後按一下「儲存」
  4. 將應用程式的公開名稱設為「<您的專案名稱>」,然後從下拉式選單中選擇「專案支援電子郵件」

啟用 Cloud Firestore

  1. 在 Firebase 控制台的「Build」專區中,按一下「Firestore Database」
  2. 在 Cloud Firestore 窗格中,按一下「建立資料庫」
  3. 設定 Cloud Firestore 資料的儲存位置。您可以保留預設值,或選擇您附近的地區。

啟用 Cloud Storage

網頁應用程式會使用 Cloud Storage for Firebase 儲存、上傳及分享圖片。

  1. 在 Firebase 控制台的「Build」專區中,按一下「Storage」
  2. 如果沒有「開始使用」按鈕,表示 Cloud Storage 已

啟用,您不需要按照下列步驟操作。

  1. 點選「開始使用」
  2. 請詳閱 Firebase 專案的安全性規則免責事項,然後點選「下一步」
  3. 系統會預先選取與 Cloud Firestore 資料庫相同的地區做為 Cloud Storage 位置。按一下「完成」即可完成設定。

根據預設安全性規則,通過驗證的使用者可以將任何內容寫入 Cloud Storage。我們會在稍後的程式碼研究室中,進一步確保儲存空間安全無虞。

4. 連結至 Firebase 專案

Firebase 指令列介面 (CLI) 可讓您使用 Firebase Hosting 在本機提供網路應用程式,以及將網路應用程式部署至 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/。您應該會看到模擬器套件使用者介面。

將應用程式導向模擬器

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) 和函式 (例如 signinsignout),並在應用程式中使用。

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

由於許多不同網頁都需要在網頁應用程式中讀取及更新 Firestore 文件,因此我們可以在 src/app/services/travel.service.ts 中實作函式,避免在每個網頁重複注入相同的 AngularFire 函式。

首先,請在服務中注入 AuthFirestore,與上一個步驟類似。定義可觀察的 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 的 updateFocdeleteDoc 函式讀取、更新或刪除該文件:

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/firestoredocDatacollectionData 函式提供。

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//stops/

這幾乎與建立旅遊貼文相同,因此請自行實作,或查看下方的實作方式:

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. 部署應用程式

現在可以開始部署應用程式了!

firebase 中的 firebase 設定複製到 src/environments/environment.prod.ts,然後執行:src/environments/environment.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.rulessecurity.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;
		}
	}
}