Angular-渐进式-Web-应用教程-一-

68 阅读30分钟

Angular 渐进式 Web 应用教程(一)

原文:Progressive Web Apps with Angular

协议:CC BY-NC-SA 4.0

一、设置要求

在这本书里,我努力带你踏上一段旅程,你可以用 Angular 创建最全面的渐进式 Web 应用(pwa)。但在我开始之前,我们将回顾一些 PWA 基础知识,并设置将在整本书中使用的环境。

渐进式 Web 应用基础

PWAs 适用于那些**快速、引人入胜、可靠、**的 web 应用,并且将尝试逐步增强用户体验,而不管它们的浏览器、平台或设备如何。换句话说,PWA 不仅仅是一个框架、工具或时髦的术语,而是一种通过利用浏览器的现代 API 来不断增强的思维方式,这使得每个用户都感到满意。

无论您选择使用哪种框架,无论您选择用哪种语言编写代码,PWAs 都必须具有特殊的特征:

  1. **即时加载:**应用应该快速加载,并且必须能够快速交互。

  2. **连接独立:**没有网络或者连接缓慢且不稳定,应用必须继续工作。

  3. **响应式、移动优先、离线优先的设计:**先针对移动进行重点和优化,移动的硬件容量较低,应用在移动上应该完全可用。

  4. **重新参与:**推送通知是向用户发送提醒的一种方式。

  5. **类原生特性:**拥有 App Shell 这样的 UI 架构,使用 Web 蓝牙这样的硬件 API,可以让我们的 web app 更像一个原生 App。

  6. **安全:**安全是最高优先级,每个 PWA 必须通过 HTTPs 服务。

  7. 可安装:可安装意味着它将被添加到设备的主屏幕上,并像本地应用一样启动。

  8. **渐进式:**无论使用何种浏览器或设备,我们的应用都应该不断发展,拥抱新功能,为每一个应用提供最佳的用户体验。

为什么有 Angular?

几年前,甚至在 React 上市之前,前端世界就被 Angular 1.x 所主宰。通过建立和最终确定 ES6 和 TypeScript 外观,以及广泛适应的新浏览器功能和标准,得到谷歌支持的 Angular 团队决定重写 AngularJS,以前称为 Angular 1.x,导向 Angular 2,现在称为 Angular。Angular 由具有 Rxjs 和 TypeScript 的可观察 API 支持,并具有独特的功能,如健壮的更改检测和路由、动画、延迟加载、令人头痛的捆绑过程、CLI 和大量其他 API。这些使得它成为一个出色的、有能力的、成熟的前端框架,被世界上许多公司信任来构建和分发复杂的 web 应用。

此外,Angular Service Worker 模块已在版本 5 中引入,在版本 6 中进行了改进, 1 现在正在定期更新,以便添加更多功能并变得稳定。尽管 Angular Service Worker 和 Angular CLI 并不是创建 PWA 的唯一选择,但它得到了很好的维护,使我们能够毫不费力地创建 Angular 应用或将它转换为 PWA。

总而言之,说你有一个一体化的框架来创建一个 web 和移动应用并不遥远,这使得 Angular 独一无二。

安装节点和 NPM

您需要确保您的计算机上安装了节点和 NPM。只需运行以下命令来检查您的节点和 NPM 版本,或者查看您是否已经安装了它们:

$ node -v
$ npm -v

需要节点 8 或更高版本以及 NPM 5 或更高版本。您可以在 https://nodejs.org 访问节点网站,根据您的操作系统下载最新版本(图 1-1 )。

img/470914_1_En_1_Fig1_HTML.jpg

图 1-1。

Node 官方网站,在那里可以下载 NodeJS 的最新版本

是 NPM 的替代品,已经存在一段时间了。如果您更喜欢使用它,您应该访问 https://yarnpkg.com/en/docs/install ,然后根据您的操作系统安装最新版本。要检查是否安装了 YARN,只需运行以下命令:

$ yarn -v

安装 Chrome

尽管我们创建了一个可以在任何浏览器下工作的 PWA,但我将坚持使用 Chrome 及其开发工具来开发和调试 Service Worker 以及其他 PWA 特性。在写这本书的时候,Chrome 有一个名为 Lighthouse 的 PWA 审计工具,内置在 Audit 标签下。如果你想下载 Chrome,可以访问 https://www.google.com/chrome/

在本书的后面,我会用 Lighthouse 评估我们的申请,并提高我们的 PWA 分数。我们持续使用应用选项卡来调试我们的服务工作器、索引数据库、Web 应用清单等。

搭建我们的项目

是时候使用 Angular CLI 搭建我们的项目了。因此,在我们继续之前,首先通过运行以下命令来全局安装 Angular CLI:

$ npm install -g @angular/cli

$ yarn global add @angular/cli

现在 CLI 已在全球范围内安装,我们可以生成一个新的 Angular 应用。

使用 CLI 生成新的 Angular App

一旦安装了 Angular CLI 版本 6(当您阅读本书时,您可能会有更高的版本),您的终端中就有了全局可用的 ng 命令。让我们通过运行以下命令来搭建我们的项目:

$ ng new lovely-offline –-routing –-style=scss

可爱-离线是我们的应用名称,路由将生成路由模块, style=scss 表示我们的样式文件的 scss 前缀。

添加有 Angular 的材料设计

Angular Material 模块可能是 web 应用最好的 UI 库之一。它将让我们快速而完美地开发我们的应用。你不仅仅局限于这个库,但是我为这个项目推荐它。要安装:

$ npm install --save @angular/material @angular/cdk @angular/animations

现在在你的编辑器或 Idea 中打开项目,然后在/src/app,下找到app.module.ts,并将BrowserAnimationsModule导入到你的应用中以启用动画支持。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

要使用每个组件,我们应该将它们的相关模块导入到ngModule中,例如:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { MatToolbarModule } from '@angular/material/toolbar';

import { MatIconModule } from '@angular/material/icon';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatToolbarModule,
    MatIconModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

需要一个主题;因此,我将在我们的项目中向style.scs添加一个可用的主题:

@import "~@angular/material/prebuilt-themes/indigo-pink.css";

建议您安装并包含hammer.js,因为该库中依赖于材料设计中的手势。

$ npm install hammerjs

安装后,在src/main.ts中导入

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

import 'hammerjs';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));

Icons 需要 Google Material Icons 字体;因此,我们将把字体 CDN 链接添加到我们的index.html文件中:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>LovelyOffline</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

现在我们的项目已经可以使用了。只需运行 ng 发球npm 启动。您可以通过输入localhost:4200在浏览器中访问该项目。

设置移动设备

没有什么比在真实设备中测试我们的应用更好的了。Android 和 Chrome 一起支持大多数 PWA 功能,包括服务工作器、推送通知和后台同步,以及更现代的浏览器 API。

如果你有一个真实的设备,并希望方便地将其连接到 Chrome dev tools,请阅读谷歌开发者网站上的文章 https://developers.google.com/web/tools/chrome-devtools/remote-debugging 。请记住,真正的设备是不必要的;你可以随时通过 Android 和 iOS 模拟器测试你的应用。

设置移动模拟器

要运行 Android 模拟器,我建议您安装 Android Studio ,并按照 Android 开发者网站上的说明进行操作: https://developer.android.com/studio/run/emulator

Mac 用户也可以在 Mac 上安装 xCode 并运行 iPhone 模拟器。从 https://developer.apple.com/xcode/ 安装 xCode 后,你应该可以在 xCode 菜单下找到打开开发者工具,然后你就可以打开模拟器打开你选中的 iPhone / iPad。

