Angular5-和-Firebase-全栈开发实用指南-三-

40 阅读22分钟

Angular5 和 Firebase 全栈开发实用指南(三)

原文:zh.annas-archive.org/md5/f113aa7bff00c98a8b38bd392d37f2d0

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:单元测试我们的应用程序

在本章中,我们将详细讲解 Angular 测试。我们将从 Angular 测试的基本介绍开始,了解用于单元测试的工具和技术。我们将使用 Angular 测试框架为我们的登录组件编写单元测试,并配置一个依赖模块。我们还将对用户服务进行单元测试。作为测试的一部分,我们将为依赖服务或组件创建存根,以便我们只关注正在测试的类。我们将使用 Angular 框架对组件和服务进行单元测试,以便初始化依赖模块。我们将单独测试 Angular 管道,以便直接使用 new 关键字初始化管道。最后,我们将查看代码覆盖率。

在本章中,我们将涵盖以下主题:

  • Angular 测试简介

  • 单元测试 Angular 组件

  • 单元测试 Angular 服务

  • 单元测试 Angular 管道

  • 代码覆盖率简介

Angular 测试简介

单元测试是软件开发生命周期的重要组成部分。单元测试的好处如下:

  • 它有助于使我们的实现与设计保持一致

  • 它有助于保护我们的应用程序免受回归的影响

  • 如果我们有良好的测试用例,重构会变得更容易

Angular 提供了各种工具和技术来测试我们的应用程序。作为单元测试的一部分,我们将使用以下技术:

  • Jasmine:它为我们编写单元测试提供了基本框架。它附带一个 HTML 测试运行器,并在浏览器上运行。

  • Angular 测试框架:它与 Angular 框架一起提供,有助于为正在测试的 Angular 代码创建测试环境。它还为我们提供了访问 DOM 元素的能力。

  • Karma:我们使用 Karma 工具运行我们的应用程序。我们使用以下命令运行单元测试:

$ng test

我们可以编写两种类型的 Angular 测试:

  • 使用 Angular 测试框架进行单元测试:我们将使用 Angular 测试框架为我们的组件和服务编写单元测试。这将创建一个测试环境,并为我们提供访问 Angular 框架各个元素的能力。

  • 隔离单元测试:我们可以编写不依赖 Angular 的独立单元测试。这种单元测试对于测试服务和管道非常有用。在本章中,我们将以这种方式测试我们的日期管道。

值得注意的是,当你能够做到的时候,始终最好坚持使用隔离单元测试,并尽可能少地编写集成和端到端测试,因为隔离单元测试最容易维护。

单元测试 Angular 组件

在本节中,我们将编写第一个针对登录组件的 Angular 测试。编写测试用例涉及的步骤如下:

  1. 识别测试的类:编写单元测试用例的第一步是识别依赖项。登录组件的constructor显示了所有依赖项,并且依赖于UserServiceRouterAuthenticationServiceAngularFireAuth
constructor(private userService: UserService,
         private router: Router,
         private authService: AuthenticationService,
         private angularFireAuth: AngularFireAuth)
  1. 创建所有模拟或存根类:一旦我们识别了所有依赖项,我们需要消除这些外部依赖项,并专注于测试的组件类。因此,我们创建模拟或存根类来消除这些依赖项。

在登录组件中,我们使用用户服务通过getUser()方法检索用户信息,因此我们创建了一个具有getUser()方法的UserServiceStub,该方法返回封装在Observable对象中的模拟用户;我们为模拟用户创建了一个包含用户详情的测试数据类,如下所示:

class UserServiceStub {

   getUser(): Observable<User> {
      return Observable.of(mockUserJSON);
   }

}

这是user-test-data.ts文件的示例:

export const mockUserJSON = {
   email: 'user@gmail.com',
   friendcount: 0,
   image: '',
   mobile: '9999999999',
   name: 'User',
   uid: 'XXXX'
};

登录组件中我们使用认证服务进行登录和重置密码,因此我们创建了一个具有空的login()resetPassword()方法的AuthenticationServiceStub类:

class AuthenticationServiceStub {

   login(email: string, password: string) {}

   resetPassword(email: string) {}
}

AngularFireAuth类是 Angular 的 Fire 库的一部分。这个类负责我们应用中的认证,并包含一个auth对象,因此我们也为auth类创建了一个存根:

class AngularFireAuthStub {
   readonly auth: AuthStub = new AuthStub();,
}

这是AuthStub类:

class AuthStub {

   onAuthStateChanged() {
      return Observable.of({uid: '1234'});
   }
}

最后,我们使用路由器导航到应用中的页面。这个提供者是 Angular 框架的一部分。我们也为这个类创建了一个存根:

class RouterStub {
   navigateByUrl(url: string) {
      return url;
   }
}
  1. 创建测试套件:一旦我们消除了外部依赖项,我们可以在 Jasmine 框架的describe()方法中创建测试套件。此方法接受测试套件的description和用于 Jasmine 框架的specDefinitions函数来调用 spec 的内部套件:
describe(description: string, specDefinitions: () => void)
  1. 创建测试环境:我们为要测试的组件创建 Angular 测试环境。Angular 提供了一个TestBed类来创建测试环境;它初始化依赖模块、提供者、服务和组件。我们在beforeEach()方法中调用TestBed.configureTestingModule()方法,以便在每次测试用例执行之前配置模块:
beforeEach(async(() => {

  TestBed.configureTestingModule({
    declarations: [
      LoginComponent,
      ErrorAlertComponent
    ],
    imports: [
      CommonModule,
      BrowserModule,
      FormsModule
    ],
    providers: [
      {provide: UserService, useClass: UserServiceStub},
      {provide: Router, useClass: RouterStub},
      {provide: AuthenticationService, useValue: mockAuthService},
      {provide: AngularFireAuth, useClass: AngularFireAuthStub}
    ]
  }).compileComponents();
}));
  1. 初始化测试对象:一旦我们配置了模块,我们可以使用TestBed.createComponent()创建一个登录组件固定装置,并初始化登录组件和调试元素:
beforeEach(() => {
   fixture = TestBed.createComponent(LoginComponent);
   component = fixture.componentInstance;
   de = fixture.debugElement;
   fixture.detectChanges();
});
  1. 编写第一个测试用例:最后的步骤是编写测试用例。我们的第一个测试用例是检查登录组件是否已实例化。我们将在it()方法中编写测试用例,并使用expect()来验证实例:
it('Should instantiate LoginComponent', async(() => {
   expect(component instanceof LoginComponent).toBe(true,
      'LoginComponent not created');
}));
  1. 销毁创建的实例:在每个测试用例之后,我们在afterEach()方法中清除实例,如下所示:
afterEach(async(() => {
   fixture.detectChanges();
   fixture.whenStable().then(() => fixture.destroy());
}));
  1. 运行测试:最后,我们使用以下命令运行我们的第一个测试用例:
$ng test

在其成功运行后,它将在浏览器中打开,并显示状态为 1 个 spec 成功,0 个失败:

图片

  1. 添加更多单元测试:在下一个测试用例中,我们检查当用户输入他们的电子邮件和密码并点击登录按钮时,我们的服务中的 login 方法是否被调用。首先,我们初始化 DOM 元素,例如电子邮件输入文本、密码输入文本和登录按钮。接下来,我们初始化电子邮件和密码的默认值,并 spyOn 服务中的登录方法,以便在用户点击登录按钮时调用此模拟方法。点击登录按钮后,我们调用 detectChanges() 通知 DOM 刷新元素。最后,我们验证 login() 方法应该被调用:
it('Should call login', async(() => {
   const loginButton = de.query(By.css('#login-btn'));
   expect(loginButton).not.toBeNull('Login button not found');

   spyOn(mockAuthService, 'login').and.callThrough();
   de.query(By.css('#email')).nativeElement.value = 
   'user@gmail.com';
   de.query(By.css('#password')).nativeElement.value = 'password';
   fixture.detectChanges();

   // Click on Login button 
   loginButton.nativeElement.click();
   fixture.detectChanges();
   expect(mockAuthService.login).toHaveBeenCalled();
}));

现在是 login.component.spec.ts 文件:

import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
import {Router} from '@angular/router';
import {UserService} from '../../services/user.service';
import {Observable} from 'rxjs/Observable';
import {User} from '../../services/user';
import {mockUserJSON} from '../../test-data/user-test-data';
import {AuthenticationService} from '../../services/authentication.service';
import {AngularFireAuth} from 'angularfire2/auth';
import {CommonModule} from '@angular/common';
import {BrowserModule, By} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {DebugElement} from '@angular/core';
import {ErrorAlertComponent} from '../../shared/error-alert/error-alert.component';

