Angular5 和 Firebase 全栈开发实用指南(二)
原文:
zh.annas-archive.org/md5/f113aa7bff00c98a8b38bd392d37f2d0译者:飞龙
第五章:创建用户个人资料页面
在本章中,我们将编写一个用户个人资料组件。我们将介绍 RxJS(ReactiveX),这是一个流行的异步编程库。在本节中,我们将使用 RxJS 的 Observable 将认证模块的组件传递到用户模块的组件。我们将使用这个传递的用户模型来填充用户个人资料组件。我们将编辑用户数据并更新 Firebase 认证和数据库。作为编辑的一部分,我们将实现一个可重用的编辑组件,该组件使用 bootstrap 模态来获取用户输入。最后,我们将看到当密码更改时 Firebase 会话令牌的生命周期。
在本章中,我们将涵盖以下主题:
-
RxJS 简介
-
在模块组件之间传递数据
-
SASS 简介
-
创建用户个人资料组件
-
增强更新操作的服务
-
创建一个编辑对话框组件
-
更新操作的 Firebase 会话
RxJS 简介
RxJS 是一个流行的异步和基于事件的编程库。在本节中,我们将仅介绍这个库的基础知识,以便您理解使用这个库的真实原因。更多详情,您可以参考 RxJS 的官方站点 reactivex.io/rxjs/。
这里是该库的一些关键术语:
可观察的: 这是一个可以消费的值或事件的集合。例如,可观察的可以是一个数字数组的集合:
let observable = Rx.Observable.from([1, 2, 3]);
订阅: 要从可观察的中读取数据,我们需要订阅,然后通过观察者传递事件或值:
let subscription = observable.subscribe(x => console.log(x));
subject: 这是可观察的的扩展,用于向多个观察者广播事件或值。这个例子将在我们应用程序的使用案例中介绍。这是对 RxJS 库的非常基础的理解。
在模块组件之间传递数据
我们在前一章中完成了我们的认证模块。在本节中,我们将介绍在模块组件之间传递数据,这是我们应用程序开发的一个重要部分。在实现一个网络应用程序时,我们总是面临如何从一个组件模块传递数据到另一个组件模块的问题。我们的初步想法是将模型存储在一个公共应用程序类中,例如单例类,然后在其他组件中检索它。Angular 提供了许多传递数据的方法,我们已经在 Angular 绑定中提到了这一点。当我们在同一模块中具有父子关系的组件时,这种方法很有用。
单例类是一种软件设计模式,它限制类的实例化只能有一个对象。这个单一的对象对应用程序的所有组件都是可用的。
我们在 service 类中使用 RxJS 库的 subject 来将数据传递到不同模块的组件。这种设计有助于创建一个独立的模块。
执行以下步骤以将认证模块的数据传递到用户模块:
- 存储用户模型:第一步是使用
service类中的subject存储数据。我们在用户服务中存储用户模型。我们使用BehaviorSubject,它是subject类的扩展,用于存储用户模型。
RxJS 的行为主题向订阅者发出最新的数据。
null, and this will be populated with the latest user model:
private subject: BehaviorSubject<User> = new BehaviorSubject(null);
我们在 subject 中保存用户模型。我们从登录和注册组件调用此方法:
public saveUser(user: User){
this.subject.next(user);
}
- 在服务类中创建方法:我们可以在
UserService类中创建getSavedUser()。此方法返回subject,调用者需要订阅或使用getValue()方法来检索保存的User对象:
public getSavedUser(): BehaviorSubject<User>{
return this.subject;
}
- 在组件类中检索用户模型:我们可以使用
getValue()方法从subject中检索值。您也可以订阅并检索用户模型:
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
}
目前为止,这是完整的 user.service.ts:
import {Injectable} from '@angular/core';
import {AngularFireDatabase} from 'angularfire2/database';
import {User} from './user';
import {USERS_CHILD} from './database-constants';
import {Observable} from 'rxjs/Observable';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
/**
* User service
*
*/
@Injectable()
export class UserService {
private subject: BehaviorSubject<User> = new
BehaviorSubject(null);
/**
* Constructor
*
* @param {AngularFireDatabase} fireDb provides the functionality
for Firebase Database
*/
constructor(private fireDb: AngularFireDatabase) {
}
public addUser(user: User): void {
this.fireDb.object(`${USERS_CHILD}/${user.uid}`).set(user);
this.saveUser(user);
}
public getUser(uid: string): Observable<User> {
return this.fireDb.object<User>
(`${USERS_CHILD}/${uid}`).valueChanges();
}
public saveUser(user: User) {
this.subject.next(user);
}
public getSavedUser(): BehaviorSubject<User> {
return this.subject;
}
public updateEmail(user: User, newEmail: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({email:
newEmail});
this.saveUser(user);
}
public updateMobile(user: User, mobile: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({mobile:
mobile});
this.saveUser(user);
}
public updateName(user: User, name: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({name:
name});
this.saveUser(user);
}
}
SASS 简介
SASS(系统化出色的样式表) 是一个 CSS 预处理器,它为现有的 CSS 添加了更多功能。它有助于添加变量、嵌套规则、混入、继承等。这个特性有助于以更系统化的方式组织我们的样式表。
SASS 提供了两种风味:
-
SASS
-
SCSS
SASS 是两种语法中较老的一种,而 SCSS 是更常用的一种。在这本书中,我们使用了 SCSS 格式。一些受支持的功能如下:
- 部分(Partial):部分是一个可重用的 CSS 元素,可以包含在其他 SCSS 文件中。这有助于将我们的 CSS 模块化成更小的可重用元素,跨越 SCSS 文件。一个部分的文件名包含一个前导下划线,这样编译器就知道这是一个部分文件,不会转换为 CSS 文件,例如,
_shared.scss。我们可以使用@import指令将部分文件导入到其他组件中。
以下是一个在其他 SCSS 文件中包含部分的示例,如下所示:
@import "../../shared/shared";
.chat-message-main-container {
}
- 扩展:这类似于高级编程语言中的继承。我们将编写 CSS 属性在公共类选择器中,然后在其他类选择器中扩展它。
以下是一个 @extend 的示例:
.user-profile{
margin-top: 10px;
}
.user-profile-name{
@extend .user-profile;
border-color: green;
}
- 混入(Mixin):这用于将可以在整个应用程序中使用的声明分组。这类似于类中的方法签名:
@mixin message-pointer($rotate , $skew) {
transform: rotate($rotate) skew($skew);
-moz-transform: rotate($rotate) skew($skew);
-ms-transform: rotate($rotate) skew($skew);
-o-transform: rotate($rotate) skew($skew);
-webkit-transform: rotate($rotate) skew($skew);
}
创建用户配置文件组件
在本节中,我们将创建一个用户配置文件组件。在成功登录或注册后,用户将被导向其配置文件页面。此组件显示用户信息,如姓名和电子邮件,并提供编辑功能以更改用户信息。以下创建用户配置文件组件的步骤:
- 创建用户配置文件模板:第一步是创建用户配置文件页面的模板。在这个模板中,我们将显示姓名、电子邮件、手机和密码。这些信息中的每一项都有一个编辑按钮。
首先,我们创建一个包含所有用户信息元素的 div 容器。我们使用 *ngIf 来检查用户数据:
<div class="user-profile" *ngIf="user">
</div>
第二步,我们在 div 中为每个用户信息创建 div,包括 label、user.name 和 Edit 按钮:
<div class="user-profile-name">
<label>Name: </label>
<div class="user-profile-name-value">{{user?.name}}</div>
<button (click)="onNameChange()" type="button" class="btn btn-
default btn-sm user-profile-name-btn">
Edit
</button>
</div>
下面是完整的 user-profile.component.html:
<div class="user-profile" *ngIf="user">
<div class="person-icon">
<img [src]="profileImage" style="max-width: 100%; max-height:
100%;">
</div>
<div class="user-profile-name">
<label>Name: </label>
<div class="user-profile-name-value">{{user?.name}}</div>
<button (click)="onNameChange()" data-toggle="modal" data-
target="#editModal" type="button"
class="btn btn-default btn-sm user-profile-name-btn">
Edit
</button>
</div>
<div class="user-profile-email">
<label>Email: </label>
<div class="user-profile-email-value">{{user?.email}}</div>
<button (click)="onEmailChange()" data-toggle="modal" data-
target="#editModal" type="button"
class="btn btn-default btn-sm">
Edit
</button>
</div>
<div class="user-profile-mobile">
<label>Mobile: </label>
<div class="user-profile-mobile-value">{{user?.mobile}}</div>
<button (click)="onMobileChange()" data-toggle="modal" data-
target="#editModal" type="button"
class="btn btn-default btn-sm user-profile-mobile-btn">
Edit
</button>
</div>
<div class="user-profile-password">
<label>Password: </label>
<div class="user-profile-password-value">****</div>
<button (click)="onPasswordChange()" data-toggle="modal" data-
target="#editModal" type="button"
class="btn btn-default btn-sm user-profile-password-btn">
Edit
</button>
</div>
<div class="user-profile-btn">
<button type="button" (click)='onLogout()' class="btn btn-
info">LOGOUT</button>
</div>
</div>
使用样式表对用户信息进行对齐。我们使用 SCSS 的嵌套来从容器到子选择器的类选择器:
.user-profile{
width: 50%;
margin-left: 24px;
margin-top: 10px;
.user-profile-name{
text-align: left;
margin-top: 10px;
.user-profile-name-value{
display: inline-block;
margin-left: 10px;
}
.user-profile-name-btn{
margin-left: 100px;
}
}
}
下面的 user-profile.component.scss 代码是完整的:
.user-profile{
width: 50%;
margin-left: 24px;
margin-top: 10px;
.user-profile-name{
text-align: left;
margin-top: 10px;
.user-profile-name-value{
display: inline-block;
margin-left: 10px;
}
.user-profile-name-btn{
margin-left: 100px;
}
}
.user-profile-email{
text-align: left;
margin-top: 20px;
.user-profile-email-value{
display: inline-block;
margin-left: 10px;
}
}
.user-profile-mobile{
text-align: left;
margin-top: 20px;
.user-profile-mobile-value{
display: inline-block;
margin-left: 10px;
}
.user-profile-mobile-btn{
margin-left: 110px;
}
}
.user-profile-password{
text-align: left;
margin-top: 20px;
.user-profile-password-value{
display: inline-block;
margin-left: 10px;
}
.user-profile-password-btn{
margin-left: 154px;
}
}
.user-profile-btn{
margin-top: 20px;
}
}
- 创建用户资料组件: 我们将在组件中定义检索用户模型和处理事件的逻辑。
第一步是从 service 类中检索用户模型。我们实现 onInit 接口并重写 ngOnInit 来检索用户模型,如下所示:
export class UserProfileComponent implements OnInit {
private user: User;
constructor(private authService: AuthenticationService,
private userService: UserService,
private router: Router) {
}
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
}
}
OnInit 是一个生命周期钩子接口,由 Angular 框架管理。它有一个 ngOnInit() 方法,当组件和指令完全初始化时会被调用。
下面的 user-profile.component.ts 是目前的完整版本:
import {Component, OnInit, ViewChild} from '@angular/core';
import {AuthenticationService} from '../../services/authentication.service';
import {Router} from '@angular/router';
import {User} from '../../services/user';
import {UserService} from '../../services/user.service';
import {EditDialogComponent} from '../../edit-dialog/edit-dialog.component';
import {EditType} from '../../edit-dialog/edit-details';
@Component({
selector: 'app-friends-userprofile',
styleUrls: ['user-profile.component.scss'],
templateUrl: 'user-profile.component.html'
})
export class UserProfileComponent implements OnInit {
profileImage: any = '../../../assets/images/person_edit.png';
user: User;
@ViewChild(EditDialogComponent) editDialog: EditDialogComponent;
constructor(private authService: AuthenticationService,
private userService: UserService,
private router: Router) {
}
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
}
onLogout(): void {
this.authService.signout().then(() => {
this.navigateToLogin();
});
}
navigateToLogin() {
this.router.navigateByUrl('/app-friends-login');
}
}
我们的用户资料页面视图应该是这样的:
增强更新操作的服务
在本节中,我们将增强现有的服务以提供用户信息的更新。作为此练习的一部分,我们将讨论如何更新 Firebase 身份验证和数据库。
我们将更新以下用户信息:
- 用户名: 此数据存储在 Firebase 数据库中,因此我们添加新的
updateAPI 来执行此操作。我们在用户服务中添加了updateName()方法,并在 Firebase 中更新存储的用户数据:
public updateName(user: User, name: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({name:
name});
this.saveUser(user);
}
- 用户电子邮件: 此数据存储在 Firebase 身份验证和数据库中,因此我们需要在两个地方更新它。
我们需要在我们的身份验证服务中添加一个 changeEmail() 方法:
public changeEmail(email: string): Promise<any> {
return this.angularFireAuth.auth.currentUser.updateEmail(email);
}
一旦在身份验证服务中完成此操作,我们就可以使用用户服务在 Firebase 数据库中更新新的电子邮件:
public updateEmail(user: User, newEmail: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({email:
newEmail});
this.saveUser(user);
}
现在编辑移动密码与前面的代码相同,你可以遵循以下代码。authentication.service.ts 和 user.service.ts 的更新版本如下:
import {Injectable} from '@angular/core';
import {AngularFireAuth} from 'angularfire2/auth';
/**
* Authentication service
*
*/
@Injectable()
export class AuthenticationService {
/**
* Constructor
*
* @param {AngularFireAuth} angularFireAuth provides the
functionality related to authentication
*/
constructor(private angularFireAuth: AngularFireAuth) {
}
public signup(email: string, password: string): Promise<any> {
return
this.angularFireAuth.auth.createUserWithEmailAndPassword(
email, password);
}
public login(email: string, password: string): Promise<any> {
return this.angularFireAuth.auth.signInWithEmailAndPassword(
email, password);
}
public resetPassword(email: string): Promise<any> {
return
this.angularFireAuth.auth.sendPasswordResetEmail(email);
}
public isAuthenticated(): boolean {
const user = this.angularFireAuth.auth.currentUser;
return user ? true : false;
}
public signout(): Promise<any>{
return this.angularFireAuth.auth.signOut();
}
public changeEmail(email: string): Promise<any> {
return
this.angularFireAuth.auth.currentUser.updateEmail(email);
}
public changePassword(password: string): Promise<any> {
return
this.angularFireAuth.auth.currentUser.updatePassword
(password);
}
}
下面的 user.service.ts 文件是更新后的版本:
import {Injectable} from '@angular/core';
import {AngularFireDatabase} from 'angularfire2/database';
import {User} from './user';
import {USERS_CHILD} from './database-constants';
import {Observable} from 'rxjs/Observable';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
/**
* User service
*
*/
@Injectable()
export class UserService {
private subject: BehaviorSubject<User> = new BehaviorSubject(null);
/**
* Constructor
*
* @param {AngularFireDatabase} fireDb provides the functionality
for Firebase Database
*/
constructor(private fireDb: AngularFireDatabase) {
}
public addUser(user: User): void {
this.fireDb.object(`${USERS_CHILD}/${user.uid}`).set(user);
this.saveUser(user);
}
public getUser(uid: string): Observable<User> {
return this.fireDb.object<User>
(`${USERS_CHILD}/${uid}`).valueChanges();
}
public saveUser(user: User) {
this.subject.next(user);
}
public getSavedUser(): BehaviorSubject<User> {
return this.subject;
}
public updateEmail(user: User, newEmail: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({email:
newEmail});
this.saveUser(user);
}
public updateMobile(user: User, mobile: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({mobile:
mobile});
this.saveUser(user);
}
public updateName(user: User, name: string): void {
this.fireDb.object(`${USERS_CHILD}/'${user.uid}`).update({name:
name});
this.saveUser(user);
}
}
创建编辑对话框组件
编辑对话框组件用于获取用户输入以更新 Firebase 中的用户信息。此组件被重复使用以获取所有其他用户详细信息的信息,如姓名、电子邮件、手机和密码。此组件包含在用户资料组件中,当用户点击编辑按钮时,编辑对话框会显示出来。
创建一个编辑 dialog 组件的步骤如下:
- 创建编辑对话框模板: 第一步是创建编辑对话框的模板。此模板包含标题、文本标题和一个输入框。
我们使用 Bootstrap 模态框创建一个编辑对话框。它有一个输入框来接收用户输入。
第一步是创建一个带有isVisible条件的div容器,并且当用户点击“编辑”按钮时,这个变量会动态变化:
<div *ngIf="isVisible" class="modal fade show in danger" id="editModal" role="dialog" />
我们使用表单元素来获取用户输入,它有一个submit按钮,如下所示:
<div class="modal-dialog">
<form name="form" (ngSubmit)="onSubmit(editFormData)"
#editFormData='ngForm'>
</form>
</div>
由于前面的模板也用于不同的编辑目的,我们需要动态更改标题、标题等文本。我们可以使用单向 Angular 绑定来分配变量:
<p>This will change your {{bodyTitle}}</p>
以下是完全的edit-dialog.component.html文件:
<div *ngIf="isVisible" class="modal fade in" id="editModal" role="dialog">
<div class="modal-dialog">
<form name="form" (ngSubmit)="onSubmit(editFormData)"
#editFormData='ngForm'>
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-
dismiss="modal">×</button>
<h4 class="modal-title">{{titleMessage}}</h4>
</div>
<div class="modal-body">
<p>This will change your {{bodyTitle}}</p>
<div class="form-group">
<label for="editDetail">{{bodyLabel}}
</label>
<input type="text" class="form-control"
name="editValue" (ngModel)="editValue"
id="editDetail"/>
</div>
</div>
<div class="modal-footer form-group">
<button type="submit" class="btn btn-default"
[disabled]="!editFormData.form.valid">
Edit
</button>
<button type="button" class="btn btn-default"
data-dismiss="modal"
(click)="hide()">Close</button>
</div>
</div>
</form>
</div>
</div>
- 创建编辑对话框组件:当用户点击“编辑”按钮时,此组件从用户资料组件接收一个变量输入。它接收用户在对话框中的输入,并将其传递给
EditDetails类以更新信息。
我们将使用构造函数模式从UserProfileComponent获取输入变量。
构造函数模式是一种用于创建复杂对象的创建型模式。这基本上是在类中的构造函数接受许多参数时使用。这减少了构造函数的复杂性。
在我们的情况下,我们需要参数来动态更改标题、label。我们将为每个变量输入创建多个方法——例如,对于标题,我们将创建一个setTitle()方法并返回this,即类的实例。这有助于在单行中链接方法调用:
public setTitle(title: string): EditDialogComponent {
this.titleMessage = title;
return this;
}
我们将需要使用show和hide方法切换isVisible变量,如下所示:
public show() {
this.isVisible = true;
}
public hide() {
this.isVisible = false;
}
现在这是完整的edit-dialog.component.ts文件:
import {Component, ViewChild} from '@angular/core';
import {AuthenticationService} from '../services/authentication.service';
import {UserService} from '../services/user.service';
import {User} from '../services/user';
import {EditDetails, EditType} from './edit-details';
@Component({
selector: 'app-edit-dialog',
templateUrl: './edit-dialog.component.html',
})
export class EditDialogComponent {
isVisible: boolean;
titleMessage: string;
bodyTitle: string;
bodyLabel: string;
editType: EditType;
editDetails: EditDetails;
constructor(private authService: AuthenticationService,
private userService: UserService) {
this.editDetails = new EditDetails(authService, userService);
}
public setTitle(title: string): EditDialogComponent {
this.titleMessage = title;
return this;
}
public setBodyTitle(bodyTitle: string): EditDialogComponent {
this.bodyTitle = bodyTitle;
return this;
}
public setBodyLabel(bodyLabel: string): EditDialogComponent {
this.bodyLabel = bodyLabel;
return this;
}
public setEditType(editType: EditType): EditDialogComponent {
this.editType = editType;
return this;
}
public show() {
this.isVisible = true;
}
public hide() {
this.isVisible = false;
}
private onSubmit(editFormData): void {
this.editDetails.edit(this.editType,
editFormData.value.editValue);
}
}
- 创建更新操作:当用户提供了更新所需的新数据并点击
submit时,将调用onSubmit()方法。对于每个更新操作,我们将调用EditDetails类的edit()方法。
现在这是完整的edit-details.ts文件:
import {AuthenticationService} from '../services/authentication.service';
import {UserService} from '../services/user.service';
import {User} from '../services/user';
export enum EditType {
NAME,
EMAIL,
MOBILE,
PASSWORD
}
export class EditDetails {
constructor(private authService: AuthenticationService,
private userService: UserService) {
}
public edit(editType: EditType, value: string) {
switch (editType) {
case EditType.NAME:
this.editName(value);
break;
case EditType.EMAIL:
this.editEmail(value);
break;
case EditType.MOBILE:
this.editMobile(value);
break;
case EditType.PASSWORD:
this.editPassword(value);
break;
}
}
private editName(name: string) {
const user: User = this.userService.getSavedUser().getValue();
user.name = name;
this.userService.updateName(user, name);
alert('Name changed successfully');
}
private editEmail(newEmail: string) {
this.authService.changeEmail(newEmail).then(() => {
const user: User =
this.userService.getSavedUser().getValue();
user.email = newEmail;
this.userService.updateEmail(user, newEmail);
alert('Email changed successfully');
}).catch(function (error) {
const errorMessage = error.message;
alert(errorMessage);
});
}
private editMobile(mobile: string) {
const user: User = this.userService.getSavedUser().getValue();
user.mobile = mobile;
this.userService.updateMobile(user, mobile);
alert('Mobile changed successfully');
}
private editPassword(value: string) {
const newPassword: string = value;
this.authService.changePassword(newPassword).then(() => {
alert('Password changed successfully');
}).catch(function (error) {
const errorMessage = error.message;
alert(errorMessage);
});
}
}
- 使用编辑对话框组件:最后,我们将在用户资料组件中使用编辑对话框组件。第一步是将此组件包含在
user-profile.component.html中,如下所示:
<div class="user-profile" *ngIf="user">
...
</div>
<app-edit-dialog></app-edit-dialog>
第二步是在user-profile.component.ts中初始化编辑对话框组件,如下所示:
export class UserProfileComponent implements OnInit {
@ViewChild(EditDialogComponent) editDialog: EditDialogComponent;
...
}
当用户点击任何“编辑”按钮时,我们需要初始化变量并调用show()方法:
onNameChange() {
this.editDialog.setTitle('Do you want to edit name?')
.setBodyTitle('name')
.setBodyLabel('Enter new name')
.setEditType(EditType.NAME)
.show();
}
以下是在user-profile.component.ts中其他更新操作的其他方法:
onEmailChange() {
this.editDialog.setTitle('Do you want to edit email?')
.setBodyTitle('email')
.setBodyLabel('Enter new email')
.setEditType(EditType.EMAIL)
.show();
}
onMobileChange() {
this.editDialog.setTitle('Do you want to edit mobile?')
.setBodyTitle('mobile')
.setBodyLabel('Enter new mobile')
.setEditType(EditType.MOBILE)
.show();
}
onPasswordChange() {
this.editDialog.setTitle('Do you want to edit password?')
.setBodyTitle('password')
.setBodyLabel('Enter new password')
.setEditType(EditType.PASSWORD)
.show();
}
最后,我们在用户模块中配置编辑组件,如下所示:
import {NgModule} from '@angular/core';
import {EditDialogComponent} from '../edit-dialog/edit-dialog.component';
/**
* User Module
*/
@NgModule({
imports: [
...
],
declarations: [
...
EditDialogComponent
]
})
export class UserModule {
}
现在,当用户点击“编辑”按钮时,以下编辑对话框将出现:
更新操作的 Firebase 会话
当用户编辑他们的电子邮件和密码时,Firebase 会要求用户重新登录。当我们在AngularFireAuth中调用updatePassword()方法时,Firebase 会抛出错误,这个错误是为了安全原因而添加的。
此操作敏感,需要最近一次的认证。在重试此请求之前请重新登录。
前面的消息显示在我们的应用程序的警告对话框中,要编辑电子邮件或密码,我们需要立即注销并刷新会话,然后执行操作。
提升用户体验的最佳方式是让用户通过弹出窗口注销并刷新令牌。我们不会将此行为作为本书的一部分进行实现,所以你可以将其作为练习。
摘要
恭喜你完成本章!这是最先进的章节之一。我们涵盖了与编程范式相关的重要概念。我们讨论了从一个模块组件向另一个模块组件传递数据。作为其中的一部分,我们使用最少的依赖开发了两个独立的模块。我们介绍了 RxJS 库。我们开发了一个编辑组件并将其包含在用户资料组件中。最后,我们介绍了 Firebase 的安全功能,在编辑敏感信息(如电子邮件或密码)时,该功能将使会话过期。
在下一章中,我们将增强我们的朋友应用,添加用户的“朋友”功能。我们还将检索朋友列表并在列表中显示它们。我们将添加分页功能以便导航到朋友列表。
第六章:创建用户的友情列表
在本章中,我们将转向 Angular 和 Firebase 的更高级功能。我们将使用 Firebase 列表检索我们的用户友情列表。我们将使用 Bootstrap 提供的卡片组件显示友情列表。我们将使用 Firebase 过滤器实现分页概念。最后,我们将讨论 Angular 管道。
在本章中,我们将涵盖以下主题:
-
创建用户的友情模板
-
创建朋友的服务
-
创建朋友组件
-
创建我们的第一个日期管道
创建用户的友情模板
在本节中,我们将介绍一个稍微复杂一些的模板,使用 Bootstrap 卡片组件。我们将检索定义大小的朋友列表,并在卡片项中显示用户的友情列表。我们将调用 Firebase API 获取三个项目,并使用 *ngFor 指令循环朋友的列表。
卡片是一个灵活且可扩展的容器。它有显示标题、页脚、标题等选项。我们将使用以下属性:
-
card-img-top:用于在顶部显示朋友的图片。 -
card-title:用于显示朋友的姓名。 -
card-text:用于显示他们的电子邮件和电话号码。 -
card-footer:用于使用自定义管道显示日期。我们将在本章的后续部分实现自定义管道。
<div *ngFor="let friend of friends" class="card">
<img class="card-img-top" src={{friend.image}}
alt="Card image cap">
<div class="card-block">
<h4 class="card-title">{{friend.name}}</h4>
<p class="card-text">{{friend.email}} | {{friend.mobile}}</p>
</div>
<div class="card-footer">
<small class="text-muted">Friends from {{friend.time |
friendsdate}}</small>
</div>
</div>
在我们显示第一页后,我们需要左右图标来滚动到下一页和上一页。这些图标将根据列表中的总项目数显示,并且 isLeftVisible 将从 component 类设置:
<div *ngIf="isLeftVisible" (click)="onLeft()" class="left"></div>
以下是完整的 user-friends.component.html 文件:
<div class="main_container">
<div *ngIf="friends" class="content_container">
<div *ngIf="isLeftVisible" (click)="onLeft()" class="left">
<img src="img/left.png">
</div>
<div class="card-deck list">
<div *ngFor="let friend of friends" class="card">
<img class="card-img-top" src={{friend.image}}
alt="Card image cap">
<div class="card-block">
<h4 class="card-title">{{friend.name}}</h4>
<p class="card-text">{{friend.email}} |
{{friend.mobile}}</p>
</div>
<div class="card-footer">
<small class="text-muted">Friends from
{{friend.time | friendsdate}}</small>
</div>
</div>
</div>
<div *ngIf="isRightVisible" (click)="onRight()" class="right">
<img src="img/right.png">
</div>
</div>
<div *ngIf="!friends || friends.length === 0"
class="no_info_container">
<h1>No friends in your list</h1>
</div>
</div>
我们将类选择器分配给元素以应用样式。在朋友列表页面中,我们使用 display:inline 水平对齐元素。同时,左图标、卡片列表和右图标依次显示,因此我们使用 float: left。
以下是到目前为止完整的 user-friends.component.scss 文件:
.main_container {
margin-top: 10px;
margin-left: 80px;
.content_container {
display: inline;
.list {
float: left;
.card-img-top {
height: 180px;
width: 260px;
background-image:
url('../../../assets/images/person.png');
}
}
.left {
float: left;
margin-top: 140px;
}
.right {
float: left;
margin-top: 140px;
}
}
}
创建朋友的服务
我们将在朋友的组件部分引入一个额外的服务。这个服务将从 Firebase 获取朋友的详细信息。在本节中,我们将涵盖以下主题:
-
在我们的数据库中创建 Firebase 节点
-
实现
Friend类 -
实现朋友的服务
在我们的数据库中创建 Firebase 节点
现在,我们已经如下一张图所示在 Firebase 中预先填充了朋友的详细信息。我们引入了一个名为 user-details 的单独节点。这将存储所有用户信息,我们不需要查询用户节点以获取更多信息,因为这会增加查询性能。
以下是对 Firebase 此实例的一些关键观察:
-
我们尚未实现添加朋友的功能;因此,我们将手动添加朋友的信息。
-
我们使用 UID 关系来列出用户的好友。在这种情况下,UID
qu3bXn9tTJR7j4PBp9LzBGKxHAe2是用户 ID,而另一个 UID—8wcVXYmEDQdqbaJ12BPmpsCmBMB2—是当好友注册应用时生成的朋友 ID。 -
在 Firebase 中,我们重复很多数据。这是在 NoSQL 数据库中组织数据时的常见模式,因为它避免了多次对数据库的访问。尽管这增加了写入时间,但有助于我们的应用在读取数据时进行扩展。它防止了大型查询减慢我们的数据库速度,以及读取时间较长的嵌套节点。
Firebase 数据库中的好友节点如下:
实现好友模型类
我们将实现 Friend 模型类,以映射从 Firebase 中获取的朋友 JSON 对象的数组。这个类与 User 模型类类似,将责任分离到单独的类中是一种良好的实践。
这个类有 name、email、mobile、uid、time 和 image 属性。时间属性用于显示友谊的持续时间,并以毫秒为单位存储。我们需要使用 Angular 管道将毫秒时间转换为可读的日期格式。
以下为完整的 friend.ts 文件:
export class Friend {
name: string;
mobile: string;
email: string;
uid: string;
time: string;
image: string;
constructor(name: string,
mobile: string,
email: string,
uid: string,
time: string,
image: string) {
this.name = name;
this.mobile = mobile;
this.email = email;
this.uid = uid;
this.time = time;
this.image = image;
}
}
实现好友服务
作为此服务的一部分,我们需要检索好友列表。AngularFireDatabase 提供了一个列表 API 来检索好友列表。此服务包含以下三个方法,以提供完整的分页功能:
- 获取第一页:
getFirstPage()方法接受uid和pageSize作为参数。这些参数用于从 Firebase 检索第一页pageSize数据。我们在查询函数的第二个参数中传递pageSize:
getFirstPage(uid: string, pageSize: number): Observable<Friend[]>
{
return this.fireDb.list<Friend>
(`${USER_DETAILS_CHILD}/${FRIENDS_CHILD}/${uid}`,
ref => ref.limitToFirst(pageSize)
).valueChanges();
}
- 获取下一页:
loadNextPage()接受uid、friendKey和pageSize参数。uid和friendKey用于设置查询。这意味着它们从最后检索的friendKey数据中检索下一个pageSize数据:
loadNextPage(uid: string, friendKey: string, pageSize: number): Observable<Friend[]> {
return this.fireDb.list<Friend>
(`${USER_DETAILS_CHILD}/${FRIENDS_CHILD}/${uid}`,
ref => ref.orderByKey().startAt(friendKey)
.limitToFirst(pageSize + 1)
).valueChanges();
}
- 获取上一页:
loadPreviousPage()方法接受uid、friendKey和pageSize。后两个参数用于从起始friendKey元素检索前一个pageSize数据:
loadPreviousPage(uid: string, friendKey: string, pageSize: number): Observable<Friend[]> {
return this.fireDb.list<Friend>
(`${USER_DETAILS_CHILD}/${FRIENDS_CHILD}/${uid}`,
ref => ref.orderByKey().startAt(friendKey)
.limitToLast(pageSize + 1)
).valueChanges();
}
这是完整的 friends.service.ts:
import {Injectable} from '@angular/core';
import {AngularFireDatabase} from 'angularfire2/database';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import {Friend} from './friend';
import {FRIENDS_CHILD, USER_DETAILS_CHILD} from './database-constants';
/**
* Friends service
*
*/
@Injectable()
export class FriendsService {
/**
* Constructor
*
* @param {AngularFireDatabase} fireDb provides
the functionality related to authentication
*/
constructor(private fireDb: AngularFireDatabase) {
}
getFirstPage(uid: string, pageSize: number): Observable<Friend[]>
{
return this.fireDb.list<Friend>
(`${USER_DETAILS_CHILD}/${FRIENDS_CHILD}/${uid}`,
ref => ref.limitToFirst(pageSize)
).valueChanges();
}
loadNextPage(uid: string, friendKey: string, pageSize: number):
Observable<Friend[]> {
return this.fireDb.list<Friend>
(`${USER_DETAILS_CHILD}/${FRIENDS_CHILD}/${uid}`,
ref =>
ref.orderByKey().startAt(friendKey)
.limitToFirst(pageSize + 1)
).valueChanges();
}
loadPreviousPage(uid: string, friendKey: string, pageSize: number):
Observable<Friend[]> {
return this.fireDb.list<Friend>
(`${USER_DETAILS_CHILD}/${FRIENDS_CHILD}/${uid}`,
ref =>
ref.orderByKey().startAt(friendKey)
.limitToLast(pageSize + 1)
).valueChanges();
}
}
创建好友组件
这是我们的好友页面的主要控制器。在这个组件中,我们需要管理导航和我们的下一页和上一页图标的可见性。在本节中,我们将涵盖以下两个主要内容:
-
显示下一页和上一页
-
图标的可见性
为了显示下一页和上一页,我们已创建了显示朋友信息的 API。我们已经扩展了 OnInit 接口,并在 ngOnInit 上调用 getFirstPage(),使用 uid 和 pageSize 作为过滤参数,如下所示:
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
this.friendService.getFirstPage(this.user.getUid() , this.pageSize)
.subscribe(friends => {
this.friends = friends;
...
});
}
ngOnInit() 方法在页面加载时运行。
因此,我们将使用朋友服务中的 API 检索下一页和前一页,如下所示;唯一的区别是我们还将传递朋友uid,这样我们就可以从最后检索的项目开始检索下一页的大小数据:
next() {
this.friendService.loadNextPage(this.user.getUid() ,
this.friends[this.friends.length - 1].getUid(),
this.pageSize
).subscribe(friends => {
this.friends = friends;
...
});
}
现在,我们将继续到下一部分。我们需要处理下一个和上一个图标,为此我们需要朋友的总数。在我们之前的讨论中,我们得到了大小为pageSize。为了解决这个问题,我们必须在我们的 Firebase 用户节点中创建friendcount。每次我们添加一个朋友,我们就增加计数。我们在User类中添加了这个属性;其他所有部分保持不变:
private friendcount: number
然后,在ngOnInit中,我们将检索总项目数,如下所示:
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
this.totalCount = this.user.getFriendcount();
this.friendService.getFirstPage(this.user.getUid() ,
this.pageSize)
.subscribe(friends => {
...
let count: number = this.friends.length;
this.currentCount = count;
this.leftArrowVisible();
this.rightArrowVisible();
});
}
接下来,我们将当前计数初始化为检索的项目,然后根据总数和当前计数调用可见性:
leftArrowVisible(): void{
this.isLeftVisible = this.currentCount > this.pageSize;
}
rightArrowVisible(): void{
this.isRightVisible = this.totalCount > this.currentCount;
}
这是完整的user-friends.component.ts文件:
import {Component, OnInit} from '@angular/core';
import {FriendsService} from '../../services/friends.service';
import {Friend} from '../../services/friend';
import {UserService} from '../../services/user.service';
import {User} from '../../services/user';
import 'firebase/storage';
import {Router} from '@angular/router';
@Component({
selector: 'app-friends-userfriends',
styleUrls: ['user-friends.component.scss'],
templateUrl: 'user-friends.component.html'
})
export class UserFriendsComponent implements OnInit {
friends: Friend[];
totalCount: number;
pageSize = 3;
currentCount = 0;
previousCount = 0;
isLeftVisible = false;
isRightVisible = true;
user: User;
constructor(private friendService: FriendsService,
private userService: UserService) {
}
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
this.totalCount = this.user.friendcount;
this.friendService.getFirstPage(this.user.uid, this.pageSize)
.subscribe(friends => {
this.friends = friends;
const count: number = this.friends.length;
this.currentCount = count;
this.leftArrowVisible();
this.rightArrowVisible();
});
}
onLeft(): void {
this.previous();
}
onRight(): void {
this.next();
}
next() {
this.friendService.loadNextPage(this.user.uid,
this.friends[this.friends.length - 1].uid,
this.pageSize
).subscribe(friends => {
this.friends = friends;
const count: number = this.friends.length;
this.previousCount = count - 1;
this.currentCount += this.previousCount;
this.leftArrowVisible();
this.rightArrowVisible();
});
}
previous() {
this.friendService.loadPreviousPage(this.user.uid,
this.friends[0].uid,
this.pageSize
).subscribe(friends => {
this.friends = friends;
const count: number = this.friends.length;
this.currentCount -= this.previousCount;
this.leftArrowVisible();
this.rightArrowVisible();
});
}
leftArrowVisible(): void {
this.isLeftVisible = this.currentCount > this.pageSize;
}
rightArrowVisible(): void {
this.isRightVisible = this.totalCount > this.currentCount;
}
}
用户的友谊页面显示三个有导航功能的朋友:
创建我们的第一个日期管道
管道接受输入作为其数据,并将其转换为所需的输出。它用于将数据转换为可用的形式。
我们使用管道将时间转换为人类友好的日期格式。要创建管道,我们实现PipeTransform接口并重写transform方法。在这个方法中,我们获取以毫秒为单位的日期,并使用 moment 库将时间转换为特定的日期格式。我们提供了选择器名称,该名称用于 HTML 标签中的输入数据:
import * as moment from 'moment';
import {Pipe, PipeTransform} from '@angular/core';
/**
* It is used to format the date
*/
@Pipe({
name: 'friendsdate'
})
export class FriendsDatePipe implements PipeTransform {
transform(dateInMillis: string) {
if (dateInMillis === '0' || dateInMillis === '-1') {
return 'Invalid Date';
}
return moment(dateInMillis, 'x').format('MM/DD/YY');
}
}
Moment 是一个用于格式化、操作或解析日期的 JavaScript 库。
创建管道后,我们在user模块中添加它:
@NgModule({
imports: [
...
],
declarations: [
...
FriendsDatePipe
]
})
export class UserModule {
}
最后,我们将friendsdate管道添加到模板中从friend对象中的time值,如下所示:
<div class="card-footer">
<small class="text-muted">Friends from {{friend.getTime() | friendsdate}}</small>
</div>
摘要
在本章中,我们涵盖了大量的重要概念。我们介绍了现在大多数应用中使用的卡片组件。我们用样式装饰了我们的视图,并创建了一个新的服务。我们讨论了 Firebase 列表,然后提供了过滤选项。这为我们朋友的列表实现了分页。最后,我们讨论了 Angular 管道,我们使用它将时间转换为人类友好的日期格式。
在下一章中,我们将介绍 Firebase 存储,并学习如何存储个人资料图片以及如何检索它。
第七章:探索 Firebase 存储
在本章中,我们将继续探索 Firebase 的其他功能。如今,图像、音频和视频已成为任何网站开发的必要组成部分。考虑到这一点,Firebase 引入了存储功能。
我们将查看如何使用 Firebase 存储 API 上传个人资料图片。我们将使用 Firebase 门户上传一些随机图片,并使用 API 下载上传的图片以显示在我们的朋友列表中。然后,我们将查看如何在 Firebase 存储中删除文件。最后,我们将介绍错误处理。
在本章中,我们将涵盖以下主题:
-
介绍 Firebase 存储
-
配置 Firebase 存储
-
上传个人资料图片
-
下载朋友的照片
-
删除个人资料图片
-
Firebase 存储中的错误处理
介绍 Firebase 存储
Firebase 存储为应用程序开发者提供了在存储中存储各种内容类型的灵活性。它存储图像、视频和音频。内容存储在 Google Cloud 存储桶中,并且可以从 Firebase 和 Google Cloud 访问。
Firebase 存储与 Firebase 身份验证集成,并提供了强大的安全性。我们还可以应用声明式安全模型来控制对内容访问的控制。我们将在稍后的部分中更详细地研究这一点。
Firebase 存储提供了以下关键特性:
-
缩放:它由 Google Cloud Storage 支持,可以扩展到 PB 级别的容量。您可以在
cloud.google.com/storage/了解更多关于 Google Cloud Storage 的信息。 -
安全性:每个上传的文件都可以使用存储安全规则进行保护。
Firebase 存储的默认安全规则如下:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth!=null;
}
}
}
- 网络:Firebase 存储在文件的上传和下载过程中自动处理网络问题。
配置 Firebase 存储
要在我们的应用程序中配置 Firebase 存储,我们需要存储桶 URL。这可以在标题下的“存储”选项卡中的“文件”标签内找到,如下面的截图所示;在我们的案例中,应用程序朋友的存储桶 URL 为gs://friends.4d4fa.appspot.com:
这是修改后的environment.ts文件,其中包含存储桶 URL:
export const environment = {
production: false,
firebase: {
apiKey: 'XXXX',
authDomain: 'friends-4d4fa.firebaseapp.com',
databaseURL: 'https://friends-4d4fa.firebaseio.com',
projectId: 'friends-4d4fa',
storageBucket: 'friends-4d4fa.appspot.com',
messagingSenderId: '321535044959'
}
};
上传个人资料图片
在本节中,我们将介绍如何将文件上传到 Firebase 存储。在用户资料页面上,我们将在页面顶部添加用户个人资料图片。
上传和显示用户个人资料图片的步骤如下:
- 在用户资料模板中添加 HTML 属性:我们提供了一个
input标签,用于从文件选择器中获取用户选择的图片。通常,在具有文件类型的input标签中,我们有一个按钮来选择文件;然而,在这种情况下,我们需要用户点击默认图片。我们使用内置样式隐藏了input标签中的按钮,如下所示:
<div class="person-icon">
<img [src]="profileImage" style="max-width: 100%; max-height:
100%;">
<input (change)="onPersonEdit($event)" required accept=".jpg"
type="file" style="opacity: 0.0; position: absolute; top:120px;
left: 30px; bottom: 0; right:0; width: 200px; height:200px;" />
</div>
- 在样式表中添加默认图片:最初,我们通过在
user-profile.component.ts文件中声明默认图片路径来显示默认图片,如下所示:
export class UserProfileComponent implements OnInit {
profileImage: any = '../../../assets/images/person_edit.png';
...
}
以下是对 user-profile.component.html 文件进行的修改:
<div class="user-profile" *ngIf="user">
<div class="person-icon">
<img [src]="profileImage" style="max-width: 100%; max-height:
100%;">
<input (change)="onPersonEdit($event)" required accept=".jpg"
type="file" style="opacity: 0.0; position: absolute;
top:120px; left: 30px; bottom: 0; right:0; width: 200px;
height:200px;" />
</div>
...
</div>
<app-edit-dialog></app-edit-dialog>
下面是修改后的 user-profile.component.scss 文件:
.user-profile{
width: 50%;
margin-left: 24px;
margin-top: 10px;
.person-icon{
width: 200px;
height: 200px;
}
...
}
- 在用户个人资料组件中处理点击事件:我们将实现
onPersonEdit()方法,它接受event作为其参数。如以下代码所示,我们需要从事件对象中检索选定的文件并将它们传递给UserService:
onPersonEdit(event) {
const selectedFiles: FileList = event.target.files;
const file = selectedFiles.item(0);
this.userService.addProfileImage(this.user, file);
}
- 在用户服务中添加方法:在
user.service.ts中,我们在构造函数中初始化 Firebase 存储实例,如下所示:
@Injectable()
export class UserService {
private fbStorage: any;
private basePath = '/profile';
/**
* Constructor
*
* @param {AngularFireDatabase} fireDb provides the functionality
related to authentication
*/
constructor(private fireDb: AngularFireDatabase) {
this.fbStorage = fireDb.app.storage();
}
...
}
现在我们实现用户服务中的 addProfileImage() 方法。
- 首先,我们为 Firebase 存储中的图片存储创建路径:
`${this.basePath}/${file.name}`
- 其次,我们调用 Firebase 存储引用的
put()方法,如下所示:
this.fbStorage.ref(`${this.basePath}/${file.name}`).put(file)
在成功上传后,我们在 Firebase 的用户节点中保存下载 URL 并刷新缓存的用户对象:
public addProfileImage(user: User, file: File) {
this.fbStorage.ref(`${this.basePath}/${file.name}`).
put(file).then(
snapshot => {
const imageUrl: string = snapshot.downloadURL;
this.fireDb.object(`${USERS_CHILD}/${user.uid}`).
update({image: imageUrl});
user.image = imageUrl;
this.saveUser(user);
}).catch((error) => {
...
});
}
- 刷新用户个人资料图片:在成功上传后,我们需要更新我们用户个人资料页面中的图片。我们在用户个人资料组件中订阅
user可观察对象并更新个人资料图片,如下所示:
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
this.userService.getSavedUser().subscribe(
(user) => {
if (this.user.image) {
this.profileImage = this.user.image;
}
}
);
}
在刷新成功后,用户个人资料页面将如下所示:
下载好友图片
在用户个人资料页面,我们在 Firebase 存储中上传了个人资料图片。我们还在 Firebase 的用户节点中存储了一个可下载的 URL,好友可以通过 UID 访问。在获取好友列表后,我们必须调用另一个 Firebase API 从我们的用户节点获取可下载的 URL。以下是可以下载的 URL:
https://firebasestorage.googleapis.com/v0/b/friends-4d4fa.appspot.com/o/profile%2Fclaire.jpg?alt=media&token=e00012af-c71c-48eb-92bc-4a0c9f989cbd
在 HTML 中,图片通过 <img> 标签定义。src 属性指定了图片的 URL 地址,如下所示:
<img src="img/url">
在 user-friends.component.html 文件中,我们添加了带有可下载 URL 的默认图片:
<img class="card-img-top" src={{friend.image}} alt="Card image cap">
在 user-friends.component.scss 文件中,我们使用了 background-image 并添加了 width 和 height 以确保图片能够适应卡片布局,如下所示:
.main_container {
margin-top: 10px;
margin-left: 80px;
.content_container {
display: inline;
.list {
float: left;
.card-img-top {
height: 180px;
width: 260px;
background-image:
url('../../../assets/images/person.png');
}
}
.left {
float: left;
margin-top: 140px;
}
.right {
float: left;
margin-top: 140px;
}
}
}
Firebase 提供了一个 API,可以通过 Firebase 存储获取可下载的 URL:
public getDownloadURL(user: User, file: File) { this.fbStorage.child('images/claire.jpg').getDownloadURL().then((url) => {
// assign to the img src
}).catch((error) => {
// Handle any errors
});
}
删除个人资料图片
Firebase 存储提供了一个 API 用于从 Firebase 删除文件。delete 操作与其他 Firebase 存储 API 类似。我们尚未在我们的应用程序中实现此用例;然而,您可能在您的应用程序中需要此概念:
public deleteFile() {
this.fbStorage.child('images/claire.jpg').delete().then(function()
{
// File deleted successfully
}).catch((error) => {
// Handle any errors
});
}
处理 Firebase 存储中的错误
Firebase 存储根据不同的条件抛出错误,如下所示:
-
storage/unknown:这可能是因为任何未知错误。这与
switch...case语句中的默认条件类似。 -
storage/object_not_found:当文件/图片引用在 Firebase 存储位置不可用时发生。
-
storage/bucket_not_found:当 Firebase 存储桶未配置时,此错误发生。
-
storage/project_not_found: 当 Firebase 项目未配置 Firebase 存储时,此错误发生。
-
storage/quota_exceeded: 当免费套餐计划到期并被要求升级到付费计划时,此错误发生。
-
storage/unauthenticated: 当用户未认证但仍然能够访问 Firebase 存储中的文件和图片时,此错误发生。
-
storage/unauthorized: 当未经授权的用户访问 Firebase 存储中的文件/图片时,此错误发生。
-
storage/retry_limit_exceeded: 当由于网络缓慢或无网络而导致用户超过重试限制时,此错误发生。
-
storage/invalid_checksum: 当客户端的校验和与服务器的不匹配时,此错误发生。
-
storage/canceled: 当用户干预上传或下载操作时,此错误发生。
-
storage/invalid_event_name: 当提供给 Firebase 存储 API 的无效事件名称时,此错误发生。正确的事件是 running,progress 和 pause。
-
storage/invalid-argument: Firebase 存储
put()方法接受文件、Blob和UInt8作为参数。当我们传递错误参数时,此错误发生。
当我们在应用程序中遇到错误时,我们实现 Promise 的then()方法,检索错误消息并在警告对话框中显示它。
这里是user.service.ts类中修改后的addProfileImage()方法:
public addProfileImage(user: User, file: File) {
this.fbStorage.ref(`${this.basePath}/${file.name}`).put(file).then(
snapshot => {
...
}).catch((error) => {
const errorMessage = error.message;
alert(errorMessage);
});
}
摘要
在本章中,我们讨论了 Firebase 存储。我们将个人资料图片上传到 Firebase 存储,并将可下载的 URL 存储在我们的数据库中的用户节点中。我们在 HTML 的img标签中显示了图片,因为这有助于从 Firebase 存储中下载图片。我们介绍了 Firebase 安全,用户需要正确认证才能访问 Firebase 存储中的图片/文件。最后,我们讨论了 Firebase 存储的错误处理。
在下一章中,我们将介绍我们应用程序更有趣和令人兴奋的部分。我们将创建一个聊天应用程序,并介绍 Firebase 如何支持实时更新。
第八章:创建聊天组件
在本章中,我们将在现有应用中创建我们的聊天应用,并查看使用 Firebase 数据库的实时消息更新。我们将在本章和下一章中解释聊天功能。
由于我们已经在上一章创建了组件,因此在本章中我们将设计一个涉及多个组件的更复杂组件。根据一般规则,我们将将其创建为一个模块,因此主组件将是聊天组件,它将包含消息列表组件、表单组件和消息组件。在实现聊天功能时,我们将探索更多数据绑定方式。在本章中,我们将编写更复杂的 SCSS。我们相信,如果您正确地遵循本章,大多数 Angular 内容将更加清晰,您将能够自己构建更复杂的组件。
在本章中,我们将涵盖以下主题:
-
创建聊天模块
-
创建颜色变量
-
创建聊天组件
-
创建聊天消息列表组件
-
创建消息视图的 mixin
-
创建聊天消息组件
-
创建聊天消息表单组件
创建聊天模块
创建模块的第一步是定义路由并将它们包含在聊天模块中。在聊天路由模块中,我们创建聊天路由并在 RouterModules 中配置它们。
以下是目前完整的chat-routing.module.ts文件:
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ChatComponent} from './chat.component';
export const ROUTES: Routes = [
{path: 'app-friends-chat/:id', component: ChatComponent}
];
/**
* Chat Routing Module
*/
@NgModule({
imports: [
RouterModule.forChild(ROUTES)
],
exports: [
RouterModule
]
})
export class ChatRoutingModule { }
聊天模块包含所有组件、模块和服务的声明。在聊天功能中,我们有以下四个组件:
-
聊天组件:这是主组件,它封装了消息列表和消息表单组件。
-
聊天消息列表组件:这是一个消息列表,显示列表中的消息。它调用消息组件来填充文本框中的消息。
-
聊天消息表单组件:这是一个表单,它接受用户输入的消息并将其添加到 Firebase 数据库中。
-
聊天消息组件:此组件显示用户输入的消息和发布消息的日期。
以下是目前完整的chat.module.ts文件:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {UserService} from '../services/user.service';
import {ChatMessageComponent} from './chat-message/chat-message.component';
import {ChatMessageListComponent} from './chat-message-list/chat-message-list.component';
import {ChatMessageFormComponent} from './chat-message-form/chat-message-form.component';
import {ChatComponent} from './chat.component';
import {ChatRoutingModule} from './chat-routing.module';
/**
* Chat Module
*/
@NgModule({
imports: [
CommonModule,
BrowserModule,
FormsModule,
ChatRoutingModule
],
declarations: [
ChatMessageComponent,
ChatMessageListComponent,
ChatMessageFormComponent,
ChatComponent
],
providers: [
UserService
]
})
export class ChatModule {
}
最后,我们将聊天模块包含到应用模块中,如下所示;以下是目前的修改后的app.module.ts文件:
...
@NgModule({
declarations: [
...
],
imports: [
...
ChatModule,
],
providers: [
...
],
bootstrap: [AppComponent]
})
export class AppModule {
}
目前,我们的聊天模块是主应用模块的一部分。现在,我们将实现这些组件。
创建颜色变量
在本节中,我们讨论 SCSS 中的变量支持。在 CSS 中,我们需要为每个属性声明颜色代码,我们没有在另一个 CSS 属性中重用相同颜色代码的机制:
#messages {
background-color: #F2F2F2 !important;
}
在我们的应用中,我们使用变量和部分来在整个应用中重用相同的颜色。我们使用变量在颜色文件中声明所有颜色,如下所示。此文件在 SCSS 中被称为部分,通常以下划线声明。
以下是目前完整的_colors.scss文件:
$mercury_solid: #e5e5e5;
$sushi: #8BC34A;
$concrete_solid: #F2F2F2;
$iron: #E1E2E3;
$pickled_bluewood: #2d384a;
首先,我们将部分导入到另一个 SCSS 文件中,然后使用变量来访问颜色。在以下示例中,我们使用$concrete_solid变量来重复使用该颜色:
@import "../../shared/colors";
.chat-message-list-main-container {
#messages {
background-color: $concrete_solid !important;
}
}
SCSS 变量帮助我们集中管理所有颜色在一个文件中,这样,当我们更改一个文件中的颜色组合时,这将在我们的整个应用程序中反映出来。
创建聊天组件
聊天组件是主要容器,它包含消息列表组件和消息表单组件。
它使用 Bootstrap 组件创建消息列表列视图。
div class="chat-main-container">
<div class="main_container">
<div class="col-md-8 col-md-offset-2">
...
</div>
</div>
</div>
聊天模板封装了聊天消息列表和聊天消息表单引用。
以下是目前的完整chat.component.html:
div class="chat-main-container">
<div class="main_container">
<div class="col-md-8 col-md-offset-2">
<app-chat-message-list [friendUid]="uid">
</app-chat-message-list>
<app-chat-message-form [friendUid]="uid">
</app-chat-message-form>
</div>
</div>
</div>
我们使用margin-top和margin-left将主容器对齐到页面中间,如下所示,以下是目前的完整chat.component.scss:
.chat-main-container {
margin-top: 10px;
margin-left: 80px;
p {
font-size: 10px;
}
}
聊天组件声明了模板、样式表和选择器。以下是目前的完整chat.component.ts:
import {Component} from '@angular/core';
@Component({
selector: 'app-friends-chat',
styleUrls: ['chat.component.scss'],
templateUrl: 'chat.component.html',
})
export class ChatComponent {
}
聊天组件为其他子组件提供布局。
创建聊天消息列表组件
聊天消息列表组件以列表布局显示消息文本。它调用消息组件来填充消息文本的数据和时间。
首先,我们在容器div中创建列表,并使用#scrollContainer标签标记消息列表div,因为这有助于在收到新消息时将列表滚动到聊天窗口的底部。我们使用@ViewChild注解在组件中读取此标签:
<div class="chat-message-list-main-container">
<div #scrollContainer class="message-list-container"
id="messages">
...
</div>
</div>
最后,我们包括了聊天消息选择器并循环消息。以下是目前完整的chat-message-list.component.html:
<div class="chat-message-list-main-container">
<div #scrollContainer class="message-list-container" id="messages">
<app-chat-message *ngFor="let message of messages;"
[message]="message">
</app-chat-message>
</div>
</div>
在以下 HTML div标签中,我们在模板中包含两个选择器——我们添加了一个类选择器和 ID 选择器:
<div #scrollContainer class="message-list-container" id="messages"></div>
我们使用 SCSS 文件中选择器的名称后跟哈希来读取 ID 选择器以设置background-color:
#messages {
background-color: $concrete_solid !important;
}
我们使用box-shadow和border-radius属性为列表容器提供提升的外观:
.message-list-container {
...
box-shadow: inset 0 3px 6px rgba(0, 0, 0, .05);
border-radius: 8px;
}
以下是目前的完整chat-message-list.component.scss:
.chat-message-list-main-container {
#messages {
background-color: #F2F2F2 !important;
}
.message-list-container {
position: relative;
padding: 15px 15px 15px;
border-color: #e5e5e5 #eee #eee;
border-style: solid;
border-width: 1px 0;
background-color: #E1E2E3;
box-shadow: inset 0 3px 6px rgba(0, 0, 0, .05);
height: 60vh;
overflow-y: scroll;
background-color: #2d384a !important;
border-radius: 8px;
}
p {
font-size: 10px;
}
}
在聊天消息列表组件中,我们使用@ViewChild读取滚动容器,如下所示:
@ViewChild('scrollContainer') private scrollContainer: ElementRef
然后,我们在组件中实现AfterViewChecked来处理底部滚动。
生命周期方法在组件视图在变更检测期间被检查时被调用。
interface AfterViewChecked{
ngAfterViewChecked: void
}
我们覆盖了生命周期方法,并在每次收到新消息后,将消息列表滚动到最后一条消息的底部。我们还使用ChangeDetectorRef类检测组件变化。这是必需的,因为我们需要强制 Angular 检查组件的变化,因为滚动事件在 Angular 的作用域之外运行:
ngAfterViewChecked() {
this.scrollToBottom();
this.cdRef.detectChanges();
}
scrollToBottom(): void {
try {
this.scrollContainer.nativeElement.scrollTop =
this.scrollContainer.nativeElement.scrollHeight;
} catch(err) {
console.log("Error");
}
}
以下是目前的完整chat-message-list.component.ts:
import {AfterViewChecked, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-chat-message-list',
styleUrls: ['chat-message-list.component.scss'],
templateUrl: 'chat-message-list.component.html'
})
export class ChatMessageListComponent implements OnInit , AfterViewChecked{
@ViewChild('scrollContainer') private scrollContainer: ElementRef;
constructor(private messageService: MessagingService,
private userService: UserService,
private cdRef: ChangeDetectorRef) {
}
ngAfterViewChecked() {
this.scrollToBottom();
this.cdRef.detectChanges();
}
scrollToBottom(): void {
try {
this.scrollContainer.nativeElement.scrollTop =
this.scrollContainer.nativeElement.scrollHeight;
} catch(err) {
console.log("Error");
}
}
}
创建消息视图的 mixin
在本节中,我们将介绍 SCSS 混合。这个特性提供了将 CSS 属性分组的能力,我们可以在我们的应用程序中重用这个混合。就像类方法一样,我们也可以提供参数来使混合更加灵活。
我们将在我们的应用程序中使用这个混合来为聊天功能添加消息指针。我们将通过在方法名前加上 @mixin 关键字来声明混合,并添加如 $rotate 和 $skew 这样的参数。
我们在 _shared.scss 中为我们的聊天消息创建了混合:
@mixin message-pointer($rotate , $skew) {
transform: rotate($rotate) skew($skew);
-moz-transform: rotate($rotate) skew($skew);
-ms-transform: rotate($rotate) skew($skew);
-o-transform: rotate($rotate) skew($skew);
-webkit-transform: rotate($rotate) skew($skew);
}
我们在这个消息 SCSS 中使用这个混合。首先,我们需要在我们的消息文件中导入共享 SCSS 文件,然后我们使用 @include 来调用混合并传递参数。
以下为示例 chat-message.component.scss 文件:
@import "../../shared/shared";
.chat-message-main-container {
.message-bubble::before {
...
@include message-pointer(29deg , -35deg);
...
}
}
创建聊天消息组件
消息组件是消息文本容器。它显示消息和时间。典型的聊天有一个气泡视图布局。我们为我们的聊天功能设计了此视图。我们声明以下三个类变量,我们在 SCSS 文件中使用它们:
-
message-bubble:这个选择器为消息气泡视图布局 -
class.sender:这会将发送者的所有消息对齐到容器的左侧 -
class.receiver:这会将接收者的所有消息对齐到容器的右侧
现在的完整 chat-message.component.html 文件如下:
<div class="chat-message-main-container">
<div class="message-bubble" [class.receiver]="isReceiver(message)"
[class.sender]="isSender(message)">
<p>{{ message.message }}</p>
<div class="timestamp">
{{ message.timestamp | date:"MM/dd/yy hh:mm a" }}
</div>
</div>
</div>
我们使用类选择器并为消息框提供样式。这包括以下两个主要部分:
-
消息框:这为视图提供了阴影效果
-
消息指针:这为消息框提供了一个指针
现在的完整 chat-message.component.scss 文件如下:
@import "../../shared/shared";
.chat-message-main-container {
.message-bubble {
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 0 6px #B2B2B2;
display: inline-block;
padding: 10px 18px;
position: relative;
vertical-align: top;
width: 400px;
}
.message-bubble::before {
background-color: #ffffff;
content: "\00a0";
display: block;
height: 16px;
position: absolute;
top: 11px;
@include message-pointer(29deg , -35deg);
width: 20px;
}
.sender {
display: inherit;
margin: 5px 45px 5px 20px;
}
.sender::before {
box-shadow: -2px 2px 2px 0 rgba(178, 178, 178, .4);
left: -9px;
}
.receiver {
display: inherit;
margin: 5px 20px 5px 170px;
}
.receiver::before {
box-shadow: 2px -2px 2px 0 rgba(178, 178, 178, .4);
right: -9px;
}
}
消息框看起来如下:
最后,我们在组件中编写事件方法。我们从我们的服务中检索保存的用户 UID 并编写逻辑来识别接收者和发送者。
现在的完整 chat-message.component.ts 文件如下:
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
import {UserService} from '../../services/user.service';
import {Message} from '../../services/message';
@Component({
selector: 'app-chat-message',
styleUrls: ['chat-message.component.scss'],
templateUrl: 'chat-message.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatMessageComponent implements OnInit {
@Input() message: Message;
uid: string;
constructor(private userService: UserService) {
}
ngOnInit() {
this.uid = this.userService.getSavedUser().getValue().uid;
}
isReceiver(message: Message) {
return this.uid === message.receiverUid;
}
isSender(message: Message) {
return this.uid === message.senderUid;
}
}
创建聊天消息表单组件
在聊天消息表单组件中,我们实现消息表单以将消息发送到 Firebase 并用新消息更新列表。
对于这些操作,我们需要以下两个元素:
-
带有文本区域的输入:输入文本允许用户输入他们的消息。我们在输入文本中使用
(key.enter)来处理键盘的 Enter 键,这会调用sendMessage()方法。 -
发送按钮:这会调用
sendMessage()方法并更新 Firebase 数据库。
现在的完整 chat-message-form.component.html 文件如下:
<div class="chat-message-form-main-container">
<div class="chat-message-form-container">
<input type="textarea" placeholder="Type a message"
class="message-text" [(ngModel)]="newMessage"
(keyup.enter)="sendMessage()">
<button (click)="sendMessage()"
class="btn btn-outline-success my-2 my-sm-0"
type="submit">SEND</button>
</div>
</div>
我们使用 chat-message-form-container 类选择器来设置边框的 border-radius 和 message-text 来设置输入文本相关的属性。
现在的完整 chat-message-form.component.scss 文件如下:
@import "../../shared/colors";
.chat-message-form-main-container {
.chat-message-form-container {
padding: 9px 50px;
margin-bottom: 14px;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 4px;
.message-text {
display: block;
padding: 9.5px;
margin: 0 0 10px;
font-size: 13px;
line-height: 1.42857143;
color: #333;
word-break: break-all;
word-wrap: break-word;
background-color: #ffffff;
border: 1px solid $sushi;
border-radius: 4px;
width: 100%;
}
}
}
在聊天消息表单组件中,我们从用户服务中保存的用户对象中检索 UID。
以下为现在的完整 chat-message-form.component.ts 文件:
import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import {MessagingService} from '../../services/messaging.service';
import {Message} from '../../services/message';
import {UserService} from '../../services/user.service';
@Component({
selector: 'chat-message-form',
styleUrls: ['chat-message-form.component.scss'],
templateUrl: 'chat-message-form.component.html'
})
export class ChatMessageFormComponent implements OnInit {
uid: string;
newMessage: string;
constructor(private messageService: MessagingService,
private userService: UserService) { }
ngOnInit() {
this.uid = this.userService.getSavedUser().getValue().getUid();
}
sendMessage() {
}
}
最后,我们的聊天功能将如下所示:
摘要
在本章中,我们使用多个组件设计了一个更复杂的 UI 组件。我们实现了聊天模块并将其集成到主应用程序中。我们涵盖了新的 SCSS 功能,如变量、部分和混入。这真正帮助我们模块化我们的代码,并展示了如何在 SCSS 中实现可重用性。我们将一个大聊天组件分解成更小的组件,然后集成这些小组件。
在下一章中,我们将把我们的组件与服务集成。我们将为我们的聊天应用程序设计 Firebase 数据库。然后,我们将订阅实时数据库并获取即时更新。
第九章:将聊天组件与 Firebase 数据库连接
在本章中,我们将集成我们的聊天组件与新的消息服务。我们将讨论一种新的方法,使用路由参数将我们的数据传递到聊天模块。一旦从用户朋友列表组件传递了朋友的 UID,然后我们将这个朋友的 UID 传递给不同的聊天组件,因为消息列表和消息表单组件需要这些数据。我们还将为我们的聊天应用设计数据库,因为良好的设计可以避免数据重复。一旦数据库准备就绪,我们将从消息服务查询数据,并将消息服务与聊天组件集成。
在本章中,我们将涵盖以下主题:
-
使用路由参数传递数据
-
将朋友数据传递到不同的聊天组件
-
设计聊天应用的 Firebase 数据库
-
创建消息服务
-
将服务集成到聊天组件中
使用路由参数传递数据
当用户点击“聊天”按钮时,我们使用路由参数将朋友 UID 传递给聊天组件。
在第五章,“创建用户个人资料页面”中,我们使用 RxJS 库中的BehaviorSubject传递用户数据。在本节中,我们将使用路由链接的参数来传递朋友的 UID。我们执行以下三个步骤来使用路由参数传递数据:
-
添加路由参数 ID
-
将路由链接到参数
-
读取参数
添加路由参数 ID
在前面列表中提到的第一步,我们需要将朋友的 UID 参数添加到路由链接中。我们在此处将 ID 参数添加到路径元素中,如下所示:
export const ROUTES: Routes = [
{ path: 'app-friends-chat/:id', component: ChatComponent }
];
当用户点击“聊天”按钮时,此 ID 将作为http://localhost:4200/friends-chat/8wcVXYmEDQdqbaJ12BPmpsCmBMB2添加到 URL 中。
将路由链接到参数
在前面列表中提到的第二步,我们需要将朋友的 UID 链接到路由链接。以下是有两种方法来实现这一点:
- 路由链接指令:我们可以直接使用
routerLink指令来链接参数 ID,如下所示:
<div *ngFor="let friend of friends" class="card" [routerLink]="['/app-friends-chat' , friend.getUid()]"></div>
- 程序化使用路由:当用户点击“聊天”按钮时,我们将 UID 传递给方法参数,并使用路由传递数据。
我们在我们的用户朋友列表中添加一个聊天按钮。我们修改用户朋友的模板,如下所示:
<div *ngFor="let friend of friends" class="card">
...
<button (click)="onChat(friend.uid)" class="btn btn-outline-
success my-2 my-sm-0" type="submit">Chat</button>
</div>
...
</div>
当用户点击“聊天”按钮时,将 UID 作为参数调用onChat()方法。最后,我们调用router方法将id作为路由参数传递:
onChat(id: string): void {
this.router.navigate(['/app-friends-chat' , id]);
}
读取参数
我们使用ActivatedRoute来读取参数 ID。该组件提供了一组参数来读取 ID。我们订阅路由参数,并将订阅对象存储在成员变量中,并在 Angular 的生命周期ngOnDestroy()方法中取消订阅。
OnDestroy 是一个 Angular 生命周期钩子接口。它有一个 ngOnDestroy() 方法,当组件被销毁时调用,用于清理逻辑。
以下是到目前为止的完整 chat.component.ts 文件:
import {Component} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'friends-chat',
styleUrls: ['chat.component.scss'],
templateUrl: 'chat.component.html',
})
export class ChatComponent {
uid: string;
private sub: any;
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
this.uid = params['id'];
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
我们将在下一节中介绍如何将 UID 数据传递给其他聊天组件。
将朋友数据传递给不同的聊天组件
一旦我们在聊天模块中有了朋友的 UID,我们可以使用 Angular 数据绑定将朋友的 UID 传递给我们的消息列表和消息表单组件。我们执行以下两个步骤来将数据传递给两个聊天组件:
- 声明输入变量:我们使用 Angular 的
@Input注解在两个聊天组件中声明输入变量。以下代码片段显示了消息列表和消息表单组件的更改。
chat-message-list.component.ts:
export class ChatMessageListComponent implements OnInit , AfterViewChecked{
@Input() friendUid: string;
}
chat-message-form.component.ts file:
export class ChatMessageFormComponent implements OnInit {
@Input() friendUid: string;
- 将数据绑定到输入:我们可以将输入变量
friendUid绑定到从用户模块传递来的uid:
<div class="col-md-8 col-md-offset-2">
<app-chat-message-list [friendUid]="uid">
</app-chat-message-list>
<app-chat-message-form [friendUid]="uid">
</app-chat-message-form>
</div>
我们将使用这个朋友的 UID 来读取或更新 Firebase 数据库。
设计用于聊天的 Firebase 数据库
设计 Firebase 数据库是编写聊天功能中最关键的部分。
聊天涉及两个人之间的交流。它有一个发送者和接收者,两人都可以看到相同的文本消息。我们必须将两个用户与相同的消息关联起来,这主要涉及两个步骤:
- 创建新的聊天 ID:第一步是为两个人关联唯一的消息 ID。我们创建一个唯一的消息 ID,并使用他们的 UID 将两个用户关联起来。如图所示,我们将 "-KvMW57CNfA40GJNDF-F" 密钥与两个 UID 关联起来。
当用户点击他们朋友页面上的聊天按钮时,我们将检查密钥并创建新的 ID,如下所示:
const key = this.fireDb.createPushId();
AngularFireDatabase 有一个 createPushId() 方法来创建唯一的 ID。
这里是来自 messaging.service.ts 的示例代码:
freshlyCreateChatIDEntry(uid: string, friendUid: string): string {
const key = this.fireDb.createPushId();
this.fireDb.object(`${USER_DETAILS_CHILD}/${CHAT_MESSAGES_CHILD}/${uid}/${friendUid}`).set({key: key});
this.fireDb.object(`${USER_DETAILS_CHILD}/${CHAT_MESSAGES_CHILD}/${friendUid}/${uid}`).set({key: key});
return key;
}
- 将消息与密钥关联:一旦我们为用户创建了密钥,我们就在 Firebase 数据库中创建新的节点,将此密钥作为节点关联,并将消息推送到此节点,以便两个用户都能访问。在数据库模式中,我们将创建具有 UIDs 的接收者和发送者节点,以便我们知道谁发送了消息,并根据此条件在聊天窗口中排列消息。
我们将密钥存储在服务的成员变量中,以便我们使用这个密钥向我们的 Firebase 数据库推送一条新消息。
以下是从 messaging.service.ts 的示例代码:
createNewMessage(newMessage: Message) {
const messageKey = this.fireDb.createPushId();
this.fireDb.object(`${MESSAGE_DETAILS_CHILD}/${this.key}/
${messageKey}`).set(newMessage).catch(error => {
console.log(error);
});
}
创建消息服务
创建消息服务的第一步是定义数据模型。我们创建一个消息模型,具有属性;消息模型由以下四个属性组成:
-
消息:这包含一个字符串类型的消息。
-
发送者 UID:发送者 UID 用于知道特定消息的发送者身份,并编写逻辑在聊天窗口的左侧面板上显示文本消息。
-
接收者 UID:接收者 UID 用于识别特定消息的接收者,并编写逻辑在聊天窗口的右侧面板上显示文本消息。
-
时间戳:此属性用于显示发送消息的日期和时间。
目前为止的message.ts文件如下:
export class Message {
message: string;
senderUid: string;
receiverUid: string;
timestamp: number;
constructor(message: string,
senderUid: string,
receiverUid: string,
timestamp: number) {
this.message = message;
this.senderUid = senderUid;
this.receiverUid = receiverUid;
this.timestamp = timestamp;
}
}
作为聊天功能的一部分,我们声明一个常量以了解 Firebase 数据库的节点,如下所示。
目前为止的database-constants.ts文件如下:
export const USER_DETAILS_CHILD = 'user-details';
export const CHAT_MESSAGES_CHILD = "chat_messages";
export const MESSAGE_DETAILS_CHILD = "message_details";
我们消息功能的核心部分是服务。此服务负责创建聊天 ID、推送新消息和获取消息。作为此应用程序的一部分,我们引入了以下四个 API:
-
isMessagePresent():当用户点击聊天按钮时,我们检查是否存在消息键,并将键存储在此服务的成员变量中。我们使用此键将任何新消息推送到 Firebase 数据库。 -
freshlyCreateChatIdEntry():当用户开始与朋友进行新的沟通时,我们调用此 API 创建键并将其存储在 Firebase 数据库中。 -
getMessages():此 API 用于订阅对话中的所有消息,并且当收到新消息时,我们也会收到更新。 -
createNewMessage():当用户点击发送按钮时,我们调用此 API 将新消息存储在 Firebase 数据库中。新消息包括消息文本、发送者 UID、接收者 UID 和时间戳。
目前为止的messaging.service.ts文件如下:
import {Injectable} from '@angular/core';
import {AngularFireDatabase} from 'angularfire2/database';
import {CHAT_MESSAGES_CHILD, MESSAGE_DETAILS_CHILD, USER_DETAILS_CHILD} from './database-constants';
import {FirebaseApp} from 'angularfire2';
import 'firebase/storage';
import {Observable} from 'rxjs/Observable';
import {Message} from './message';
/**
* Messaging service
*
*/
@Injectable()
export class MessagingService {
key: string;
/**
* Constructor
*
* @param {AngularFireDatabase} fireDb provides the functionality
related to authentication
*/
constructor(private fireDb: AngularFireDatabase) {
}
isMessagePresent(uid: string, friendUid: string): Observable<any> {
return
this.fireDb.object(`${USER_DETAILS_CHILD}/${CHAT_MESSAGES_CHILD}/
${uid}/${friendUid}`).valueChanges();
}
createNewMessage(newMessage: Message) {
const messageKey = this.fireDb.createPushId();
this.fireDb.object(`${MESSAGE_DETAILS_CHILD}/${this.key}/
${messageKey}`).set(newMessage).catch(error => {
console.log(error);
});
}
freshlyCreateChatIDEntry(uid: string, friendUid: string): string {
const key = this.fireDb.createPushId();
this.fireDb.object(`${USER_DETAILS_CHILD}/
${CHAT_MESSAGES_CHILD}/${uid}/${friendUid}`).set({key: key});
this.fireDb.object(`${USER_DETAILS_CHILD}/
${CHAT_MESSAGES_CHILD}/${friendUid}/${uid}`).set({key: key});
return key;
}
getMessages(key: string): Observable<Message[]> {
return this.fireDb.list<Message>
(`${MESSAGE_DETAILS_CHILD}/${key}`).valueChanges();
}
setKey(key: string) {
this.key = key;
}
}
在下一节中,我们将此服务集成到我们的聊天组件中。
将我们的服务集成到聊天组件中
最后,我们将消息服务集成到组件中。我们将涵盖以下三个用例:
- 检查新聊天的消息键:当用户与朋友开始对话时,我们调用消息服务的
isMessagePresent()API。对于新的对话,此聊天键将不存在,我们需要创建新的键并使用相同的键进行后续沟通:
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
this.messageService.isMessagePresent(this.user.getUid(),
this.friendUid).subscribe(snapshot => {
let snapshotValue = snapshot.val();
let friend: Friend;
if (snapshotValue == null) {
console.log("Message is empty");
this.key =
this.messageService.freshlyCreateChatIDEntry
(this.user.getUid(), this.friendUid);
} else {
this.key = snapshotValue.key;
}
this.messageService.setKey(this.key);
this.subscribeMessages();
});
}
- 订阅消息列表:我们将调用
getMessages()方法来获取消息列表的可观察对象。然后我们将订阅这个可观察对象以获取消息列表,并将其分配给messages成员变量:
subscribeMessages() {
this.messageService.getMessages(this.key)
.subscribe(
messages => {
this.messages = messages;
});
}
目前为止的chat-message-list.component.ts文件如下:
import {AfterViewChecked, ChangeDetectorRef, Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core';
import {MessagingService} from '../../services/messaging.service';
import {Message} from '../../services/message';
import {UserService} from '../../services/user.service';
import {User} from '../../services/user';
@Component({
selector: 'app-chat-message-list',
styleUrls: ['chat-message-list.component.scss'],
templateUrl: 'chat-message-list.component.html'
})
export class ChatMessageListComponent implements OnInit, AfterViewChecked {
@Input() friendUid: string;
private user: User;
messages: Message[];
key: string;
@ViewChild('scrollContainer') private scrollContainer:
ElementRef;
constructor(private messageService: MessagingService,
private userService: UserService,
private cdRef: ChangeDetectorRef) {
}
ngOnInit() {
this.user = this.userService.getSavedUser().getValue();
this.messageService.isMessagePresent(this.user.uid ,
this.friendUid).subscribe(snapshot => {
if (snapshot == null) {
console.log('Message is empty');
this.key =
this.messageService.freshlyCreateChatIDEntry
(this.user.uid, this.friendUid);
} else {
this.key = snapshot.key;
}
this.messageService.setKey(this.key);
this.subscribeMessages();
});
}
ngAfterViewChecked() {
this.scrollToBottom();
this.cdRef.detectChanges();
}
scrollToBottom(): void {
try {
this.scrollContainer.nativeElement.scrollTop =
this.scrollContainer.nativeElement.scrollHeight;
} catch (err) {
console.log('Error');
}
}
subscribeMessages() {
this.messageService.getMessages(this.key)
.subscribe(
messages => {
this.messages = messages;
});
}
}
- 向 Firebase 数据库发送消息:当用户输入消息并点击发送按钮时,我们创建新的消息对象,并在消息服务中调用
createNewMessage()方法,这将负责将消息发送到 Firebase 数据库。
sendMessage() {
const message: Message = new Message(this.newMessage,
this.uid, this.friendUid, Date.now());
this.messageService.createNewMessage(message);
}
目前为止的chat-message-form.component.ts文件如下:
import {Component, Input, OnInit} from '@angular/core';
import {MessagingService} from '../../services/messaging.service';
import {Message} from '../../services/message';
import {UserService} from '../../services/user.service';
@Component({
selector: 'app-chat-message-form',
styleUrls: ['chat-message-form.component.scss'],
templateUrl: 'chat-message-form.component.html'
})
export class ChatMessageFormComponent implements OnInit {
@Input() friendUid: string;
uid: string;
newMessage: string;
constructor(private messageService: MessagingService,
private userService: UserService) {
}
ngOnInit() {
this.uid = this.userService.getSavedUser().getValue().uid;
}
sendMessage() {
const message: Message = new Message(this.newMessage,
this.uid, this.friendUid, Date.now());
this.messageService.createNewMessage(message);
}
}
最后,我们为我们的朋友应用程序创建了一个完全功能的聊天功能。
摘要
现在我们已经到达了朋友应用中聊天功能的结尾。在这一章中,我们涵盖了路由参数,并使用它将朋友的 UID 传递给我们的聊天模块。我们通过使用 @Input 绑定将这个 UID 传递给不同的聊天组件。然后,我们讨论了如何为我们的聊天功能设计 Firebase 数据库。我们在服务中使用了 Firebase 数据库 API,并创建了四个 API 来在我们的聊天功能中执行不同的操作。最后,我们将这个服务与聊天组件集成。
在下一章中,我们将讨论 Angular 中的单元测试。我们还将讨论 Jasmine 框架,并使用这个框架来对我们的应用程序进行单元测试。