Angular2 官网文章阅读指南

本系列是根据angular.io的Docs栏目按顺序翻译。首先目的并不是为了翻译工作,主要是针对自己Angular2学习并到过程做纪录;其次每一篇并非按官方直译,都是结合自身理解做叙述。

基础

开发指南

如有您对Angular2.x感兴趣,也可加入QQ群:485843913。

Angular2 动态创建组件

在ng1.x时我写过Angular 动态创建 directive;而在Angular2应用是由一个庞大的组件树来构建业务,一般而言我们是通过 template 模板来动态加载组件。

一、原因

我依然以1.x所遇到的项目实现为原型。

我们所认知的编辑器是为创建一个富HTML,而为了让用户更好的体验(总不能让客户写HTML语言吧)所产生的按表单填写数据最终生成有效的HTML。那么自然我们不可能只放一种像百度编辑器就完事,你让用创建一个轮播广告几乎是不可能;所以需要有很多组件,比如:商品列表、富文本、图片广告、轮播广告等等,而每一种组件又不可能是统一界面。

二、创建组件

首先需要先创建了两个组件 richconfig

src/components/rich.ts

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

@Component({
  selector: 'rich',
  template: `
    <p>Rich Content</p>
  `
})

export class RichComponent { }

src/components/config.ts

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

@Component({
  selector: 'config',
  template: `
    <p>Config Content</p>
  `
})

export class ConfigComponent { }

三、利用模板动态加载组件

template 模板里面加载组件是最常用加载方式,而且非常简单,只需要存放相应的 selector 模板引擎会自己查找并编译。

//our root app component
import {Component} from '@angular/core'

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

@Component({
  selector: 'template-load',
  providers: [],
  template: `
      <h1>方式一:通过 template 动态加载</h1>
      <config></config>
      <rich></rich>
  `,
  directives: [ ConfigComponent, RichComponent ]
})
export class TemplateLoadComponent {
  constructor() {

  }
}

这样的方式很明显的一点是,template的结构将决定组件在组件树当中所处的位置。假如我们要把 configrich 组件对换位置,不得不对 template 重新编写,这样更别说让用户来拖拽来调整组件显示位置这么高级的办法了。

四、利用 DynamicComponentLoader 实现动态加载

DynamicComponentLoader 支持两种不同的加载方式来动态创建组件;但在这之前我们需要先做点什么。

首先,我示例里是根据按钮点击事件创建相应的组件,所以我们需要先创建一个组件映射表。

import {ConfigComponent} from 'src/components/config'
import {RichComponent} from 'src/components/rich'

export class DynamicLoadComponent {
  compMaps: {[key: string]: any} = {},
  constructor() {
    this.compMaps.config = ConfigComponent;
    this.compMaps.rich = RichComponent;
  }
}

这样我们可以模板里面定义添加事件,并传递相应的key值,下面是分别定义两个按钮用来创建 configrich 组件,以及 container1 元素用于放置组件位置。

<button type="button" (click)="add('config')">Add Config</button>
<button type="button" (click)="add('rich')">Add Rich</button>
<div id="container1"></div>

准备工作完成后,我们分别来看一下二种方式的差异:

1、loadAsRoot

loadAsRoot(type: Type, overrideSelectorOrNode: string|any, injector: Injector, onDispose?: () => void, projectableNodes?: any[][]) : Promise<ComponentRef<any>>

loadAsRoot 方法会创建一个 type 组件实例,然后放到 overrideSelectorOrNode 放处位置的第一个元素下面,不管你动态加载几次永远都只有一份组件实例

constructor(public dcl:DynamicComponentLoader, public _injector:Injector) {
}

add(comp) {
  this.dcl.loadAsRoot(this.compMaps[comp], "#container1", this._injector);
}

#container1 位置可以在整个文档的任意位置,只要该ID是存在。

最终的DOM结构像这样:

<div id="container1">
   <p>Config Content</p>
</div>

loadAsRoot 方法很好理解,而且很霸道,始终只会创建一个组件实例。

2、loadNextToLocation

loadNextToLocation(type: Type, location: ViewContainerRef, providers?: ResolvedReflectiveProvider[], projectableNodes?: any[][]) : Promise<ComponentRef<any>>

同样也是创建一个 type 组件实例,并插入到指定 location 位置的后面,ViewContainerRef 是一个视图容器,所以通过 loadNextToLocation 方法加载的组件,都会放入容器里面。

首先需要在模板中定义一个本地模板变量 container2,并在组件类中获取该变量。

@Component({
   template: `
    <div #container2></div>
   `
})

export class DynamicLoadComponent {
  @ViewChild('container2', {read: ViewContainerRef}) target;
}

add 方法也非常简单:

  add(type) {
    this.dcl.loadNextToLocation(this.compMaps[comp], this.target)
  }

最终的DOM结构像这样:

<config>
  <p>Config Content</p>
</config>
<rich>
  <p>Rich Content</p>
</rich>

3、传递数据

两种方法返回的结果都是一个 Promise 类型,并传递一个 ComponentRef 的新创建组件对象。


this.dcl.loadNextToLocation(this.compMaps[comp], this.target).then(function(cmpRef) { // doing })

可以通过 cmpRef 参数进行数据传递,比如:

cmpRef.instance.someProperty = someData;

五、总结

文章中只是做个抛砖引玉,实际项目应该做更深一层的抽象,Angular2是以组件为出发点,不能有太多了依赖。但是,文章至少已经解决最核心的问题了。文章并没有给出完整代码,如您感兴趣见Plunker

DynamicComponentLoader 从变更纪录来看非常频繁,在前两个BETA版本中都对其做大量的变动;但不管怎么变,是越来越精简。

Angular2 路由

Angular组件路由使一个视图至另一个视图的导航任务组件。

一、概要

浏览器是一个最熟悉的导航模型应用了,输入一个URL地址后浏览器导航相应URL页面,点击网页上的链接导航至另一个新的页面,浏览器的“后退”或“前近”可以导航至我们浏览器的历史页面。

Angular组件路由(也叫路由)就是借用这个模型,可以理解为通过浏览器的网址来生成客户端视图,并通过一些可选参数指定当前显示哪些视图。当然也可以把路由绑定至一个视图组件里,当点击链接时,导航至适合的视图。并且记录在浏览器历史列表,以便浏览器的前进或后退正常工作。

二、基础知识

先从组件路由基本概念开始,慢慢深入。

1、<base href>

大部分带有路由应用程序都会在 index.html<head> 加上 <base> 元素,用来设置基准URL(也就是应用程序的根路径)。

<base href="/">

2、导入 Router

Angular组件路由是个可选组件,所以并不包括在 core 里,需要我们单独引用 @angular/router 模块。

import { ROUTER_PROVIDERS } from '@angular/router';

3、配置

当浏览器URL改变时,路由会查找相应的 RouteDefinition 来确定组件的显示。

一般路由配置都在宿主组件内,(在Angular1.x经常会看到一些项目所有路由都在一个js文件当中)而且非常简单,通过 @Routers 装饰注册路由。

@Component({ ... })
@Routes([
  {path: '/crisis-center', component: CrisisListComponent},
  {path: '/heroes',        component: HeroListComponent},
  {path: '/hero/:id',      component: HeroDetailComponent}
])
export class AppComponent  implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.navigate(['/crisis-center']);
  }
}

