Firebase Angular Web 框架 Codelab

1. 您将创建的内容

在此 Codelab 中,您将使用 Angular 库 AngularFire 中的最新功能构建了一个带实时协作式地图,并在其中构建了一个旅行博客。最终的 Web 应用将由一个旅行博客组成,您可以在其中将图片上传到您去过的每个地点。

AngularFire 将用于构建 Web 应用、用于本地测试的 Emulator Suite、用于跟踪用户数据的 Authentication、用于保留数据和媒体的 Firestore 和 Storage(由 Cloud Functions 提供支持),最后使用 Firebase Hosting 部署应用。

学习内容

  • 如何使用 Emulator Suite 在本地使用 Firebase 产品进行开发
  • 如何使用 AngularFire 增强 Web 应用
  • 如何在 Firestore 中保留数据
  • 如何在 Storage 中保留媒体内容
  • 如何将您的应用部署到 Firebase Hosting
  • 如何使用 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 仅使用 Web 框架代码库:

  • 📁? webframework:在此 Codelab 中,您将基于这些起始代码进行构建。

安装依赖项

克隆完成后,请先在根文件夹和 functions 文件夹中安装依赖项,然后再构建 Web 应用。

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 项目命名为 <您的项目>。记住 Firebase 项目的 ID。
  3. 点击创建项目

重要提示:您的 Firebase 项目将命名为 <your-project>,但 Firebase 会自动为其分配一个唯一的项目 ID,格式为 <your-project>-1234。此唯一标识符是项目的实际标识方式(包括在 CLI 中),而 <your-project> 只是一个显示名称。

我们要构建的应用使用适用于 Web 应用的 Firebase 产品:

  • Firebase Authentication:可让用户轻松登录您的应用。
  • Cloud Firestore:用于将结构化数据保存在云端,并在数据发生变化时获得即时通知。
  • Cloud Storage for Firebase:可用于将文件保存到云端。
  • Firebase Hosting:用于托管和提供您的资源。
  • 用于与内部和外部 API 交互的函数

其中部分产品需要特殊配置,或者需要使用 Firebase 控制台启用。

将 Firebase Web 应用添加到项目中

  1. 点击 Web 图标以创建新的 Firebase Web 应用。
  2. 在下一步中,您将看到一个配置对象。将此对象的内容复制到 environments/environment.ts 文件中。

启用 Google 登录功能以进行 Firebase 身份验证

为了让用户能够使用其 Google 帐号登录 Web 应用,我们将使用 Google 登录方法。

如需启用 Google 登录功能,请执行以下操作:

  1. 在 Firebase 控制台中,找到左侧面板中的构建部分。
  2. 点击 Authentication,然后点击登录方法标签页(或点击此处直接转到标签页)。
  3. 启用 Google 登录提供方,然后点击保存
  4. 将应用的公开名称设置为 <your-project-name>,然后从下拉菜单中选择项目支持电子邮件地址

启用 Cloud Firestore

  1. 在 Firebase 控制台的构建部分中,点击 Firestore 数据库
  2. 点击 Cloud Firestore 窗格中的创建数据库
  3. 设置 Cloud Firestore 数据的存储位置。你可以将其保留为默认值,或选择你附近的区域。

启用 Cloud Storage

Web 应用使用 Cloud Storage for Firebase 存储、上传和分享照片。

  1. 在 Firebase 控制台的构建部分中,点击存储
  2. 如果没有显示开始使用按钮,则表示 Cloud Storage 已经

而不需要遵循以下步骤。

  1. 点击开始使用
  2. 阅读有关 Firebase 项目安全规则的免责声明,然后点击下一步
  3. Cloud Storage 位置已预先选定,以及您为 Cloud Firestore 数据库选择的区域。点击完成以完成设置。

设置默认安全规则后,任何通过身份验证的用户都可以向 Cloud Storage 写入任何内容。在此 Codelab 后面的内容中,我们将提高存储空间的安全性。

4.关联到您的 Firebase 项目

通过 Firebase 命令行界面 (CLI),您可以使用 Firebase Hosting 在本地提供您的 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! 消息后,您就可以使用模拟器了。

您应该会看到您的旅行应用界面(尚未正常运行):

现在,让我们开始构建吧!

5. 将 Web 应用连接到模拟器

根据模拟器日志中的表,Cloud Firestore 模拟器在监听端口 8080,而 Authentication 模拟器在监听端口 9099。

打开 EmulatorUI

在网络浏览器中,导航到 http://127.0.0.1:4000/。您应该会看到 Emulator Suite 界面。

路由应用以使用模拟器

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. 添加身份验证

现在,已经为应用设置了模拟器,接下来可以添加 Authentication 功能,以确保每位用户在发布消息前都已登录。

为此,我们可以直接从 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 模拟器中查看结果。

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

这与创建旅行帖子几乎完全相同,因此请挑战自己来实现它,或者查看下面的实现:

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 以获取所有行程的可观察数组。

如果只需要当前用户的行程,请使用 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

现在,您将实现 Storage 来存储图片和其他类型的媒体。

Cloud Firestore 最适合用于存储结构化数据,例如 JSON 对象。Cloud Storage 旨在存储文件或 blob。在此应用中,您将使用它来允许用户分享他们的旅行照片。

与 Firestore 类似,使用 Storage 存储和更新文件也需要每个文件的唯一标识符。

让我们在 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 Hosting!现在,您可以在 Firebase 控制台中访问所有数据和分析内容。

如需了解与 AngularFire、函数、安全规则的更多功能,别忘了查看下面的可选步骤以及其他 Firebase Codelab

11. 可选:AngularFire 身份验证保护措施

除 Firebase Authentication 之外,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;
		}
	}
}