将 Android 模拟器连接到 Chrome 开发工具

你现在应该可以将你的 Android 模拟器连接到 Chrome 开发工具了。请参考“设置移动设备”一节。

摘要

在本章中,我们已经了解了 PWA 的基础知识,然后我们使用 CLI 搭建了我们的项目。Angular 的材料已经添加到我们的项目,以风格我们的应用。

此外,我们还回顾了本课程中需要用到的其他工具,如 Node、NPM、YARN 和 Chrome 我们还学习了如何设置我们的真实设备和模拟器,以便正确测试我们的应用。

Footnotes 1

在我写这本书的时候,Angular 是第 6 版,但是当你读这本书的时候,它可能有更高的版本。

 

二、部署到 Firebase 作为后端

Firebase 被认为是后端即服务,它现在是谷歌云平台的一部分,但仍然是一个独立的实体。它提供不同的服务,如托管、实时数据库和云功能。

在这一章中,我将向你展示如何将我们的应用部署到 Firebase。值得一提的是,Firebase 并不是唯一的选择。然而,由于它易于设置和部署,我鼓励您使用 Firebase 作为我们的主机服务器。

此外,我们可能需要为我们的应用编写一些后端逻辑;因此,为了利用无服务器架构并减少我们对后端系统的担忧,Firebase Function 是最佳选择之一,而前端仍将是我们的主要关注点。

最后但同样重要的是,为了持久化我们的数据,我们将使用 Firebase Firestore,它为我们提供了尽可能快速地存储和检索数据的最佳被动能力,并在需要时内置了对每个集合和文档的 JSON 访问。

设置您的帐户

让我们从打开开始吧。使用您的 Gmail 凭据登录,但如果您没有任何凭据,请首先注册一个 Google 帐户,然后继续操作。

*登录后,继续并点击“转到控制台”您将被重定向到控制台,在那里您可以看到您的项目。

创建项目

现在是时候添加您的项目了;只需点击添加项目,如图 2-1 所示。

img/470914_1_En_2_Fig1_HTML.jpg

图 2-1

Firebase 控制台,您应该点击添加项目来创建一个新项目

您应该会看到一个新的视图,它会询问您有关项目的详细信息,例如项目名称。我选择用 Awesome-Apress-PWA 来命名我的项目。

您可能需要更改您的组织或云 Firestore 位置;但是,默认设置应该足以开始使用。请记住,如果您更改了云 Firestore 的位置,在创建项目之前,您将无法更改它。

我将让**“使用默认设置共享 Firebase 数据的 Google Analytics”“条款和条件”处于选中状态现在,点击创建项目**按钮,如图 2-2 所示。

img/470914_1_En_2_Fig2_HTML.jpg

图 2-2

Firebase 项目详细模型

您的项目可能需要几秒钟才能准备就绪。一旦项目准备就绪,您就可以继续项目的仪表板(参见图 2-3 )。

img/470914_1_En_2_Fig3_HTML.jpg

图 2-3

几秒钟后,项目就准备好了,所以只需点击“继续”按钮就可以重定向到仪表板

部署到火力基地

我们选择 Firebase 是因为它易于在我们的项目中使用,您很快就会看到使用 Firebase CLI(命令行界面)进行部署是多么容易。

生成新的 Angular 应用

在开始之前,我们需要使用 Angular CLI(命令行界面)生成一个新的 Angular app。如果您的计算机上没有全局安装@angular/cli,您应该首先运行以下命令:

$ npm install -g @angular/cli

要生成一个新的 Angular 应用,并设置好路由scss ,我们可以运行:

$ ng new lovely-offline     --routing           --style=scss
   Name of project      enable routing      styling with scss

安装完所有 NPM 依赖项后,您就可以准备好构建和部署您的应用了。

├── README.md
├── angular.json
├── e2e
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── app
│   ├── assets
│   ├── browserslist
│   ├── environments
│   ├── favicon.ico
│   ├── index.html
│   ├── karma.conf.js
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   ├── test.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.spec.json
│   └── tslint.json
├── tsconfig.json
└── tslint.json

现在让我们为生产构建我们的应用。

$ ng build --prod
> ng build
Date: 2018-08-26T17:20:35.649Z
Hash: e6da8aa80ad79bc41363
Time: 6332ms

chunk {main} main.js, main.js.map (main) 11.6 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 227 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 5.22 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 16 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.18 MB [initial] [rendered]

构建是成功的,现在是时候将我们的应用部署到 Firebase 了。让我们安装 Firebase CLI。

$ npm install -g firebase-tools

现在 firebase 命令在我们的命令行中是全局可用的。在部署之前,我们需要确保我们有足够的权限;因此,我们现在应该登录 Firebase 来设置我们的凭证,所以只需运行:

$ firebase login

问卷如下所示:

Allow Firebase to collect anonymous CLI usage and error reporting  information? (Y/n) Y

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/........

Waiting for authentication...

一旦您看到验证 URL,您将被重定向到浏览器,以便登录您的 Google 帐户。然后,你要通过点击允许访问来授予 Firebase CLI 足够的权限,如图 2-4 所示。

img/470914_1_En_2_Fig4_HTML.jpg

图 2-4

点击允许授予 Firebase CLI 访问您的帐户的权限

一旦获得许可,你应该会在浏览器中看到一条成功的消息,如图 2-5 所示。

img/470914_1_En_2_Fig5_HTML.jpg

图 2-5

授予 Firebase CLI 权限后,浏览器中出现成功消息

您还会在终端中看到如下所示的成功消息,这意味着 Firebase CLI 现在有足够的权限访问您的 Firebase 项目。

Success! Logged in as mhadaily@gmail.com

正在初始化应用

下一步是初始化 Firebase 项目。这将把您的本地 Angular 应用链接到我们刚刚创建的 Firebase 应用。为此,请确保您位于项目的根目录下,并运行:

$ firebase init

点击上面的命令后,Firebase CLI 会在您的终端中询问您几个问题,以便构建您的 Firebase 项目,并创建将我们的应用部署到 Firebase 的必要需求。让我们一步步复习每个问题。

特征选择

如下所示,第一个问题是关于我们希望使用哪些 Firebase 特性:

Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
  ◯ Database: Deploy Firebase Realtime Database Rules
> ◉ Firestore: Deploy rules and create indexes for FirestoreFunctions: Configure and deploy Cloud FunctionsHosting: Configure and deploy Firebase Hosting sites
  ◯ Storage: Deploy Cloud Storage security rules

Firebase 实时数据库 1 和 Firestore 2 是两个 NoSQL 数据库服务,用于存储和同步客户端和服务器端开发的数据。Firebase 的云函数允许您自动运行后端代码,以响应由 Firebase 特性和 HTTPS 请求触发的事件。你的代码存储在谷歌的云中,在一个托管的环境中运行。Firebase 托管为您的 web 应用、静态和动态内容以及微服务提供快速、安全的托管。云存储是为需要存储和提供用户生成的内容(如照片或视频)的应用开发人员而构建的。

我将为这个项目选择 Firestore功能主机功能,因为我将在本书中通篇使用它们。一旦你选择了你需要的,按下进入进入下一步。

项目选择

如下所示,第二个问题显示了您在 Firebase 中的项目,由于我们已经创建了一个项目,我将选择该项目并按下 enter 键继续。请注意,您也可以在这一步中创建一个项目。

