本文最初发布在Auth0.com博客上 ,并经许可在此处重新发布。
在这个由两部分组成的教程系列中,我们将学习如何构建一个使用Auth0身份验证保护Node后端和Angular前端安全的应用程序。 我们的服务器和应用程序还将使用自定义令牌对Firebase Cloud Firestore数据库进行身份验证,以便用户在使用Auth0登录后可以安全方式留下实时评论。 可以在angular-firebase GitHub存储库中找到Angular应用程序代码,在firebase-auth0-nodeserver存储库中找到Node API。
本教程的第一部分“ 使用Auth0验证Firebase和Angular:第1部分”介绍了:
- Auth0和Firebase的介绍和设置
- 实施一个安全的Node API,以生成自定义Firebase令牌并为我们的应用程序提供数据
- 具有模块和延迟加载的Angular应用程序体系结构
- 具有服务和路由保护的Auth0的角度身份验证
- 共享的Angular组件和API服务。
使用Auth0对Firebase和Angular进行身份验证:第2部分
本教程的第2部分将介绍:
我们完成的应用程序将如下所示:
让我们从使用Auth0:第1部分对Firebase和Angular进行身份验证的结尾处停下来的地方继续 。
显示狗:异步和NgIfElse
让我们实现应用程序的主页-狗列表。 设置Angular应用程序的体系结构时,我们为此组件创建了脚手架。
重要说明:确保您的Node.js API正在运行。 如果您需要有关API的更新知识,请参阅如何使用Auth0认证Firebase和Angular:第1部分-Node API 。
狗组件类
立即打开dogs.component.ts
类文件并实现以下代码:
// src/app/dogs/dogs/dogs.component.ts
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ApiService } from '../../core/api.service';
import { Dog } from './../../core/dog';
import { Observable } from 'rxjs/Observable';
import { tap, catchError } from 'rxjs/operators';
@Component({
selector: 'app-dogs',
templateUrl: './dogs.component.html'
})
export class DogsComponent implements OnInit {
pageTitle = 'Popular Dogs';
dogsList$: Observable<Dog[]>;
loading = true;
error: boolean;
constructor(
private title: Title,
private api: ApiService
) {
this.dogsList$ = api.getDogs$().pipe(
tap(val => this._onNext(val)),
catchError((err, caught) => this._onError(err, caught))
);
}
ngOnInit() {
this.title.setTitle(this.pageTitle);
}
private _onNext(val: Dog[]) {
this.loading = false;
}
private _onError(err, caught): Observable<any> {
this.loading = false;
this.error = true;
return Observable.throw('An error occurred fetching dogs data.');
}
}
导入后,我们将设置一些本地属性:
-
pageTitle
:设置页面的<h1>
和<title>
-
dogsList$
:我们的API HTTP请求返回的可观察对象,以获取狗列表数据 -
loading
:在发出API请求时显示加载图标 -
error
:如果在从API提取数据时出错,则显示错误。
我们将使用声明性异步管道来响应API GET
请求返回的dogsList$
observable。 使用异步管道,我们不需要在DogsComponent
类中进行订阅或取消订阅:订阅过程将自动进行管理! 我们只需要设置可观察的。
通过将Title
和ApiService
传递给构造函数,使它们对我们的类可用,然后将我们的dogsList$
设置dogsList$
可观察的。 我们将使用RxJS运算符tap
(以前称为do
运算符)和catchError
来调用处理程序函数。 tap
运算符执行副作用,但不影响发射的数据,因此非常适合设置其他属性。 _onNext()
函数会将loading
设置为false
(因为已成功发射数据)。 _onError()
函数将适当地设置loading
和error
并抛出错误。 如前所述,我们不需要订阅或取消订阅可观察到的dogsList$
因为异步管道(我们将在模板中添加)将为我们处理。
在初始化组件时,我们将使用ngOnInit()
监视OnInit生命周期挂钩以设置文档<title>
。
我们的Dogs组件类就是这样!
狗组件模板
让我们转到dogs.component.html
的模板:
<!-- src/app/dogs/dogs/dogs.component.html -->
<h1 class="text-center">{{ pageTitle }}</h1>
<ng-template #noDogs>
<app-loading *ngIf="loading"></app-loading>
<app-error *ngIf="error"></app-error>
</ng-template>
<div *ngIf="dogsList$ | async as dogsList; else noDogs">
<p class="lead">
These were the top <a href="http://www.akc.org/content/news/articles/the-labrador-retriever-wins-top-breed-for-the-26th-year-in-a-row/">10 most popular dog breeds in the United States in 2016</a>, ranked by the American Kennel Club (AKC).
</p>
<div class="row mb-3">
<div *ngFor="let dog of dogsList" class="col-xs-12 col-sm-6 col-md-4">
<div class="card my-2">
<img class="card-img-top" [src]="dog.image" [alt]="dog.breed">
<div class="card-body">
<h5 class="card-title">#{{ dog.rank }}: {{ dog.breed }}</h5>
<p class="text-right mb-0">
<a class="btn btn-primary" [routerLink]="['/dog', dog.rank]">Learn more</a>
</p>
</div>
</div>
</div>
</div>
</div>
<app-comments></app-comments>
此模板中有几件事,我们将仔细研究:
...
<ng-template #noDogs>
<app-loading *ngIf="loading"></app-loading>
<app-error *ngIf="error"></app-error>
</ng-template>
<div *ngIf="dogsList$ | async as dogsList; else noDogs">
...
<div *ngFor="let dog of dogsList" ...>
...
这段代码声明性地做了一些非常有用的事情。 让我们来探索。
首先,我们有一个带有模板引用变量 ( #noDogs
)的<ng-template>
元素 。 <ng-template>
元素永远不会直接呈现。 它旨在与结构性指令(例如NgIf)一起使用。 在这种情况下,我们使用<ng-template #noDogs>
创建了一个嵌入式视图,其中包含加载和错误组件。 这些组件中的每个组件都将根据条件进行渲染。 除非指示,否则noDogs
嵌入式视图本身不会渲染。
那么我们如何(何时)告诉此视图进行渲染?
下一个<div *ngIf="...
实际上是一个NgIfElse,使用星号前缀作为语法糖 。我们还将async管道与dogsList$
observable并设置一个变量,以便我们可以在我们的流中引用流的发射值template( as dogsList
)。如果dogsList$
observable出现问题,我们可以通过else noDogs
语句告诉模板渲染<ng-template #noDogs>
视图,这是在成功从中获取数据之前进行的。 API,或者观察者抛出错误。
如果dogsList$ | async
dogsList$ | async
已成功发出一个值,div将呈现,并且我们可以使用NgForOf( *ngFor
)结构性指令来显示每个dogsList
值(如我们的组件类中所指定的,它是Dog
的数组)狗的信息。
正如您在其余HTML中所看到的那样,将为每只狗显示图片,等级,品种以及指向其个人详细信息页面的链接,我们将在下面创建它们。
通过浏览到应用程序的主页http:// localhost:4200来查看浏览器中的Dogs组件。 Angular应用程序应该向API发出请求,以获取狗列表并显示它们!
注意:我们还包括了<app-comments>
组件。 由于我们已经生成了此组件,但尚未实现其功能,因此它应该在用户界面中显示为“注释有效!”的文本。
要测试错误处理,可以停止API服务器(服务器的命令提示符或终端中的Ctrl+c
)。 然后尝试重新加载页面。 由于无法访问API,因此应该显示错误组件,并且我们应该在浏览器控制台中看到相应的错误:
带有路由参数的狗详细信息
接下来,我们将实现我们的Dog组件。 该路由组件用作每条狗的详细信息页面。 在本教程的第一部分中,我们已经设置了Dog模块架构以及路由和延迟加载 。 我们现在要做的就是执行!
提醒:您可能从第1部分中回想起,狗详细信息页面受AuthGuard
路由保护器保护 。 这意味着访问者必须经过身份验证才能访问页面。 此外, API调用需要访问令牌才能返回数据。
狗组件类
打开dog.component.ts
类文件并添加:
// src/app/dog/dog/dog.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { ApiService } from '../../core/api.service';
import { DogDetail } from './../../core/dog-detail';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { tap, catchError } from 'rxjs/operators';
@Component({
selector: 'app-dog',
templateUrl: './dog.component.html',
styles: [`
.dog-photo {
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: cover;
min-height: 250px;
width: 100%;
}
`]
})
export class DogComponent implements OnInit, OnDestroy {
paramSub: Subscription;
dog$: Observable<DogDetail>;
loading = true;
error: boolean;
constructor(
private route: ActivatedRoute,
private api: ApiService,
private title: Title
) { }
ngOnInit() {
this.paramSub = this.route.params
.subscribe(
params => {
this.dog$ = this.api.getDogByRank$(params.rank).pipe(
tap(val => this._onNext(val)),
catchError((err, caught) => this._onError(err, caught))
);
}
);
}
private _onNext(val: DogDetail) {
this.loading = false;
}
private _onError(err, caught): Observable<any> {
this.loading = false;
this.error = true;
return Observable.throw('An error occurred fetching detail data for this dog.');
}
getPageTitle(dog: DogDetail): string {
const pageTitle = `#${dog.rank}: ${dog.breed}`;
this.title.setTitle(pageTitle);
return pageTitle;
}
getImgStyle(url: string) {
return `url(${url})`;
}
ngOnDestroy() {
this.paramSub.unsubscribe();
}
}
该组件与我们的Dogs清单组件非常相似,只是有一些主要区别。
我们将导入必要的依赖项,并在类中私下使用ApiService
和Title
服务。
狗细节组件依赖于路由参数来确定我们需要为获取数据,狗。 route参数与十种最受欢迎的狗列表中的所需狗的排名相匹配,如下所示:
# URL for dog #2:
http://localhost:4200/dog/2
为了在组件类中访问此参数,我们需要导入ActivatedRoute接口 ,将其传递给构造函数,然后订阅激活的路由的observable params
。
然后,我们可以将rank
参数传递给我们的getDogByRank$()
API服务方法。 当组件被销毁时,我们还应取消订阅可观察到的路由参数。 我们的dog$
可观察到的可以使用类似于我们的Dogs列表组件的tap
和catchError
处理程序。
我们还需要两种方法来帮助我们的模板。
getPageTitle()
方法使用API数据来生成包含狗的等级和品种的页面标题。
getImgStyle()
方法使用API数据返回背景图像CSS值。
狗组件模板
现在,让我们在dog.component.html
模板中使用以下方法:
<!-- src/app/dog/dog/dog.component.html -->
<ng-template #noDog>
<app-loading *ngIf="loading"></app-loading>
<app-error *ngIf="error"></app-error>
</ng-template>
<div *ngIf="dog$ | async as dog; else noDog">
<h1 class="text-center">{{ getPageTitle(dog) }}</h1>
<div class="row align-items-center pt-2">
<div class="col-12 col-sm-6">
<div
class="dog-photo rounded mb-2 mb-sm-0"
[style.backgroundImage]="getImgStyle(dog.image)"></div>
</div>
<ul class="list-unstyled col-12 col-sm-6">
<li><strong>Group:</strong> {{ dog.group }}</li>
<li><strong>Personality:</strong> {{ dog.personality }}</li>
<li><strong>Energy Level:</strong> {{ dog.energy }}</li>
</ul>
</div>
<div class="row">
<div class="col">
<p class="lead mt-3" [innerHTML]="dog.description"></p>
<p class="clearfix">
<a routerLink="/" class="btn btn-link float-left">← Back</a>
<a
class="btn btn-primary float-right"
[href]="dog.link"
target="_blank">{{ dog.breed }} AKC Info</a>
</p>
</div>
</div>
</div>
总体而言,此模板的外观和功能与“狗列表”组件模板相似,不同之处在于,我们不对数组进行迭代。 相反,我们只显示一只狗的信息,页面标题是动态生成的,而不是静态生成的。 我们将在Bootstrap CSS类的帮助下,使用可观察对象发出的dog
数据(来自dog$ | async as dog
)显示详细信息。
完成后,组件在浏览器中应如下所示:
要进入任何狗的详细信息页面, AuthGuard
都会提示未经AuthGuard
验证的用户首先登录。 他们通过身份验证后,将被重定向到其请求的详细信息页面。 试试看!
注释模型类
现在我们的狗列表和详细信息页面已完成,是时候添加实时评论了!
我们要做的第一件事是建立注释的形状,以及初始化新注释实例的方法。 让我们在Angular应用中实现comment.ts
类:
// src/app/comments/comment.ts
export class Comment {
constructor(
public user: string,
public uid: string,
public picture: string,
public text: string,
public timestamp: number
) {}
// Workaround because Firestore won't accept class instances
// as data when adding documents; must unwrap instance to save.
// See: https://github.com/firebase/firebase-js-sdk/issues/311
public get getObj(): object {
const result = {};
Object.keys(this).map(key => result[key] = this[key]);
return result;
}
}
与我们的Dog
和DogDetail
模型不同,我们的Comment
模型是一个类 ,而不是一个接口 。 我们最终将在注释表单组件中初始化Comment
实例,为此,必须有一个类。 另外,Firestore在将文档添加到集合时仅接受常规JS对象,因此我们需要在类中添加一个将实例解包到对象的方法。 另一方面,接口仅提供对象的描述 。 这足以满足Dog
和DogDetail
,但不足以进行Comment
。
渲染后,我们希望注释看起来像这样:
如您所见,每个评论都有一个用户名,图片,评论文本以及日期和时间。 注释还需要一个唯一标识符,在数据中以uid
。 此唯一的ID确保用户有权删除自己的评论,但不能删除其他人留下的评论。
现在,我们已经对注释的外观有了一定的了解,让我们开始设置Firebase Firestore规则。
Firebase Cloud Firestore和规则
我们将使用Firebase的Cloud Firestore数据库存储应用程序的评论。 Cloud Firestore是NoSQL,灵活,可扩展,由云托管的数据库,可提供实时功能。 在撰写本文时,Firestore处于beta版,但它是所有新的移动和Web应用程序的推荐数据库。 您可以在此处阅读有关在实时数据库(RTDB)和Cloud Firestore之间进行选择的更多信息。
提醒:如果您需要快速入门Firebase产品,请重新阅读如何使用Auth0 –第1部分:Firebase和Auth0来认证Firebase和Angular 。
Firestore将数据组织为集合中的 文档 。 如果您有像MongoDB这样的面向文档的NoSQL数据库的经验,则应该熟悉此数据模型。 现在,选择Cloud Firestore作为我们的数据库。
- 登录到在本教程的第1部分中创建的Firebase项目 。
- 单击侧边栏菜单中的数据库 。
- 在数据库页面标题旁边的下拉列表中,选择Cloud Firestore 。
添加收藏夹和第一个文档
默认情况下,将显示“ 数据”选项卡,并且数据库中目前没有任何内容。 让我们添加集合和文档,以便我们可以在Angular中查询数据库并返回某些内容。
点击+添加收藏集 。 为您的收藏comments
命名,然后单击“ 下一步”按钮。 系统将提示您添加第一个文档。
在文档ID字段中,点击自动ID 。 这将自动为您填充一个ID。 接下来,添加我们先前在comment.ts
模型中建立的字段,其中包含适当的类型和一些占位符数据。 我们只需要此种子文档,直到我们知道清单在Angular应用程序中正确呈现为止,然后我们可以使用Firebase控制台将其删除,并使用前端的表单正确输入评论。
但是,由于我们还没有构建表单,因此种子数据会有所帮助。 输入正确的字段和类型后,可以随意填充值。 这是一个建议:
user <string>: Test User
uid <string>: abc-123
picture <string>: https://cdn.auth0.com/avatars/tu.png
text <string>: This is a test comment from Firebase console.
timestamp <number>: 1514584235257
注意:一旦设置了Firebase安全规则, 带有uid
值的注释将 不会对任何经过身份验证的真实用户进行验证。 如果我们以后想删除种子文件,则需要使用Firebase控制台将其删除。 正如您将在以下规则中看到的那样,我们无权使用Angular应用程序中的SDK方法将其删除。
输入假用户的评论后,单击“ 保存”按钮。 新的集合和文档应填充在数据库中。 这提供了我们可以在Angular应用程序中查询的数据。
Firebase规则
接下来,让我们设置Firestore数据库的安全性。 现在切换到“ 规则”选项卡。
Firebase安全规则提供了后端安全性和验证 。 在应用程序的Node API中,我们验证了用户是否已使用Auth0和JWT身份验证中间件来访问端点 。 我们已经在API和Angular应用中设置了Firebase身份验证,并且将使用规则功能来授权数据库后端的权限。
规则是一种表达式,可以评估该表达式以确定是否允许请求执行所需的操作。 — Cloud Firestore安全规则参考
在Firebase数据库规则编辑器中添加以下代码。 我们将在下面详细介绍。
// Firebase Database Rules for Cloud Firestore
service cloud.firestore {
match /databases/{database}/documents {
match /comments/{document=**} {
allow read: if true;
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.uid
&& request.resource.data.text is string
&& request.resource.data.text.size() <= 200;
allow delete: if request.auth != null
&& request.auth.uid == resource.data.uid;
}
}
}
Firestore具有规则请求方法 : read
和write
。 读取包括get
和list
操作。 写入包括create
, update
和delete
操作。 我们将实现read
, create
和delete
规则。
注意:我们不会在我们的应用程序中添加评论编辑功能,因此不包括update
。 但是,如果您想自己添加此功能,请随时添加update
规则!
当用户请求match
文档路径match
时,将执行规则。 路径可以全称,也可以使用通配符。 我们的规则适用于我们创建的comments
集合中的所有文档。
我们希望所有人 (匿名用户和经过身份验证的用户)都能阅读评论。 因此, allow read
的条件就是if true
。
我们只希望经过身份验证的用户能够创建新评论。 我们将验证用户是否已登录,并确保要保存的数据具有与用户的身份验证uid
(Firebase规则中的request.auth.uid
)匹配的uid
属性。 另外,我们可以在此处进行一些字段验证。 我们将检查请求的数据是否具有text
属性,该属性是字符串,并且为200个字符或更少(我们还将很快在Angular应用中添加此验证)。
最后,我们只希望用户能够删除自己的评论。 如果通过身份验证的用户的UID使用resource.data.uid
匹配现有注释的uid
属性,我们可以allow delete
。
注意:您可以在Firebase文档中了解有关request和resource关键字的更多信息。
评论组件
现在我们的数据库已经准备好了,是时候返回我们的Angular应用程序并实现实时注释了!
我们要做的第一件事是显示评论。 我们希望评论能够实时异步更新,因此让我们探索如何使用Cloud Firestore数据库和angularfire2 SDK进行操作 。
注释组件类
我们已经为Comments模块创建了架构,所以让我们开始构建comments.component.ts
:
// src/app/comments/comments/comments.component.ts
import { Component } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';
import { Observable } from 'rxjs/Observable';
import { map, catchError } from 'rxjs/operators';
import { Comment } from './../comment';
import { AuthService } from '../../auth/auth.service';
@Component({
selector: 'app-comments',
templateUrl: './comments.component.html',
styleUrls: ['./comments.component.css']
})
export class CommentsComponent {
private _commentsCollection: AngularFirestoreCollection<Comment>;
comments$: Observable<Comment[]>;
loading = true;
error: boolean;
constructor(
private afs: AngularFirestore,
public auth: AuthService
) {
// Get latest 15 comments from Firestore, ordered by timestamp
this._commentsCollection = afs.collection<Comment>(
'comments',
ref => ref.orderBy('timestamp').limit(15)
);
// Set up observable of comments
this.comments$ = this._commentsCollection.snapshotChanges()
.pipe(
map(res => this._onNext(res)),
catchError((err, caught) => this._onError(err, caught))
);
}
private _onNext(res) {
this.loading = false;
this.error = false;
// Add Firestore ID to comments
// The ID is necessary to delete specific comments
return res.map(action => {
const data = action.payload.doc.data() as Comment;
const id = action.payload.doc.id;
return { id, ...data };
});
}
private _onError(err, caught): Observable<any> {
this.loading = false;
this.error = true;
return Observable.throw('An error occurred while retrieving comments.');
}
onPostComment(comment: Comment) {
// Unwrap the Comment instance to an object for Firestore
// See https://github.com/firebase/firebase-js-sdk/issues/311
const commentObj = <Comment>comment.getObj;
this._commentsCollection.add(commentObj);
}
canDeleteComment(uid: string): boolean {
if (!this.auth.loggedInFirebase || !this.auth.userProfile) {
return false;
}
return uid === this.auth.userProfile.sub;
}
deleteComment(id: string) {
// Delete comment with confirmation prompt first
if (window.confirm('Are you sure you want to delete your comment?')) {
const thisDoc: AngularFirestoreDocument<Comment> = this.afs.doc<Comment>(`comments/${id}`);
thisDoc.delete();
}
}
}
首先,我们将导入必要的angularfire2依赖项以使用Firestore,集合和文档。 我们还需要catchError
Observable
, map
和catchError,我们的Comment
模型和AuthService
。
接下来,我们将宣布成员。 私有_commentsCollection
是一个Firestore集合,其中包含Comment
形状的项目。 comments$
observable是一个流,该流的值采用Comment
数组的形式。 然后,我们有了通常的loading
和error
属性。
将AngularFirestore
和AuthService
传递给构造函数后,我们需要从Cloud Firestore获取集合数据。 我们将使用angularfire2方法collection()
进行此操作,将Comment
指定为类型,传递我们集合的名称( comments
), 按timestamp
对结果进行排序,并限制为最后15条注释。
接下来,我们将使用_commentsCollection
创建可观察的_commentsCollection
comments$
。 我们将使用map()
和catchError()
RxJS运算符来处理发出的数据和错误。
在我们的_onNext()
私有处理程序中,我们将loading
和error
设置为false
。 我们还将Firestore文档ID添加到comments$
流发出的数组中的每个项目中。 我们需要这些ID,以便用户删除个别评论。 为了将ID添加到发出的值,我们将使用snapshotChanges()
方法访问meta 。 然后,我们可以使用Spread运算符将文档id
map()
到返回的数据中。
注意:您可能会注意到,在我们的狗或狗可观察对象的成功方法中,我们没有将error
设置为false
,但是我们在这里这样做。 每当 任何用户实时添加评论时, 评论流就会发出一个值 。 因此,作为响应,我们可能需要异步重置错误状态。
私有_onError()
处理函数应该从我们的其他组件中看起来非常熟悉。 它设置loading
和error
属性并引发错误。
当用户使用评论表单组件(稍后将构建)提交评论时,将运行onPostComment()
方法。 onPostComment()
有效负载将包含一个Comment
实例,该实例包含用户的注释数据,然后需要将其解包装为普通对象才能保存在Firestore中。 我们将使用Angular Firestore add()
方法保存解包后的注释对象。
canDeleteComment()
方法检查当前用户是否为任何给定注释的所有者。 如果他们创建了评论,则也可以将其删除。 此方法验证登录用户的userProfile.sub
属性是否与注释的uid
相匹配。
当用户单击图标删除评论时, deleteComment()
方法将运行。 此方法将打开一个确认对话框,以确认操作,如果确认,则使用id
参数从Firestore集合中删除正确的注释文档。 (这就是为什么我们在映射observations comments$
observable发出的值时需要在数据中添加文档id
的原因。)
注意:请记住,我们的Firestore规则还阻止用户删除他们未创建的评论。 我们应始终确保访问权限强制在前端和适当的安全后端两种 。
注释组件模板
现在让我们在UI中使用我们的类功能。 打开comments.component.html
文件并添加:
<!-- src/app/comments/comments/comments.component.html -->
<section class="comments py-3">
<h3>Comments</h3>
<ng-template #noComments>
<p class="lead" *ngIf="loading">
<app-loading [inline]="true"></app-loading>Loading comments...
</p>
<app-error *ngIf="error"></app-error>
</ng-template>
<div *ngIf="comments$ | async; let commentsList; else noComments">
<ul class="list-unstyled">
<li *ngFor="let comment of commentsList" class="pt-2">
<div class="row mb-1">
<div class="col">
<img [src]="comment.picture" class="avatar rounded">
<strong>{{ comment.user }}</strong>
<small class="text-info">{{ comment.timestamp | date:'short' }}</small>
<strong>
<a
*ngIf="canDeleteComment(comment.uid)"
class="text-danger"
title="Delete"
(click)="deleteComment(comment.id)">×</a>
</strong>
</div>
</div>
<div class="row">
<div class="col">
<p class="comment-text rounded p-2 my-2" [innerHTML]="comment.text"></p>
</div>
</div>
</li>
</ul>
<div *ngIf="auth.loggedInFirebase; else logInToComment">
<app-comment-form (postComment)="onPostComment($event)"></app-comment-form>
</div>
<ng-template #logInToComment>
<p class="lead" *ngIf="!auth.loggedIn">
Please <a class="text-primary" (click)="auth.login()">log in</a> to leave a comment.
</p>
</ng-template>
</div>
</section>
我们将主要使用Bootstrap类来设置注释的样式,然后再添加一些自定义CSS。 我们的注释模板(如我们的狗和狗组件模板)具有一个<ng-template>
,并将异步管道与NgIfElse一起使用以显示适当的UI。
评论列表应显示评论的picture
(作者的用户头像),用户name
以及使用DatePipe格式化的timestamp
。 我们将注释的uid
传递给canDeleteComment()
方法,以确定是否应显示删除链接。 然后,使用绑定到innerHTML
属性来显示注释text
。
最后,我们将创建元素以显示评论表单或一条消息,指示用户如果要发表评论,请登录。
注意:当用户提交评论时,我们的<app-comment-form>
将使用事件绑定来发出名为postComment
的事件。 CommentsComponent
类侦听该事件,并使用我们创建的onPostComment()
方法处理该事件,并使用$event
有效负载将提交的注释保存到Firestore数据库。 在下一节中创建表单时,我们将连接(postComment)
事件。
注释组件CSS
最后,打开comments.component.css
文件,让我们在注释列表中添加一些样式:
/* src/app/comments/comments/comments.component.css */
.avatar {
display: inline-block;
height: 30px;
}
.comment-text {
background: #eee;
position: relative;
}
.comment-text::before {
border-bottom: 10px solid #eee;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
content: '';
display: block;
height: 1px;
position: absolute;
top: -10px; left: 9px;
width: 1px;
}
评论表格组件
现在我们有了实时更新的评论列表,我们需要能够在前端添加新评论。
注释表单组件类
打开comment-form.component.ts
文件,让我们开始吧:
// src/app/comments/comment-form/comment-form.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Comment } from './../../comment';
import { AuthService } from '../../../auth/auth.service';
@Component({
selector: 'app-comment-form',
templateUrl: './comment-form.component.html'
})
export class CommentFormComponent implements OnInit {
@Output() postComment = new EventEmitter<Comment>();
commentForm: Comment;
constructor(private auth: AuthService) { }
ngOnInit() {
this._newComment();
}
private _newComment() {
this.commentForm = new Comment(
this.auth.userProfile.name,
this.auth.userProfile.sub,
this.auth.userProfile.picture,
'',
null);
}
onSubmit() {
this.commentForm.timestamp = new Date().getTime();
this.postComment.emit(this.commentForm);
this._newComment();
}
}
如前所述,我们需要从此组件向父CommentsComponent
发出一个事件,该事件将新注释发送到Firestore。 CommentFormComponent
负责使用从经过身份验证的用户及其表单输入中收集的适当信息构造Comment
实例,并将该数据发送给父级。 为了发出postComment
事件,我们将导入Output
和EventEmitter
。 我们还需要我们的Comment
类和AuthService
来获取用户数据。
我们的评论表单组件的构件包括输出装饰 ( postComment
),其为EventEmitter类型的Comment
,并commentForm
,这将是一个实例Comment
来存储表单数据。
在ngOnInit()
方法中,我们将使用私有_newComment()
方法创建一个新的Comment
实例。 此方法将本地commentForm
属性设置为带有已验证用户name
, sub
和picture
的Comment
的新实例。 注释text
为空字符串, timestamp
设置为null
(提交表单时将添加timestamp
)。
当在模板中提交评论表单时,将执行onSubmit()
方法。 此方法添加timestamp
并发出带有commentForm
数据作为其有效载荷的postComment
事件。 它还调用_newComment()
方法来重置评论表单。
注释表单组件模板
打开comment-form.component.html
文件并添加以下代码:
<!-- src/app/comments/comment-form/comment-form.component.html -->
<form (ngSubmit)="onSubmit()" #tplForm="ngForm">
<div class="row form-inline m-1">
<input
type="text"
class="form-control col-sm-10 mb-2 mb-sm-0"
name="text"
[(ngModel)]="commentForm.text"
maxlength="200"
required>
<button
class="btn btn-primary col ml-sm-2"
[disabled]="!tplForm.valid">Send</button>
</div>
</form>
评论表单模板非常简单。 表单的唯一字段是文本输入,因为所有其他注释数据(如名称,图片,UID等)都动态添加到了类中。 我们将使用简单的模板驱动表单来实现我们的评论表单。
<form>
元素侦听(ngOnSubmit)
事件,我们将使用onSubmit()
方法处理该事件。 我们还将添加一个名为#tplForm
的模板引用变量,并将其设置为ngForm
。 这样,我们可以在模板本身中访问表单的属性。
<input>
元素应具有绑定到commentForm.text
的[(ngModel)]
。 这是我们在用户在表单字段中键入内容时要更新的属性。 回想一下,我们将Firestore规则设置为接受200个字符或更少的注释文本,因此我们将此maxlength
和required
属性一起添加到前端,以便用户无法提交空注释。
最后,如果表单无效,则应[disabled]
<button>
提交表单的<button>
。 我们可以使用添加到<form>
元素的tplForm
参考变量来参考valid
属性。
实时评论
在浏览器中验证注释是否按预期显示。 到目前为止,唯一的注释应该是我们直接在Firebase中添加的种子注释。 提取并呈现后,我们的评论列表应如下所示:
如果用户已通过身份验证,则应显示评论表单。 登录并尝试添加评论。
删除种子评论
用户可以删除自己的评论。 如果用户是评论的所有者,则评论的日期和时间旁边应显示一个红色的x
。 单击此删除图标提示确认,然后实时删除评论。
请记住,我们在Firebase中添加的种子文档无法在Angular应用中删除,因为其uid
属性与任何实际用户的数据都不匹配。 让我们现在手动将其删除。
打开Firebase控制台并查看Firestore comments
集合。 查找包含种子注释的文档。 使用右上方的菜单下拉菜单,选择删除文档以将其删除:
现在,添加到我们数据库中的所有注释都应该可以由其作者在后端删除。
在Angular App中添加评论
添加注释后,它们应该会显示出来,这很棒,但是并不能真正显示Firestore数据库的真正实时性。 我们也可以使用传统的服务器和数据库在UI中添加注释而无需刷新,只需更新视图即可。
为了真正看到我们的实时数据库在工作,请在第二个浏览器中打开该应用程序,然后使用其他登录名进行身份验证。 在两个浏览器都可见的情况下,在一个浏览器中添加评论。 它将同时出现在第二个浏览器中。
这就是Firebase的实时数据库可以做到的!
结论
恭喜你! 您现在有了一个Angular应用,该应用通过Auth0对Firebase进行身份验证,并基于可扩展的体系结构构建。
我们的教程的第一部分, 如何使用Auth0验证Firebase和Angular:第1部分 ,内容包括:
- Auth0和Firebase的介绍和设置
- 实施一个安全的Node API,以生成自定义Firebase令牌并为我们的应用程序提供数据
- 具有模块和延迟加载的Angular应用程序体系结构
- 具有服务和路由保护的Auth0的角度身份验证
- 共享的Angular组件和API服务。
本教程的第二部分介绍:
- 使用异步管道和NgIfElse显示数据
- 使用路线参数
- 用类建模数据
- Firebase Cloud Firestore数据库和安全规则
- 使用angularfire2在Angular中实现Firestore数据库
- 具有组件交互作用的简单模板驱动形式。
角度测试资源
如果您有兴趣了解有关在Angular中进行测试的更多信息,而本教程未涵盖此内容,请查看以下一些资源:
其他资源
您可以在以下位置找到有关Firebase,Auth0和Angular的更多资源:
下一步是什么?
希望您学到了很多有关使用Angular构建可扩展应用程序以及使用自定义令牌对Firebase进行身份验证的知识。 如果您正在寻找可以扩展我们所构建内容的想法,请参考以下建议:
- 实施不适当的语言过滤器以进行评论
- 实施授权角色以创建具有删除其他人评论的权限的管理员用户
- 添加功能以支持评论编辑
- 使用其他Firestore集合向单个狗详细信息页面添加评论
- 添加测试
- 以及更多!
From: https://www.sitepoint.com/authenticating-firebase-angular-auth0-2/