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

1. 課程內容

在本程式碼研究室中,您可以使用 AngularFire 最新的 Angular 程式庫,利用即時協作地圖打造旅遊網誌。最終版網頁應用程式包含旅遊網誌,您可以將圖片上傳到每個曾經造訪的地點。

AngularFire 會用於建構網頁應用程式、用於本機測試的模擬器套件、驗證來追蹤使用者資料、透過 Firestore 和 Storage 保存資料和媒體,並採用 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 存放區包含適用於多個平台的範例專案。

本程式碼研究室僅使用網頁架構存放區:

  • 📁? 網路架構:在本程式碼研究室中,您將建構的起始程式碼。

安裝依附元件

複製完成後,請先在根資料夾和 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. 登入 Firebase
  2. 在 Firebase 控制台中,按一下「新增專案」,然後將 Firebase 專案命名為 <your-project>。記下 Firebase 專案的專案 ID。
  3. 按一下 [Create Project] (建立專案)

重要事項:您的 Firebase 專案會命名為 <your-project>,但 Firebase 會自動為其指派專屬專案 ID,格式為 <your-project>-1234。這是專案實際識別方式 (包括在 CLI 中) 的識別方式,<your-project> 則只是顯示名稱。

我們要建構的應用程式使用了適用於網頁應用程式的 Firebase 產品:

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

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

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

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

啟用 Google 登入 Firebase 驗證

為允許使用者透過 Google 帳戶登入網頁應用程式,我們將使用 Google 登入方式。

如何啟用 Google 登入功能:

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

啟用 Cloud Firestore

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

啟用 Cloud Storage

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

  1. 在 Firebase 控制台的「建構」專區中,按一下「儲存空間」
  2. 如果沒看到「開始使用」按鈕,表示 Cloud Storage 已在

,您不需要執行下列步驟。

  1. 點選「Get Started」(開始使用)
  2. 詳閱 Firebase 專案安全性規則免責事項,然後點選「下一步」
  3. 系統已根據您為 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,且驗證模擬器正在監聽通訊埠 9099。

開啟 EmulatorUI

透過網路瀏覽器前往 http://127.0.0.1:4000/。您應該會看到 Emulator 套件 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 驗證

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);
		})
	}
}

登入頁面現在開始運作了!嘗試登入,並在 Authentication Emulator 中查看結果。

7. 設定 Firestore

在這個步驟中,您將新增功能來發布和更新儲存在 Firestore 中的旅遊網誌文章。

與驗證功能類似,已預先封裝 AngularFire 中的 Firestore 函式。每份文件都屬於一個集合,而每份文件也可以包含巢狀集合。你必須瞭解 Firestore 中的文件 path,才能建立及更新旅遊網誌文章。

導入 TravelService

由於許多不同頁面都需要在網頁應用程式中讀取及更新 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 的 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 函式,您現在可以查看實際運作情況。

在應用程式中使用 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,以取得所有行程的可觀測陣列。

如果您只需要傳輸目前使用者的行程,請使用 query 函式

確保安全性的其他方法包括實作安全性規則,或搭配使用 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 而設計,在這個應用程式中,您將使用此功能讓使用者分享自己的旅遊相片。

同樣地,使用 Firestore 儲存及更新檔案時,每個檔案都需要有一個不重複的 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 設定從 src/environments/environment.ts 複製到 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、函式、安全性規則的功能,別忘了參考下方選用步驟,以及其他 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;
		}
	}
}