class RouterStub {
  navigateByUrl(url: string) {
    return url;
  }
}

class UserServiceStub {

  getUser(): Observable<User> {
    return Observable.of(mockUserJSON);
  }

}

class AuthenticationServiceStub {

  login(email: string, password: string) {
  }

  resetPassword(email: string) {
  }
}

class AngularFireAuthStub {
  readonly auth: AuthStub = new AuthStub();
}

class AuthStub {

  onAuthStateChanged() {
    return Observable.of({uid: '1234'});
  }
}

describe('LoginComponent with tests', () => {

  let fixture: ComponentFixture<LoginComponent>;
  let component: LoginComponent;
  let de: DebugElement;
  const mockAuthService: AuthenticationServiceStub = new 
  AuthenticationServiceStub();

  beforeEach(async(() => {

    TestBed.configureTestingModule({
      declarations: [
        LoginComponent,
        ErrorAlertComponent
      ],
      imports: [
        CommonModule,
        BrowserModule,
        FormsModule
      ],
      providers: [
        {provide: UserService, useClass: UserServiceStub},
        {provide: Router, useClass: RouterStub},
        {provide: AuthenticationService, useValue: mockAuthService},
        {provide: AngularFireAuth, useClass: AngularFireAuthStub}
      ]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    de = fixture.debugElement;
    fixture.detectChanges();
  });

  afterEach(async(() => {
    fixture.detectChanges();
    fixture.whenStable().then(() => fixture.destroy());
  }));

  it('Should instantiate LoginComponent', async(() => {
    expect(component instanceof LoginComponent).toBe(true,
      'LoginComponent not created');
  }));

  it('Should call login', async(() => {
    const loginButton = de.query(By.css('#login-btn'));
    expect(loginButton).not.toBeNull('Login button not found');

    spyOn(mockAuthService, 'login').and.callThrough();
    de.query(By.css('#email')).nativeElement.value = 'user@gmail.com';
    de.query(By.css('#password')).nativeElement.value = 'password';
    fixture.detectChanges();

    // Login button is enabled
    expect(loginButton.nativeElement.disabled).toBe(false);
    loginButton.nativeElement.click();
    fixture.detectChanges();
    expect(mockAuthService.login).toHaveBeenCalled();
  }));

});

单元测试 Angular 服务

在本节中,我们单元测试 Angular 服务,并为我们的用户服务编写测试用例。单元测试服务的步骤与我们的组件相同。

编写单元测试用例的第一步是分析依赖组件,因为我们看到用户服务依赖于 AngularFireDatabase 并初始化 Firebase 存储对象:

constructor(private fireDb: AngularFireDatabase)

因此,我们为这个依赖对象创建了一个模拟,例如 AngularFireDatabaseStub,它包含其他依赖模拟,如 AngularFireAppStubAngularFireObjectStub 对象引用以及 object() 方法:

class AngularFireDatabaseStub {

   app: AngularFireAppStub = new AngularFireAppStub;

   angularFireObject: AngularFireObjectStub;

   constructor(angularFireObject: AngularFireObjectStub) {
      this.angularFireObject = angularFireObject;
   }

   object(pathOrRef: PathReference): AngularFireObjectStub {
      return this.angularFireObject;
   }
}

以下是一个带有空模拟方法的 AngularFireAppStub 模拟类:

class AngularFireAppStub {

   storage() {}
}

以下是一个空的模拟方法的 AngularFireObjectStub 模拟类:

class AngularFireObjectStub {

   set() {}

   valueChanges() {}

   update() {}

}

下一步是使用 TestBed 初始化测试环境,并通过 TestBed.get() 获取用户服务对象:

const angularFireObject: AngularFireObjectStub = new AngularFireObjectStub();
const mockAngularFireDatabase: AngularFireDatabaseStub = new AngularFireDatabaseStub(angularFireObject);
let userService: UserService;

beforeEach(() => {
   TestBed.configureTestingModule({
      providers: [
         {provide: AngularFireDatabase, useValue: 
          mockAngularFireDatabase},
         {provide: UserService, useClass: UserService}
      ]
   });
   userService = TestBed.get(UserService);
});

现在,我们将开始编写我们的用户服务的测试用例。我们将涵盖以下用户服务的测试用例:

  • 第一个测试用例是将用户添加到 Firebase 数据库。我们将向 Angular 的 fire 对象的 set 方法添加 spyOn 并使用模拟用户调用添加用户方法;然后我们期望 Angular fire 对象的 set 方法被调用:
it('Add user', () => {
   spyOn(angularFireObject, 'set');
   userService.addUser(mockUserJSON);
   expect(angularFireObject.set).toHaveBeenCalled();
});

下一个测试用例是从 Firebase 数据库接收我们的用户。我们添加 spyOn Angular 的 fire 对象的值变化方法,它返回一个模拟用户。然后我们调用 getUser 方法,订阅 Observable 对象,然后验证方法调用,并使用预期值测试我们的模拟用户的内容:

it('getUser return valid user', () => {
   spyOn(angularFireObject, 
   'valueChanges').and.returnValue(Observable.of(mockUserJSON));
   userService.getUser(mockUserJSON.uid).subscribe((user) => {
      expect(angularFireObject.valueChanges).toHaveBeenCalled();
      expect(user.uid).toBe(mockUserJSON.uid);
      expect(user.name).toBe(mockUserJSON.name);
      expect(user.mobile).toBe(mockUserJSON.mobile);
      expect(user.email).toBe(mockUserJSON.email);
   });

});
  1. 下一个测试用例是将用户保存到 member 变量中。在这个测试用例中,我们将一个模拟用户保存到一个 Observable 中,然后使用 get 方法检索用户,并验证模拟用户的所有属性:
it('saveUser saves user in Subject', () => {
   userService.saveUser(mockUserJSON);
   userService.getSavedUser().subscribe((user) => {
      expect(user.uid).toBe(mockUserJSON.uid);
      expect(user.name).toBe(mockUserJSON.name);
      expect(user.mobile).toBe(mockUserJSON.mobile);
      expect(user.email).toBe(mockUserJSON.email);
   });

});
  1. 下一个测试用例是更新 Firebase 数据库中的电子邮件,并更新用户服务类中的缓存用户对象;我们 spyOn Angular 的 fire 对象的 update 方法,传递一个新电子邮件以更新 email 方法,这将更新 Firebase 数据库和缓存用户对象,测试 Firebase 数据库调用,从 get 方法检索用户,并验证模拟用户的所有属性:
it('updateEmail update the email', () => {
   spyOn(angularFireObject, 'update');
   userService.saveUser(mockUserJSON);
   mockUserJSON.email = 'user1@gmail.com';
   userService.updateEmail(mockUserJSON , mockUserJSON.email);
   userService.getSavedUser().subscribe((user) => {
      expect(angularFireObject.update).toHaveBeenCalled();
      expect(user.email).toBe(mockUserJSON.email);
   });

});

现在是 user.service.spec.ts 文件:

import {UserService} from './user.service';
import {AngularFireDatabase, PathReference} from 'angularfire2/database';
import {FirebaseApp} from 'angularfire2';
import {mockUserJSON} from '../test-data/user-test-data';
import {AngularFireAuth} from 'angularfire2/auth';
import {TestBed} from '@angular/core/testing';
import {Observable} from 'rxjs/Observable';
import {User} from './user';

class AngularFireDatabaseStub {

   app: AngularFireAppStub = new AngularFireAppStub;

   angularFireObject: AngularFireObjectStub;

   constructor(angularFireObject: AngularFireObjectStub) {
      this.angularFireObject = angularFireObject;
   }

   object(pathOrRef: PathReference): AngularFireObjectStub {
      return this.angularFireObject;
   }
}

class AngularFireAppStub {

   storage() {}
}

class AngularFireObjectStub {

   set() {}

   valueChanges() {}

   update() {}

}

describe('User service test suites', () => {

   const angularFireObject: AngularFireObjectStub = new 
   AngularFireObjectStub();
   const mockAngularFireDatabase: AngularFireDatabaseStub = new 
   AngularFireDatabaseStub(angularFireObject);
   let userService: UserService;

   beforeEach(() => {
      TestBed.configureTestingModule({
         providers: [
            {provide: AngularFireDatabase, useValue: 
             mockAngularFireDatabase},
            {provide: UserService, useClass: UserService}
         ]
      });
      userService = TestBed.get(UserService);
   });

   it('Add user', () => {
      spyOn(angularFireObject, 'set');
      userService.addUser(mockUserJSON);
      expect(angularFireObject.set).toHaveBeenCalled();
   });

   it('getUser return valid user', () => {
      spyOn(angularFireObject, 
      'valueChanges').and.returnValue(Observable.of(mockUserJSON));
      userService.getUser(mockUserJSON.uid).subscribe((user) => {
         expect(angularFireObject.valueChanges).toHaveBeenCalled();
         expect(user.uid).toBe(mockUserJSON.uid);
         expect(user.name).toBe(mockUserJSON.name);
         expect(user.mobile).toBe(mockUserJSON.mobile);
         expect(user.email).toBe(mockUserJSON.email);
      });

   });

   it('saveUser saves user in Subject', () => {
      userService.saveUser(mockUserJSON);
      userService.getSavedUser().subscribe((user) => {
         expect(user.uid).toBe(mockUserJSON.uid);
         expect(user.name).toBe(mockUserJSON.name);
         expect(user.mobile).toBe(mockUserJSON.mobile);
         expect(user.email).toBe(mockUserJSON.email);
      });

   });

   it('updateEmail update the email', () => {
      spyOn(angularFireObject, 'update');
      userService.saveUser(mockUserJSON);
      mockUserJSON.email = 'user1@gmail.com';
      userService.updateEmail(mockUserJSON , mockUserJSON.email);
      userService.getSavedUser().subscribe((user) => {
         expect(angularFireObject.update).toHaveBeenCalled();
         expect(user.email).toBe(mockUserJSON.email);
      });

   });

   it('updateMobile update the mobile', () => {
      spyOn(angularFireObject, 'update');
      userService.saveUser(mockUserJSON);
      mockUserJSON.mobile = '88888888';
      userService.updateEmail(mockUserJSON , mockUserJSON.mobile);
      userService.getSavedUser().subscribe((user) => {
         expect(angularFireObject.update).toHaveBeenCalled();
         expect(user.mobile).toBe(mockUserJSON.mobile);
      });
   });
});

单元测试 Angular 管道

Angular 管道单元测试是独立于 Angular 测试环境测试类的示例。在这个例子中,我们测试了我们的朋友日期管道类,并在测试类中创建了对象:

const pipe = new FriendsDatePipe();

在此对象上,我们将编写以下两个测试用例:

  1. 首先,测试绿色字段场景,即我们传递一个有效的日期(以毫秒为单位),并测试转换后的人类可读日期格式

  2. 第二,测试边缘情况场景,即我们传递一个无效日期-1,并期望返回一个字符串值"Invalid Date"

import {FriendsDatePipe} from './friendsdate.pipe';

describe('friendsdatepipe', () => {

   const pipe = new FriendsDatePipe();

   it('Transform dateInMillis to MM/DD/YY', () => {
      expect(pipe.transform('1506854340801')).toBe('10/01/17');
   });

   it('Transform invalid date', () => {
      expect(pipe.transform('-1')).toBe('Invalid Date');
   });

});

代码覆盖率

应用程序的代码覆盖率反映了我们代码的整体覆盖率。这为我们提供了代码行和函数覆盖率的概述,以便我们可以编写更多的测试用例来覆盖代码的其他部分。

我们可以在package.json中启用代码覆盖率,如下所示:

"scripts": {
  "ng": "ng",
  "start": "ng serve",
  "build": "ng build",
  "test": "ng test --sourcemaps false",
  "coverage": "ng test --sourcemaps false --watch=false 
   --code-coverage",
  "lint": "ng lint",
  "e2e": "ng e2e"
}

我们将执行以下命令,该命令运行测试用例,并创建一个coverage文件夹,其中包含index.html以显示覆盖率统计信息:

$ng test --codeCoverage

当我们打开index.html时,它显示了一个包含覆盖率概览的美丽表格:

图片

摘要

在本章中,我们介绍了单元测试,并讨论了 Angular 单元测试中的各种术语和术语。我们为我们的登录组件实现了单元测试,覆盖了TestBed,并配置了我们的模块。我们为我们的服务编写了单元测试,为外部依赖类创建了存根,并在我们的模块中注入了这些存根类。我们还为 Angular 管道编写了隔离的测试用例。最后,我们讨论了代码覆盖率,并为我们规格运行了代码覆盖率。

在下一章中,我们将讨论调试技术。这将帮助我们更快地解决问题和调试。

第十一章:调试技巧

调试被认为是软件开发最重要的部分。这个过程提高了效率并减少了开发时间。在本章中,我们将讨论不同的方法,并涵盖调试应用程序所需的所有方面。我们将从使用 Chrome 的开发者工具进行 Angular 和 HTML 调试开始,并简要介绍 Augury 用于调试 Angular 组件。我们将继续进行 TypeScript 调试,因为这有助于检查我们的 TypeScript 代码中的错误。我们将涵盖 CSS,这样我们就可以从工具本身设计许多样式元素。最后,我们将查看使用 Postman 和开发者工具的网络 API 调用。作为本章的一部分,我们将介绍 Chrome 的浏览器工具。

在本章中,我们将介绍以下主题:

  • Angular 调试

  • 调试网络应用程序

  • TypeScript 调试

  • CSS 调试

  • 调试网络

Angular 调试

在本节中,我们将从 Chrome 的 Augury 开始。这个工具以非常棒的方式展示了各种 Angular 组件及其依赖关系。它为我们提供了对应用程序以及不同构建块之间关系的洞察。

安装 Augury

安装 Augury 的最佳方式是使用 Chrome 的网络商店。另外,您也可以从 Augury 网站安装它。按照以下步骤安装 Augury:

  1. 在您的 Chrome 浏览器中打开 chrome://apps/

  2. 在右下角点击“网络商店”。

  3. 在网络商店页面,将 augury 输入搜索框并按 Enter 键,Augury 将出现在右侧面板上,如下所示;在我的情况下,这显示为 ADDED,但对于新安装,将出现一个“添加到 Chrome”按钮,您需要点击:

最后,当 Augury 安装完成后,一个带有黄色背景的黑色月亮将出现在 Chrome 浏览器右上角,如下所示:

使用 Augury 的功能

Augury 提供了良好的功能,有助于我们一眼预览数据和组件。在本章中,我们将介绍以下功能:

  • 组件树

  • 路由树

  • NgModules

组件树

当您启动 Augury 控制台时,您可以看到的第一个视图是组件树。以下截图显示了我们的朋友应用程序的用户配置文件视图:

上述组件树显示了 AppComponentUserProfileComponent 的层次视图。在右侧面板上,我们有“属性”选项卡,其中包含以下内容:

  • 查看源代码:这显示了组件的实际源代码

  • 变更检测:这显示了我们在组件中是否使用了变更检测

  • 状态:这显示了类的所有实例成员变量

  • 依赖关系:这显示了组件与其他组件或提供者的依赖关系

另一个视图是注入器图,它显示了特定组件或提供者被注入的位置:

路由树

Augury 为应用程序提供路由信息。这有助于查看应用程序中使用的所有路由。此选项位于组件树选项旁边,如图所示:

图片

NgModule

NgModule 是 Augury 中添加的另一个有用功能。它位于路由树旁边。它提供了有关特定应用程序模块中配置的所有导入、导出、提供者、声明和提供 InDeclarations 的信息,如下所示:

图片

调试 Web 应用程序

在本节中,我们将介绍 Chrome 开发者工具。这些工具为我们提供了分析应用程序的许多功能。我们将介绍以下主题:

  • HTML DOM

  • 布局预览

HTML DOM

您可以通过在浏览器页面上右键单击鼠标并选择“检查”或按F12来打开 Chrome 开发者工具。Chrome 开发者工具将以多个标签页打开。

您可以点击“元素”标签页,它显示以<html>标签为根的 DOM 元素。您可以通过每个元素上的右箭头图标进一步展开它,如下所示:

图片

上述预览有助于调试 HTML 元素。

布局预览

布局预览是一个很好的功能,可以预览与实际浏览器视图相同的布局。此工具提供双向视图,您可以使用鼠标箭头键在浏览器中的网页上悬停,并显示您的开发者工具中的实际 HTML 元素。您还可以在开发者工具中悬停在 HTML 元素上,并在浏览器中查看高亮视图,如图所示的后继截图。当您悬停在浏览器上时,您将看到实际使用的.user-profile样式的<div>标签,并且当您点击该元素时,开发者工具中的元素将展开。

图片

此布局预览有助于调试我们的实时应用程序。

调试 TypeScript

这又是调试实时 TypeScript 代码的一个重要方面。Chrome 工具提供了良好的机制来调试代码。在本节中,我们将介绍 Chrome 开发者工具中“源”标签页的以下功能:

  • 查看和搜索源文件

  • 设置断点和监视实时值

  • 在控制台窗口中添加代码

查看和搜索源文件

我们可以在开发者工具的“源”标签页中看到我们的 TypeScript 文件。所有文件都显示在左侧面板下的webpack://文件夹中。文件夹将如下所示:

图片

您也可以使用 Ctrl + P 命令搜索文件。源代码将显示在中间面板中:

图片

设置断点和监视实时值

实时调试是调试中最有趣的部分,并且对于调试遗留代码非常有帮助,因为它通过在代码中设置断点来帮助了解代码流。

您可以通过单击代码行中的数字来启用断点,如下所示:

图片

当你刷新页面时,应用程序会停在断点行。你可以使用右上角的面板遍历代码,该面板有跳过、进入、退出和恢复应用程序的命令。你还可以使用“观察”面板右下角的加号图标添加观察值;如下所示,我们在右侧的“观察”面板上添加了一个用户对象:

图片

这是一个快速调试问题或了解应用程序流程的方法。

在控制台窗口中添加代码

开发者工具提供了一种在部署的代码上添加代码并实时编码的功能。我们可以打开一个文件,然后点击行来向现有文件添加实现。

在以下屏幕截图中,我们在控制台窗口中添加了console.log以记录日志:

图片

此编辑器还提供内容辅助,这是根据用户请求的上下文敏感内容补全。

当我们将以下命令输入代码的任何一行时,会出现内容辅助;一个窗口会出现所有引用选项,你可以通过输入特定的字母键进一步筛选选项:

$Ctrl + Space bar

查看 HTML 页面的内容辅助:

图片

此功能有助于向现有文件添加代码,并更快地调试代码。

调试 CSS

调试 CSS 是 Web 开发的另一个方面。在本节中,我们将介绍调试和设计实时 CSS 元素。Chrome 开发者工具提供了一个更改样式元素和添加新元素的选择。我们将介绍以下功能:

  • 探索样式面板

  • 发现和修改样式

探索样式面板

一旦你打开了 Chrome 的开发者工具,打开“元素”选项卡并点击任何 HTML <div> 标签;你应该在右侧看到样式面板出现。此面板由三个标签组成:

  • 样式:这显示了应用于特定 HTML 元素的所有样式:

图片

  • 计算值:这显示了应用于特定 HTML 元素的所有计算值;这还显示了包含内容、填充、边框和边距信息的盒模型,关于所选 HTML 元素:

图片

  • 事件监听器:这显示了特定 HTML 元素可用的所有点击事件:

图片

发现和修改样式

在本节中,我们将修改 HTML 元素的样式,作为练习的一部分,我们将修改现有的用户个人资料页。我们执行以下步骤:

  1. 打开朋友应用程序。

  2. 前往用户个人资料页。

  3. 将鼠标悬停在用户个人资料页上,如下所示:

图片

  1. 在浏览器页面上单击用户个人资料页;它打开 Chrome 开发者工具,突出显示 HTML 元素,在右侧面板中你可以看到.user-profile样式。

  2. 当你悬停在样式元素上时,会出现所有样式规则的复选框,我们可以取消选中复选框来禁用特定的样式并查看用户个人资料页上的效果。在以下示例中,我们禁用了宽度样式规则:

图片

  1. 我们可以向现有的样式规则中添加新的样式。当我们悬停在溢出图标上时,会出现一个工具提示,其中包含添加新样式的选项,并且,随着我们添加样式,它还支持内容辅助:

图片

  1. 最后,我们可以通过点击现有项来编辑现有的样式。在以下示例中,我们将宽度从 50%更改为 60%:

图片

工具中的此选项有助于调试我们网页中的样式。

网络调试

网络调试在理解 API 调用及其响应方面非常有用。在 Firebase API 调用中,我们不会发现它有很大用处,因为 Firebase 数据库门户提供了 JSON 响应的视图。当我们探索网络调用的实时调试时,网络调试工具非常方便。在本节中,我们将讨论以下两个工具:

  • Postman:这与 Augury 扩展类似;您可以从 Chrome 扩展程序安装 Postman,或者您可以从www.getpostman.com/下载特定操作系统的安装程序。这个工具在开发初期阶段非常有用,因为它有助于理解 API 和响应,并相应地集成 API。您可以使用授权、头和正文创建 HTTP 方法,如 GET、POST、PUT 或 DELETE:

图片

  • Chrome 开发者工具中的网络标签页:这在 Chrome 开发者工具中实时调试网络调用时非常有用。这显示了页面加载时的所有网络调用。您还可以应用过滤器以查看特定网络调用类型:

图片

网络调试有助于确认来自服务器的预期响应。

摘要

在本章中,我们涵盖了调试技术的不同方面。我们从与浏览器相关的调试技术开始,这有助于分析和预览 HTML 元素。我们涵盖了 TypeScript 调试,并在部署的应用程序文件上设置了断点。我们还涵盖了 CSS 调试。我们在 CSS 面板中禁用并添加了样式。最后,我们涵盖了网络调试,其中我们讨论了 Postman 工具和 Chrome 开发者网络标签页。调试技术对于成为一名高效的 Web 开发者有很大帮助。

在下一章中,我们将把我们的应用程序部署到 Firebase 服务器。我们还将启用 Firebase 安全,因为这使我们的应用程序更加安全。

第十二章:Firebase 安全和托管

Firebase 提供灵活的安全规则,语法类似于 JavaScript,这有助于我们结构化数据和索引常用数据。安全规则与 Firebase 身份验证集成,有助于根据用户定义读取和写入访问权限。在本章中,我们将为 Firebase 数据库中的用户和聊天节点添加安全规则。Firebase 安全规则提供了一个很好的模拟器,在将新规则发布到生产环境之前进行检查。我们还将索引用户及其朋友的数据,以便更快地进行查询。最后,我们将应用程序部署到 Firebase 服务器。我们将设置不同的部署环境,以便我们可以在预发布服务器上测试我们的应用程序,然后将应用程序部署到生产服务器。

在本章中,我们将介绍以下主题:

  • 介绍 Firebase 安全

  • 为用户添加安全规则

  • 为聊天消息添加安全规则

  • 索引用户及其朋友

  • 设置多个部署环境

  • 在 Firebase 托管朋友应用

介绍 Firebase 安全

Firebase 提供了管理我们应用程序安全性的工具,因为我们可以在 Firebase 数据库中添加规则并验证数据输入。Firebase 为我们应用程序提供了以下安全:

  • 身份验证:确保我们的应用程序安全的第一步是识别用户。Firebase 身份验证支持多种身份验证机制,例如 Google、Facebook、电子邮件和密码身份验证。

  • 授权:一旦用户经过身份验证,我们就需要控制对数据库中数据的访问。Firebase 安全规则具有内置的变量和函数,例如auth对象,它有助于控制用户的读取和写入操作。

前往 Firebase 门户并导航到数据库|规则选项卡。默认的 Firebase 安全规则如下;auth!= null条件表示只有经过身份验证的用户才能访问 Firebase 数据库中的数据。

图片

Firebase 安全规则提供了以下四种类型的函数:

  • .read:我们可以为数据定义此函数,以控制用户的读取操作。

以下示例显示,只有已登录的用户才能读取自己的用户数据:

{
    "rules":{
        "users":{
            "$uid":{
                ".read": "auth != null && $uid === auth.uid"
            }
        }
    }
}
  • .write:我们为此数据定义此函数,以控制用户的写入操作。

以下示例显示,只有已登录的用户才能在其自己的用户数据节点上写入:

{
    "rules":{
        "users":{
            "$uid":{
                ".write": "auth != null && $uid === auth.uid"
            }
        }
    }
}
  • .validate:此函数维护数据的完整性,此变量提供数据验证。

以下示例验证name字段为字符串:

{
  "rules":{
     "users":{
        "$uid":{
           "name": {
              ".validate":"newData.isString()"                  
            }
          }
        }
     }
}
  • .indexOn:这为查询和排序数据提供了子索引。

在以下示例中,我们索引了用户数据的name字段。

{
  "rules":{
     "users":{
        ".indexOn": ["name"],
        "$uid":{
           "name": {
              ".validate":"newData.isString()"                  
            }
          }
        }
     }
}

Firebase 安全规则还提供了以下预定义变量,用于定义安全规则:

  • root:此变量提供了一个RuleDataSnapshot实例,用于从 Firebase 数据库的根访问数据。

以下根变量用于从根用户节点遍历 Firebase 数据库路径:

{
  "rules":{
     "users":{
        "$uid":{
           "image": {               
             ".read":"root.child('users').
             child(auth.uid).child('image').val() === ''"                  
            }
          }
        }
     }
}
  • newData:此变量提供了一个表示插入操作后存在的新的RuleDataSnapshot实例。

以下示例验证新的数据是否为字符串:

"name": {
    ".validate":"newData.isString()"                  
}
  • data:此变量提供了一个表示插入操作之前存在的数据的RuleDataSnapshot实例。

以下示例显示当前name字段中的数据不为空:

"user": {
   "$uid":{
      ".read":"data.child('name').val() != null"
   }
}
  • $variables:此变量代表动态 ID 和键。

在以下示例中,唯一 ID 被分配给$uid变量:

"user": {
   "$uid":{
   }
}
  • auth:这代表auth对象,它提供了用户的 UID。

在以下示例中,我们访问auth对象以获取用户的 UID:

"$uid":{
    ".write": "auth != null && $uid === auth.uid"
 }
  • now:此变量提供当前时间的毫秒数,有助于验证时间戳。

在以下示例中,我们得出结论,时间戳大于当前时间:

"$uid":{
    ".write": "newData.child('timestamp').val() >   now"
 }

为用户添加安全规则

在我们的应用程序中,用户详情起着至关重要的作用,因此我们需要为用户详情提供安全规则。我们已经看到了默认的安全设置。默认情况下,只有认证用户可以访问我们 Firebase 数据库的任何部分。我们将修改用户节点的安全规则,而暂时保留其他节点的默认安全规则。

如您从以下截图中所见,对于users节点,对于具有相同唯一用户 ID 的认证用户,允许进行readwrite操作;我们还需要验证用户节点中的数据类型以保持数据完整性:

为了验证我们的安全规则的变化,Firebase 提供了一个模拟器来测试我们的更改,在将其部署到生产环境之前。您将在“RULES”标签页的右上角看到一个“SIMULATOR”选项。此工具提供了模拟操作,而实际上并不在数据库中执行任何 CRUD 操作。我们将在模拟器上测试以下场景:

  • 认证用户的成功读取操作:打开模拟器并启用“Authenticated”开关按钮;它会在“Auth token”文本框中提供一个模拟的 uid。在“Location”文本框中,我们输入路径为/users/6e115890-7802-4f56-87ed-4e6ac359c2e0并点击“RUN”按钮。当出现“Simulated read allowed”消息时,此操作将成功,如以下截图所示:

  • 具有认证用户和正确数据的成功写入操作:在这种情况下,我们将提供包含用户 UID 的路径,字符串名称数据作为 JSON 有效载荷在模拟器中。当我们点击“运行”时,当出现“模拟写入允许”的消息时,这个操作被认为是成功的,如下面的截图所示。接下来的截图显示两个勾号,这表明我们的授权和数据验证已经成功。

图片

  • 使用不同 UID 的失败写入操作:在这种情况下,我们在用户路径位置提供了一个错误的 UID,然后执行相同的写入操作。这个操作失败了,导致出现“模拟写入被拒绝”的消息和写入标签上的一个叉号,如下所示;你可以通过点击“详情”按钮查看更多错误详情:

图片

  • 数据验证失败的写入操作:在这种情况下,我们在位置中提供了正确的用户 UID 路径,但在有效载荷中提供了错误的数据类型。例如,我们将为字符串名称数据提供数字数据类型。这个操作在数据验证标签中失败,如下所示:

图片

为聊天消息添加安全规则

在本节中,我们将启用聊天消息的安全规则。消息详情节点包含两个标识符,如下所示:

  • $identifierKey:第一个是标识符键,用于会话中的用户,并且这个键也存储在用户详情节点中。在以下示例中,"-L-0uxNuc6gC95iQytu9"是标识符键。

  • $messageKey:第二个是消息键,在我们向节点推送新消息时生成。在以下示例中,"-L-125Am3LVQQQiN_xlG"是消息键:

"message_details" : {
  "-L-0uxNuc6gC95iQytu9" : {
   "-L-125Am3LVQQQiN_xlG" : {
     "message" : "Hello",
     "receiverUid" :"2HIvnEJvN0O03PtByU2ACBhSMDe2",
     "senderUid" : "YnmOB5rTAwVErXcmMuJkHDEb4i92",
     "timestamp" : 1511862854520
   }
  }
 }

我们将为消息详情节点定义以下安全规则:

  • 读取权限:我们只授予认证用户读取权限

  • 写入权限:我们授予认证用户写入权限,并在数据推送发生之前检查是否存在任何新数据

  • 验证:我们验证消息中的所有字段,以确保在插入任何新数据时数据完整性得到保持,如下所示:

图片

最后,我们将在模拟器中验证新规则,以检查它们是否有效:

图片

索引用户和好友

Firebase 通过使用任何常见的子键收集节点来提供数据的查询和排序。当数据增长时,这个查询会变慢。为了提高性能,Firebase 建议你在特定的子字段内进行索引。Firebase 将键索引到服务器以提高查询性能。

作为本节的一部分,我们将对我们的用户数据进行索引,以便搜索或找到好友,这在任何社交应用中都很常见。为了实现这一点,我们将执行以下任务:

  • 在用户数据的名称字段中创建索引:我们在用户数据的名称字段中提供了一个索引。我们将使用.indexOn标签为名称字段,如下所示:

图片

  • 创建基于文本查询数据的服务:在这个任务中,我们将根据搜索文本查询用户数据。我们将提供orderByChild作为用户的名称字段。

这是friends-search.service.ts

import {Injectable} from '@angular/core';
import {AngularFireDatabase} from 'angularfire2/database';
import {Observable} from 'rxjs/Observable';
import {User} from './user';
import {FRIENDS_CHILD, USER_DETAILS_CHILD} from './database-constants';

/**
 * Friends search service
 *
 */
@Injectable()
export class FriendsSearchService {

  constructor(private db: AngularFireDatabase) {
  }

  getSearchFriends(start, end): Observable<User[]> {
    return this.db.list<User>('/users',
      ref => ref.orderByChild('name').limitToFirst(10).
      startAt(start).endAt(end)
    ).valueChanges();
  }

}
  • 修改模板:我们修改应用模板,以便在搜索文本下方显示搜索结果的下拉菜单。

这是修改后的app.component.html文件:

<h1 class="title">Friends - A Social App</h1>
<div class="nav-container">
<nav class="navbar navbar-expand-lg navbar-light bg-color">
  <div class="collapse navbar-collapse" id="navbarNav">
    ...
    <div class="form-container">
    <form class="form-inline my-2 my-lg-0">
      <div class="dropdown">
        <input class="form-control mr-sm-2" type="text" 
         (keyup)="onSearch($event)" name="searchText"
         data-toggle="dropdown" placeholder="Search friends..." 
         aria-label="Search">
        <div class="dropdown-menu" aria-
         labelledby="dropdownMenuButton">
          <div class="list-group" *ngFor="let user of users">
            <div class="list-group-item list-group-item-action 
             flex-column align-items-start">
              <div class="d-flex w-100 justify-content-between">
                <label>{{user?.name}}</label>
                <button type="button" class="btn btn-light" 
                 (click)="onAddFriend(user)">ADD</button>
              </div>
            </div>
          </div>
        </div>
      </div>
      <button class="btn btn-success my-2 my-sm-0" 
       type="submit">Search</button>
    </form>
    </div>
  </div>
</nav>
</div>
<router-outlet></router-outlet>
  • 修改组件:当应用程序组件加载时,我们在ngOnInit()方法中查询所有用户,当用户点击搜索文本框时,用户列表会显示所有名称。我们还通过用户类型在文本框中过滤列表,然后调用onSearch()方法,并使用查询范围查询 Firebase 数据库。

这是到目前为止完整的app.component.ts文件:

import {Component, OnInit} from '@angular/core';
import {AuthenticationService} from './services/authentication.service';
import {User} from './services/user';
import {FriendsSearchService} from './services/friends-search.service';

@Component({
  selector: 'app-friends',
  styleUrls: ['app.component.scss'],
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {

  startAt: string;

  endAt: string;

  users: User[];

  searchText: string;

  authenticationService: AuthenticationService;

  constructor(private authService: AuthenticationService,
              private friendsSearchService: FriendsSearchService) {
    this.authenticationService = authService;

  }

  ngOnInit() {
    console.log(this.currentLoginUser);
    this.searchText = '';
    this.onSearchFriends(this.searchText);
  }

  onSearch(event) {
    const text = event.target.value;
    this.onSearchFriends(text);
  }

  onSearchFriends(searchText) {
    const text = searchText;
    this.startAt = text;
    this.endAt = text + '\uf8ff';
    this.friendsSearchService.getSearchFriends(this.startAt, 
    this.endAt)
      .subscribe(users => this.users = users);
  }
}

\uf8ff字符是 Unicode 范围内的一个非常高的代码点,它允许您匹配以您的搜索文本开头的所有值。

设置多个环境

当我们的应用程序准备部署时,我们需要为开发和生产环境分别分离 Firebase 项目,以便在部署到生产环境之前在开发环境中测试我们的代码更改。我们将遵循以下步骤来设置一个单独的环境:

  • 在 Firebase 中创建新的预发布项目:因为我们不能使用相同的 Firebase 功能,例如数据库和存储,所以我们需要分别设置生产和预发布环境。然后我们将创建一个名为friends-staging的新项目;该项目具有新的 Firebase 环境变量:

图片

  • 创建新的环境变量:新的 Firebase 项目有一个新的环境变量,您可以从 Firebase 项目中获取配置。因此,导航到项目概览 | 项目设置 | 将 Firebase 添加到您的 Web 应用。

将以下代码复制到新的环境文件中,如下所示;我们有两个环境文件用于预发布和生产:

  • environment.prod.ts文件将生产环境设置为true,这用于生产环境:
export const environment = {
   production: true,
   firebase: {
      apiKey: 'XXXX',
      authDomain: 'friends-4d4fa.firebaseapp.com',
      databaseURL: 'https://friends-4d4fa.firebaseio.com',
      projectId: 'friends-4d4fa',
      storageBucket: 'friends-4d4fa.appspot.com',
      messagingSenderId: '321535044959'
   }
};
  • environment.ts文件将生产环境设置为false;这将用于预发布环境:
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 工具:一旦创建了一个新的 Firebase 项目,您将需要安装 Firebase 工具并使用以下命令登录 Firebase 门户:
$ npm install -g firebase-tools
$ firebase login

上述命令将打开您的 Gmail 权限页面;点击 ALLOW 将允许列出所有可用的项目:

图片

  • 使用新的 Firebase 项目:一旦我们给予权限,我们需要根据当前使用的环境添加可用的项目。假设我们需要在开发中的新功能下进行测试,我们可以选择一个测试环境,并为测试环境提供一个别名名称以供将来使用。我们可以使用别名名称切换环境,如下所示:
$ firebase use --add
$ firebase use staging

这将在基本项目目录中创建.firebaserc文件,其外观如下:

{
  "projects": {
    "default": "friends-4d4fa",
    "staging": "friends-staging"
  }
}

在 Firebase 上托管朋友应用程序

Firebase 支持作为服务提供托管,在 Firebase 上部署应用程序很容易。大多数应用程序采用两阶段部署,即首先进行测试,然后是生产。一旦我们在测试环境中测试了应用程序,我们就可以将其部署到生产环境中。部署应用程序的步骤如下:

  1. 第一步是构建应用程序,这会创建一个包含index.html和其他所需文件的dist文件夹。您只需添加一个--prod选项即可用于生产构建:
$ ng build
  1. 下一步是初始化应用程序项目。我们将执行 init 命令,并在命令提示符中使用空格键选择 Firebase 功能;对于我们的朋友应用程序,我们将使用 Firebase 的数据库、存储和托管功能,当我们选择相应的功能时,它将创建默认的数据库和存储规则:
$ firebase init

它还会创建一个firebase.json文件,其外观如下:

{
  "hosting": {
    "public": "src",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "database": {
    "rules": "database.rules.json"
  },
  "storage": {
    "rules": "storage.rules"
  }
}
  1. 最后,我们使用以下命令部署我们的应用程序;一旦我们的应用程序部署完成,我们就可以在 Firebase 托管的项目中查看已部署的应用程序:
$ firebase deploy

已部署的应用程序将出现在 Firebase 门户中。您可以通过导航到 DEVELOP | Hosting,在右侧面板中查看已部署的应用程序,如下所示:

  1. 最后,您可以使用以下命令或通过粘贴 URL 作为friends-staging.firebaseapp.com打开您的实时应用程序:
$ firebase open

摘要

在本章中,我们介绍了 Firebase 的安全机制。我们为我们的朋友应用程序数据库添加了安全规则,使我们的应用程序更加安全。我们为数据库中的用户节点name字段建立了索引,以便搜索查询更快。然后我们在朋友应用程序中使用了搜索 API。最后,我们为我们的应用程序创建了多个环境,以便我们能够将测试和生产分离。然后我们在 Firebase 上部署了我们的应用程序。

在下一章中,我们将学习 Firebase 云消息、Google 分析以及广告。

第十三章:使用 Firebase 扩展我们的应用程序

在本章中,我们将探讨 Firebase 如何提供云消息以吸引我们的用户。我们将向我们的应用程序添加 Firebase 云消息功能。我们将介绍 Google 分析,它提供了一个良好的仪表板来分析我们的应用程序并相应地采取行动,因为这有助于进一步改进我们的应用程序。最后,我们将讨论 Google 广告,它有助于使我们的应用程序货币化。

在本章中,我们将涵盖以下主题:

  • Firebase 云消息简介

  • 将 FCM 添加到我们的应用程序

  • Google 数据分析

  • 了解 Google 广告

Firebase 云消息简介

Firebase 云消息 (FCM) 是一个跨平台服务,用于在不同平台之间可靠地传递消息。它使用推送方法发送消息,我们可以向客户端发送多达 4 KB 的数据。它支持许多用例,例如通过促销消息与用户互动,并在用户处于后台时发送消息。

它有两个主要模块:

  • 受信任的服务器:此服务器用于向客户端发送消息,可以是 Firebase 控制台或服务器 SDK 实现。

  • 客户端应用程序:这包括 Web、Android 或 iOS 中的客户端应用程序。它从受信任的服务器接收消息。

将 FCM 添加到我们的应用程序

在本节中,我们将配置我们的应用程序中的 FCM。我们将执行以下步骤来配置 FCM:

  1. 创建一个 manifest.json 文件:我们的第一步是在src文件夹内创建一个manifest.json文件。此文件包含gcm_sender_id,它授权客户端访问受信任的 FCM 服务器,并使 FCM 能够向我们的应用程序发送消息。对于桌面浏览器,客户端 ID——103953800507——对于 Web 应用程序是固定的,因此您不需要更改它。

网页清单文件是一个简单的 JSON 文件,在其中我们可以指定应用程序的配置,例如其名称、显示和方向。

这是manifest.json的代码:

{
   "name": "Friends",
   "short_name": "Friends",
   "start_url": "/index.html",
   "display": "standalone",
   "orientation": "portrait",
   "gcm_sender_id": "103953800507"
}
  1. index.html 中配置 manifest.json:一旦我们创建了 manifest 文件,我们就将文件引用包含在index.html中。

然后我们包含修改后的index.html

<!DOCTYPE html>
<html>
   <link rel="manifest" href="/manifest.json">
</head>
<body>
<app-friends>
   Loading...
</app-friends>
</body>
</html>
  1. 创建服务工作者:在我们创建 manifest 文件之后,我们创建一个 Firebase 服务工作者来处理来自受信任服务器的传入推送消息,并将我们的 Firebase 应用程序与消息发送者ID注册,我们可以通过导航到项目概览 > 项目设置 > 云消息获取此ID

服务工作者是一种在后台运行的 Web 工作者,它有助于推送通知。

现在的firebase-messaging-sw.js文件如下:

importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.9.0/firebase-messaging.js');

firebase.initializeApp({
   'messagingSenderId': '807434545532'
});

const messaging = firebase.messaging();
  1. angular-cli.json 中引用 manifest 和 service worker:接下来,我们在angular-cli.json中提及服务工作者和 manifest 文件的引用;以下是被修改的angular-cli.json
...
"apps": [
   {
      "assets": [
         "assets",
         "favicon.ico",
         "firebase-messaging-sw.js",
         "manifest.json"
      ],
      ...
   }
]
  1. 创建 FCM 服务:此服务类用于接收客户端令牌并将令牌插入 Firebase 数据库。它还用于在令牌过期时注册令牌刷新。创建此服务类的步骤如下:

第一个步骤是通过警报对话框获取用户通知权限,一旦用户点击允许按钮,我们就从 Firebase 消息对象调用getToken()来获取令牌。我们将此令牌发送到 Firebase 数据库,以便我们可以在将来使用此令牌向所有用户发送促销消息。我们还创建了一个onTokenRefresh()方法,以便在令牌过期时刷新我们的令牌。

第二个步骤是在应用处于前台时调用onMessage()来注册推送通知消息。

现在的fcm-messaging.service.ts文件如下:

import {Injectable} from '@angular/core';
import {AngularFireDatabase} from 'angularfire2/database';
import {AngularFireAuth} from 'angularfire2/auth';
import 'firebase/messaging';

@Injectable()
export class FcmMessagingService {

   messaging = null;

   constructor(private angularFireDatabase: AngularFireDatabase, 
   private afAuth: AngularFireAuth) {
      this.messaging = angularFireDatabase.app.messaging();
   }

   getPermission() {
      this.messaging.requestPermission()
         .then(() => {
            console.log('Permission granted.');
            this.getToken();
         })
         .catch((err) => {
            console.log('Permission denied', err);
         });
   }

   getToken() {
      this.messaging.getToken()
         .then((currentToken) => {
            if (currentToken) {
               console.log(currentToken);
               this.sendTokenToServer(currentToken);
            } else {
               console.log('No token available');
            }
         })
         .catch((err) => {
            console.log('An error occurred while retrieving token. 
            ', err);
         });
   }

   onMessage() {
      this.messaging.onMessage((payload) => {
         console.log('Message received. ', payload);
      });
   }

   onTokenRefresh() {
      this.messaging.onTokenRefresh(function () {
         this.messaging.getToken()
            .then(function (refreshedToken) {
               console.log('Token refreshed.');
               this.sendTokenToServer(refreshedToken);
            })
            .catch(function (err) {
               console.log('Unable to retrieve refreshed token ', 
               err);
            });
      });

   }

   sendTokenToServer(token) {
      this.afAuth.authState.subscribe(user => {
         if (user) {
            const data = {[user.uid]: token};
            this.angularFireDatabase.object('fcm-
            tokens/').update(data);
         }
      });
   }
}
  1. 在应用组件中注册 Firebase 消息以更新:一旦我们创建了服务方法,我们就调用用户权限、令牌并在我们的应用组件中注册消息更新,如下面的app.component.ts所示:
import {Component, OnInit} from '@angular/core';
import {AuthenticationService} from './services/authentication.service';
import {FcmMessagingService} from './services/fcm-messaging.service';

@Component({
   selector: 'app-friends',
   styleUrls: ['app.component.scss'],
   templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {

   ...
   authenticationService: AuthenticationService;

   constructor(private authService: AuthenticationService,
            private friendsSearchService: FriendsSearchService,
            private fcmService: FcmMessagingService) {
      this.authenticationService = authService;
   }

   ngOnInit() {
      this.fcmService.getPermission();
      this.fcmService.onMessage();
      this.fcmService.onTokenRefresh();
   }
    ...
}

最后,我们的应用程序现在已准备好发送推送通知消息。您可以通过 curl 命令或 Postman 请求发送消息。

要从 Postman 发送推送通知,我们需要以下详细信息:

  • URL:这是一个已在我们可信服务器上注册的 FCM 端点。其唯一的 URL 是fcm.googleapis.com/fcm/send

  • 内容类型:这是发送到服务器的内容类型,在我们的案例中是 JSON 类型,作为application/json

  • 授权:这是我们的 Firebase 项目的服务器密钥。您可以通过导航到 Firebase 门户中的项目概览|项目设置|CLOUD MESSAGING|服务器密钥来找到此密钥。

  • 正文:这包含标题、正文、操作和目标发送者ID。发送者令牌 ID 已保存在我们的 Firebase 数据库中。

这是一个 Postman 请求的 Headers 标签的示例:

Postman 的 Body 标签将如下所示:

出现在您屏幕右下角的提示应该如下所示:

Google 数据分析

Google 分析是 Google 提供的一项免费服务,它提供了关于我们网站访问者和流量的统计数据。它提供了关于访问者和地理的更有价值的信息。它还提供了关于访问者在使用我们的网站时的行为信息。以下是将 Google 分析注册到您的应用程序的步骤。

创建 Google 分析账户:我们可以通过执行以下步骤使用现有的 Gmail 账户或新 Gmail 账户创建 Google 分析账户:

  1. 打开浏览器并粘贴分析 URL (analytics.google.com/analytics)

  2. 点击注册按钮

  3. 填写您的实时应用程序 URL 和表单信息

  4. 点击获取跟踪 ID按钮

将跟踪代码集成到我们的应用程序中:在成功注册后,我们可以将生成的全局站点标签集成到我们的应用程序中。请查看以下 index.html 中的示例全局站点代码:

<head>
...
<script async src="img/js?id=UA-108905892-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-108905892-1');
</script>
...
</head>

在成功注册后,我们的 Google 分析控制台应该看起来像这样:

了解 Google Adsense

Google Adsense 为我们的网络应用程序提供了一个盈利平台。Firebase 广告不支持此网络应用程序,因此我们将与 Google Adsense 合作以使我们的应用程序盈利。在本节中,我们将探讨如何将广告添加到我们的应用程序中。

创建 Adsense 账户:我们可以使用现有的 Gmail 账户创建 Adsense 账户,或者通过以下步骤创建一个新的 Gmail 账户:

  1. 打开浏览器并粘贴 Adsense URL (www.google.com/adsense )

  2. 点击注册按钮

  3. 填写您的实时应用程序 URL 和地址详情

  4. 点击提交按钮

将 Adsense 脚本添加到我们的应用程序中:当您点击提交按钮时,Google Adsense 将提供步骤以将您的网站注册为广告。它提供了代码,需要粘贴到我们的应用程序的 index.html 中。请查看以下示例脚本,以粘贴到 index.html 中:

<head>
...
<script async src="img/adsbygoogle.js"></script>
<script>
  (adsbygoogle = window.adsbygoogle || []).push({
    google_ad_client: "ca-pub-6342144115183345",
    enable_page_level_ads: true
  });
</script>
...
</head>

在成功注册后,我们的 Google Adsense 控制台看起来如下:

摘要

在本章中,我们介绍了 Firebase 云消息传递。我们将 FCM 集成到我们的应用程序中,这有助于吸引我们的用户。我们还介绍了 Google 分析,并展示了如何在我们的应用程序中启用分析。这为我们提供了关于应用程序使用的良好视角。最后,我们讨论了 Google Adsense,它有助于使我们的应用程序盈利。

在下一章中,我们将讨论渐进式网络应用PWA)并添加一些功能以使我们的应用程序符合 PWA 规范。

第十四章:将我们的应用转化为 PWA

渐进式 Web 应用PWA)是开发 Web 应用的一种新方式。作为本章的一部分,你将了解 PWA 并探索使应用符合 PWA 规范的功能。作为其中的一部分,我们将把我们的朋友应用添加到移动主屏幕上,这样我们的朋友应用就成为了其他原生移动应用的组成部分。我们还将涵盖我们应用的离线模式,以便我们可以向用户展示可浏览的数据。最后,我们将使用Lighthouse工具对我们的应用进行审计,该工具为我们提供了关于我们渐进式 Web 应用的宝贵见解。

在本章中,我们将涵盖以下主题:

  • PWA 简介

  • 服务工人简介

  • 将我们的应用添加到手机主屏幕

  • 启用离线模式

  • 使用 Lighthouse 进行合规性测试

PWA 简介

PWA 是一种使用增强功能为用户提供类似移动应用体验的 Web 应用。这种 Web 应用满足某些要求,并部署到 Firebase 服务器上,通过 URL 可访问并由 Google 搜索引擎索引。近年来,渐进式 Web 应用开发是一种范式转变,使您的 Web 应用能够普遍可用。

以下是一些使应用符合 PWA 规范的功能:

  • 网站和应用的强大功能:该应用针对移动设备和浏览器进行了优化,可以完美地工作。它具有移动应用的所有功能,例如离线模式和推送通知。

  • 无需应用商店:类似于网站,我们不需要应用商店,并且可以通过最新的软件更新立即使用。

  • 类似应用:Web 应用看起来像移动应用。它与其他移动应用一起出现,并以正常应用的方式占据整个屏幕。

  • 连接无关性:这些应用不依赖于网络类型。它们在弱网络环境下也能很好地工作,为用户提供无缝的使用体验。

  • 主屏幕添加:这允许用户将我们的网站添加到他们的主屏幕上,使其成为应用家族的一部分。用户可以频繁启动应用而无需打开浏览器。

  • 安全:此应用在 HTTPS 上运行,因此它们免受攻击或黑客攻击。

  • 推送通知:随着服务工人的出现,可以向 Web 应用发送推送通知。这对与我们的应用吸引用户非常有帮助。

  • 可搜索:类似于网站,此应用可以通过 Google 搜索进行搜索。我们可以通过关键词优化我们的网站,以便 PWA 应用可被用户识别并轻松搜索。

  • 可链接:这种应用可以通过链接轻松分享,就像普通 Web 应用一样。

服务工人简介

服务工作者(service worker)是一个在后台运行的脚本。此后台脚本不与 DOM 元素交互。这有助于支持推送通知和离线模式等功能,并且服务工作者将在未来得到大幅增强以支持其他新功能。

以下是为服务工作者(service worker)的先决条件:

  • 浏览器支持: 服务工作者在 Chrome、Firefox 和 Opera 浏览器中得到支持,并且对其他浏览器的支持也将很快扩展。

  • HTTPS 支持: 超文本传输协议安全(HTTPS)是 HTTP 的安全版本,也是 PWA 的先决条件之一。这确保了浏览器和服务器之间所有通信都是加密的。

将我们的应用程序添加到手机主屏幕

这是渐进式 Web 应用程序(Progressive Web Apps)最重要的功能之一,提供了许多优势,如下所示:

  • 更易访问: 用户通常将最常用的应用程序放在主屏幕上,因为这提供了更方便的应用程序访问。

  • 参与度: 用户可以更频繁地与我们的应用程序互动。

使我们的 Web 应用程序出现在主屏幕上的步骤如下:

  1. 为了使我们的 Web 应用程序具有移动应用的外观,我们将按照以下代码修改 manifest.json 文件。

  2. 使用从 Firebase 门户提供的已部署应用程序 URL 在您的手机 Chrome 浏览器中打开朋友应用。页面会提示您将应用程序添加到主屏幕。

这是 manifest.json 文件:

{
   "name": "Friends",
   "short_name": "Friends",
   "icons": [
      {
         "src": "/assets/images/android-chrome-192x192.png",
         "sizes": "192x192",
         "type": "image/png"
      }
   ],
   "theme_color": "#689f38",
   "background_color": "#689f38",
   "start_url": "/index.html",
   "display": "standalone",
   "orientation": "portrait",
   "gcm_sender_id": "103953800507"
}

查看以下属性的详细描述:

  • name: 当添加到主屏幕的横幅出现,并且 Chrome 提供修改名称的选项时,此名称将显示。

  • short_name: 这将出现在手机主屏幕中的应用程序图标下方。在我们的应用程序中,nameshort_name 是相同的。

  • icons: 根据 PWA 标准,推荐的图标大小为 192 x 192,此图标将出现在手机主屏幕上。

  • background_color: 指定的背景颜色将作为图标的背景颜色显示。

  • theme_color: 当用户点击主屏幕上的朋友应用时,此颜色将出现在您的移动应用启动屏幕上。

  • display: 当页面打开时,Android Chrome 会提供原生样式,因为它移除了导航栏并将标签页切换到任务切换器。

  • start_url: 此页面是 Web 应用程序中的 index.html,通常这是我们主页。

  • orientation: 这强制执行纵向或横向方向。

我们的应用程序在手机主屏幕上的外观如下所示:

我们应用程序的启动屏幕如下所示:

启用离线模式

在本节中,我们将介绍如何为我们的应用程序启用离线模式,这有助于用户在没有互联网连接的情况下打开我们的 Web 应用程序。

为了支持离线模式,我们必须在客户端浏览器中缓存资源,为此,我们使用 precache 插件通过服务工作者来缓存我们的资源。它使用 sw-precache 创建服务工作者文件。涉及的步骤如下:

  1. 安装插件:第一步是在我们的当前项目中使用以下命令安装 precache 插件:
$npm install --save-dev sw-precache-webpack-plugin
  1. 创建预缓存 JavaScript:precache 插件使用 precache 配置文件来定义客户端浏览器中要缓存的资源。有关 precache 插件的更多详细信息,请参阅 github.com/goldhand/sw-precache-webpack-plugin

这是完整的 precache.config.js

var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
module.exports = {
 navigateFallback: '/index.html',
 navigateFallbackWhitelist: [/^(?!\/__)/],
 stripPrefix: 'dist',
 root: 'dist/',
 plugins: [
  new SWPrecacheWebpackPlugin({
   cacheId: 'friend-cache',
   filename: 'service-worker.js',
   staticFileGlobs: [
    'dist/index.html',
    'dist/**.js',
    'dist/**.css'
   ],
   stripPrefix: 'dist/assets/',
   mergeStaticsConfig: true
  }),
 ]
};
  1. 配置 package.json:一旦我们创建了配置文件,我们需要创建一个新的构建标签称为 pwa 并在 package.json 中引用缓存文件。

这是修改后的 package.json

...
"scripts": {
  "ng": "ng",
  "start": "ng serve",
  "build": "ng build",
  "test": "ng test --sourcemaps false",
  "coverage": "ng test --sourcemaps false --watch=false --code-coverage",
  "lint": "ng lint",
  "e2e": "ng e2e",
  "pwa": "ng build --prod && sw-precache --root=dist --config=precache-config.js"
}
...
  1. 注册服务工作者:一旦我们创建了新的构建,我们需要在 index.html 中注册由 precache 插件创建的服务工作者,如下所示。这是修改后的 index.html
...
body>
  <app-root></app-root>

  <script>
    if ('serviceWorker' in navigator) {
      console.log("Will the service worker register?");
      navigator.serviceWorker.register('/service-worker.js')
        .then(function(reg){
          console.log("Service Worker Registered");
        }).catch(function(err) {
        console.log("Service Worker Not Registered: ", err)
      });
    }
  </script>
</body>
...
  1. 运行新的构建脚本:一旦我们配置了服务工作者,我们可以使用以下命令运行生产构建;这将创建所有包含服务工作者的文件到它们的分发文件夹中:
$ng pwa
  1. 部署:最后,我们将新创建的文件部署到 Firebase。一旦部署,我们可以在手机的首页上打开应用程序,并且它将在客户端浏览器中缓存所有必需的资源。

使用 Lighthouse 进行合规性测试

Lighthouse 是一个开源的自动化工具。它审计应用程序的性能、可访问性、渐进式网络应用程序等。这可以通过 Chrome 开发者工具中的“审计”标签获得。因此,前往 Chrome 开发者工具,然后打开“审计”标签,并点击“执行审计...”按钮

为了在我们的应用程序中看到改进,我们可以在这个工具的两个阶段中使用它:

  • 无需任何 PWA 变更:我们可以在我们的应用程序中不进行任何前面的更改运行此工具,并查看性能。由于我们的应用程序不符合 PWA 标准,我们的分数将不会很好。

查看以下截图,显示我们运行 Lighthouse 时的分数——它在五个审计中失败,分数以红色显示:

  • 应用 PWA 变更:现在,在我们的朋友应用程序中应用本章中讨论的所有 PWA 变更,然后运行此工具并查看我们的审计性能。如图所示,我们的 PWA 分数为 82,并以绿色显示:

摘要

在本章中,我们讨论了渐进式网络应用。我们涵盖了 PWAs 及其所有关键特性。我们讨论了支持推送通知、离线模式等服务工作者。我们增强了我们的网络应用的 manifest.json,并将我们的应用程序添加到手机主屏幕。我们使用 sw-precache 插件启用了离线缓存。最后,我们使用 Lighthouse 工具评估了我们的应用程序的 PWA 合规性。

最后,我们来到了本书的结尾,但这并不是网络应用开发的终点。本书向您介绍了一种实用的 Angular 和 Firebase 方法。您需要将这一知识传承下去,开发另一个实时应用程序;这将给您带来极大的信心。

祝您使用 Angular 和 Firebase 的下一个应用程序一切顺利!