有好几种 RouteDefinition 路由定义方式,大部分都是用URL地址来映射组件。

其中第三条路由 :id 是一个路由参数,比如URL地址 /hero/42 中 “42” 值就是 id 参数值。

4、Router Outlet

现在我们已经知道如何配置路由,当浏览器请求URL地址 /heroes 时,路由会查找 RouteDefinition 匹配到 HeroListComponent,那么匹配到的组件放在哪呢?我们就需要在当前组件模板里加入 RouterOutlet

<router-outlet></router-outlet>

我把它叫成点位符更贴切一点。

5、路由链接

知道如何配置和显示,那接下来就是如何导航,正常我们是通过浏览器的地址栏输入网站,可大部分是用户去触发某个链接进行导航的。

可以在一个锚标签添加一个 RouterLink 指令,模板表达式绑定的是一个字符串数组类型的网址。第一眼一定会很奇怪为什么是数组?这是因为还可能需要URL可选参数,虽然还是很奇怪。

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

6、总结一下

先通过 @RouterConfig 配置组件 AppComponent 的路由,再利用 RouterOutlet 来指定命中路由在哪显示,最后使用 RouterLinks 来处理用户点击链接的导航。

现在 AppComoponent 已经变成了一个路由组件。

路由 意思
Router 类定义了当前活动URL地址环境,可以用它在组件类里进行导航管理。
@RouteConfig 路由装饰器,接收一个 RouteDefinitions 路由定义对象数组。
RouteDefinition 路由定义对象。
RouterOutlet 匹配到组件的显示位置。
RouterLink 用于模板的链接导航。
Link Parameters Array 链接参数数组,可以用在模板的 RouterLink 或 Router.navigate 方法。
Routing Component 配置路由的组件。

三、一个示例

官网用了蛮长的来说明这个示例是怎么回事,大概是这样的:有两个列表分别 “Crisis Center”(记A) 和 “Heroes”(记B),点击列表的项时会进入编辑状态。这在之前二者都是一样的,不同在于编辑页面。

对于A有保存和取消按钮,不管点哪个按钮最后都会回到A列表,而且如果你改动 Name 后点击浏览器后退时会有一个对话框告诉你是不是要放弃编辑;反之B编辑时就很简单只有一个返回按钮,因为你每一次变动都会自动保存。

一个很长的动画,有耐心的可以先看一看效果。

router-anim

那么好,接下来会按里程碑式一步步说说是怎么实现的。

四、里程碑一:开始Router入门

很简单,通过路由来控制两个视图显示。

router-1-anim

1、设置 <base href>

组件路由使用浏览器 history.pushState 来导航,这样的URL地址看起来就像跟直接从服务端URL一样,例如:localhost:3000/crisis-center

现代浏览器都支持HTML5 pushState,为了让 pushState 更好工作,还需要在 index.html 添加一个 <base href="/"> 元素,这样同样可以确保所有请求的资源前缀。

