Firebase Angular Web 框架程式碼實驗室

1. 你將創造什麼

在此 Codelab 中,您將使用我們的 Angular 庫AngularFire中的最新內容建立一個帶有即時協作地圖的旅遊部落格。最終的網路應用程式將包含一個旅遊博客,您可以在其中將圖像上傳到您去過的每個位置。

AngularFire 將用於建立Web 應用程序,模擬器套件用於本地測試,身份驗證用於追蹤用戶數據,Firestore 和儲存用於持久保存數據和媒體,由Cloud Functions 提供支持,最後使用Firebase Hosting 來部署應用程式。

你將學到什麼

  • 如何使用 Emulator Suite 在本地開發 Firebase 產品
  • 如何使用 AngularFire 增強您的 Web 應用程式
  • 如何在 Firestore 中保留數據
  • 如何將媒體保存在儲存中
  • 如何將您的應用程式部署到 Firebase 託管
  • 如何使用 Cloud Functions 與資料庫和 API 交互

你需要什麼

  • Node.js版本 10 或更高版本
  • 用於建立和管理 Firebase 專案的 Google 帳戶
  • Firebase CLI版本 11.14.2 或更高版本
  • 您選擇的瀏覽器,例如 Chrome
  • 對 Angular 和 Javascript 有基本了解

2. 取得範例程式碼

從命令列克隆 Codelab 的GitHub 儲存庫

git clone https://github.com/firebase/codelab-friendlychat-web

或者,如果您沒有安裝 git,則可以將儲存庫下載為 ZIP 檔案

Github 儲存庫包含適用於多個平台的範例專案。

此 Codelab 僅使用 webframework 儲存庫:

  • 📁 webframework :您將在本 Codelab 中建構的起始程式碼。

安裝依賴項

克隆後,在建置 Web 應用程式之前在根目錄和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. 按一下「建立專案」

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

我們要建立的應用程式使用可用於網頁應用程式的 Firebase 產品:

  • Firebase 驗證可輕鬆讓您的使用者登入您的應用程式。
  • Cloud Firestore將結構化資料保存在雲端,並在資料變更時獲得即時通知。
  • Cloud Storage for Firebase將檔案保存在雲端。
  • Firebase Hosting用於託管和服務您的資產。
  • 與內部和外部 API 互動的函數

其中一些產品需要特殊配置或需要使用 Firebase 控制台啟用。

將 Firebase Web 應用程式加入到專案中

  1. 按一下 Web 圖示以建立新的 Firebase Web 應用程式。
  2. 在下一步中,您將看到一個配置物件。將此物件的內容複製到environments/environment.ts檔案中。

啟用 Google登入以進行 Firebase 身份驗證

為了允許用戶使用其 Google 帳戶登入網路應用程序,我們將使用Google登入方法。

若要啟用Google登入:

  1. 在 Firebase 控制台中,找到左側面板中的「建置」部分。
  2. 按一下「驗證」 ,然後按一下「登入方法」標籤(或按一下此處直接前往此處)。
  3. 啟用Google登入供應商,然後按一下「儲存」
  4. 將應用程式的面向公眾的名稱設定為<your-project-name>並從下拉式選單中選擇項目支援電子郵件

啟用 Cloud Firestore

  1. 在 Firebase 控制台的「建置」部分中,按一下Firestore 資料庫
  2. 按一下 Cloud Firestore 窗格中的建立資料庫
  3. 設定 Cloud Firestore 資料的儲存位置。您可以將其保留為預設值或選擇離您較近的區域。

啟用雲端儲存

此 Web 應用程式使用 Cloud Storage for Firebase 來儲存、上傳和分享圖片。

  1. 在 Firebase 控制台的「建置」部分中,按一下「儲存」
  2. 如果沒有「開始」按鈕,則表示雲端儲存已啟用

已啟用,您無需執行以下步驟。

  1. 單擊開始
  2. 閱讀有關 Firebase 專案安全規則的免責聲明,然後按一下「下一步」
  3. 預先選擇的 Cloud Storage 位置與您為 Cloud Firestore 資料庫選擇的區域相同。按一下“完成”完成設定。

使用預設安全規則,任何經過驗證的使用者都可以向 Cloud Storage 寫入任何內容。稍後我們將在此 Codelab 中提高儲存的安全性。

4. 連接到您的 Firebase 項目

Firebase 命令列介面 (CLI) 可讓您使用 Firebase 託管在本地提供您的 Web 應用程序,以及將您的 Web 應用程式部署到 Firebase 專案。

確保您的命令列正在存取應用程式的本機webframework目錄。

將 Web 應用程式碼連接到您的 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. 將 Web 應用程式連接到模擬器

根據模擬器日誌中的表,Cloud Firestore 模擬器正在偵聽連接埠 8080,並且驗證模擬器正在偵聽連接埠 9099。

開啟模擬器UI

在網頁瀏覽器中,導覽至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 身份驗證

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 中儲存的旅遊部落格文章的功能。

與身份驗證類似,Firestore 功能是從 AngularFire 預先打包的。每個文件都屬於一個集合,每個文件也可以有嵌套的集合。建立和更新旅遊部落格文章需要了解 Firestore 中文件的path

實施旅遊服務

由於許多不同的頁面需要讀取和更新網頁應用程式中的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/firestore中的docDatacollectionData函數提供。

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/ 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功能

確保安全的其他方法包括實施安全性規則,或將 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. 配置存儲

您現在將實現儲存來儲存圖像和其他類型的媒體。

Cloud Firestore 最適合儲存結構化數據,例如 JSON 物件。雲端儲存旨在儲存檔案或 blob。在此應用程式中,您將使用它來允許用戶分享他們的旅行照片。

與 Firestore 類似,使用 Storage 儲存和更新檔案需要每個檔案有唯一的識別碼。

我們來實作TraveService中的功能:

上傳文件

導航至src/app/services/travel.service.ts並從 AngularFire 注入儲存:

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 存取檔案之間的主要區別在於,儘管它們都遵循資料夾結構路徑,但基本 url 和路徑組合是透過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);
}

上傳圖像時,媒體檔案本身將上傳到存儲,並且 url 會相應地儲存在 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 Codelab

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