Select a default Firebase project for this directory: (Use arrow keys)
[don't set up a default project]
> awesome-apress-pwa (awesome-apress-pwa)
  [create a new project]

数据库设置

Firebase Firestore 是一个可扩展和灵活的 NoSQL 3 实时数据库,用于存储和同步客户端或服务器端应用开发的数据。该数据库使我们的数据在多个客户端应用之间保持同步,并提供离线功能。Firestore 中的数据保存包含映射到值的字段的文档。集合是文档的容器,它不仅允许我们组织数据,还允许我们构建查询。

因为我们已经在步骤特征选择步骤中选择了 Firestore 服务,所以如下所示,第三个问题是关于数据库规则文件,以编写关于我们的项目数据库的所有规则。我继续使用默认名称,即 database.rules.json:

 What file should be used for Database Rules? (database.rules.json)

功能设置

Firebase 中的云功能让我们可以在 HTTPS 请求上运行后端代码,而不需要一个实际的服务器来维护、管理和存储我们在谷歌的云管理环境中的代码。为了在我们的 app 中实现无服务器 4 架构,我们要使用函数来编写和运行我们必不可少的后端代码。

由于我们已经在特性选择步骤中选择了使用 Firebase 函数特性,如下所示,第四个问题要求选择我们想要的语言来编写函数

What language would you like to use to write Cloud Functions? (Use arrow keys)
> JavaScript
  TypeScript

JavaScript 是我现在的选择,因为我们在这本书里不会有很多函数;因此,我保持简单。如果您喜欢,可以继续使用 TypeScript。

在选择语言之后,Firebase CLI 提供了一个林挺工具来帮助我们在下一个问题中找到可能的错误和样式问题,如下所示。如果您喜欢强制样式化并捕捉云函数中可能的 bug,请继续使用 y。

Do you want to use ESLint to catch probable bugs and enforce style? (Y/N) y

最终设置

我将继续回答最后三个问题,以完成我的项目初始化。

如果您想现在安装依赖项,请在下一个问题中输入 Y。

Do you want to install dependencies with npm now? (Y/n)

下一步,我们需要定义我们的随时部署应用的位置。默认情况下,在 Angular 里面是dist目录;因此,我也输入dist来设置我的公共目录。所以,我会如下图继续回答问题:

What do you want to use as your public directory? (public) dist

最后,我们的应用将在前端有一个路由系统,这意味着我们将创建一个单页面应用。因此,当 Firebase CLI 被询问是否重写所有到 index.html 的 URL 时,我们应该回答 Y,以确保我们的前端正在单独处理路由,而不考虑我们的服务器路由。

尽管我们正在开发单页面应用,但这绝对不是创建 PWA 所必需的。注意,在本书中,我们将通过 Angular 制作单页 PWA。让我们继续最后一个问题,Y 如下图所示:

Configure as a single-page app (rewrite all urls to /index.html)? (y/N) y

使用 Firebase CLI 初始化我们的应用已经完成!初始化后,我们的应用结构看起来像下面的树。

.
├── README.md
├── angular.json
├── database.rules.json   -> firebase databse rules
├── dist
├── e2e
├── firebase.json -> firebase configs
 ├── functions-> firebase cloud funtions directory
 │   ├── index.js
 │   ├── node_modules
 │   ├── package-lock.json
 │   └── package.json
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── app
│   ├── assets
│   ├── browserslist
│   ├── environments
│   ├── favicon.ico
│   ├── index.html
│   ├── karma.conf.js
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   ├── test.ts
│   ├── tsconfig.app.json
│   ├── tsconfig.spec.json
│   └── tslint.json
├── tsconfig.json
└── tslint.json

Angular 项目设置中的调整

在我们可以部署我们的应用之前,我们需要对位于 Angular.json 中的 Angular 设置进行微小的更改。 Angular CLI 能够构建多个应用,每个应用都可以简单地放在 dist 文件夹中。然而,我们现在只想处理一个应用,我们需要将它构建在 dist 文件夹中,Firebase 将在那里找到并部署它。因此,我们应该从

   "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/lovely-offline",  // outputPath showes where to build

   "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist ",  // build app just in dist

通过从输出路径中移除我们的应用名称,我们强制 Angular CLI 构建所有文件并将其放入 dist 文件夹中。现在是时候最终将我们的应用部署到 Firebase 上了。

部署我们的应用

当我们在项目目录的根目录中时,我们可以简单地运行以下命令:

$ firebase deploy

部署开始…

> firebase deploy

=== Deploying to 'awesome-apress-pwa'...

i  deploying database, functions, hosting
Running command: npm --prefix "$RESOURCE_DIR" run lint

> functions@ lint ~/awesome-apress-pwa/functions
> eslint .

✓  functions: Finished running predeploy script.
i  database: checking rules syntax...
✓  database: rules syntax for database awesome-apress-pwa is valid
i  functions: ensuring necessary APIs are enabled...
✓  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  hosting[awesome-apress-pwa]: beginning deploy...
i  hosting[awesome-apress-pwa]: found 14 files in dist
✓  hosting[awesome-apress-pwa]: file upload complete
i  database: releasing rules...
✓  database: rules for database awesome-apress-pwa released successfully
i  hosting[awesome-apress-pwa]: finalizing version... 

✓  hosting[awesome-apress-pwa]: version finalized
i  hosting[awesome-apress-pwa]: releasing new version...
✓  hosting[awesome-apress-pwa]: release complete

✓  Deploy complete!

Project Console: https://console.firebase.google.com/project/awesome-apress-pwa/overview
Hosting URL: https://awesome-apress-pwa.firebaseapp.com

祝贺您-部署成功完成,现在网站可在 https://awesome-apress-pwa.firebaseapp.com 访问。

设置角火5

AngularFire2 是 Angular 支持 Firebase 功能的官方库。它由可观察的实时绑定、身份验证和离线数据支持提供支持。我强烈建议实现这个库,以便让我们的开发过程更容易处理 Firebase。

要安装,请运行以下命令:

$ npm install firebase @angular/fire –-save

要添加一个 Firebase 配置,打开/src/environment/environment.ts file, and添加如下设置:

export const environment = {
  production: false,
  firebase: {
    apiKey: '<your-key>',
    authDomain: '<your-project-authdomain>',
    databaseURL: '<your-database-URL>',
    projectId: '<your-project-id>',
    storageBucket: '<your-storage-bucket>',
    messagingSenderId: '<your-messaging-sender-id>'
  }
};

要找到你的 app 配置,打开 Firebase 控制台,在项目总览页面,点击齿轮图标,点击项目设置,如图 2-6 所示。

img/470914_1_En_2_Fig6_HTML.jpg

图 2-6

点按齿轮图标以查看项目设置菜单

从项目设置视图中,找到将 Firebase 添加到您的 web app (参见图 2-7 )。

img/470914_1_En_2_Fig7_HTML.jpg

图 2-7

单击将 Firebase 添加到您的应用按钮查看项目设置

替换environment.ts中的项目设置。(参见图 2-8 )。

img/470914_1_En_2_Fig8_HTML.jpg

图 2-8

复制要在 environment.ts 中替换的项目设置

导航到/src/app/app.module.ts并注入 Firebase 提供程序。Injector 确保在应用中正确指定了 Firebase 配置。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AngularFireModule } from 'angularfire2';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

AngularFire 是一个模块化的软件包,支持不同的 Firebase 特性。AngularFirestoreModuleAngularFireAuthModuleAngularFireDatabaseModuleAngularFireStorageModule可以单独添加到 **@NgModules 中。**例如,在这个应用中,我们将分别添加AngularFireStoreModuleAngularFireAuthModule,以获得对数据库和认证特性的支持。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AngularFireModule } from 'angularfire2';

