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');
}

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();
    }
  }
  // ...
}

[基础系列] 如何更好的组织 Angular2 文件结构

当我的文章写到最后时,我发现了在Github里面已经有个非常专业的Angular2样板项目angular2-webpack-starter,他完全包括我想要的东西甚至很多我并未考虑的,比如:单元测试、热替换等。所以我删除我的文章,只保留一些我认为有必要声明的理论知识;转而对angular2-webpack-starter的简单翻译事宜。

一、写在前面

angular2-webpack-starter已经有完整的解决方案,而且非常优秀。但在此之前我会解释一些理论问题。

1、模块规范

模块是一个极为重要的概念,别人写的功能代码你可以直接拿过来用;但,如果你的代码没有按指定的规范来写,那怎么可能做到代码相互使用呢?虽然 ES6 已经有模块的概念,但目前整体Javascript发展来看还需要一段时间。

目前最常用的有CommonJS、AMD、CMD。这三者的主要区别在于,从加载模块的角度出发:

CommonJS
比较适合服务端,因为当我们使用 require('math') 来加载一个模块时,由于模块文件在服务端都是存在本地硬盘上,所以非常快的加载进来。而如果我们把这一行代码放在客户端里面,我们就会遇到一个问题 异步加载,因此才会有AMD规范这一说。

AMD
AMD为的就是解决CommonJS在前端异步加载的不足,当然国产的CMD也是如此,只不过二者存在加载顺序上的区别,不过现在已经没有区别了,因为AMD也改为跟CMD相同原理的加载模式。

SystemJS
万能的加载器,她的定位包括ES6代码,当然既然是万能就少不了:CSS、图片。

三者的规范都是为了解决一些模块加载问题,那么选用什么规范更合理呢?这取决于你的需求,合理需求选用合理的规范。

2、我的选择?

我个人比较偏爱CommonJS,因为我们大部分项目都是单页应用,换句话说CommonJS先天性不足,并不会对我们有什么影响。当然相比较其它规范,有着更多的优势:

  • CommonJS更容易被流行的打包工具所接受,比如:Webpack、Browserify
  • CommonJS比SystemJS更容易
  • CommonJS是node.js格式,允许组件更容易在服务端渲染
  • CommonJS服务端和前端共用代码
  • CommonJS生成的文档可以省20%的空间

二、细说angular2-webpack-starter

angular2-webpack-starter是使用Webpack来构建针对Angular2和TypeScript的样板项目,还包括E2E和Karma单元测试,他列举几项使用样板项目的理由:

  • 它是构建Angular2文件组织的最佳实践。
  • 基于Webpack编译TypeScript。
  • 自动化单元测试。
  • 热替换。
  • 使用Material Design。

1、快速启动

首先要确保 node版本4.0 以上。

# 从git克隆一下最新版本
git clone --depth 1 https://github.com/angularclass/angular2-webpack-starter.git

# 切换至项目根目录
cd angular2-webpack-starter

# 先添加一些全局库,这样下次使用时不需要再重新获取 
# 在安装webpack时,我出现了 AttributeError: ‘MSVSProject’ object has no attribute ‘iteritems’ 异常
# 后来我更改为: npm install webpack -g 既然成功了。
# 如有其它异常见:http://asdfblog.com/npm-install-errors.html(欢迎提交更多异常和解决方案)
cnpm install typings webpack-dev-server rimraf webpack -g

# 使用cnpm(淘宝NPM 镜像)安装组件
cnpm install

# 启动
# 如果收到 `Cannot find name 'Promise'.` 异常时,表明未正确安装ts定义
# 重新运行:npm run typings -- install
npm start

# 获取启动一个带有热替换的服务
npm run server:dev:hmr

最后在浏览器访问:http://0.0.0.0:3000 或 http://localhost:3000,效果如下:

test

2、文件结构

angular2-webpack-starter使用组件风格来管理文件,这很符合Angular2的发展新标准,这样有利于代码可维性和行为逻辑封装。一个组件的基本组成包括:组件类、样式、模板文件、单元测试等。具体结构如下:

angular2-webpack-starter/
 ├──config/                    * 配置文件夹
 |   ├──helpers.js             * 配置辅助类
 |   ├──spec-bundle.js         * 测试文件所需要的额外配置
 |   ├──karma.conf.js          * karma 配置文件(单元测试)
 |   ├──protractor.conf.js     * protractor 配置文件(端对端测试)
 │   ├──webpack.common.js      * webpack 开发、产品、测试模式通用配置文件 
 │   ├──webpack.dev.js         * webpack 开发模式配置文件 
 │   ├──webpack.prod.js        * webpack 产品模式配置文件 
 │   └──webpack.test.js        * webpack 测试模式配置文件
 │
 ├──src/                       * 程序源代码
 |   ├──main.browser.ts        * webpack的entry入口
 |   ├──index.html             * index.html模板文件 
 |   ├──polyfills.ts           * polyfills 文件(提供支持ES6特性)
 |   ├──vendor.ts              * 第三方组件(目的为webpack能够正常打包)
 |   ├──custom-typings.d.ts    * 自定义ts定义,ts编译时非常严格
 │   ├──app/                   * APP应用源文件
 │   │   ├──app.spec.ts        * 组件app.ts的单元测试
 │   │   ├──app.e2e.ts         * 端对端的测试文件 
 │   │   └──app.ts             * App.ts 组件入口
 │   │
 │   └──assets/                * 静态文件
 │       ├──icon/              * icon文件 
 │       ├──service-worker.js  * 忽略
 │       ├──robots.txt         * 相关搜索引擎
 │       └──human.txt          * 见:humanstxt.org
 │
 ├──nitrous.json               * 一个逆天的构建在线测试站(需要费用)
 ├──tslint.json                * typescript lint 配置文件
 ├──typedoc.json               * typescript 文档生成配置
 ├──tsconfig.json              * typescript 编译配置文件
 ├──typings.json               * ts定义配置文件
 └──package.json               * npm配置文件

app/home/ 相当于一个页面的组件都包括在该文件夹下,同时文件夹又嵌套着页面所需要的其他组件,构建一个完整的ng2网站是由若干组件组合而成,一个页面可以被细分出不同子组件,整体组件的结构图跟一个树结构一样。

3、入门指南

在【1、快速启动】已经做了很多注释以及可能发生的QA,不再赘述。重点来看angular2-webpack-starter提供的一些运行指令。

其它运行指令

编译文件

# 编译开发版本
npm run build:dev
# 编译生产环境版本
npm run build:prod

编译成功后会在根目录下生成一个 dist 文件夹。

热替换模式

npm run server:dev:hmr

意味者进入开发模式后,你可以打开 http://localhost:3000,当你修改ts文件时会自动重新编译,并以热替换的形式变更页面效果。(非常赞喔,特别是安装ssd硬盘的,响应更佳!)

监听和编译

npm run watch

意味者进入开发模式后,你可以打开 http://localhost:3000,当你修改ts文件时会自动重新编译。

运行测试

npm run test

运行并监听测试

npm run watch:test

运行端对端测试

npm run watch:test

4、自定义类型定义(TS)

当使用第三方模块时还需要包含模块的类型定义,可以尝试使用 typings install node --save 来安装TS定义。但是并不是所有模块都已经在TS中已经有注册相应的类型定义文件。那么,我们只能自己来写这些TS定义信息(这也是没有办法TS编译器在编译过程中严格规定所有类型都需要有明确的定义),例如:

declare module "my-module" {
  export function doesSomething(value: string): string;
}

或者,可以定义一个全局的 any 类型变量,例:

declare var assert: any;

总结

angular2-webpack-starter是一个非常棒的样板项目,你可以直接在此基础上做产品开发,即便自己建也未必能够这么周到;同时项目已经被Star:2415可见有多么热门。

angular2-webpack-starter常见问题清单已经有很多可能发生的问题并已经给出了具体解决方案,但凡在使用过程有问题也可以去提交issues。

之前一直在群里(QQ群:485843913)经常会提出Angular2环境搭建问题,特别是拿官网的快速入门做为敲门砖,但是官网的示例在现实生产环境中一点也不合格;我相信angular2-webpack-starter是最简单解决我们生产环境下所有问题的最佳样板项目了。读懂她,才算真正才能开始入门Angualr2吧。

Angular2的五分钟入门在Windows下的实现