HTML5模式导航是组件路由默认风格,我们也可以用老式的哈希(#)风格,后面会谈到。

2、导入组件路由模块

前面就说过路由模块是独立模块,需要从 angular/router 模块中获取。

import { Routes, Router, ROUTER_DIRECTIVES } from '@angular/router';

3、引导程序中注册路由服务

需要在 main.ts 文件里添加启动时注册路由服务。

import { bootstrap }        from '@angular/platform-browser-dynamic';
import { ROUTER_PROVIDERS } from '@angular/router';

import { AppComponent }     from './app.component';
bootstrap(AppComponent, [
  ROUTER_PROVIDERS
]);

从路由模块中导入 ROUTER_PROVIDERS,它实现一个默认配置的项,并注入到 provider 中。

4、AppComponent组件

和往常一样 AppComponent 组件是我们根组件,模板里包括两个导航链接和一个Router Outlet,通过点击导航链接显示不同的组件。

shell-and-outlet

对应的模板:

template: `
  <h1>Component Router</h1>
  <nav>
    <a [routerLink]="['/crisis-center']">Crisis Center</a>
    <a [routerLink]="['/heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

RouterOutlet

组件模板中 <router-outlet> 标签就是点位符,告诉命中路由的组件放在哪。

RouterLink属性绑定

RouterLink 属性指令,模板表达式的右边是一个数组对象来表示路由器导航所需要的数据:

  • 下标0是固定的URL部分。
  • 下标1以后放置的是可选参数数据。
<a [routerLink]="['/heroes']">Heroes</a>

除此之外,我们还可以在组件类里面使用 Route 导航管理器进行跳转。

this.router.navigate(['/heroes']);

总结

对于组件而言是一个带有层级结构的,那么当一个组件被放置时就已经被确立其URL地址结构了;所以组件自身理应包括路由的定义。如何更好在代码里面编写路由注册代码,应该在构建项目时就要加以考虑。

路由这一课题Angular2.x一直在变动,但整体的使用上并没有太多不同。

Angular2 结构指令

一个单页应用程序(SPA)最大特征就是对DOM树的操作,是根据程序状态对整段DOM的显示和消失。

一、什么是结构指令

Angular有三种指令类型:

  1. 组件
  2. 属性指令
  3. 结构指令

组件实际是一个带有模板的指令,这是最常见的三种指令,我们的应用程序也是基于指令来构建的。

属性指令改变的是元素的外观或行为,例如内置 NgStyle 指令,可以根据绑定组件的属性值同时改变多个元素的样式。

而结构指令是对DOM的添加或移除,这会改变DOM的布局。在模板语法章节就介绍过 ngIfngSwitchngFor

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>
<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

二、NgIf 案例研究

ngIf 指令通过一个布尔类型变量来改变DOM的显示或消失。

<p *ngIf="condition">
  condition is true and ngIf is true.
</p>
<p *ngIf="!condition">
  condition is false and ngIf is false.
</p>

ngIf 指令并不是隐藏元素,使用浏览器开发工具我们可以看到,当条件为 true 时显示第一段DOM,反之显示第二段。

为什么要移除而不是隐藏?

我们可以通过设置CSS display 样式为 none 来隐藏元素,元素保留在DOM中只是看不见而已。相反,就用 ngIf

当我们隐藏一个元素,组件的行为还是继续着,像事件监听、变化检测、子组件的事件监听等依然还会在执手,哪怕我们压根看不见,很明显隐藏这一点来说最大的可能是性能问题;当然他也有好处,那就是当元素想要再一次被显示时,会非常快。

ngIf 就不同,设置为 false 时,会释放组件及其子组件的所有资源,并从DOM里移除元素;当然坏处就是,当你很快又需要该元素时,就要重新构建了。

虽然两种方法都有利弊,如果我们确认不使用的元素在短时间内不会再用那么 ngIf 最合理,反之。

接下来,我们用一个示例来说明二者的区别,通过设置css样式和 ngIf 对一个组件 heavy-loader (假设组件非常复杂)的显示或隐藏。

<div><!-- Visibility -->
  <button (click)="isVisible = !isVisible">show | hide</button>
  <heavy-loader [style.display]="isVisible ? 'inline' : 'none'" [logs]="logs"></heavy-loader>
</div>
<div><!-- NgIf -->
  <button (click)="condition = !condition">if | !if</button>
  <heavy-loader *ngIf="condition" [logs]="logs"></heavy-loader>
</div>
<h4>heavy-loader log:</h4>
<div *ngFor="let message of logs">{{message}}</div>

同时还记录组件的创建和销毁日志,这是利用内置 ngOnInitngOnDestroy 生命周期钩子来实现,效果如下:

heavy-loader-toggle

一开始在DOM都显示这两个组件,首先我们反复切换可见性,利用CSS样式由于每一次的可见性都是同一个实例,所以日志没有任何改变。

相反,当我们反复切换第二 ngIf 的可见性时,每一次都会出现创建新实例和销毁日志。

三、<template> 标签

结构指令像 ngIf,使用HTML template标签。对于非Angular应用而言 <template> 标签 display 样式值为 none,内容是不可见的文档片断;而Angular应用中,会移除所有 <template> 标签,并存储起来。

比如以下代码片断,中间的 Hip! 是用一个 <template> 包裹着。

<p>
  Hip!
</p>
<template>
  <p>
    Hip!
  </p>
</template>
<p>
  Hooray!
</p>

左边是非Angular应用,右边是Angular应用,二者的DOM区别。

template-in-out-of-a2

显然,默认情况下Angular会把 <template> 标签替换成一个空内容的 <script> 标签(虽然我本地的测试里并不会有这个空的script标签),不过呢,他可以做更多的事,比如 ngSwitch 指令在 <template> 标签中的应用:

<div [ngSwitch]="status">
  <template [ngSwitchWhen]="'in-mission'">In Mission</template>
  <template [ngSwitchWhen]="'ready'">Ready</template>
  <template ngSwitchDefault>Unknown</template>
</div>

当其中一个 ngSwitch 条件为 true 时,Angular会把标签里模板内容插入DOM当中。

四、关于指令中的 * 号

看看下面代码有什么特别的?

<div *ngIf="hero">{{hero}}</div>
<div *ngFor="let hero of heroes">{{hero}}</div>

上面指令名称有前缀星号(*),星号是一个语法糖,简化了 ngIfngFor 二者的读写,在模板引擎里最后还是会转换成 <template> 模式。

以下是 ngIf 语法糖与 <template> 的写法:

<!-- Examples (A) and (B) are the same -->
<!-- (A) *ngIf paragraph -->
<p *ngIf="condition">
  Our heroes are true!
</p>

<!-- (B) [ngIf] with template -->
<template [ngIf]="condition">
  <p>
    Our heroes are true!
  </p>
</template>

只要不傻肯定用第一种写法。值得注意的是从A扩展到B,先将段落及其内容移到 <template> 标签里面,属性绑定部分作为 <template> 标签的指令。

接下来看看 *ngFor 是如何转换:

<!-- Examples (A) and (B) are the same -->

<!-- (A) *ngFor div -->
<div *ngFor="let hero of heroes">{{ hero }}</div>

<!-- (B) ngFor with template -->
<template ngFor let-hero [ngForOf]="heroes">
  <div>{{ hero }}</div>
</template>

基本上和 ngIf 如出一辙;额外的细微是多了 ngForOf 属性绑定和 hero 本地模板变量。

五、创建结构指令

让我们创建一个 Unless 指令,类似 ngIf,只不过条件是反着的,当为 false 时才会显示内容。创建指令类似于创建组件:

  • 导入 Directive 装饰器。
  • 添加指令的CSS属性选择器标识符。
  • 指定一个公共的 Input 属性属性用于绑定(通常是指令的名字)。
  • 指令类实现。

先从以下代码开始:

import { Directive, Input } from '@angular/core';

@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
}

指令的选择器用的 [myUnless],了解CSS选择器的就很清楚,用中括号来表示某个HTML元素属性。同时命名也尽可能避免 ng 开头,这一点说过很多次,无非就是害怕冲突。

现在还需要分别定义一个 TemplateRef 用来访问模板内容,以及 ViewContainerRef 用来访问视图容器。

constructor(
  private templateRef: TemplateRef<any>,
  private viewContainer: ViewContainerRef
  ) { }

重点来了,给指令定义一个输入属性 myUnless (名称和 selector 一样),并且接收一个布尔类型,用于条件判断添加或移除模板内容。

@Input() set myUnless(condition: boolean) {
  if (!condition) {
    this.viewContainer.createEmbeddedView(this.templateRef);
  } else {
    this.viewContainer.clear();
  }
}

整体的逻辑非常简单,指令所有代码如下:

import { Directive, Input } from '@angular/core';
import { TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[myUnless]' })
export class UnlessDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
    ) { }
  @Input() set myUnless(condition: boolean) {
    if (!condition) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

现在来试试怎么运用这个指令,同 *ngIf 一样,只不过改成 *myUnless

<p *myUnless="condition">
  condition is false and myUnless is true.
</p>

<p *myUnless="!condition">
  condition is true and myUnless is false.
</p>

Angular2 管道(pipes)

每一个应用程序都是一些非常简单的任务开始:获取数据、转换数据,并把它们显示给用户。

获取数据可以是像创建一个变量那样简单,也可以像从Websocket获取数据流那么复杂。

一旦数据达到,我们可以调用 toString 直接在屏幕上显示原始的数据。但这样可能用户体验很差,几乎所有人都需要一个简单的生日日期(1988-4-15)而非原始字符串格式(Fri Apr 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time))。

我们发现,应用程序中重复着相同的转换非常多,事实上,我们想将这些转换直接在HTML模板里应用。这一过程叫做管道(pipe)。

一、使用管道

管道是将数据输入并将其转换后输出,比如把生日日期转换成更友好的日期:

import { Component } from '@angular/core'

@Component({
  selector: 'hero-birthday',
  template: `<p>The hero's birthday is {{ birthday | date }}</p>`
})
export class HeroBirthday {
  birthday = new Date(1988,3,15); // April 15, 1988
}