import { AngularFirestoreModule } from 'angularfire2/firestore';

import { AngularFireAuthModule } from 'angularfire2/auth';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; 

import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule, // needed for database features
    AngularFireAuthModule, // needed for auth features,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

很好,AngularFirestore provider 现在可以访问 Firebase 数据库集合,以修改/删除或执行更多操作。比如打开**/src/app/app . component . ts**,注入AngularFirestore

import { Component } from '@angular/core';

import { AngularFirestore } from 'angularfire2/firestore';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'lovely-offline';

  constructor(db: AngularFirestore) {

  }
}

下一步是绑定特定 Firestore 集合。例如,在将来,我们将创建一个名为 notes 的集合。下面的代码演示了我们如何访问所有数据,并在我们的视图中显示这些数据。

import { Component } from '@angular/core';
import { AngularFirestore } from 'angularfire2/firestore';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <h1>Bind Firestore collection example</h1>
    <ul>
      <li class="text" *ngFor="let note of notes$ | async">
        {{note.title}}
      </li>
    </ul>
    <router-outlet></router-outlet>
  `,
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  notes$: Observable<any[]>;
  constructor(db: AngularFirestore) {
    this.notes$ = db.collection('notes').valueChanges();
  }
}

摘要

本章介绍了一种将我们的 Angular 应用部署到 Firebase 的简单方法,并介绍了一些服务,如 Firestore 云功能,以管理和运行我们的后端代码。即使我们没有深入研究每个功能,但这足以启动并使应用运行。

AngularFire2 是 Firebase 的官方 Angular 库,它已经在我们的应用中设置好了,在接下来的章节中解释了如何将它注入我们的组件,以便访问 Firestore 和其他 Firebase 功能。

现在,我们已经准备好了部署需求,我们准备进入下一章,创建我们的应用框架,并准备好开始构建 PWA。

Footnotes 1

https://firebase.google.com/docs/database/

  2

https://firebase.google.com/docs/firestore/

  3

https://en.wikipedia.org/wiki/NoSQL 阅读更多。

  4

https://en.wikipedia.org/wiki/Serverless_computing 阅读更多。

  5

https://github.com/angular/angularfire2

 

*

三、完成 Angular 应用

到目前为止,我们已经回顾了基础知识和需求,并设置了在云中托管、存储数据和运行功能的先决条件。对你来说这可能听起来有点无聊,但是随着我们继续每一章,它会变得更加令人兴奋,因为我们将通过添加更多的功能来逐渐构建一个真正的 PWA。

现在,是时候步入现实世界,创建一个有效的应用了。在这一章中,我们将实现一个在 Firebase 中保存个人笔记的程序。这个应用将具有用户认证功能,让用户保存,编辑和删除他们的个人帐户中的笔记。我们将分别为这些功能创建 ui 和路由。

此外,本章还有两个目标。首先,当我们继续下一章时,你将看到我们如何从头开始一个应用,并理解我们如何将它转换成 PWA。其次,您将看到我们如何将现有的应用转换为 PWA。那么,我们还在等什么?我们开始吧。

实现我们的用户界面

首先,我们需要创建一个看起来不错的应用。我们为我们的 UI 所选择的至少要包含以下特征:现代快速一致通用灵活移动优先反应灵敏和用户友好。Angular Material1是其中最好的一种,它完美地符合 Angular,帮助我们快速开发我们的应用,同时它看起来很好,满足我们的需求。

安装和设置 Angular 材质、CDK 和动画

Angular CLI 6+提供了一个新命令ng add,以便用正确的依赖关系更新 Angular 项目,执行配置更改,并执行初始化代码(如果有)。

使用 Angular CLI 自动安装@angular/material

我们现在可以使用这个命令来安装@angular/material :

ng add @angular/material

您应该会看到以下消息:

> ng add @angular/material

Installing packages for tooling via npm.
npm WARN @angular/material@6.4.6 requires a peer of @angular/cdk@6.4.6 but none is installed. You must install peer depen
dencies yourself.

+ @angular/material@6.4.6

added 2 packages from 1 contributor and audited 24256 packages in 7.228s
found 12 vulnerabilities (9 low, 3 high)
  run `npm audit fix` to fix them, or `npm audit` for details
Installed packages for tooling via npm.

UPDATE package.json (1445 bytes)

UPDATE angular.json (3942 bytes)

UPDATE src/app/app.module.ts (907 bytes)

UPDATE src/index.html (477 bytes)

UPDATE src/styles.scss (165 bytes)

added 1 package and audited 24258 packages in 7.297s

太棒了——Angular CLI 为我们处理了所有配置。然而,为了更好地理解它是如何详细工作的,我还将继续手动添加 Angular 材质到我的项目中,如下所述。

手动安装@ angular/材料

您可以使用 NPM 或纱来安装软件包,所以使用最适合您的项目。我继续讲npm

npm install --save @angular/material @angular/cdk @angular/animations

要在软件包安装后启用动画支持,BrowserAnimationsModule应该是:

imported into our application.

import { BrowserModule } from '@angular/platform-browser';

import { NgModule } from '@angular/core';
import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { AngularFireAuthModule } from 'angularfire2/auth';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { environment } from '../environments/environment';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFirestoreModule, // needed for database features
    AngularFireAuthModule,  // needed for auth features,
    BrowserAnimationsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

要在安装软件包后启用动画支持,应该导入BrowserAnimationsModule

字体和图标帮助我们的应用看起来更好,感觉更好。因此,我们将添加 Roboto 和材料图标字体到我们的应用中。要包含它们,修改index.html,并在<head></head>:之间添加以下链接

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">

最后,我们需要包含一个主题。在我写这本书的时候,@angular/material库中有预先构建的主题,如下所示:

  • deeppurple-amber.css

  • indigo-pink.css

  • pink-bluegrey.css

  • purple-green.css

打开angular.json,并添加一个主题 CSS 文件到建筑师➤建立➤风格,所以它看起来像下面的配置:

"architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"

            ],
            "styles": [
              {
                "input": "node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
              },
              "src/styles.scss"
            ],
            "scripts": []
          },

太好了——我们已经为我们的 UI 添加了我们需要的东西;现在让我们为我们的应用创建一个基本框架。

创建核心模块/共享模块

Angular 中受益于延迟加载和代码分割的一种常见方式是模块化应用,同时保持其基于组件的方法。这意味着我们将尽可能多的组件封装到一个模块中,并通过导入到其他模块中来重用这个模块。首先,我们将生成 SharedModule 以导入到所有其他模块中,并公开将在我们的应用和 CoreModule 中重用的所有公共组件和模块,CoreModule【】将在我们的根模块AppModule,中仅导入一次,并包含所有的提供者,这些提供者是单例的,并将在应用启动时立即初始化。

运行以下命令来生成核心模块。

ng generate module modules/core
> ng g m modules/core
CREATE src/app/modules/core/core.module.spec.ts (259 bytes)
CREATE src/app/modules/core/core.module.ts (188 bytes)

Angular CLI 生成的CoreModule位于模块文件夹**中。**让我们再执行一次这个命令来生成SharedModule located in the 模块文件夹 :

ng generate module modules/shared
> ng g m modules/shared
CREATE src/app/modules/shared/shared.module.spec.ts (275 bytes)
CREATE src/app/modules/shared/shared.module.ts (190 bytes)

为了确保CoreModule不会被多次导入,我们可以为这个模块创建一个防护。只需将以下代码添加到您的模块中:

export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(`CoreModule has already been loaded. Import Core modules in the AppModule only.`);
    }
  }
}

因此,我们的核心模块如下所示:

import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule,
  ],
  providers: []
})
export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(`CoreModule has already been loaded. Import Core modules in the AppModule only.`);
    }
  }
}

让我们将CoreModule导入到AppModule中。现在我们准备开始创建我们的第一个共享组件。

页眉、页脚和正文组件

在本节中,我们将基于图 3-1 所示的简单草图创建我们的第一个应用——一个主应用布局。

img/470914_1_En_3_Fig1_HTML.png

图 3-1

初始应用草图

我们将继续发展,而我们心中有这个草图。首先,让我们创建一个名为LayoutModule的模块,它包含页脚、页眉和菜单组件,然后将这个模块导入到AppModule中,以重用app.component.ts文件中的页眉/页脚。

ng g m modules/layout
import LayoutModule into AppModule:
...imports: [
    CoreModule,
    LayoutModule,...

通过运行以下命令,将分别生成页脚和页眉组件。

ng generate component modules/layout/header
ng generate component modules/layout/footer

我们已经创建了SharedModule;然而,我们需要在这个模块中做一些改变。首先,我们作为共享模块或共享组件导入的内容也应该导出。Angular 材料是一种模块化包装;也就是说,我们应该导入 UI 所需的模块。然后,我将在这个应用中根据我们的需要添加尽可能多的角状材料模块。以后可以添加或删除模块和组件。

最后,我们的SharedModule看起来像下面的代码:

const SHARED_MODULES = [
  CommonModule,
  MatToolbarModule,
  MatCardModule,
  MatIconModule,
  MatButtonModule,
  MatDividerModule,
  MatBadgeModule,
  MatFormFieldModule,
  MatInputModule,
  MatSnackBarModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatMenuModule,
  ReactiveFormsModule,
  FormsModule,
  RouterModule
];
const SHARED_COMPONENTS = [];
@NgModule({
  imports: [ ...SHARED_MODULES2  ],
  declarations: [ ...SHARED_COMPONENTS ],
  exports: [ ...SHARED_MODULES,    ...SHARED_COMPONENTS  ],
})
export class SharedModule { }

SharedModule导入LayoutModule后,我们可以根据所需的材料组件设计页眉/页脚。

以下是标题组件:

// header.component.html

<mat-toolbar color="primary">
  <span>ApressNote-PWA</span>
  <span class="space-between"></span>
  <button mat-icon-button [mat-menu-trigger-for]="menu">
    <mat-icon>more_vert</mat-icon>
  </button>
</mat-toolbar>
<mat-menu x-position="before" #menu="matMenu">
  <button mat-menu-item>Home</button>
  <button mat-menu-item>Profile</button>
  <button mat-menu-item>Add Note</button>
</mat-menu>

// header.component.scss

.space-between {
    flex:1;
}

// header.component.ts

import { Component, OnInit } from '@angular/core';
@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent { }

下面是页脚组件:

// footer.component.html

<footer>
  <div class="copyright">Copyright Apress - Majid Hajian</div>
</footer>
<div class="addNote">
  <button mat-fab>
    <mat-icon>add circle</mat-icon>
  </button>
</div>

// footer.component.scss

footer{
    background: #3f51b5;
    color: #fff;
    display: flex;
    box-sizing: border-box;
    padding: 1rem;
    flex-direction: column;
    align-items: center;
    white-space: nowrap;
}
.copyright {
    text-align: center;
}
.addNote {
 position: fixed;
 bottom: 2rem;
 right: 1rem;
 color: #fff;
}

// footer.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-footer',
  templateUrl: './footer.component.html',
  styleUrls: ['./footer.component.scss']
})
export class FooterComponent { }

现在在style.scss文件中添加一些自定义的 CSS 行来调整我们的布局:

html, body { height: 100%; }
body { margin: 0; font-family: 'Roboto', sans-serif; }
.appress-pwa-note {
    display: flex;
    flex-direction: column;
    align-content: space-between;
    height: 100%;
}
.main{
    display: flex;
    flex:1;
}
mat-card {
 max-width: 80%;
 margin: 2em auto;
 text-align: center;
}

mat-toolbar-row {
 justify-content: space-between;
}

最后,添加页脚、页眉和必要的修改到app.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
  <div class="appress-pwa-note">
    <app-header></app-header>
    <div class="main">
      <router-outlet></router-outlet>
    </div>
    <app-footer></app-footer>
  </div>
  `,
})
export class AppComponent { }