官网提供的是在linux的步骤,而实际直接拿这些步骤在windows下也可以实现,但唯一就是无法 --watch TypeScript文件,那就是扯蛋,改一下代码就要重新编译,谁受得了。

那么我来尝试一下直接使用Gulp来搭建。

一、创建项目

虽然Angular2允许我们使用TypeScript、Dart、ES5、ES6来写代码,但是出于Angular2也拥抱TypeScript,那么变成我们唯一最好的选择也是TypeScript。

首先创建一个空文件夹,然后通过TSD(什么是TSD见我之前的文章)来安装Angular2包。

tsd install angular2 es6-promise rx rx-lite

接着分别创建 app.ts和index.html 两个空文件。

二、tsc 编译TypeScript

tsc --watch -m commonjs -t es5 --emitDecoratorMetadata app.ts

这是官网提供的命令,意思是说使用 tsc.exe 把 app.ts 编译为ES5标准的JavaScript,其中 –watch 是关键,他可以直接对 app.ts 进行监听,一但app.ts发生变化就立即重新编译。

而正是这一步,在windows下是无法监听的。因此我这里尝试用Gulp来编译TypeScript代码。

三、Gulp 编译TypeScript

首先我们更改一下之前创建的两个文件存放路径,新建立一个 src 文件夹用来存放所有 *.ts 文件。

其次需要安装相应的node组件,采用 gulp-typescript 来编译TypeScript代码。

npm install --save-dev gulp gulp-typescript

最后我们整体的目录结构看起来像这样子:

ng2
├─dist
├─node_modules
├─src
    └─app.ts
├─typings
    ├─angular2
    ├─es6-promise
    └─rx
├─gulpfile.js
├─index.html
├─package.json
└─tsd.json

最后的重点就是 gulpfile.js 配置,这里我写两个Gulp任务,一个是编译、一个是监听。

var gulp = require('gulp'),
    ts = require('gulp-typescript');

// 编译任务
gulp.task('default', function() {
    var tsResult = gulp.src('src/*.ts')
        .pipe(ts({
            noImplicitAny: true,
            module: 'commonjs',
            target: 'ES5' // 按ES5标准输出
        }));

    return tsResult.js.pipe(gulp.dest('dist/'));
});

// 监听任务
gulp.task('watch', ['default'], function() {
    gulp.watch('src/*.ts', ['default']);
});

这样,我们可以直接使用命令 gulp watch 运行gulp,一但我们的 src 文件夹有什么变动,就会立即重新编译,并把结果以 app.js 命名输出在 dist 文件夹中。

四、导入Angular

在 app.ts 里引用 angular2 包,同时这种引用在VS当中还可以起到对angular2的智能提醒作用。

/// <reference path="../typings/angular2/angular2.d.ts" />

:好像这里的path无法使用 / 来表示根目录,只能以 ../ 的形式一点点查。

导入 angular2 的核心模块。

import { Component, View, bootstrap } from 'angular2/angular2';

假设这些代码是在VS下,那么你们还会发现在 Component 上按F12都可以直接跳转到他的所在的文件,不亏是和M$合作,所以如果在VS下开发NG2,体验就不用多说了。

五、定义一个组件

在NG2中,应用基于组件的结构用其来表示UI,以下是创建一个完整的 <my-app> 组件。

// Annotation section
@Component({
  selector: 'my-app'
})

@View({
  template: '<h1>Hello {{ name }}</h1>'
})

// Component controller
class MyAppComponent {
  name: string;
  constructor() {
    this.name = 'Alice';
  }
}

假设你完全没有TypeScript知识的话,那看这一段实时会头疼。

一个Angular2组件包含两部分,用ES6的class来表示组件的Controller(有Angular1.x经验的知道,它是用于组件控制器)和以注解的方式告诉组件应该放在页面的什么地方和什么内容。怎么看都有点像ReactJS。

@Component 和 @View 注解

在Angular2当中会有大量使用TypeScript的注解,也是TypeScript1.5的一个新功能,它是将额外的数据附加到类当中,相当于配置元数据,就拿上面的来说,@Component 它把界面上某个DOM元素选择器关联起来,以便于Angular知道应该把结果插入到哪?熟悉1.x的人知道,当插入组件到页面时也会一并产生一个的注释代码,而在2.x里面就没有这些。