重点看组件模板:

<p>The hero's birthday is {{ birthday | date }}</p>

插值表达式绑定组件 birthday 属性,然后通过管道操作符(|),而操作符的右边是一个日期管道(Date pipe)函数。这便是管道的调用方式。

二、内置管道

Angular自带一堆管道,比如:DatePipeUpperCasePipeLowerCasePipeCurrencyPipePercentPipe,他们都可以在任何模板中使用;另外还可以通过API Reference 查看所有内置的管道。

三、管道参数化

一个管理可以接受任意数量的可选参数来微调输出,参数之间用冒号(:)隔开,比如: currency:'EUR'slice:1:5

现在给生日模板增加一个日期格式参数。

<p>The hero's birthday is {{ birthday | date:"yyyy-MM-dd" }} </p>

参数值可以是任何有效的模板表达式,如:字符串文本、组件属性。换句话说,我们可通过一个绑定的方式来控制我们的格式,比如通过组件类属性值来控制我们的生日日期格式。

template: `
  <p>The hero's birthday is {{ birthday | date:format }}</p>
  <button (click)="toggleFormat()">Toggle Format</button>
`

按钮点击事件是调用 toggleFormat 方法,方法是用来切换短日期和长日期。

export class HeroBirthday2 {
  birthday = new Date(1988,3,15); // April 15, 1988
  toggle = true; // start with true == shortDate

  get format()   { return this.toggle ? 'shortDate' : 'fullDate'}
  toggleFormat() { this.toggle = !this.toggle; }
}

结果如下:

date-format-toggle-anim

四、链式管道

我们可以通过若干有用的管道依次转移输出,这便叫链式管道。下面示例,我们先通过 DatePipe 管道和 UpperCasePipe 管道的组合,将生日日期先转换成短日期,然后再把结果大写化。最终显示的是:APR 15, 1988。

The chained hero's birthday is
{{ birthday | date | uppercase}}

如果我们需要给日期指定一个日期格式,可以利用括号来改变管道的执行顺序,比如:

The chained hero's birthday is
{{ ( birthday | date:'fullDate' ) | uppercase}}

五、自定义管道

ExponentialStrengthPipe 是一个计算次幂的自定义管道:

import { Pipe, PipeTransform } from '@angular/core';
/*
 * Raise the value exponentially
 * Takes an exponent argument that defaults to 1.
 * Usage:
 *   value | exponentialStrength:exponent
 * Example:
 *   {{ 2 |  exponentialStrength:10}}
 *   formats to: 1024
*/
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

这个管道有几个关键点:

  • 管道是一个使用管道元数据装饰的类。
  • 类必须实现 PipeTransform 接口 transform 方法,方法接收一个输入参数,和若干可选参数,最后返回转换后的值。
  • 管道的 transform 方法有一个可选参数 exponent
  • 我们通过 @Pipe 装饰器来告诉Angular这是一个管道类,所以还需要从Angular库中加载它。
  • @Pipe 装饰器有一个 name 属性,用来指定管道名称。名称必须是有效的JavaScript标识符,这里叫 exponentialStrength

现在演示如何在组件中调用自定义管道。

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

import { ExponentialStrengthPipe } from './exponential-strength.pipe';

@Component({
  selector: 'power-booster',
  template: `
    <h2>Power Booster</h2>
    <p>
      Super power boost: {{2 | exponentialStrength: 10}}
    </p>
  `,
  pipes: [ExponentialStrengthPipe]
})
export class PowerBooster { }

两个注意点:

  1. 自定义管道和内置管道使用方式完全相同。
  2. 必须在 @Component 装饰器的 pipes 数组里列出我们的管道。

六、Power Boost计算器

目前自定义的管道计算都是很死,现在我把他升级成一个利用 ngModel 双向绑定管道的输入和可选参数,这样我们只就可以在文本框输入数值。

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

import { ExponentialStrengthPipe } from './exponential-strength.pipe';

@Component({
  selector: 'power-boost-calculator',
  template: `
    <h2>Power Boost Calculator</h2>
    <div>Normal power: <input [(ngModel)]="power"></div>
    <div>Boost factor: <input [(ngModel)]="factor"></div>
    <p>
      Super Hero Power: {{power | exponentialStrength: factor}}
    </p>
  `,
  pipes: [ExponentialStrengthPipe]
})
export class PowerBoostCalculator {
  power = 5;
  factor = 1;
}

power-boost-calculator-anim

七、管道和变化检测

Angular每次JavaScript事件(如:按键、移动鼠标、定时器触发、服务器响应)后会执行数据绑定值变化检测,这可能会带来性能问题,Angular会尽可能在适当的时候提高性能。

如果我们使用管道,Angular会选择使用更简单、更快速的变化检测算法。现在来看看有无管道时Angular是如何优化性能的。

1、无管道

示例是一个显示 heroes 数组列表,并在每一变动时都会重新更新显示,这是一种比较积极变化检测策略。

组件模板内容:

New hero:
  <input type="text" #box
          (keyup.enter)="addHero(box.value); box.value=''"
          placeholder="hero name">
  <button (click)="reset()">Reset</button>
  <div *ngFor="let hero of heroes">
    {{hero.name}}
  </div>

组件类提供 heroes 属性,将新 hero 推入数组中,以及可以重置数据。

export class FlyingHeroesComponent {
  heroes:any[] = [];
  canFly = true;
  constructor() { this.reset(); }

  addHero(name:string) {
    name = name.trim();
    if (!name) { return; }
    let hero = {name, canFly: this.canFly};
    this.heroes.push(hero)
  }

  reset() { this.heroes = HEROES.slice(); }
}

任何对 heroes 数组进行操作都会重新被刷新显示。

2、Flying Heroes 管道

让我们来添加一个 FlyingHeroesPipe 管道用于过滤出那只能飞的 hero。先在组件模板里使用自定义的管道。

<div *ngFor="let hero of (heroes | flyingHeroes)">
  {{hero.name}}
</div>

FlyingHeroesPipe 自定义管道如下:

import { Pipe, PipeTransform } from '@angular/core';

import { Flyer } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
  transform(allHeroes:Flyer[]) {
    return allHeroes.filter(hero => hero.canFly);
  }
}

当我们运行示例会发现很奇怪,添加的 hero 并不会显示。虽然没有得到我们想要的效果,但是Angular一切都是正常的,只不过使用了一个忽略变更列表的变化检测算法。

看看是如何新增一个 hero 的:

this.heroes.push(hero)

我们是将新 hero 对象推入到数组里面,数组的对象引并没有发生改变,它还是同一个数组,所以从Angular角度来说,相同数组、没有变化、不需要更新。

我们可以解决这个问题,就是使用 concat 将老数据和新数据连接成一些新的副本,并重新赋值给 heroes,这样数组的对象引用地址就变成新的,Angular也能检测到新的变化。

这里通过新增一个 Mutate Array 选择框来表示我们是采用 push (理解为:变改数组) 或 concat(理解为:替换数组) 来插入新 hero的。

flying-heroes-anim

通过替换数组这种方式比较变异的写法,而且很难控制,我们对数组的操作都要重新产生一个新副本,这肯定会让人崩溃。

因此,Angular提供另一种管道,用来解决检测变化时对对象引用依然有效的办法。

八、Pure和Impure管道

管道有两种类型:pureimpure,其中pure是默认类型,我们可以在管道元数据里指定 pure:false 表明管道是 impure 类型。

@Pipe({
  name: 'flyingHeroes',
  pure: false
})

现在我们来看看 pure 和 impure 两者的区别。

1、Pure管道

