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

获取数据可以是像创建一个变量那样简单,也可以像从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');
}