此外注解其实也有可能出现在ES7的标准当中。在VS IDE下是允许被智能感知的,开发起来杠杠的。

六、Bootstrap

在1.x当中我们启动一个Angular程序有两种方式 ng-app=""angular.bootstrap(),在2.x中只能用后者。我们在 app.ts 最底部加上:

bootstrap(MyAppComponent);

七、定义HTML

<!-- index.html -->
<html>

<head>
    <title>Angular 2 Quickstart</title>
    <script src="https://github.jspm.io/jmcriffey/bower-traceur-runtime@0.0.87/traceur-runtime.js"></script>
    <script src="https://jspm.io/system@0.16.js"></script>
    <script src="https://code.angularjs.org/2.0.0-alpha.28/angular2.dev.js"></script>
</head>

<body>
    <!-- The app component created in app.ts -->
    <my-app></my-app>
    <script>System.import('dist/app');</script>
</body>

</html>

其中引用了 traceur-runtime.js 和 angular2.dev.js,前者是ES6编译器,后者是angular核心包,而 system.js 就是个万能的模块加载器(就像require.js一样)。

八、运行

需要有一个HTTP服务,来运行我们的angular2。这里直接使用 npm install -g http-server 安装,最后:

http-server

在浏览器里访问 http://localhost:8080/ 就可以看到:

ng2-5-min

喔,对于,如果你想正确的运行还需要开个VPN,因为上面引用的库都是直接国外的。!@#¥%……&*()…………

这是我完整的代码,下载后按第8步安装 http-server 运行即可。

函数响应式编程(FRP)

一、什么是FRP

FRP全称叫函数响应式编程(Functional Reactive Programming),是一种编程模型,简单点说可以把若干不同数据在不做任何修改的情况下转换成新的数据,一直交替直到符合系统需求的数据。

比如:百度搜索框的自动补全,input输入下拉列表实时更新。其中input输入我们可以理解为最开始的数据,然后请求服务端,返回JSON数据,最后再转换后HTML写入DOM中。这是典型的一种外部输入数据(信号),数据(信号)的变化通过事件通知,最后经过一系列响应最后反馈给DOM。

这种FRP编程范式,已经有很多第三方类库,比如:ReactiveX(JS/.NET/Java/Swift/Scala)、Flapjax。而Angular2也把Rx(即ReactiveX JS版本)作为依赖关系。

二、一个Suggest示例

这是一个查询维基百科词语的Suggest(实在找不到百度有提供API),虽然很普通很常见,但我们还是画个流程图,以便于我们更清晰需要做什么。

FRP-Suggest

用户通过input输入词语,向维基API服务端请求,服务端处理完成后返回结果,再根据结果转换成相应的HTML代码,最后插入到DOM中;整个过程周而复始。

其实我们会发现整个过程的触发实际都是在一个时间线上,通过这一时间线对数据进行过滤、转换等等,而最后将数据(input输入的单词)转换成一个DOM列表。这就是FRP思想。而往往在语言实现上我们会以事件流的行式来表现。

那么我们来看一下代码是怎么写(完整代码,采用ES6编写,所以最好在FF下查看)。

var ob = Rx.Observable.fromEvent(qEl, 'keyup') // 创建观察手
  .map(e => e.target.value) // 
  .filter(text => text.length > 2) // 过滤
  .throttle(200) // 节流阀,防止按键过快倒置请求过频
  .distinctUntilChanged() // 忽略和上一次相同的操作,以免浪费资源
  .flatMapLatest(searchWikipedia) // 将当前的数据通过 searchWikipedia 映射成为一个新的数据
  .subscribe(data => { // 创建订阅方法,即拉取最新数据。
    var res = data[1];
    resultElr.empty();
    $.each(res, (_, value) => $('<li>' + value + '</li>').appendTo(resultElr));
  }, error => {
    resultElr.empty();
    $('<li>Error: ' + error + '</li>').appendTo(resultElr);
  });

// 这里必须返回一个带promise的函数
function searchWikipedia(term) {
    return $.ajax({
        url: 'http://en.wikipedia.org/w/api.php',
        dataType: 'jsonp',
        data: {
            action: 'opensearch',
            format: 'json',
            search: term
        }
    }).promise();
}