Angular只会检测到输入值变更才会执行pure管道。pure变更指的是原始输入值的变化(StringNumberBooleanSymbol)或对象引用的变化(DateArrayFunctionObject`)。

Angular会忽略对象内部的变化,一个对象引用检查要比一个深差异检查要快得多,这样Angular才能快速判断它是否可以跳过管道或屏幕更新。正因为有对象引用的受限,所以我们需要另一种管道的变化检测策略 impure 管道。

2、Impure管道

Angular每次组件变化检测周期时都会执行 impure 管道,每个按钮或鼠标移动时都会执行管道。所以实施一个 impure 管道要很注意了,因为管道执行不足够快,有可能会让用户体验变得很差。

3、impure 管道示例

现在我们把 FlyingHeroesPipe 用 impure 管道实现一个 FlyingHeroesImpurePipe 管道。

@Pipe({
  name: 'flyingHeroes',
  pure: false
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}

唯一不同的是,只是在pipe元数据增加一个 pure:false在线示例

4、impure异步管道

Angular里有一个非常有趣的 AsyncPipe 管道,授收的是 PromiseObservable 输入值,并自己订阅它,最终返回产生的值。

它也有状态,管道维护 Observable 的订阅并保持从 Observable 传递值。

下面示例,演示 async 管道与一个 Observable 消息字符串的使用方法。

import { Component } from '@angular/core';
import { Observable } from 'rxjs/Rx';
// Initial view: "Message: "
// After 500ms: Message: You are my Hero!"
@Component({
  selector: 'hero-message',
  template: `
    <h2>Async Hero Message and AsyncPipe</h2>
    <p>Message: {{ message$ | async }}</p>
    <button (click)="resend()">Resend</button>`,
})
export class HeroAsyncMessageComponent {
  message$:Observable<string>;
  constructor() { this.resend(); }
  resend() {
    this.message$ = Observable.interval(500)
      .map(i => this.messages[i])
      .take(this.messages.length);
  }
  private messages = [
    'You are my hero!',
    'You are the best hero!',
    'Will you be my hero?'
  ];
}

异步管道保存样本是放在组件代码里,组件里面并没有订阅异步数据源。

5、impure 缓存管道

让我们再写一个 impure 管道,通过 HTTP 请求远程服务端数据。

如果请求的网址已经改变,管道会重新发起服务器请求;如果请求的是已经缓存的网址,则直接返回缓存的结果。

import { Pipe, PipeTransform } from '@angular/core';
import { Http }                from '@angular/http';
@Pipe({
  name: 'fetch',
  pure: false
})
export class FetchJsonPipe  implements PipeTransform{
  private fetched:any = null;
  private prevUrl = '';
  constructor(private _http: Http) { }
  transform(url: string): any {
    if (url !== this.prevUrl) {
      this.prevUrl = url;
      this.fetched = null;
      this._http.get(url)
        .map( result => result.json() )
        .subscribe( result => this.fetched = result );
    }
    return this.fetched;
  }
}

然后我们为了证明只调用一次请求,在模板里使用了两次管道。

template: `
  <h2>Heroes from JSON File</h2>
  <div *ngFor="let hero of ('heroes.json' | fetch) ">
    {{hero.name}}
  </div>
  <p>Heroes as JSON:
  {{'heroes.json' | fetch | json}}
  </p>
`,

尽管我们多次调用管道,但是通过浏览器开发者工具,确实只有一个请求。

hero-list

6、JsonPipe

Angular内建的 JsonPipe,将输入对象转换成JSON字符串输出,用来打印一次测试信息倒是很方便哈。

九、在类里使用管道

Angular2.x的管道其实就是1.x的过滤器,而管道有时候也需要在类里面调用,比如:DatePipe 是Angular内置管道,存放在 angular/common 库里面。

前面我们示例中所有的管道实际都是一个类,因此,如果我们想在某个组件类调用管道,可以先加载相应模块,然后按类实例调用 transform 方法即可。

import { DatePipe } from '@angular/common';

export class HeroBirthday {
  year = new DatePipe().transform(new Date(1988,3,15), 'y');
}

Angular2 NPM包

Angular应用和Angular自身依赖者很多第三方包,这些软件包的维护和安装都通过NPM来管理。Angualr建议利用快速入门的package.json做为基础。

{
  "dependencies": {
    "@angular/common": "2.0.0-rc.0",
    "@angular/compiler": "2.0.0-rc.0",
    "@angular/core": "2.0.0-rc.0",
    "@angular/http": "2.0.0-rc.0",
    "@angular/platform-browser": "2.0.0-rc.0",
    "@angular/platform-browser-dynamic": "2.0.0-rc.0",
    "@angular/router-deprecated": "2.0.0-rc.0",
    "@angular/upgrade": "2.0.0-rc.0",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.6",
    "zone.js": "^0.6.12",
    "angular2-in-memory-web-api": "0.0.5",
    "bootstrap": "^3.3.6"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.2.0",
    "typescript": "^1.8.10",
    "typings": "^0.8.1"
  }
}

npm有非常丰富的第三方包可以选择,我们可以增加和减少来满足应该需求。接下来解释这些包是什么意思、干什么用的。

一、dependencies和devDependencies节点

package.json 将依赖分成dependencies和devDependencies。前者是运行应用程序所需要包;后者是开发应用所需要包,可以在部署生产环境时除掉。

npm install my-application --production

二、依赖

package.jsondependencies 节点的依赖包分为三种类型:

  • 功能 功能包提供我们的应用框架和实用功能。
  • Polyfills Polyfills 插件消除JavaScript在各浏览器上的差异。
  • 其它 其它第三方库,比如:bootstrap

1、功能包

@angular/core – 应用程序所需要的关键运行库,包括所有元数据装饰器、ComponentDirective、依赖注入以及组件生命周期钩子。

@angular/common – Angular提供常用Service、管道和指令(表单)。

@angular/compiler – Angular模板编译器。

@angular/platform-browser – 有关DOM与浏览器相关的一切,尤其是帮助DOM渲染。包还包括引导应用启动方法,以及预编译模板方法。

@angular/platform-browser-dynamic – 动态编译?

**@angular/http ** – Angular Http客户端。

@angular/router – 组件路由。

@angular/upgrade – 用于升级1.x至2的实用工具集。

system.js – 兼容ES2015模块规范的动态模块加载器,也可以选择其他库代替,例如Webpack。SystemJS恰好是我们所有系列中的示例样本。

2、Polyfills 插件

Angular应用环境需要一些polyfills,这些polyfills列表都放在 dependencies 节点当中。

es6-shim – ES6引擎,用于解决低浏览器版本的一些ES6特性,不过一般只是在开发时会用它来做API转换,实现部署时这些东西应该都是移除的。

reflect-metadata – 一个依赖在Angular和TypeScript中间的元数据编译器。

rxjs – 一种利用 Observables 流的异步编程接口。

zone.js – 用于解决异步任务之间的持久性传递,2.x的事件绑定诸如 (click) 而无须像 1.x 中还要定义一个 ngClick 指令,这里面 zone 功不可没。

3、其它辅助库

bootstrap – 不用多话,最常用的HTML和CSS框架。

angular2-in-memory-web-api – 之前示例有出现过,用于模拟一个远程WEB API服务接口,用在测试上蛮方便的。

三、devDependencies

package.jsondevDependencies 节点列出的包是帮助我们开发应用,不需要被部署到生产环境中,

concurrently 一个同时运行多个 npm 命令的实用工具(支持os/x,Windows,Linus)

lite-server 一个轻型的静态文件服务器。

typescript TypeScript编译器。

typings TypeScript定义文件的管理器。

当然还有一些像测试和构建等工具也是放在这里。

四、总结

其实Angular2利用了在大量第三方库,好好的认知这些库对学习Angualr2非常有帮助。对于devDependencies节点,如何去构建一套便于团队开发的项目样本,就需要对各种测试、构建工具非常熟悉。

官网文章还有一小节是介绍 peerDependencies 节点,它是为了解决一些库的兼容性依赖问题,在我们目前接触的示例中从未体现过,但是在Angular源代码中有该节点。可在我写文章时,目前最新的源代码已经没有这个节点,我猜想可能是因为 npm3.x 的关系,因为npm3解决一个路径深度的问题。

Angualr2 生命周期钩子

Angular会管理一个组件的生命周期,包括组件的创建、渲染、子组件创建与渲染、当数据绑定属性变化时的校验、DOM移除之前毁销。

Angular提供组件生命周期钩子便于我们在某些关键点发生时添加操作。

一、组件生命周期钩子

指令和组件实例有个生命周期用于创建、更新和销毁,开发者可通过 angular2/core 库实现一个或多个这些生命周期钩子接口,每个接口都有一个单独的钩子方法,都是以接口名称加上 ng 前缀,比如:OnInit 接口有个钩子方法名为 ngOnInit,我们可以组件类实现它,比如:

import { OnInit } from 'angular2/core';

export class PeekABoo implements OnInit {
  constructor(private _logger:LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() { this._logIt(`OnInit`); }

  protected _logIt(msg:string){
    this._logger.log(`#${nextId++} ${msg}`);
  }
}

不是所有指令或组件都能实现所有钩子方法,有的只能用在组件上。

Angular只能调用已经在指令/组件定义过钩子方法。

可选接口

从技术角度而言,对于JavaScript和TypeScript开发者来说接口是可选的,JavaScript语言并没有接口概念,Angular在运行过程中也没TypeScript接口说法,因为当TypeScript编译以后接口就消失了。

进一步来讲,其实对于Angular而言用不用钩子接口并不关心的,只要组件类存在钩子的命令规范,比如组件类有 onOnInit(),Angular自然就会调用。

尽管如此,大家还是按照接口的写法吧。

那么,刚才说过钩子接口只对指令或组件有效,而部分只对组件有效。

1、指令和组件都有效的钩子

