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 项目
- 登录 Firebase。
- 在 Firebase 控制台中,点击添加项目,然后将您的 Firebase 项目命名为 <your-project>。记下 Firebase 项目的项目 ID。
- 点击创建项目。
重要提示:您的 Firebase 项目将被命名为 <your-project>,但 Firebase 会自动为其分配一个唯一的项目 ID,格式为 <your-project>-1234。Firebase 实际上通过这个唯一标识符来识别您的项目(包括在 CLI 中),而“<your-project>”只是显示名称。
我们要构建的应用使用可用于 Web 应用的 Firebase 产品:
- Firebase Authentication:方便用户登录您的应用。
- Cloud Firestore:用于在云端保存结构化数据,并在数据发生变化时即时收到通知。
- Cloud Storage for Firebase,用于在云端保存文件。
- Firebase Hosting:用于托管和提供您的资源。
- 用于与内部和外部 API 进行交互的函数。
其中一些产品需要特殊配置,或者需要使用 Firebase 控制台启用。
向项目添加 Firebase Web 应用
- 点击“Web”图标以创建新的 Firebase Web 应用。
- 在下一步中,您会看到一个配置对象。将此对象的内容复制到
environments/environment.ts
文件中。
为 Firebase Authentication 启用 Google 登录
为了允许用户使用其 Google 账号登录 Web 应用,我们将使用 Google 登录方法。
要启用 Google 登录,请执行以下操作:
- 在 Firebase 控制台中,在左侧面板中找到构建部分。
- 点击 Authentication,然后点击登录方法标签页(或点击此处直接转到标签页)。
- 启用 Google 登录服务提供方,然后点击保存。
- 将应用的公开名称设置为 <your-project-name>,并从下拉菜单中选择 Project support email。
启用 Cloud Firestore
- 在 Firebase 控制台的构建部分中,点击 Firestore 数据库。
- 点击 Cloud Firestore 窗格中的创建数据库。
- 设置 Cloud Firestore 数据的存储位置。您可以保留默认值,也可以选择您附近的区域。
启用 Cloud Storage
Web 应用使用 Cloud Storage for Firebase 来存储、上传和分享照片。
- 在 Firebase 控制台的构建部分中,点击存储。
- 如果没有 Get Started 按钮,则表示 Cloud Storage 已
已启用,您无需按照以下步骤操作。
- 点击开始使用。
- 阅读关于 Firebase 项目安全规则的免责声明,然后点击下一步。
- 系统会预先选择 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. 添加身份验证
现在,为应用设置了模拟器,我们可以添加身份验证功能,以确保每位用户在发布消息之前都已登录。
为此,我们可以直接从 AngularFire 导入 signin
函数,并使用 authState
函数跟踪用户的身份验证状态。修改登录页面函数,以便页面在加载时检查用户身份验证状态。
注入 AngularFire 身份验证
在 src/app/pages/login-page/login-page.component.ts
中,从 @angular/fire/auth
导入 Auth
,并将其注入 LoginPageComponent
。身份验证提供方(如 Google)以及 signin
、signout
等函数也可以直接从同一软件包导入,并在应用中使用。
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 的 updateFoc
和 deleteDoc
函数读取、更新或删除该文档:
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
中的 docData
和 collectionData
函数提供。
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/
这与创建旅游帖子几乎完全相同,因此请挑战您自行实现它,或者查看下面的实现:
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. 配置存储空间
现在,您将实现 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.rules
和 security.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;
}
}
}