到目前为止,一切顺利——基于草图的初始骨架现已准备就绪,如图 3-2 所示。

让我们继续前进,创建不同的页面和路由。

注意

你会在 www.github.com/mhadaily/awesome-apress-pwa/chapter03/01-material-design-and-core-shared-modules-setup 中找到所有的代码。

img/470914_1_En_3_Fig2_HTML.jpg

图 3-2

初始应用外壳

登录/个人资料页面

我们需要创建页面,以便我的用户可以注册,登录,并看到他们的个人资料。首先,我们创建UserModule,包括路由:

ng generate module modules/user --routing

因为我们要延迟加载这个模块,我们至少需要一个路径和一个组件。要生成组件,请继续运行以下命令:

ng generate component modules/user/userContainer --flat
flag --flat ignores creating a new folder for this component.

一旦组件生成,我们应该将它添加到UserModule declarations,然后在UserModuleRouting中定义我们的路径——路径/user可以相应地在AppRoutingModule中延迟加载。

// UserModuleRouting

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { UserContainerComponent } from './user-container.component';

const routes: Routes = [
  {
    path: '',
    component: UserContainerComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class UserRoutingModule { }

//AppModuleRouting

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'user',
    loadChildren: './modules/user/user.module#UserModule',
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]

})
export class AppRoutingModule { }

添加登录、注册和个人资料用户界面和功能

在我们继续添加登录/注册功能之前,我们必须激活 Firebase 中的登录提供者。因此,转到您的 project Firebase 控制台,在左侧菜单列表的 develop 组下找到 Authentication,然后将当前选项卡移动到 Sign-in methods。为了简单起见,我们将使用电子邮件/密码提供者;但是,您应该能够根据需要添加更多的提供者(参见图 3-3 )。

img/470914_1_En_3_Fig3_HTML.jpg

图 3-3

启用电子邮件/密码验证

让我们继续创建一个处理所有 Firebase 身份验证方法的 Angular 服务。通过运行以下命令继续:

ng generate service modules/core/firebaseAuthService

我们需要编写几个方法,检查用户登录状态,进行登录、注册和注销。

慢慢来,看看清单 3-1 ,我们在其中实现了FirebaseAuthService,以便从AngularFireAuth服务中调用必要的方法,并在整个应用中共享状态。服务方法是不言自明的。

export class AuthService {
  // expose all data
  public authErrorMessages$ = new Subject<string>();
  public isLoading$ = new BehaviorSubject<boolean>(true);
  public user$ = new Subject<User>();

  constructor(private afAuth: AngularFireAuth) {
    this.isLoggedIn().subscribe();
  }

  private isLoggedIn() {
    return this.afAuth.authState.pipe(
      first(),
      tap(user => {
        this.isLoading$.next(false);
        if (user) {
          const { email, uid } = user;
          this.user$.next({ email, uid });
        }
      })
    );
  }

  public signUpFirebase({ email, password }) {
    this.isLoading$.next(true);
    this.handleErrorOrSuccess(() => {
      return this.afAuth.auth.createUserWithEmailAndPassword(email, password);
    });
  }