钩子 目的
ngOnInit 发生于构造函数之后,用于初始化指令/组件,主要用于数据绑定的输入属性处理。
ngOnChanges 在数据绑定属性检查后被调用,在view和content children检查后其中之一发生改变之前。换个简单的说法,数据绑定属性的值发生变化前。
ngDoCheck 发生在每次变化检测时,主要用于在进行自定义的检查前扩展变化的检查或强制忽略检查。
ngOnDestroy 在Angular销毁组件/指令之前的清理工作,取消订阅监控对象和事件处理函数,以避免内存泄漏,仅会被调用一次。

2、组件有效的钩子

钩子 目的
ngAfterContentInit 在Angular将外部内容放到视图内之后。
ngAfterContentChecked 在Angular检测放到视图内的外部内容的绑定后。
ngAfterViewInit 在Angular创建了组件视图之后。
ngAfterViewChecked 在Angular检测了组件视图的绑定之后。

二、生命周期顺序

在Angular通过构造函数创建组件/指令后,就会按以下顺序调用这些生命周期钩子方法:

钩子 时间点
ngOnChanges 在 `ngOnInit` 之前,当数据绑定输入属性的值发生变化时。
ngOnInit 在第一次 `ngOnChanges` 之后。
ngDoCheck 每次Angular变化检测时。
ngAfterContentInit 在外部内容放到组件内之后。
ngAfterContentChecked 在放到组件内的外部内容每次检查之后。
ngAfterViewInit 在初始化组件视图和子视图之后。
ngAfterViewChecked 在组件视图和子视图检查之后。
ngOnDestroy 在Angular销毁组件/指令之前。

三、其他生命周期钩子

除上面列出的钩子外,Angular子系统也可能有自个生命周期钩子。例如,路由器,也有自己的钩子,使我们能够在路由导航的特定时刻做一些操作。

ngOnInitrouterOnActivate 都是属于钩子,为了避免发生冲突,二者前缀规范不同。当然第三方库也可能是会定义一个钩子,总之呀多注意点命令方面的问题。

四、生命周期示例简要说明

下面会通过一系列的示例,演示生命周期钩子的使用方法。所有示例都以 AppComponent 为父组件,每个子组件都会特定对某个生命周期的钩子方法进行演示。

所有示例的在线DEMO,最好是和在线DEMO一起阅读,因为后面并不会完整的去展示代码。以下是每个子组件的简要说明:

组件 说明
Peek-a-boo 演示每个生命周期钩子,每个钩子被调用时都会在屏幕上打印日志。
Spy 指令生命周期钩子,利用 `SpyDirective` 指令的 `ngOnInit` 和 `ngOnDestroy` 钩子,演示一个指令是如何被创建或销毁的。
OnChanges 看看Angular调用 `ngOnChanges` 钩子在 `changes` 对象每一次变化时的调用情况。
DoCheck 实现一个自定义变化检测 `ngDoCheck` 钩子方法,看看Angular如果调用钩子及它更改会日志的变化。
AfterView 演示Angular如何显示一个视图,包括 `ngAfterViewInit` 和 `ngAfterViewChecked` 钩子。
AfterContent 展示如何将项目外部内容放进组件内,且如何把项目内容和组件子视图区分开来,演示的钩子包括 `ngAfterContentInit` 和 `ngAfterContentChecked`。
Counter 演示一个自定义钩子示例。

五、Peek-a-boo

利用 PeekABooComponent 演示所有钩子。在实际项目中,我们很少需要实现所有钩子接口,这个示例为的是看看Angular调用钩子的执行顺序。这里有个快照,点击创建按钮再一次点击销毁的钩子调用过程。

peek-a-boo

根据日志消息的显示,钩子调用的顺序:OnChangesOnInitDoCheck(3次)、AfterContentInitAfterContentChecked(3次)、AfterViewInitAfterViewChecked(3次)、OnDestroy

如果我们点击 Update Hero 按钮,会看到另一个 OnChanges 和两对 DoCheckAfterContentCheckedAfterViewChecked

六、Spying OnInit and OnDestroy

现在来揭开这两个钩子到底是如何初始化和销毁。

创建一个 spy 指令,并实现 ngOnInitngOnDestroy 钩子,并注入 LoggerService,当钩子被调用时能在屏幕上打印日志。

// Spy on any element to which it is applied.
// Usage: <div mySpy>...</div>
@Directive({selector: '[mySpy]'})
export class Spy implements OnInit, OnDestroy {

  constructor(private _logger: LoggerService) { }

  ngOnInit()    { this._logIt(`onInit`); }

  ngOnDestroy() { this._logIt(`onDestroy`); }

  private _logIt(msg: string) {
    this._logger.log(`Spy #${nextId++} ${msg}`);
  }
}

我们可以把指令用到任何本地模板或组件元素当中,这样当这些元素初始化时或销毁时指令也会跟着初始化或销毁。

`<div *ngFor="let hero of heroes"  mySpy  class="heroes">
   {{hero}}
 </div>`

这里 spy 指令(mySpy)依附在 <div> 元素,意味者元素的初始化或销毁也会发生在指令身上。当添加一个新 hero 时,会多出一个初始化指令的动作,如果点击 reset,所有的指令也会跟着元素一同被销毁。

spy-directive

ngOnInitngOnDestroy 在实际项目应用中发挥很重要的作用,接着看看为什么我们需要他们?

1、ngOnInit

使用 ngOnInit 有两个主要理由:

  • 在构造函数之后执行复杂初始化。
  • 在设置组件之后设置输入属性。

ngOnInit 经常用来获取数据,比如之前我们介绍的 HTTP 客户端的文章。

为什么我们不把获取数据放在构造函数里呢?之前HTTP文章也谈过,试想如果在构造函数里调用一个远程服务时有异常,那实例到底应该是成功还是失败呢?一个类的构造函数应该是简单且安全的。

当一个组件必须很快的启用并开始工作时,就应该让Angular去调用 ngOnInit 方法来启用这些复杂的业务逻辑。

记住一个指令数据绑定输入属性是必须等构造函数完成以后才会开始设置,如果需要基于这些属性来初始化指令,也必须在 ngOnInit 里运行它。

2、ngOnDestroy

ngOnDestroy 主要用来清除一些逻辑,以及那些无法被GC自动回收释放资源,比如:退订observable、DOM事件、停止interval计时器等。

七、OnChanges

下面是一个监测 OnChanges 钩子的示例,每当输入属性发生变化时,Angular会调用 ngOnChanges 方法。

