Angular5 项目教程(四)
十四、Angular 和 UI 小部件
Angular 是许多新 JavaScript 应用的核心。但是,你需要把 Angular 和一个前端 UI 框架耦合起来,比如 Bootstrap 或者 Material Design。在这一章中,我首先介绍 Bootstrap,因为它是目前两者中比较常见的,然后我将讨论材料设计。
将 UI 小部件库与 Angular 一起使用
您可以通过两种方式使用 UI 小部件库:
- 前 Angular 方式:以正常方式使用 HTML 标记和 JavaScript。
- 使用自定义标记指令。您利用第三方定制组件和指令模块来生成 UI 小部件 HTML 标记。
预 Angular 方式
您可以使用 HTML 和 JavaScript 创建组件,这些组件的样式与您在 JQuery 或另一个早期的 JavaScript 库中使用的样式相同。
当然,HTML 是一种标记语言,也是 web 上最常见的文档格式。标记语言注释文档。包括 HTML 在内的一些标记语言有确定如何显示结构化数据的规范,它告诉计算机如何显示某些内容。在 Angular 中,我们编写动态用户界面,Angular 组件使用 HTML 标记来告诉计算机如何显示事物。
标记在模板中,模板在组件的@Component注释中指定。它有时也在@View注释中指定(稍后会详细介绍)。
Angular 的方式
您可以使用预构建和样式化的 Angular 组件和指令的模块来创建组件,这些组件作为一个模块交付,以便您可以重用它们。这是一个组件对象(就像你在前面章节中写的那些)和指令的模块,使你能够使用标签来创建一个引导 UI。这需要您使用其他人的代码,但是通过提供预构建的组件和使用它们的指令,可以节省您的时间。第十一章涵盖指令。
在这一章(和其他章节)中,我使用了 ng2-bootstrap 模块( http://valor-software.github.io/ng2-bootstrap/ ),这是 Angular 的一个 bootstrap 实现。
使用 NgBootstrap 时的预 Angular 与 Angular
图 14-1 显示了一个常见的 UI 元素:选项卡。我们将在有和没有 ng2-bootstrap 模块的情况下为同一个选项卡编写 HTML 标记。
图 14-1
Common UI element: a tab
以下是使用 HTML、CSS 和 JavaScript 的预 Angular 方法:
<div class="tabbable tabs-left" style="margin-top: 100px;">
<ul class="nav nav-tabs">
<li class="active"><a href="#pane1" data-toggle="tab" rel="popover" id="tab">Homee</a></li>
<li><a href="#pane2" data-toggle="tab" title="blah blah" id="tab1">Profile</a></li>
<li><a href="#pane3" data-toggle="tab" id="tab2">Messages</a></li>
<li><a href="#pane4" data-toggle="tab">Settings</a></li>
</ul>
<div class="tab-content">
<div id="pane1" class="tab-pane active">...</div>
<div id="pane2" class="tab-pane">...</div>
<div id="pane3" class="tab-pane">...</div>
<div id="pane4" class="tab-pane">...</div>
</div>
</div>
这是 ng2 自举模块的 Angular:
<ngb-tabset>
<ngb-tab title="Home">
<ng-template ngbTabContent>
...
</ng-template>
</ngb-tab>
<ngb-tab title="Profile">
<ng-template ngbTabContent>
...
</ng-template>
</ngb-tab>
<ngb-tab title="Messages">
<ng-template ngbTabContent>
...
</ng-template>
</ngb-tab>
<ngb-tab title="Settings">
<ng-template ngbTabContent>
...
</ng-template>
</ngb-tab>
</ngb-tabset>
该模块使代码更小。注意它调用标签ngb-tab s 而不是div s。
引导程序
Bootstrap 是一个开源的工具组,拥有 HTML 和 CSS 设计模板,用于表单、按钮、排版和导航等界面元素,以及可选的 JavaScript 扩展。Bootstrap 使开发动态网站和 web 应用变得更加容易。它兼容大多数浏览器的最新版本,包括 Firefox、Internet Explorer、Google Chrome、Opera 和 Safari,但不是在所有平台上。
Bootstrap 及更高版本还支持响应式 web 设计,它可以根据正在使用的任何设备(无论是手机、平板电脑还是台式机)的特征动态调整网页的布局。从 3.0 版本开始,Bootstrap 拥有移动优先的设计理念,默认采用响应式设计。它提供了一个网格系统,允许开发人员(他们可能缺乏响应性设计方面的技能)编写在所有设备上都同样适用的代码。没有启用响应功能的默认 12 列网格系统使用 940 像素宽的容器。添加了响应 CSS 文件后,网格将变为 724×1170 像素宽,具体取决于您的视口。在 767 像素视口下,列变得流畅并垂直堆叠。
更多关于 Bootstrap 的信息,请访问 http://getbootstrap.com 。图 14-2 展示了一个用 Bootstrap 制作的网页。
图 14-2
Web page made with Bootstrap
安装 ng 引导程序
ng-bootstrap 是 bootstrap 库的 Angular 版本,您可以使用 Bootstrap 小部件快速构建应用。源代码在 https://github.com/ng-bootstrap/ng-bootstrap 可用,演示在 https://ng-bootstrap.github.io/#/components/accordion/examples 可用。
下面是安装 ng-bootstrap 的方法:
-
以通常的方式使用 CLI 构建应用。
-
使用 npm 安装 ng-bootstrap 和 bootstrap 模块:
npm install --save @ng-bootstrap/ng-bootstrap bootstrap -
告诉 CLI 项目使用引导 CSS 文件中的样式。编辑. angular.json 并在 styles 下添加以下条目:
"../node_modules/bootstrap/dist/css/bootstrap.css", -
编辑你的模块文件(app.module.ts)并将
NgbModule指定为import。这将使NgbModule中的代码可用于该 Angular 模块:imports: [ NgbModule.forRoot(), BrowserModule ],
引导程序:示例小部件-ex100
该组件允许用户使用一组按钮来选择披萨,这些按钮的作用类似于一组单选按钮,如图 14-2 所示。
图 14-3
Selecting a pizza
让我们看一下这个例子:
-
使用 CLI 构建应用:使用以下命令:
ng new widgets-ex100 --inline-template --inline-style -
安装
ng-bootstrap和bootstrap:使用以下代码:cd widgets-ex100 npm install --save @ng-bootstrap/ng-bootstrap bootstrap -
将引导程序样式安装到项目中:编辑。angular-cli.json 并在 styles 下添加以下条目:
"../node_modules/bootstrap/dist/css/bootstrap.css",样式块应该如下所示:
"styles": [ "../node_modules/bootstrap/dist/css/bootstrap.css", "styles.css" ], -
开始
ng serve:使用该命令:ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ NgbModule.forRoot(), BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
编辑类:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-root', template: ` <div style="padding:10px"> <h2>Please select your pizza:</h2> <div [(ngModel)]="model" ngbRadioGroup name="radioBasic"> <label ngbButtonLabel class="btn btn-primary"> <input ngbButton type="radio" value="Hawaiian"> Hawaiian </label> <label ngbButtonLabel class="btn btn-primary"> <input ngbButton type="radio" value="Peperoni"> Peperoni </label> <label ngbButtonLabel class="btn btn-primary"> <input ngbButton type="radio" value="Everything"> Everything </label> </div> <hr> Your Selection: {{model}} </div> `, styles: [] }) export class AppComponent { model = 'Hawaiian'; }
你的应用应该工作在本地主机:4200。
材料设计
材料设计也使用基于网格的布局,如 Bootstrap。它支持快速响应的动画和过渡、填充、深度效果、灯光和阴影。材料具有基于纸张和墨水的颜色。
Polymer 是 web 应用用户界面材料设计的实现。它包含 Polymer library,该库为浏览器和 elements 目录提供了一个 Web 组件 API,包括一个以可视元素为特色的 paper elements 集合。图 14-4 显示一个材料设计网页。你可以在 www.material-ui.com 了解更多关于这款产品的信息。
图 14-4
Web page of Material Design
安装 Angular 材料
Angular Material 是一个 Angular 版本的素材库,您可以使用材质组件快速构建应用。源代码可在 https://github.com/jelbourn/material2-app 处获得,示例在此: https://material2-app.firebaseapp.com 。
以下是安装 Angular 材料的方法:
-
以通常的方式使用 CLI 构建应用。
-
使用 npm 安装 Angular 材质、Angular 动画和 CDK(组件开发工具包)。
npm install --save @angular/material npm install --save @angular/animations npm install --save @angular/cdk -
将图标添加到您的项目中,方法是将它们包含在 index.html 文件中:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> -
将样式文件 styles.css 重命名为 styles.scss,并将其更改为:
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; -
更改对中样式文件的引用。angular-cli.json:
"styles": [ "styles.scss" ], -
编辑 CLI 生成的模块文件 app.module.ts,确保它导入了小部件模块(如 MdButtonModule、MdCheckboxModule)、动画模块(BrowserAnimationsModule)和 hammerjs:
import {MdButtonModule, MdCheckboxModule} from '@angular/material'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { hammerjs } from 'hammerjs'; @NgModule({ ... imports: [MdButtonModule, MdCheckboxModule], [BrowserAnimationsModule] ... }
Angular 的材料设计:示例部件-ex200
该组件允许用户使用材料样式的日期选择器弹出窗口选择日期,如图 14-5 所示。
图 14-5
Date picker popup
让我们来看看这个例子:
-
使用 CLI 构建应用:
ng new widgets-ex200 --inline-template --inline-style -
安装 Angular 材料,动画和组件开发工具包。
cd widgets-ex200 npm install --save @angular/material npm install --save @angular/animations npm install --save @angular/cdk -
将图标添加到您的项目中,方法是将它们包含在 index.html 文件中:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> -
将样式文件 styles.css 重命名为 styles.scss,并将其更改为:
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; -
更改对中样式文件的引用。angular-cli.json:
"styles": [ "styles.scss" ], -
编辑 app.module.ts 并将其更改为以下内容:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import { MatNativeDateModule, MatFormFieldModule, MatInputModule, MatDatepickerModule } from '@angular/material'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, BrowserAnimationsModule, MatNativeDateModule, MatFormFieldModule, MatInputModule, MatDatepickerModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
编辑 app.component.ts 并将其更改为以下内容:
import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <mat-form-field> <input matInput [matDatepicker]="picker" placeholder="Choose a date"> <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle> <mat-datepicker #picker></mat-datepicker> </mat-form-field> `, styles: [] }) export class AppComponent { title = 'app'; } -
开始
ng serve:ng serve -
打开 web 浏览器,导航到 localhost:4200。您应该会看到应用正在运行。
你的应用应该工作在本地主机:4200。
摘要
如果你真的想构建一个完美的 Angular 应用,你应该使用一个预建的小部件库模块。许多应用使用自举和材料。我目前正在开发一个使用 Bootstrap 的应用,它的优势在于提供了一个网格系统,这消除了响应式设计的许多痛苦。
UI 小部件库无疑使用更易维护的标准代码编写漂亮的用户界面变得更加容易。不要浪费你的时间重新发明轮子写你自己的部件库!请记住,您可以在小部件库中设置主题,以便根据您的需求定制它们。
十五、路由和导航
在大多数 web 应用中,用户在执行应用任务时会从一个页面导航到下一个页面。用户可以通过以下方式导航:
- 在地址栏中输入 URL
- 跟随链接、点击按钮等等
- 在浏览器历史中后退或前进
在 Angular 应用中,用户可以以同样的三种方式导航,但他们是通过组件(Angular 应用的构建块)导航。我们可以导航,因为我们有角路由器。路由器可以将浏览器 URL 解释为导航到组件的指令,并将可选参数(包含信息)传递给组件,为其提供上下文信息,并帮助其决定要呈现的特定内容或需要做的事情。
我们可以将路由器绑定到页面上的链接,当用户单击链接时,它将导航到适当的组件。当用户点击一个按钮,从一个下拉菜单中选择,或者响应来自任何来源的其他刺激时,我们可以强制性地导航。
路由器将活动记录在浏览器的历史记录中,因此后退和前进按钮也可以工作。
客户端的路由器路由
任何包含#字符的 URL 都是片段 URL。#左边的 URL 部分标识可以访问的资源(从服务器),右边的部分称为片段标识符,指定资源内的位置(在客户机上)。例如,在 URL www.cnn.com/index.html#section2 中,片段名是 section2,它指定了文档 index.html 中的一个位置。
片段的最初目的是允许用户跳转到当前页面指定部分的链接,向上或向下滚动。现在,片段经常用于客户端导航,因为从本质上讲,它们不会调用从服务器获取资源的请求。
HTML5 浏览器可以处理 URL 的客户端和非客户端路由,包括有哈希的和没有哈希的。但是一些老的浏览器不支持未经哈希处理的 URL 的客户端路由。散列意味着 URL 的客户端部分需要在#符号之后(也就是说,它是一个片段)。
如果您正在将单页面应用部署到生产环境中,您可能需要执行以下操作:
-
在路由器上打开哈希路由。这将使您的单页应用与 HTML5 以前的浏览器更加兼容。当您在您的模块中导入路由器模块时,您应该执行以下操作:
@NgModule({ imports: [ RouterModule.forRoot(appRoutes, {useHash: true}) ], ... }) export class ... -
确保您在服务器上的 404 页面重新路由到包含单页应用的网页(例如,index.html)。如果由于某种原因,浏览器试图错误地获取服务器资源,这将把页面返回到单页应用。
路由匹配
如果您在 web 浏览器的地址栏中指定了一个 URL,并且加载了 Angular 应用,路由器将尝试查找与该 URL 匹配的路由。它检查可能的路由组合的每一个排列,直到它匹配完整的 URL。如果有多条路由可能匹配同一个 url,路由器将使用第一条可用的路由,即使第二条路由看起来更完整。
路由器 DSL
指定路由时,可以使用文本字符串来指定,例如“/customers/123”。DSL 代表“领域特定语言”,使用该术语是因为您的路由文本字符串可以用多种方式解释和匹配。您的路由字符串可以指定:
- 绝对路由。
- 从你现在所在的位置出发的相对路由。
- 引用服务器资源的路由。
- 引用客户端资源的路由(使用片段 URL)。
“DSL”是一个令人生畏的术语,但是不要担心,我们将在这一章中介绍不同类型的路由,您将很快上手!
路由器模块
在开始使用组件路由器之前,您应该知道该模块包含在节点包依赖项中,但默认情况下不包含在 Angular CLI 项目中。应用模块中不包括路由。
但是,您可以通过在ng命令的末尾添加--routing参数来改变这种情况。例如:
ng new router-ex300 --inline-template --inline-style --routing
表 15-1 列出了路由器模块中的对象。表格中有许多对象,但不要担心,经过一些练习后,它们会变得更容易理解。
表 15-1
Objects in the Router Module
| 目标 | 类型 | 描述 | | :-- | :-- | :-- | | `RouterModule` | 组件 | 一个独立的 Angular 模块,提供必要的服务提供者和在应用视图中导航的指令。 | | `Router` | | 显示活动 URL 的应用组件。管理从一个组件到下一个组件的导航。 | | `Routes` | | 定义一个路由数组,每个路由将一个 URL 路径映射到一个组件。 | | `Route` | | 定义路由器应该如何根据 URL 模式导航到组件。大多数管线由路径和元件类型组成。 | | `RouterOutlet` | 管理的 | 标记路由器显示视图位置的指令(``)。 | | `RouterLink` | 管理的 | 将可点击的 HTML 元素绑定到路由的指令。单击带有绑定到字符串或链接参数数组的`RouterLink`指令的元素会触发导航。 | | `RouterLinkActive` | 管理的 | 当包含在元素上或元素内的相关联的`RouterLink`变为活动/非活动时,从 HTML 元素添加/移除类的指令。 | | `ActivatedRoute` | | 提供给每个路由组件的服务,包含路由特定的信息,如路由参数、静态数据、解析数据、全局查询参数和全局片段。 | | `RouterState` | | 路由器的当前状态,包括当前激活的路由树以及遍历路由树的便利方法。 |简单路由:示例
这是一个披萨选择组件,它使用路由允许用户单击链接来显示不同类型的披萨,每种披萨都在自己的组件中。这个例子还展示了路由参数的使用:您可以使用small或large的size参数路由到everything比萨饼。图 15-1 所示。
图 15-1
Pizza selection
这是示例路由器-ex100:
-
使用 CLI 构建应用:使用以下代码:
ng new router-ex100 --routing --inline-template --inline-style -
开始
ng serve:使用以下代码:cd router-ex100 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑路由类:编辑 app-routing.module.ts,修改为:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { PepperoniComponent, EverythingComponent} from './app.component'; const routes: Routes = [ { path: '', redirectTo: '/pepperoni', pathMatch: 'full' }, { path: 'pepperoni', component: PepperoniComponent }, { path: 'everything/:size', component: EverythingComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } -
步骤 5–编辑组件类编辑“app.component.ts”并将其更改为以下内容:
import { Component } from '@angular/core'; import { Router, ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router'; @Component({ selector: 'pepperoni', template: ` <h2>Pepperoni</h2> <img src="https://thumb1.shutterstock.com/display_pic_with_logo/55755/161642033/stock-photo-single-slice-of-pepperoni-meat-isolated-on-white-with-path-shot-from-above-161642033.jpg"> `, styles: [] }) export class PepperoniComponent { } @Component({ selector: 'everything', template: ` <h2>Everything</h2> Size:{{size}} <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR0UXyx2jQrCBBBw2N4ofFVw2oWz7keZjDVUB4UDrASE9JHwQdi"> `, styles: [] }) export class EverythingComponent { private size: String = ''; constructor(private route: ActivatedRoute){ route.params.subscribe( (params: Object) => this.size = params['size']); } } @Component({ selector: 'app-root', template: ` <h1> Pizzas </h1> <a [routerLink]="['pepperoni']">Pepperoni</a> <a [routerLink]="['everything','small']">Everything Small</a> <a [routerLink]="['everything','large']">Everything Large</a> <router-outlet></router-outlet> `, styles: [] }) export class AppComponent { title = 'app'; } -
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent, PepperoniComponent, EverythingComponent } from './app.component'; @NgModule({ declarations: [ AppComponent, PepperoniComponent, EverythingComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
你的应用应该工作在本地主机:4200。请注意以下几点:
-
文件 app-routing.module.ts 由 CLI 生成。它为路由定义了一个模块 AppRoutingModule。这个模块包含一个数据结构,该结构设置带有伴随组件的 URL。请注意第一个 URL 如何将默认 URL 映射到另一个 URL,并注意 EverythingComponent 的路径如何指定一个
size参数。{ path: 'everything/:size', component: EverythingComponent } -
app.component.ts 文件包含所有组件。它使用
RouterLink指令来修改链路,以便与 Angular 路由器一起工作。EverythingComponent 用于显示everything披萨,并可以接受一个size参数。注意它是如何订阅路由参数对象来接收参数更新的。如果用户从everything的一个尺寸切换到另一个尺寸,更新size参数,这是必要的。constructor(private route: ActivatedRoute){ route.params.subscribe( (params: Object) => this.size = params['size']); } -
文件 app.module.ts 声明了所有组件,因此可以在 app 模块中访问它们。它还导入我们在 app.routing.module.ts 中设置的 AppRoutingModule。
嵌套路由:示例
嵌套路由意味着能够路由和导航到被导航到的其他组件内部的子组件。这在 Angular 中是完全可能的,正如你在这个例子中看到的。
嵌套路由链接 URL 具有多个“级别”,因为现在存在路由及其子级的层次结构,而不仅仅是路由。
这将是示例路由器-ex200,表 15-2 将本示例的 URL 与前一示例的 URL 进行了比较。
表 15-2
router-ex100 URLs vs. router-ex200 URLs
| 路由器-ex100 | 路由器-ex200 | | :-- | :-- | | /意大利香肠 | /意大利香肠 | | /一切 | /其他/意大利面 | | /其他/canzone |这是另一个使用路由的披萨选择组件。但是,这一次它为“other”菜单和组件使用了嵌套路由。当您点击“其他”链接时,您可以从 pasta 或 calzone 的子菜单中进行选择,如图 15-2 所示。这些菜单项的显示由嵌套路由处理。
图 15-2
Pasta or calzone ?
让我们以路由器-ex200 为例:
-
使用 CLI 构建应用:使用以下命令:
ng new router-ex200 --routing --inline-template --inline-style -
开始
ng serve:使用以下代码:cd router-ex200 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑路由类:编辑 app-routing.module.ts,修改为:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { PepperoniComponent } from './app.component'; import { OtherComponent } from './app.other-component'; import { NestedPastaComponent, NestedCalzoneComponent } from './app.other-component'; const routes: Routes = [ { path: '', redirectTo: '/pepperoni', pathMatch: 'full' }, { path: 'pepperoni', component: PepperoniComponent }, { path: 'other', component: OtherComponent, children: [ { path: '', redirectTo: 'pasta', pathMatch: 'full' }, { path: 'pasta', component: NestedPastaComponent }, { path: 'calzone', component: NestedCalzoneComponent } ] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule {} -
编辑组件类:编辑 app.component.ts 并将其更改为:
import { Component } from '@angular/core'; import { Router, ActivatedRoute, ActivatedRouteSnapshot} from '@angular/router'; @Component({ selector: 'pepperoni', template: ` <div> <h2>Pepperoni</h2> <img src="https://thumb1.shutterstock.com/display_pic_with_logo/55755/161642033/stock-photo-single-slice-of-pepperoni-meat-isolated-on-white-with-path-shot-from-above-161642033.jpg"> </div> `, styles: [] }) export class PepperoniComponent { } @Component({ selector: 'app-root', template: ` <div> <h1> Delivery Menu </h1> <a [routerLink]="['pepperoni']" routerLinkActive="router-link-active">Pepperoni Pizza</a> <a [routerLink]="['other']" routerLinkActive="router-link-active">Other Menu Items</a> <router-outlet></router-outlet> </div> `, styles: [] }) export class AppComponent { title = 'app'; } -
创建其他组件:创建 app.other-component.ts 并将其更改为:
import { Component } from '@angular/core'; @Component({ selector: 'pasta', template: ` <div> <h2>Pasta</h2> <img src="https://capetowncafe.files.wordpress.com/2015/04/spaghetti-recipe-wikipedia.jpg"> </div> `, styles: [] }) export class NestedPastaComponent { } @Component({ selector: 'calzone', template: ` <div> <h2>Calzone</h2> <img src="https://upload.wikimedia.org/wikipedia/commons/5/54/Calzone_fritto.jpg"> </div> `, styles: [] }) export class NestedCalzoneComponent { } @Component({ selector: 'other', template: ` <div> <h2>Other Menu Items</h2> <a [routerLink]="['pasta']" routerLinkActive="router-link-active">Pasta</a> <a [routerLink]="['calzone']" routerLinkActive="router-link-active">Calzone</a> <router-outlet></router-outlet> <br/> <br/> </div> `, styles: [] }) export class OtherComponent { } -
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent, PepperoniComponent } from './app.component'; import { OtherComponent, NestedCalzoneComponent, NestedPastaComponent } from './app.other-component'; @NgModule({ declarations: [ AppComponent, PepperoniComponent, OtherComponent, NestedCalzoneComponent, NestedPastaComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } -
编辑样式:编辑 styles.css 并将其更改为以下内容:
/* You can add global styles to this file, and also import other style files */ img { width:200px; border: 1px solid #000000; } a { background-color: #0066CC; color: #ffffff; border: 1px solid #000000; padding: 10px; margin: 10px; } .router-link-active { background-color: #C14242; } div { border: 1px dotted #000000; margin: 10px; padding: 10px; }
你的应用应该工作在本地主机:4200。请注意以下几点:
-
文件 app-routing.module.ts 由 CLI 生成。它为路由定义了一个模块 AppRoutingModule。这个模块包含一个数据结构,该结构设置带有伴随组件的 URL。注意,这次数据结构包含子路由,使用了
children属性:{ path: 'other', component: OtherComponent, children: [ { path: '', redirectTo: 'pasta', pathMatch: 'full' }, { path: 'pasta', component: NestedPastaComponent }, { path: 'calzone', component: NestedCalzoneComponent } ] } -
文件 app.component.ts 用于定义 app 组件和意大利香肠组件(非嵌套)。它还包含非嵌套路由器链接和路由器出口,非嵌套组件被注入到其中。
-
app.other-component.ts 文件用于定义其他组件(非嵌套)以及 pasta 和 calzone 嵌套组件。另一个组件包含嵌套路由器链路和路由器出口,嵌套组件注入其中。
-
文件 app.module.ts 声明了所有组件,以便可以在 app 模块中访问它们。它还导入我们在 app.routing.module.ts 中设置的 AppRoutingModule。
-
文件 styles.css 声明了一些用于链接和选项卡效果的样式(我承认很糟糕)。注意如何在路由器链路上设置
routerLinkActive样式来突出显示当前活动的链路(这适用于嵌套和非嵌套链路)。路由器链接:<a [routerLink]="['pepperoni']" routerLinkActive="router-link-active">Pepperoni Pizza</a>活动路由器链接的样式:
.router-link-active { background-color: #C14242; }
路由配置
Angular 应用通过使用路由器服务的单个实例进行路由。导航时,路由器会尝试解析新位置的路由。要解析路由,必须为路由器配置路由。路由被配置为一组route对象。每个route对象都需要一个路径(来解析它),通常还需要一个组件,该组件将显示在路由器出口中,用于解析路由。路由对象还可以有更多的属性(稍后将详细介绍)。
您可以配置重定向到其他路径的路由路径。例如,以下代码将空路由重定向到意大利香肠路由。在空 URL 的情况下,我们还需要添加pathMatch: 'full'属性,这样 Angular 就知道它应该完全匹配空字符串,而不是部分匹配空字符串:
const routes: Routes = [
{
path: '',
redirectTo: '/pepperoni',
pathMatch: 'full'
},
...
];
您还可以使用路径**添加一个总括路径,如果该 URL 与任何其他路径都不匹配,它将与该路径匹配:
const routes:Routes = [
...
{path: '**', component: CatchAllComponent}
];
当您配置您的路由时,您使用一组route对象来配置它们。每个route对象可以有一个data属性,该属性包含其他属性,这些属性可以在以后由该路由的目标组件提取。
以下代码设置了一个包含数据的路由,包括一条“未找到”路径的消息:
{ path: '500', component: ErrorPageComponent, data: {message: 'Unexpected Server Error'}}
下面的代码访问该数据,因此它可以用于显示消息。要么这样:
this.errorMessage = this.snapshot.data['message'];
或者这个:
this.route.data.subscribe(
(data: Data) => { this.errorMessage = data['message']; }
);
请注意,这允许您将同一组件用于不同的目的和不同的数据。例如,您还可以为路径 401 设置一个路由,该路由将重用错误页面组件,但这一次带有消息“未授权”
路由路径参数
您可以将数据参数作为 URL 路径的一部分传递给路由中的组件,例如 customer/123。
当您为接收参数的组件编写代码时,有两种不同的实现可供选择:
-
您可以从路由
snapshot(路由的一次性快照)中读取参数。当您只路由到父组件中的子组件一次并且该参数从不改变时,这很有用:constructor(route: ActivatedRoute) { this.customerId = route.snapshot.paramMap.get('id'); } -
您可以通过订阅可观察的参数图来读取参数。当您路由到父组件中的子组件时,这是很有用的,并且当某些事情发生变化时,子组件可能会重新路由(被传递一个新参数:
constructor(route: ActivatedRoute) { route.paramMap.subscribe( params => this.customerId = params.get('id') ); }
路由查询参数:示例
您可以使用查询字符串将数据参数传递给路由中的组件,例如,customer?id=123。
这与路径参数的工作方式类似。当您为接收参数的组件编写代码时,有两种不同的实现可供选择:
-
您可以从路由快照中读取查询参数,这在路由到父组件中的子组件时非常有用,并且此参数永远不会改变:
constructor(route: ActivatedRoute) { this.customerId = route.snapshot.queryParams['id']; } -
您可以通过订阅一个可观察的查询参数映射来读取参数——这在路由到父组件中的子组件时非常有用,并且子组件可以在发生变化时重新路由:
constructor(route: ActivatedRoute) { route.queryParams.subscribe( params => this.customerId = params.get('id') ); }
本例是一个在顶部显示客户列表的组件,下面是所选客户的详细信息,如图 15-3 所示。这将是示例路由器-ex300。
-
使用 CLI 构建应用:使用以下命令:
ng new router-ex300 --routing --inline-template --inline-style -
开始
ng serve:使用以下代码:cd router-ex300 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
创建客户类:创建客户
-
创建
CustomerService类:创建 customerService.ts:import { Injectable } from '@angular/core'; import { Customer } from './customer'; @Injectable() export class CustomerService { private _customers: Array<Customer> = [ new Customer(1, 'Mark', 'Atlanta', 'GA', 12000), new Customer(2, 'Peter', 'Belvue', 'CA', 5000), new Customer(3,'Jill', 'Colombia', 'SC', 2000), new Customer(4, 'Brian', 'Augusta', 'GA', 2000) ]; get customers() { return this._customers; } getCustomerById(id: number){ for (let i=0,ii=this._customers.length;i<ii;i++){ const customer = this._customers[i]; if (customer.id == id){ return customer; } } return null; } } -
编辑 app 路由模块:编辑 app-routing.module.ts,修改为:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { DetailComponent, PleaseSelectComponent } from './app.component'; const routes: Routes = [ { pathMatch: 'full', path: '', component: PleaseSelectComponent, children: [] }, { pathMatch: 'full', path: 'detail', component: DetailComponent, children: [] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } -
编辑 app 组件:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CustomerService } from './customerService'; import { Customer } from './customer'; @Component({ selector: 'pleaseSelect', template: ` <div> <h2>Please make a selection.</h2> </div> `, styles: ['div { background-color: #FFFFFF; padding: 10px; border: 1px solid #000000 }'] }) export class PleaseSelectComponent { } @Component({ selector: 'detail', template: ` <div> <h2>Customer Detail {{id}}</h2> <p>{{customer.name}}<p> <p>{{customer.city}}, {{customer.state}}</p> <p>Balance: ${{customer.balance}}</p> </div> `, styles: ['div { background-color: #FFE4E1 }'] }) export class DetailComponent { customer: Customer; constructor( private customerService: CustomerService, private route: ActivatedRoute) { route.queryParams.subscribe( (queryParams: Object) => this.customer = customerService.getCustomerById(queryParams['id'])); } } @Component({ selector: 'app-root', template: ` <div> <h1> Customer List </h1> <p *ngFor="let customer of _customerService.customers"> <a [routerLink]="['detail']" [queryParams]="{id: customer.id}" routerLinkActive="active">{{customer.name}}</a> </p> </div> <router-outlet></router-outlet> `, styles: ['div { background-color: #faebd7 }',] }) export class AppComponent { constructor(private _customerService: CustomerService){ } } -
编辑 app 模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent, DetailComponent, PleaseSelectComponent } from './app.component'; import { CustomerService } from './customerService'; import { Customer } from './customer'; @NgModule({ declarations: [ AppComponent, DetailComponent, PleaseSelectComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [CustomerService], bootstrap: [AppComponent] }) export class AppModule { } -
编辑样式:编辑 styles.css 并将其更改为以下内容:
div { padding: 10px; border: 1px solid #000000; } h1,h2 { margin: 0px; } .active { font-weight: bold; } .active::before { content: ">>> "; } .active::after { content: " <<<"; }
图 15-3
List of customers and detail
你的应用应该工作在本地主机:4200。请注意以下几点:
-
文件 customer.ts 设置了
customer类。 -
文件 customerService.ts 是注入到 app 组件和 Detail 组件中的服务。它包含一个客户列表,以及访问客户数据的方法。
-
app-routing.module.ts 文件为 Please Select 组件和 Detail 组件设置路由。
-
app.component.ts 文件设置组件。请注意它如何使用不同的语法来指定路由器链接的查询参数:
<a [routerLink]="['detail']" [queryParams]="{id: customer.id}" routerLinkActive="active">{{customer.name}}</a> -
文件 app.module.ts 声明所使用的组件,导入 app routing 模块,并将
CustomerService类设置为 Detail 和 app 组件中客户依赖注入的提供者。 -
文件“styles.css”用于设置 h1、h2 和 div 标签的通用样式。
路由器命令式导航:示例
到目前为止,我们已经编写了代码,为用户提供了单击链接进行导航的能力。命令式导航是不同的。这不是生成链接;它只是告诉路由器去某个地方,在你的代码中执行导航。导航是一个异步事件;它不会锁定代码,直到完成。本节讨论的命令式导航方法在完成时返回一个Promise对象,这是一个对成功或失败的回调。这两种方法是Router.navigate和Router.navigateByUrl。要使用命令式导航,首先需要使用构造函数注入将路由器注入到您的类中。
Router.navigate:根据一组命令或路由元素,相对(相对于当前路由)或绝对导航到一个组件。它返回一个承诺,在导航完成时解决。它使用先前在路由器链路 DSL 格式中指定的链路 DSL。基本上和点击路由器链接一样。Router.navigateByUrl:这导航到一个完整的绝对 URL 字符串。它返回一个承诺,在导航完成时解决。通常更喜欢用navigate导航,而不是这种方法,因为 URL 更脆弱。如果给定的 URL 以/开头,路由器将绝对导航。如果给定的 URL 不是以/开头,路由器将相对于该组件导航。
两种导航方法都返回一个承诺,这使用户能够添加两个回调方法来处理导航结果:第一个用于成功处理程序,第二个用于错误处理程序。下面的例子中就有这样的例子。两种导航方法都能够接受一个NavigationExtras对象的附加参数。此对象允许您将附加信息传递给路由器,以进一步指定所需的路由。
该组件允许用户在组件之间导航,也可以返回,如图 15-4 所示。它还记录导航完成的时间。这是示例路由器-ex400。
图 15-4
Navigating between components
让我们来看看这个例子:
-
使用 CLI 构建应用:使用以下命令:
ng new router-ex400 --routing --inline-template --inline-style -
开始
ng serve:使用以下代码:cd router-ex400 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑路由模块:编辑 app-routing.module.ts,修改为:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AppComponent, Component1, Component2 } from './app.component'; const routes: Routes = [ { path: 'component1', component: Component1 }, { path: 'component2', component: Component2 }, { path: '**', component: Component1 }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } -
编辑
Component类:编辑 app.component.ts,修改为:import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { Location } from '@angular/common'; @Component({ selector: 'component1', template: ` <h1> {{title}} </h1> <router-outlet></router-outlet> `, styles: [] }) export class Component1 { title = 'Component 1'; } @Component({ selector: 'component2', template: ` <h1> {{title}} </h1> <router-outlet></router-outlet> `, styles: [] }) export class Component2 { title = 'Component 2'; } @Component({ selector: 'app-root', template: ` <h1> {{title}} </h1> <button (click)="component1()">Component 1</button> <button (click)="component2()">Component 1</button> <button (click)="back()"><- Back</button> <router-outlet></router-outlet> `, styles: [] }) export class AppComponent { title = 'App Component'; constructor(private router: Router, private location: Location){} component1(){ this.router.navigate(['component1']).then(result => { console.log("navigation result: " + result)}); } component2(){ this.router.navigateByUrl("/component2"); } back(){ this.location.back(); } } -
编辑模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent, Component1, Component2 } from './app.component'; @NgModule({ declarations: [ AppComponent, Component1, Component2 ], imports: [ BrowserModule, AppRoutingModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
你的应用应该工作在本地主机:4200。请注意以下几点:
- app 组件注入路由器和位置。
- 它包含当用户单击按钮时强制导航的代码。
- 它包含导航完成时触发的回调。
- 它还在后退按钮的位置包含代码。
路由器:提取数据
您可以从注入到您的类中的Router对象中提取表 15-3 中显示的信息。
表 15-3
Extracting Data from Router
您通常使用配置对象来定义路由器路由,这一点不会改变。然而,您可以随时使用resetConfig方法将不同的配置对象重新加载到路由器中。如果您想从服务器或其他数据源加载路由,这将非常有用。
路由守卫:示例
路由使用户能够在应用中导航。有时,用户需要做一些事情才能被允许访问应用的某个部分,例如,登录。路由守卫可用于控制对某些路由的访问。
有两种主要类型的路由守卫:
- 用户能导航到一条路由吗?在这堂课上,你可以注入路由器。如果不允许用户导航到某个路由,这有助于将用户导航到另一个资源。
- 用户可以离开某条路由吗?对于提示保存更改非常有用。
该示例组件显示菜单链接。有些链接只有在用户登录后才能使用。这将是示例路由器-ex500:
-
使用 CLI 构建应用:使用以下命令:
ng new router-ex500 --routing --inline-template --inline-style -
开始
ng serve:使用以下代码:cd router-ex500 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
创建激活服务:创建 activate.service.ts 并将其更改为:
import { Injectable } from '@angular/core'; import { UserService } from './user.service'; import { CanActivate } from '@angular/router'; @Injectable() export class ActivateService implements CanActivate{ constructor(private _userService: UserService){} canActivate() { return this._userService.authenticated; } } -
编辑路由模块:编辑 app.routing.module.ts,修改为:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AuthenticatedComponent, NonAuthenticatedComponent} from './app.component'; import { UserService } from './user.service'; import { ActivateService } from './activate.service'; const routes: Routes = [ { path: 'authenticated', component: AuthenticatedComponent, canActivate: [ ActivateService ] }, { path: '**', component: NonAuthenticatedComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule], providers: [UserService, ActivateService] }) export class AppRoutingModule { } -
编辑组件:编辑 app.component.ts,修改为:
import { Component, ViewChild } from '@angular/core'; import { UserService } from './user.service'; @Component({ selector: 'non-authenticated-component', template: ` <div> <h2>Non-authenticated</h2> <p>This component can be accessed without authentication.</p> </div> `, styles: [] }) export class NonAuthenticatedComponent { } @Component({ selector: 'authenticated-component', template: ` <div> <h2>Authenticated</h2> <p>This component cannot be accessed without authentication.</p> </div> `, styles: [] }) export class AuthenticatedComponent { } @Component({ selector: 'app-root', template: ` <span *ngIf="!_userService.authenticated"> User:<input type="input" #name /> Password:<input type="input" #password /> <input type="button" (click)="login()" value="Login" />" </span> <hr/> Authenticated:{{_userService.authenticated}} <hr/> <a [routerLink]="['non-authenticated']">Non-Authenticated</a> <a [routerLink]="['authenticated']">Authenticated</a> <router-outlet></router-outlet> `, styles: [] }) export class AppComponent { loggedIn: boolean = false; @ViewChild('name') name; @ViewChild('password') password; constructor(private _userService: UserService){} login(){ this._userService.authenticate( this.name.nativeElement.value, this.password.nativeElement.value); } } -
编辑 app 模块:编辑 app.module.ts,修改为:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent, AuthenticatedComponent, NonAuthenticatedComponent } from './app.component'; import { UserService } from './user.service'; @NgModule({ declarations: [ AppComponent, AuthenticatedComponent, NonAuthenticatedComponent ], imports: [ BrowserModule, AppRoutingModule ], providers: [UserService], bootstrap: [AppComponent] }) export class AppModule { } -
创建用户服务:创建 user.service.ts 并将其更改为:
import { Injectable } from '@angular/core'; @Injectable() export class UserService { private _authenticated: boolean = false; public get authenticated(): boolean{ return this._authenticated; } public set authenticated(value: boolean){ this._authenticated = value; } public authenticate(name, password){ if ((name === 'user') && (password === 'password')){ this._authenticated = true; } } }
你的应用应该工作在本地主机:4200。请注意以下几点:
- 服务 activate.service.ts 是一个路由保护,它允许或禁止路由被激活。调用
canActivate方法:true允许激活,false不允许激活。该服务被注入到路由模块中,因此可以在路由配置中使用。 - 服务 user.service.ts 是一种跟踪用户状态的服务——无论他们是否经过身份验证。这个服务被注入到服务 activate.service.ts 和 app 组件中。
摘要
希望这一章会对你非常有用,你会用它来写你的 Angular 应用的路由。请记住,您的路由可能会变得非常复杂,可能会有多个路由器模块和路由器插座。您可以尝试将所有路由放在一个路由模块中,也可以尝试将其分散到多个模块中。一个路由模块可能更简单,但当开发人员不断地更改这个文件时,这可能会导致更多的合并冲突。请记住,您可以使用 route guards 来加强安全性,允许或阻止对各种组件的访问。
下一章将讨论反应式编程,以及 Angular 应用如何使用新技术来处理应用中的数据流。
十六、观察者、反应式编程和 RxJS
Reactive Extensions for JavaScript(RxJS)是一个 Reactive streams 库,允许您处理异步数据流,它包含在 Angular。该项目是微软与开源开发者社区合作开发的。
本章的目的是介绍 RxJS 的基本概念,并涵盖该库的一些功能。我将在另一章介绍 RxJS 和 Angular 的结合使用。
反应式编程是一种专注于数据流和变化的编程范式。它允许您轻松地表达静态或动态数据流,并且执行模型将通过数据流自动传播更改。Reactive Extensions 代码几乎可以在每个计算平台上使用,不仅仅是 JavaScript,它的目的是将 Reactive 编程的能力带到计算平台上。
异步数据流
RxJS 库使用 JavaScript 中的可观察集合组成异步和基于事件的反应式程序。什么是异步数据流?让我们来分解一下:
- 异步:在 JavaScript 中,这意味着我们可以调用一个函数并注册一个回调函数,以便在结果可用时得到通知,这样我们就可以继续执行并避免网页无响应。这用于 AJAX 调用、DOM 事件、承诺、web workers 和 WebSockets。
- 数据:JavaScript 数据类型形式的原始信息,比如数字、字符串、对象(数组、集合、映射)。
- 流:一段时间内可用的数据序列。例如,与 to 数组相比,您不需要所有的信息都存在就可以开始使用它们。
异步数据流的例子包括您正在观看的内容:
- 股票报价
- 小鸟叫声
- 计算机事件,例如鼠标点击
- Web 服务请求
可观察序列(可观察的)
在 RxJS 中,您使用可观察序列来表示异步数据流,也称为可观察序列。你可以使用 observables 来观察股票行情或鼠标点击。可观测量是灵活的,可以与推或拉模式一起使用:
- Push:当使用 push 模式时,我们订阅源数据流,并在新数据可用(发出)时立即对其做出反应。你可以听一个流,然后做出相应的反应。
- Pull:当使用 pull 模式时,我们使用相同的操作,但是是同步的。使用数组、生成器或可迭代对象时会发生这种情况。
因为可观测量是数据流,所以您可以使用由可观测量类型实现的操作符来查询它们。以下是使用可观察运算符可以做的许多事情中的一些:
- 过滤掉你不拥有的股票的变化
- 聚合—在前五秒钟内获得所有输入内容
- 对多个事件执行基于时间的操作
观察者:示例
如果说可观察物是可以观察的东西,那么观察者就是观察它们的东西,如图 16-1 所示。
图 16-1
Observables and observers
观察者是可以对事件或正在发生的事情做出反应的类。要做出响应,他们必须实现以下方法:
onNext:当被观察对象发出一个物品时,被观察对象调用这个方法。onNext将被观测者发射的物品作为参数。onError:当一个可观察对象未能创建预期的数据或遇到其他错误时,调用这个方法,停止可观察对象。被观察对象不会再呼叫onNext或onCompleted。onError方法将导致错误的原因作为其参数。onCompleted:如果没有任何错误,observable 在最后一次调用onNext后调用该方法。
在这个例子中,我们将用两个事件创建一个可观察对象,然后观察它。图 16-2 显示了您将看到的控制台。这将是示例 rxjs-ex100。
图 16-2
Console showing events
让我们来看看这个例子:
-
使用 CLI 构建应用:使用以下命令:
ng new rxjs-ex100 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd rxjs-ex100 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑类:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import * as Rx from 'rxjs'; @Component({ selector: 'app-root', template: ` `, styles: [] }) export class AppComponent { constructor(){ const array: Array<string> = ['event1', 'event2']; const observable: Rx.Observable<string[]> = Rx.Observable.of(array); const subscription: Rx.Subscription = observable.subscribe( // Observer function (x) { console.log('Next: ' + x); }, function (err) { console.log('Error: ' + err); }, function () { console.log('Completed'); } ); } }
你的应用应该工作在本地主机:4200。在浏览器中打开开发者工具,重新加载页面并查看控制台输出。请注意,app 组件执行以下操作:
- 创建一个数组。
- 从数组中创建一个可观察值。
- 订阅是从对可观察对象的订阅中创建的。该订阅实现了处理事件的观察者代码。
捐款
订阅就像是可观察对象和观察者之间的联系。图 16-3 说明了这种关系。
图 16-3
A subscription connects observable and observer
您使用订阅将可观察对象和观察者联系在一起:
const subscription: Rx.Subscription = observable.subscribe(
// Observer
function (x) {
console.log('Next: ' + x);
},
function (err) {
console.log('Error: ' + err);
},
function () {
console.log('Completed');
}
);
要取消可观察对象和观察者的链接,请在订阅中调用方法dispose:
subscription.dispose();
Observables, Observers, And Javascript ES7
ES7 是一个即将提出的 JavaScript 标准,将包含Object.observe,它将允许观察者接收一个按时间排序的变化记录序列,这些记录描述了一组被观察对象发生的一组变化。这和 RxJS 做的事情差不多,只是原生在浏览器里。它已经在一些浏览器中实现了——比如 Chrome 36.ss。
运算符:示例
操作员执行各种任务。他们的目的是为了更方便地观察一个可观察的现象。操作员执行以下操作:
- 创造可观的
- 组合可观测量
- 过滤可观测量
- 处理错误
- 执行实用程序
大多数操作符操作一个可观察值并返回一个可观察值。这允许您在一个链中一个接一个地应用操作符。链中的每一个算子都会修改前一个算子的运算所产生的可观测值。
在这个例子中,我们将创建一个带有两个事件的可观察对象,然后我们将观察它。如果您查看控制台,您会看到事件日志。这将是示例 rxjs-ex200。
让我们来看看这个例子:
-
使用 CLI 构建应用:使用以下命令:
ng new rxjs-ex200 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd rxjs-ex200 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑类:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import * as Rx from 'rxjs'; @Component({ selector: 'app-root', template: ` `, styles: [] }) export class AppComponent { constructor(){ const observable: Rx.Observable<number> = Rx.Observable.range(0,100); const subscription: Rx.Subscription = observable.subscribe( // Observer val => { console.log(`Next: ${val}`) }, err => { console.log(`Error: ${err}`) }, () => { console.log(`Completed`) } ); } }
你的应用应该工作在本地主机:4200。在浏览器中打开开发者工具,重新加载页面并查看控制台输出。请注意以下几点:
range操作符创建一系列事件。- 观察者使用箭头函数来处理事件。
产生可观测量的算子
有许多运算符仅用于创建可观测量。本节将讨论其中的几个。
从
该运算符从发出多个值的其他对象创建一个可观察对象。下面的代码从发出两个值的数组中创建一个可观察对象:
const array: Array<string> = ['event1', 'event2'];
const observable: Rx.Observable<string> = Rx.Observable.from(array);
间隔
interval创建一个在每个周期后发出一个值的可观察值,例如 0,1,2,3,4。图 16-4 显示了每 1/2 秒(500 毫秒)发射一个可观测值。
图 16-4
Observable emitting a value every half second
代码如下:
const observable: Rx.Observable<number> = Rx.Observable.interval(500);
var observable: Rx.Observable<number> = new Rx.Observable.interval(500);
(刚刚)的
将一个物品转换成一个只发射该物品的可观察物。下面的代码创建一个只发出一次 500 的可观察对象:
const observable: Rx.Observable<number> = Rx.Observable.of(500);
范围
创建一个发出整数范围的可观察对象。下面的代码创建一个发出 1 到 100 的可观察对象:
const observable: Rx.Observable<number> = Rx.Observable.range(0,100);
重复
创建一个可观察对象,该对象发出给定元素的特定次数的重复。下面的代码创建了一个发出1 2 3 1 2 3 1 2 3 1 2 3的可观察对象:
const observable: Rx.Observable<number> = Rx.Observable.range(1,3).repeat(4);
计时器
timer创建一个可观察对象,在到期时间过后和每个周期后发出一个值:
const observable: Rx.Observable<number> = Rx.Observable.timer(2000,500);
转换由可观察对象发出的项目的运算符
您已经看到了如何创建发出值的可观测量。现在让我们看看如何修改这些值。
缓冲器
buffer是一个操作符,它周期性地将可观察对象中的项目聚集成束并发射这些束,而不是一次发射一个项目。下面的代码创建一个每隔 100 毫秒发出一个值的可观察对象。然后,它每隔 5000 毫秒将发射捆绑起来:
const observable: Rx.Observable<any> = Rx.Observable
.timer(0,100)
.buffer( Rx.Observable.timer(0, 5000) );
图 16-5 显示了结果。
图 16-5
Gathering observables into bundles
地图
map是一种算子,常用于通过对每一项应用函数来变换一个可观察对象所发出的项。下面的代码只是在发出的值周围放了一个管道,结果如图 16-6 所示。
图 16-6
Putting pipes around emitted values
const observable: Rx.Observable<string> = Rx.Observable.range(0,100)
.map((val) => '|' + val + '|' );
扫描
scan是一个运算符,用于将一个函数顺序应用于一个可观察对象发出的每一项,并发出每一个连续值。就像map一样,只是第一个函数调用的结果被输入到第二个函数调用中,以此类推。以下代码的结果如图 16-7 所示:
图 16-7
Emitting values of functions applied
const observable: Rx.Observable<number> = Rx.Observable.range(1,5)
.scan((val) => { val++; return val * val } );
过滤由可观察对象发出的项目的运算符
你不需要什么都看。有时候你只需要看某些东西。
去抖:示例
debounce是用来保证观察者在一定时间内只发射一个物品的操作符。这在观察 UI 元素时很有用——例如,如果您有一个filter框,并且您不希望它响应太快,并且在多个请求中超越自己。debounce将阻止网络和计算机因过多的搜索请求而超载。
这将是示例 rxjs-ex300,其结果如图 16-8 所示。
图 16-8
Emitting only one item
这个例子有一个搜索框,它使用debounce和distinctUntilChanged方法过滤用户的输入:
-
使用 CLI 构建应用:使用以下命令:
ng new rxjs-ex300 --inline-template --inline-style -
开始
ng serve:使用以下代码:cd rxjs-ex300 ng serve -
打开应用:打开 web 浏览器并导航到 localhost:4200。你应该看到“欢迎使用 app!”
-
编辑类:编辑 app.component.ts,修改为:
import { Component } from '@angular/core'; import * as Rx from 'rxjs'; @Component({ selector: 'app-root', template: ` Search: <input type="text" (keyup)="onChange($event.target.value)"/> <div *ngFor="let log of _logs">Search: {{log}}</div> `, styles: [] }) export class AppComponent { _searchText: string; _searchSubject: Rx.Subject<string>; _logs: Array<string> = []; constructor() { // Create new Subject. this._searchSubject = new Rx.Subject<string>(); // Set the Subject up to subscribe to events and filter them by // debounce events and ensure they are distinct. this._searchSubject .debounceTime(300) .distinctUntilChanged() .subscribe( // Handle event. Log it. searchText => this._logs.push(searchText) ); } public onChange(searchText: string) { // Emit an event to the Subject. this._searchSubject.next(searchText); } }
你的应用应该工作在本地主机:4200。请注意以下几点:
- 构造函数设置 Rxjs 主题
_searchSubject来订阅事件并过滤它们。过滤后,每个事件都会添加到日志中,显示在组件中。主体是既能充当观察者又能充当被观察者的客体。因为它是观察者,所以它可以订阅一个或多个可观察的,因为它是可观察的,所以它可以通过重新发射它们来穿过它所观察的项目,它也可以发射新的项目。 - 当用户在搜索框中键入一个键时,触发
onChange方法。该方法中的代码向_searchSubjectRxjs 主题发送一个字符串事件。
明显的
distinct是用于抑制重复项发射的运算符。以下示例每 1/2 秒生成一个新值。然后,我们使用 map 将其转换为字符串“不变值”。然后我们添加distinct来抑制重复值。因此,我们只能发出一个值:
const observable: Rx.Observable<string> = Rx.Observable.interval(500)
.map((val) => 'unchanging value').distinct();
过滤器
filter是一个运算符,用于从一个可观察对象中仅发出第一个项目,或满足某个条件的第一个项目。以下代码每 1/2 秒生成一个新值,然后过滤掉任何不能被 7 整除的值:
const observable: Rx.Observable<number> = Rx.Observable.range(0,100)
.filter((val) => val % 7 === 0);
结果如图 16-9 所示。
图 16-9
Generating a new value and filtering
拿
take是一个运算符,用于只发射一个可观察对象发射的前 n 个项目。以下代码发出从 0 到 100 的新值,但只取前三个值:
const observable: Rx.Observable<number> = Rx.Observable.range(0,100)
.take(3);
结合其他可观测量的算子
表 16-1 列出了组合其他运算符的运算符。
表 16-1
Operators That Combine Other Operators
| 操作员 | 描述 | | :-- | :-- | | `And` / `Then` / `When` | 使用模式和计划中介组合由多个可观察对象发出的项目 | | `CombineLatest` | 使用指定的函数组合每个可观察对象发出的最新项目,并根据函数的结果发出项目 | | `Join` | 当一个可观察物的物品在由另一个可观察物发射的物品定义的时间范围内发射时,组合由两个可观察物发射的物品 | | `Merge` | 通过合并它们发出的光,把几个可观测的东西结合成一个 | | `StartWith` | 在从源可观察物发射物品之前发射特定的物品序列 | | `Switch` | 将发出可观察物的可观察物转换为发出由最近发出的那些可观察物发出的项目的单个可观察物。 | | `Zip` | 通过一个函数将多个可观测值的发射组合起来,并根据函数的结果为每个组合发射单个项目。 |分享
share操作符允许您将一个订阅的实例共享给一个或多个观察者。share当观察器的数量从 0 变到 1 时创建一个订阅,然后与所有后续的观察器共享该订阅,直到观察器的数量返回到 0,此时该订阅被释放。如果你想从多个地方观看同一个东西,这很有用。图 16-10 显示了一个例子,图 16-11 显示了第二个屏幕产生的控制台日志。
图 16-11
Console logs produced by the share operator
图 16-10
The share operator in action
摘要
这一重要章节介绍了异步数据流和 RxJs。我希望您已经完成了练习,因为很快我们将使用事件,这是一种异步数据流的形式,您将在应用中观察到。在处理 Angular 事件时,我们将使用主体、可观察对象和观察者,并且我们将使用本章中介绍的操作符。
下一章将详细介绍 RxJs 与 Angular 的结合使用。