  public loginFirebase({ email, password }) {
    this.isLoading$.next(true);
    this.handleErrorOrSuccess(() => {
      return this.afAuth.auth.signInWithEmailAndPassword(email, password);
    });
  }

  public logOutFirebase() {
    this.isLoading$.next(true);
    this.afAuth.auth
      .signOut()
      .then(() => {
        this.isLoading$.next(false);
        this.user$.next(null);
      })
      .catch(e => {
        console.error(e);
        this.isLoading$.next(false);
        this.authErrorMessages$.next("Something is wrong when signing out!");
      });
  }

  private handleErrorOrSuccess(
    cb: () => Promise<firebase.auth.UserCredential>
  ) {
    cb()
      .then(data => this.authenticateUser(data))
      .catch(e => this.handleSignUpLoginError(e));
  }

  private authenticateUser(UserCredential) {
    const {
      user: { email, uid }
    } = UserCredential;

    this.isLoading$.next(false);
    this.user$.next({ email, uid });
  }

  private handleSignUpLoginError(error: { code: string; message: string }) {
    this.isLoading$.next(false);
    const errorMessage = error.message;
    this.authErrorMessages$.next(errorMessage);
  }
}

Listing 3-1App/modules/core/auth.service.ts

最后,应用应该提供登录和注册的 UI 以及用户信息。回到我们的userContainerComponent ,我们将分别实现 UI 和方法。清单 3-2 到 3-4 显示了我们的 TypeScript、HTML 和 CSS。

export class UserContainerComponent implements OnInit {
  public errorMessages$ = this.afAuthService.authErrorMessages$;
  public user$ = this.afAuthService.user$;
  public isLoading$ = this.afAuthService.isLoading$;
  public loginForm: FormGroup;
  public hide = true;

  constructor(
    private fb: FormBuilder,
    private afAuthService: FirebaseAuthService
  ) {}

  ngOnInit() {
    this.createLoginForm();
  }

  private createLoginForm() {
    this.loginForm = this.fb.group({
      email: ["", [Validators.required, Validators.email]],
      password: ["", [Validators.required]]
    });
  }

  public signUp() {
    this.checkFormValidity(() => {
      this.afAuthService.signUpFirebase(this.loginForm.value);
    });
  }

  public login() {
    this.checkFormValidity(() => {
      this.afAuthService.loginFirebase(this.loginForm.value);
    });
  }

  private checkFormValidity(cb) {
    if (this.loginForm.valid) {
      cb();
    } else {
      this.errorMessages$.next("Please enter correct Email and Password value");
    }

  }

  public logOut() {
    this.afAuthService.logOutFirebase();
  }

  public getErrorMessage(controlName: string, errorName: string): string {
    const control = this.loginForm.get(controlName);
    return control.hasError("required")
      ? "You must enter a value"
      : control.hasError(errorName)
        ? `Not a valid ${errorName}`
        : "";
  }
}

Listing 3-2User-container.component.ts

<mat-card *ngIf="user$ | async as user">
  <mat-card-title>
    Hello {{user.email}}
  </mat-card-title>
  <mat-card-subtitle>
    ID: {{user.uid}}
  </mat-card-subtitle>
  <mat-card-content>
    <button mat-raised-button color="secondary" (click)="logOut()">Logout</button>
  </mat-card-content>
</mat-card>

<mat-card *ngIf="!(user$ | async)">
  <mat-card-title>
    Access to your notes
  </mat-card-title>
  <mat-card-subtitle class="error" *ngIf="errorMessages$ | async as errorMessage">
    {{ errorMessage }}
  </mat-card-subtitle>
  <mat-card-content>
    <div class="login-container" [formGroup]="loginForm">
      <mat-form-field>

        <input matInput placeholder="Enter your email" formControlName="email" required>
        <mat-error *ngIf="loginForm.get('email').invalid">{{getErrorMessage('email', 'email')}}</mat-error>
      </mat-form-field>
      <br>
      <mat-form-field>
        <input matInput placeholder="Enter your password" [type]="hide ? 'password' : 'text'" formControlName="password">
        <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
        <mat-error *ngIf="loginForm.get('password').invalid">{{getErrorMessage('password')}}</mat-error>
      </mat-form-field>
    </div>
    <button mat-raised-button color="primary" (click)="login()">Login</button>
  </mat-card-content>
  <mat-card-content><br>----- OR -----<br><br></mat-card-content>
  <mat-card-content>
    <button mat-raised-button color="accent" (click)="signUp()">Sign Up</button>
  </mat-card-content>
  <mat-card-footer>
    <mat-progress-bar *ngIf="isLoading$ | async" mode="indeterminate"></mat-progress-bar>
  </mat-card-footer>
</mat-card>

Listing 3-3
User-container.component.html

.login-container {
  display: flex;
  flex-direction: column;
  > * {
    width: 100%;
  }

}

Listing 3-4User-container.component.scss

图 3-4 显示了到目前为止我们所做的结果。

img/470914_1_En_3_Fig4_HTML.jpg

图 3-4

应用中的登录、注册和个人资料用户界面

注意

你会在 www.github.com/mhadaily/awesome-apress-pwa/chapter03/02-login-signup-profile 中找到所有的代码。

尽管我们需要做的已经实现了,但是你并没有受到限制,你可以继续添加更多的 Firebase 特性,比如忘记密码链接、无密码登录和其他登录提供者。

注模块的 Firebase CRUD 3 操作

在下一节中,我们将使用不同的视图和方法,以便在应用中列出、添加、删除和更新注释;让我们一步一步来。

建立火风暴数据库

首先要做的事情:快速开始展示如何建立我们的 Firestore 数据库。

  1. 打开浏览器,进入 Firebase 项目控制台。

  2. 数据库部分,点击云火商店的入门创建数据库按钮。

  3. 为您的云 Firestore 安全规则选择锁定模式4

  4. 点击启用,如图 3-5 所示。

img/470914_1_En_3_Fig5_HTML.jpg

图 3-5

在 Firebase 中创建新数据库时选择锁定模式

下面是数据库模式 5 ,我们的目标是创建存储我们的用户和他们的笔记。

----- users // this is a collection
      ------- [USER IDs] // this is a document
             ------ notes // this is a collection
                    ----- [NOTE DOCUMENT]
                    ----- [NOTE DOCUMENT]
                    ----- [NOTE DOCUMENT]
       ------- [USER IDs] // this is a document
             ------ notes // this is a collection
                    ----- [NOTE DOCUMENT]
                    ----- [NOTE DOCUMENT]
                    ----- [NOTE DOCUMENT]

可以在 Firestore 中手动创建收藏和文档;但是我们稍后将通过在我们的应用中实现适当的逻辑来编程实现它(参见图 3-6 )。

img/470914_1_En_3_Fig6_HTML.jpg

图 3-6

Firestore 视图一旦启用

最后一步是设置 Firestore 规则,要求用户在请求中使用唯一的 id ( uid),以便给予足够的权限来执行创建/读取/更新/删除操作。点击规则选项卡,复制粘贴以下规则(见图 3-7 )。

img/470914_1_En_3_Fig7_HTML.jpg

图 3-7

Firestore 规则

service cloud.firestore {

  match /databases/{database}/documents {
    // Make sure the uid of the requesting user matches name of the user
    // document. The wildcard expression {userId} makes the userId variable
    // available in rules.
    match /users/{userId} {
      allow read, update, delete: if request.auth.uid == userId;
      allow create: if request.auth.uid != null;
      // make sure user can do all action for notes collection if userID is matched
        match /notes/{document=**} {
          allow create, read, update, delete: if request.auth.uid == userId;
        }
    }
  }

}