OnChangesComponent 组件

ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
  for (let propName in changes) {
    let prop = changes[propName];
    let cur  = JSON.stringify(prop.currentValue);
    let prev = JSON.stringify(prop.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

ngOnChanges 钩子接收一个类型为 SimpleChange 的映射对象,包括新值和旧值。

组件里还指定两个输入属性:

@Input() hero: Hero;
@Input() power: string;

父组件绑定示例:

<on-changes [hero]="hero" [power]="power"></on-changes>

最终效果

on-changes-anim

我们看到 power 的每一次输入变化都会打印出日志,而为什么 hero 没有什么任何检测日志呢?

这是因为Angular只会对那些值会发生变化的输入属性调用 ngOnChanages,而 hero 属性是一个引用 hero 对象,Angular无法知道 hero 的 name 属性是不是已经发生变化了。所以Angular的 onChanage 钩子对于引用对象类型是无效的。

八、DoCheck

我们可以使用 DoCheck 钩子来改变变化检测结果。DoCheck 示例是在 OnChanages 示例代码基础上扩展并实现 DoCheck:

ngDoCheck() {

  if (this.hero.name !== this.oldHeroName) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
    this.oldHeroName = this.hero.name;
  }

  if (this.power !== this.oldPower) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
    this.oldPower = this.power;
  }

  if (this.changeDetected) {
      this.noChangeCount = 0;
  } else {
      // log that hook was called when there was no relevant change.
      let count = this.noChangeCount += 1;
      let noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
      if (count === 1) {
        // add new "no change" message
        this.changeLog.push(noChangeMsg);
      } else {
        // update last "no change" message
        this.changeLog[this.changeLog.length - 1] = noChangeMsg;
      }
  }

  this.changeDetected = false;
}

我们手动检测并比较新值与旧值,当发生变化时打印日志。

do-check-anim

现在我们可以检测 hero.name 的变化了,但一定要小心。ngDoCheck 钩子调用频率非常高,不管你有没有值发生变化,每个变化都会进行一次检测。

就拿上面的示例来说,哪怕你什么值也没有改变,离开焦点时也会进行一次检测,所以 ngDoCheck 最好不要做一些很复杂的事情,这样会影响用户体验。

九、AfterView

AfterView示例探讨 AfterViewInitAfterViewChecked,Angular会在创建组件的子视图(视图可以理解为指令或组件)后调用它们。

子视图是一个文本框里显示 hero 的 name。

@Component({
  selector: 'my-child',
  template: '<input [(ngModel)]="hero">'
})
export class ChildViewComponent {
  hero = 'Magneta';
}

AfterViewComponent 组件的模板内显示子视图:

template: `
  <div>-- child view begins --</div>
    <my-child></my-child>
  <div>-- child view ends --</div>`

为了能让钩子能够检测值变化,还需要使用 @ViewChild 查询类型为 ChildViewComponent 的组件,并绑定到 viewChild 属性。

export class AfterViewComponent implements  AfterViewChecked, AfterViewInit {
  private _prevHero = '';

  // Query for a VIEW child of type `ChildViewComponent`
  @ViewChild(ChildViewComponent) viewChild: ChildViewComponent;

  ngAfterViewInit() {
    // viewChild is set after the view has been initialized
    this._logIt('AfterViewInit');
    this._doSomething();
  }

  ngAfterViewChecked() {
    // viewChild is updated after the view has been checked
    if (this._prevHero === this.viewChild.hero) {
      this._logIt('AfterViewChecked (no change)');
    } else {
      this._prevHero = this.viewChild.hero;
      this._logIt('AfterViewChecked');
      this._doSomething();
    }
  }
  // ...
}

遵守单向数据流规则

_doSomething 方法用来检测当 name 超过10个字符时显示 “That’s a long name”。

// This surrogate for real business logic sets the `comment`
private _doSomething() {
  let c = this.viewChild.hero.length > 10 ? "That's a long name" : '';
  if (c !== this.comment) {
    // Wait a tick because the component's view has already been checked
    setTimeout(() => this.comment = c, 0);
  }
}

为什么 _doSomething 方法需要使用 setTimeout 来延迟更新 comment 呢?

我们必须坚持Angular单向数据流规则,换句话说,如果我们立即更新组件数据绑定 comment 属性,而此时正在检测变化Angular会抛出异常,这一点很容易发生因为当 ngAfterViewChecked 触发前会先进行 DoCheck 这之前我们也讨论过,所以这里才用 setTimeout 推迟浏览器JavaScript循环周期。

after-view-anim

十、AfterContent

AfterContent示例探讨 AfterContentInitAfterContentChecked,Angular会在项目外部内容被放进组件内后调用它们。

1、内容投影

内容投影是将外部组件的HTML内容插入到组件模板的指定位置;跟Angular1.x的transclusion类似。

本示例同 AfterView 的示例一样,只不过这一次,我们父组件模板多套一层 AfterContentComponent 组件:

`<after-content>
   <my-child></my-child>
 </after-content>`

注意, <after-content> 标签包裹着 <my-child> 标签,除非我们打算把内容也插入到组件里,否则永远不要把其他元素放到这中间。

现在看看组件模板:

template: `
  <div>-- projected content begins --</div>
    <ng-content></ng-content>
  <div>-- projected content ends --</div>`

<ngContent> 标签是外部内容的占位符,告诉Angular要在哪插入内容。

2、AfterContent 钩子

AfterContent钩子类似于AfterView钩子,关键的区别在于如何查找子组件。

  • AfterView钩子涉及 ViewChildren,子组件通过组件模板插入。
  • AfterContent钩子涉及 ContentChildren,子组件通过映射内容插入。

接下来 AfterContent 钩子只能通过属性装饰 @ContentChild 查询组件。

export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
  private prevHero = '';
  comment = '';

  // Query for a CONTENT child of type `ChildComponent`
  @ContentChild(ChildComponent) contentChild: ChildComponent;

  ngAfterContentInit() {
    // viewChild is set after the view has been initialized
    this.logIt('AfterContentInit');
    this.doSomething();
  }

  ngAfterContentChecked() {
    // viewChild is updated after the view has been checked
    if (this.prevHero === this.contentChild.hero) {
      this.logIt('AfterContentChecked (no change)');
    } else {
      this.prevHero = this.contentChild.hero;
      this.logIt('AfterContentChecked');
      this.doSomething();
    }
  }
  // ...
}

© 2016 卡片机色彩

Theme by cipchk

to top