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 中保留数据
  • 如何在存储空间中保留媒体
  • 如何将应用部署到 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 项目命名为 <your-project>。记住 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 文件中。

为 Firebase Authentication 启用 Google 登录

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

要启用 Google 登录,请执行以下操作:

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

启用 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 来存储图片和其他类型的媒体。

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、Functions、安全规则的功能,请查看下面的可选步骤以及其他 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;
		}
	}
}