列表、添加和详细注释视图

Firestore 设置完成后,下一步是创建我们的组件,以便显示注释列表、添加注释以及详细说明注释视图及其相关功能。

首先,通过运行以下命令生成一个 notes 模块,包括路由:

ng generate module modules/notes --routing

我们来看看NotesRoutingModule:

const routes: Routes = [
  {
    path: "",
    component: NotesListComponent
  },
  {
    path: "add",
    component: NotesAddComponent
  },
  {
    path: ":id",
    component: NoteDetailsComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class NotesRoutingModule {}

如您所见,已经定义了三条路径;因此,我们应该通过分别运行每个命令来生成相关组件:

ng generate component modules/notes/notesList
ng generate component modules/notes/notesAdd
ng generate component modules/notes/noteDetails

最后,通过将NotesRoutingModule添加到AppRoutingModule:中来延迟加载NotesModule

const routes: Routes = [
  {
    path: "",
    redirectTo: "/notes",
    pathMatch: "full"
  },
  {
    path: "user",
    loadChildren: "./modules/user/user.module#UserModule",
  },
  {
    path: "notes",
    loadChildren: "./modules/notes/notes.module#NotesModule"
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

认证服务

身份验证服务用于登录、注销和注册,并检查用户是否已经通过了应用的身份验证。通过调用 AngularFire Auth 服务上的适当方法,凭证被发送到 Firebase,以相应地执行每个功能。

需要注入AuthService6来处理我们 app 中的认证层:

ng generate service modules/core/auth

以下代码显示了AuthService的逻辑:

// auth.service.ts

interface User {
  uid: string;
  email: string;
}

@Injectable({
  providedIn: "root"
})
export class AuthService {
  public authErrorMessages$ = new BehaviorSubject<string>(null);
  public isLoading$ = new BehaviorSubject<boolean>(true);
  public user$ = new BehaviorSubject<User>(null);

  private authState = null;

  constructor(private afAuth: AngularFireAuth) {
    this.isLoggedIn().subscribe(user => (this.authState = user));
  }

  get authenticated(): boolean {
    return this.authState !== null;
  }

  get id(): string {
    return this.authenticated ? this.authState.uid : "";
  }

  private isLoggedIn(): Observable<User | null> {
    return this.afAuth.authState.pipe(
      map(user => {
        if (user) {
          const { email, uid } = user;
          this.user$.next({ email, uid });
          return { email, uid };
        }
        return null;
      }),
      tap(() => this.isLoading$.next(false))
    );
  }

  public getCurrentUserUid(): string {
    return this.afAuth.auth.currentUser.uid;

  }

  public signUpFirebase({ email, password }) {
    this.isLoading$.next(true);
    this.handleErrorOrSuccess(() => {
      return this.afAuth.auth.createUserWithEmailAndPassword(email, password);
    });
  }

  public loginFirebase({ email, password }) {
    this.isLoading$.next(true);
    this.handleErrorOrSuccess(() => {
      return this.afAuth.auth.signInWithEmailAndPassword(email, password);
    });
  }

  public logOutFirebase() {
    this.isLoading$.next(true);
    return this.afAuth.auth.signOut();
  }

  private handleErrorOrSuccess(
    cb: () => Promise<firebase.auth.UserCredential>
  ) {
    cb()
      .then(data => this.authenticateUser(data))
      .catch(e => this.handleSignUpLoginError(e));
  }

  private authenticateUser(UserCredential) {
    const {
      user: { email, uid }
    } = UserCredential;

    this.isLoading$.next(false);
  }

  private handleSignUpLoginError(error: { code: string; message: string }) {
    this.isLoading$.next(false);
    const errorMessage = error.message;
    this.authErrorMessages$.next(errorMessage);
  }
}

数据服务

该服务包含一组标准的 CRUD 方法(创建、读取、更新和删除)。获取所有笔记等功能;添加、更新和删除;并通过调用适当方法或从适当的 API 请求来获取详细注释。事实上,它充当了 Angular 应用和后端 API 之间的接口。

要生成数据服务,请运行以下命令:

ng generate service modules/core/data

以下代码显示了DataService的逻辑:

// data.service.ts

interface Note {
  id: string;
  title: string;
  content: string;
}

@Injectable({
  providedIn: "root"
})
export class DataService {
  protected readonly USERS_COLLECTION = "users";
  protected readonly NOTES_COLLECTION = "notes";

  public isLoading$ = new BehaviorSubject<boolean>(true);

  get timestamp() {
    return new Date().getTime();
  }

  constructor(private afDb: AngularFirestore, private auth: AuthService) {}

  getUserNotesCollection() {
    return this.afDb.collection(
      this.USERS_COLLECTION + "/" + this.auth.id + "/" + this.NOTES_COLLECTION,
      ref => ref.orderBy("updated_at", "desc")
    );
  }

  addNote(data): Promise<DocumentReference> {
    return this.getUserNotesCollection().add({
      ...data,
      created_at: this.timestamp,
      updated_at: this.timestamp
    });
  }

  editNote(id, data): Promise<void> {
    return this.getUserNotesCollection()
      .doc(id)
      .update({
        ...data,
        updated_at: this.timestamp
      });
  }

  deleteNote(id): Promise<void> {
    return this.getUserNotesCollection()
      .doc(id)
      .delete();
  }

  getNote(id): Observable<any> {
    return this.getUserNotesCollection()
      .doc(id)
      .snapshotChanges()
      .pipe(
        map(snapshot => {
          const data = snapshot.payload.data() as Note;
          const id = snapshot.payload.id;
          return { id, ...data };
        }),
        catchError(e => throwError(e))
      );
  }

  getNotes(): Observable<any> {
    return this.getUserNotesCollection()
      .snapshotChanges()
      .pipe(
        map(snapshot =>
          snapshot.map(a => {
            //Get document data
            const data = a.payload.doc.data() as Note;
            //Get document id
            const id = a.payload.doc.id;
            //Use spread operator to add the id to the document data
            return { id, ...data };
          })
        ),
        tap(notes => {
          this.isLoading$.next(false);
        }),
        catchError(e => throwError(e))
      );
  }
}

认证守卫

因为这个应用要求用户在执行任何操作之前进行身份验证,所以我们应该确保所有的路由都受到保护。

AuthGuard 有助于保护对身份验证路由的访问。因为我们需要在一个惰性加载模块上设置这个防护,所以应该实现CanLoad

Ng generate guard modules/notes/auth

以下代码显示了AuthGuard的逻辑:

// auth.guard.ts

@Injectable()
export class AuthGuard implements CanLoad {
  constructor(private auth: AuthService, private router: Router) {}

  canLoad(): Observable<boolean> {
    if (!this.auth.authenticated) {
      this.router.navigate(["/user"]);
      return of(false);
    }
    return of(true);
  }
}

我们应该在我们的AppRoutingModule中提供AuthGuard。记住将这种保护添加到提供者中是很重要的。

  {
    path: "notes",
    loadChildren: "./modules/notes/notes.module#NotesModule",
    canLoad: [AuthGuard]
  }

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  providers: [AuthGuard],
  exports: [RouterModule]

})

NoteList、NoteAdd 和 NoteDetail 组件

我们已经准备好了应用中需要的所有服务层和路由。应用的其余部分只是为 NotesList、NoteAdd 和 NoteDetail 组件实现适当的 UI 和组件逻辑(清单 3-5 到 3-13 )。因为很简单,所以我希望你看一下组件,最后,图 3-8 将展示结果。

export class NotesListComponent implements OnInit {
  notes$: Observable<Note[]>;
  isDbLoading$;

  constructor(private db: DataService) {}

  ngOnInit() {
    this.notes$ = this.db.getNotes();
    this.isDbLoading$ = this.db.isLoading$;
  }
}

Listing 3-5// Notes-list.component.ts

<div *ngIf="notes$ | async as notes; else notFound">
  <app-note-card *ngFor="let note of notes" [note]="note" [loading]="isDbLoading$ | async" [routerLink]="['/notes', note.id]">
  </app-note-card>
</div>
<ng-template #notFound>
  <mat-card>
    <mat-card-title>
      Either you have no notes
    </mat-card-title>
  </mat-card>
</ng-template>

Listing 3-6// Notes-list.component.html

@Component({
  selector: "app-note-card",
  templateUrl: "./note-card.component.html",
  styleUrls: ["./note-card.component.scss"]
})
export class NoteCardComponent {
  @Input()
  note;

  @Input()
  loading;

  @Input()
  edit = true;
}

Listing 3-7// Notes-card.component.ts

<mat-card>
  <mat-card-title>{{ note.title }}</mat-card-title>
  <mat-card-subtitle>{{ note.created_at | date:"short" }}</mat-card-subtitle>
  <mat-card-content>{{ note.content }}</mat-card-content>
  <mat-card-footer class="text-right">
    <button color="primary" *ngIf="edit"><mat-icon>edit</mat-icon></button>
    <mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
  </mat-card-footer>
</mat-card>

Listing 3-8// Notes-card.component.html

export class NotesAddComponent {
  public userID;
  public errorMessages$ = new Subject();

  constructor(
    private router: Router,
    private data: DataService,
    private snackBar: SnackBarService
  ) {}

  onSaveNote(values) {
    this.data
      .addNote(values)
      .then(doc => {
        this.router.navigate(["/notes"]);
        this.snackBar.open(`Note ${doc.id} has been succeffully saved`);
      })
      .catch(e => {
        this.errorMessages$.next("something is wrong when adding to DB");
      });
  }

  onSendError(message) {
    this.errorMessages$.next(message);
  }

}

Listing 3-9// Notes-add.component.ts

<mat-card>
  <mat-card-title>New Note</mat-card-title>
  <mat-card-subtitle class="error" *ngIf="errorMessages$ | async as errorMessage">
    {{ errorMessage }}
  </mat-card-subtitle>
  <mat-card-content>
    <app-note-form (saveNote)="onSaveNote($event)" (sendError)="onSendError($event)"></app-note-form>
  </mat-card-content>
</mat-card>

Listing 3-10// Notes-add.component.html

export class NoteFormComponent implements OnInit {
  noteForm: FormGroup;

  @Input()
  note;

  @Output()
  saveNote = new EventEmitter();

  @Output()
  sendError = new EventEmitter();

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.createForm();

    if (this.note) {
      this.noteForm.patchValue(this.note);
    }
  }

  createForm() {
    this.noteForm = this.fb.group({
      title: ["", Validators.required],
      content: ["", Validators.required]
    });
  }

  addNote() {
    if (this.noteForm.valid) {
      this.saveNote.emit(this.noteForm.value);
    } else {
      this.sendError.emit("please fill all fields");
    }
  }
}

Listing 3-11// Notes-form.component.ts

<div class="note-container" [formGroup]="noteForm">
  <mat-form-field>
    <input matInput placeholder="Enter your title" formControlName="title" required>
  </mat-form-field>
  <br>
  <mat-form-field>
    <textarea matInput placeholder="Leave a comment" formControlName="content" required cdkTextareaAutosize></textarea>
  </mat-form-field>
</div>
<br>
<br>
<div class="text-right">
  <button mat-raised-button color="primary" (click)="addNote()">Save</button>
</div>

Listing 3-12// Notes-form.component.html

export class NoteDetailsComponent implements OnInit {
  public errorMessages$ = new Subject();
  public note$;
  public isEdit;

  private id;

  constructor(
    private data: DataService,
    private route: ActivatedRoute,
    private snackBar: SnackBarService,
    private router: Router
  ) {}

  ngOnInit() {
    const id = this.route.snapshot.paramMap.get("id");
    this.id = id;
    this.note$ = this.data.getNote(id);
  }

  delete() {
    if (confirm("Are you sure?")) {
      this.data
        .deleteNote(this.id)
        .then(() => {
          this.router.navigate(["/notes"]);
          this.snackBar.open(`${this.id} successfully was deleted`);
        })
        .catch(e => {
          this.snackBar.open("Unable to delete this note");
        });
    }
  }

  edit() {
    this.isEdit = !this.isEdit;
  }

  saveNote(values) {
    this.data
      .editNote(this.id, values)
      .then(() => {
        this.snackBar.open("Successfully done");
        this.edit();
      })
      .catch(e => {
        this.snackBar.open("Unable to edit this note");
        this.edit();
      });
  }

  sendError(message) {
    this.errorMessages$.next(message);
  }
}

Listing 3-13// Notes-details.component.ts

img/470914_1_En_3_Fig8_HTML.jpg

图 3-8

添加注释、详细信息和注释列表视图

<div *ngIf="note$ | async as note; else spinner">

    <mat-card *ngIf="isEdit">
        <mat-card-subtitle class="error" *ngIf="errorMessages$ | async as errorMessage">
            {{ errorMessage }}
        </mat-card-subtitle>
        <mat-card-content>
            <app-note-form [note]="note" (saveNote)="saveNote($event)" (sendError)="sendError($event)"></app-note-form>
        </mat-card-content>
    </mat-card>

    <app-note-card *ngIf="!isEdit" [note]="note" [loading]="isDbLoading$ | async"></app-note-card>

    <button mat-raised-button color="accent" (click)="delete()"><mat-icon>delete</mat-icon></button>
    <button mat-raised-button color="primary" (click)="edit()"><mat-icon>edit</mat-icon></button>

</div>

<ng-template #spinner>
    <mat-spinner></mat-spinner>
</ng-template>

Listing 3-14// Notes-details.component.html

注意

如果你觉得舒服,可以看看最后的代码。你可以在 github . com/mha daily/chapter 03/03-note-list-add-edit-update-delete/中找到。克隆项目并导航到文件夹。然后运行以下命令:

npm install // to install dependencies

npm start // to run development server

npm run deploy // to deploy to firebase

摘要

前三章的目标是揭示 PWA 的基本原理;工具;一步一步地一起创建一个应用。听起来可能与 PWA 无关;然而,正如我们在本书中继续的那样,一章接一章,一节接一节,我们将努力使我们的应用逐步变得更好,最终拥有一个带 Angular 的伟大 PWA。

从下一章开始,我们将深入实现离线功能、缓存、推送通知、新的现代浏览器 API,以及更多,只是为了创建一个类似本机的应用,以便在移动和网络上获得更好的用户体验。虽然这在几年前还是不可能的,但是现在它在主流浏览器中得到了广泛的支持。

Footnotes 1

https://material.angular.io/

  2

pread 运算符(三个点……)有助于连接数组。

  3

https://en.wikipedia.org/wiki/Create,_read,_update_and_delete

  4

https://firebase.google.com/docs/firestore/quickstart

  5

https://en.wikipedia.org/wiki/Database_schema

  6

https://angular.io/guide/dependency-injection