三、事件流处理

由上面的代码,可以了解FRP的几种常见事件流处理方法,而这些方法无疑就是方便对事件流的组合和转换。

1、映射

代码中 map(function)flatMapLatest(function),将事件流通过function映射成一个新的事件流。mapflatMapLatest 的区别在于,后者我们可以提供一个带有Promise对象,而最后被映射的是具体的值,而非带有值具体的值的Promise对象,恩,有点绕。以前面的示例,假设我们使用 map 来操作,那么我们需要这样:

ob.map(function(word) {
    return Rx.Observable.fromPromise(searchWikipedia(word));
});

2、过滤

代码中 filter(function)distinctUntilChanged(),事件流通过function判断结果为 true 的作为一个新的事件流。distinctUntilChanged 我的理解是比较,那么他也是相当于过滤中的一类吧。

使用中常见而代码又未提及的还有很多。

3、合并

merge(es1, es2, ...),把事件流和es1…3事件流合并成一个新的事件流。

4、条件

Rx.js提供了非常多的条件表达式,比如:

skip(n),跳过n个事件流,skip在rx当中还有很多个辅助版本。

四、行为能力

1、时间

FRP是以时间线来处理数据的,所以我时间也变成了最基本的一个。如上面的 throttle 表示200毫秒才会更新一次,换句话在这200毫秒发生的多个事件流最后都以最后一次的事件流为准。

正因为这种以时间线来处理数据,完全可以遏止像这种增量式搜索的资源浪费,就变得非常的简单。

2、计算

像一些加减乘除以及一些常用的统计 sum()min()max() 等等。

Rx.js还提供非常非常多的一些方便对事件流操作的方法,有兴趣可以看:,见:Observable Instance Methods

五、异步HTTP

我们再看一个异步请求HTTP的示例,当然如果只是一次请求看不出RX有什么特效。但假设我们需要同时两次请求,先查询列表,再根据结果再请求详细数据,这是多么觉见的场景。以下是通过常用的jQuery来实现。

$.get('/data1')
 .done(function(res) {
    $.get(res.data, function(res) {
      // process
    });
  });

以上已经尽可能短的描述两次请求的方式,看起来很干净,好像并无多余??等等,怎么看都怎么觉得奇怪,这好像又进入callback的深渊。那么看看FRP里面是怎么实现的:


var result2El = $('#result2'), source = Rx.Observable.fromEvent($('#demo2'), 'click'); source .map(() => 'node' ) .map(function(data) { result2El.html('开始查询带有[' + data + ']的词条...'); return data; }) .flatMapLatest(searchWikipedia) .map(function(res) { var data = res[1]; result2El.html('开始查询[' + data[0] + ']词条的内容...'); return data[0]; }) .flatMapLatest(searchWikipediaContent) .subscribe(data => { result2El.html('结果:' + JSON.stringify(data)); }, error => { result2El.html('错误'); });

怎么样,多么符合线性逻辑思考思维。对代码感兴趣点这

六、为什么需要FRP

对于FRP这种编程模型来讲,他是通过事件流的方式把数据进行重组,同样一个业务我们只需要依赖事件的组装,而不再过多的关注业务细节问题;转向把细节交给各种子函数来处理。其实我个人理解相当于把模块化开发思维更细致的转化到代码层面。

对于这种带有时间线性的开发方式,也非常符合程序员的线性表达业务逻辑思维方式。

在新版的Angular2,其中事件处理也采用这种方式。而对于FRP实现的框架而言,最重要的是有序的事件传播。如前面我们创建一个观察手,而这个观察手监听着 keyup 事件,一但事件发生变化,立即执行变通过一系列的事件流处理最终以DOM的形式展示。

最后

Angular2把RxJs做为依赖库并存着,虽然FRP有实现有很多,我也不知道Google和M$什么时候这么亲密,因为Rx也是M$提供的,而Angualr2的首选开发语言也是TypeScript。而市面上还存在其他很多FRP库(例如:Flapjax、Fran、Yampa),也许提供的API接口各有不同,但他们的FRP思想是一样的。

Rx不光是有JavaScript中使用,前面已经有所提及,它可以在.Net、Java等等后端开发上使用。

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top