阅读(2584) (0)

Angular 管道

2022-06-28 10:41:31 更新

用管道转换数据

管道用来对字符串、货币金额、日期和其他显示数据进行转换和格式化。管道是一些简单的函数,可以在模板表达式中用来接受输入值并返回一个转换后的值。例如,你可以使用一个管道把日期显示为 1988 年 月 15 日,而不是其原始字符串格式。

本主题中使用的范例应用,参阅现场演练 / 下载范例

Angular 为典型的数据转换提供了内置的管道,包括国际化的转换(i18n),它使用本地化信息来格式化数据。数据格式化常用的内置管道如下:

  • DatePipe​:根据本地环境中的规则格式化日期值。
  • UpperCasePipe​:把文本全部转换成大写。
  • LowerCasePipe ​:把文本全部转换成小写。
  • CurrencyPipe ​:把数字转换成货币字符串,根据本地环境中的规则进行格式化。
  • DecimalPipe​:把数字转换成带小数点的字符串,根据本地环境中的规则进行格式化。
  • PercentPipe ​:把数字转换成百分比字符串,根据本地环境中的规则进行格式化。

你还可以创建管道来封装自定义转换,并在模板表达式中使用自定义管道。

先决条件

要想使用管道,你应该对这些内容有基本的了解:

  • Typescript 和 HTML5 编程
  • 带有 CSS 样式的 HTML 模板
  • 组件

在模板中使用管道

要应用管道,请如下所示在模板表达式中使用管道操作符(​|​),紧接着是该管道的名字,对于内置的 ​DatePipe ​它的名字是 ​date ​。这个例子中的显示如下:

  • app.component.html​ 在另一个单独的模板中使用 ​date ​来显示生日。
  • hero-birthday1.component.ts​ 使用相同的管道作为组件内嵌模板的一部分,同时该组件也会设置生日值。

  • src/app/app.component.html
  • <p>The hero's birthday is {{ birthday | date }}</p>
  • src/app/hero-birthday1.component.ts
  • import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-hero-birthday',
      template: "<p>The hero's birthday is {{ birthday | date }}</p>"
    })
    export class HeroBirthdayComponent {
      birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
    }

该组件的 ​birthday ​值通过管道操作符(​|​)流向 ​date ​函数。

使用参数和管道链来格式化数据

可以用可选参数微调管道的输出。例如,你可以使用 ​CurrencyPipe ​和国家代码(如 EUR)作为参数。模板表达式 ​{{ amount | currency:'EUR' }}​ 会把 ​amount ​转换成欧元。紧跟在管道名称( ​currency ​)后面的是冒号(​:​)和参数值(​'EUR'​)。

如果管道能接受多个参数,就用冒号分隔这些值。例如,​{{ amount | currency:'EUR':'Euros '}}​ 会把第二个参数(字符串 ​'Euros '​)添加到输出字符串中。你可以使用任何有效的模板表达式作为参数,比如字符串字面量或组件的属性。

有些管道需要至少一个参数,并且允许使用更多的可选参数,比如 ​SlicePipe ​。例如, ​{{ slice:1:5 }}​ 会创建一个新数组或字符串,它以第 1 个元素开头,并以第 5 个元素结尾。

范例:格式化日期

下面的例子显示了两种不同格式(​'shortDate'​ 和 ​'fullDate'​)之间的切换:

  • 该 ​app.component.html​ 模板使用 ​DatePipe ​(名为 ​date​)的格式参数把日期显示为 04/15/88 。
  • hero-birthday2.component.ts​ 组件把该管道的 format 参数绑定到 ​template ​中组件的 ​format ​属性,并添加了一个按钮,其 click 事件绑定到了该组件的 ​toggleFormat()​ 方法。
  • hero-birthday2.component.ts​ 组件的 ​toggleFormat() ​方法会在短格式(​'shortDate'​)和长格式(​'fullDate'​)之间切换该组件的 ​format ​属性。

  • src/app/app.component.html
  • <p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>
  • src/app/hero-birthday2.component.ts (template)
  • template: `
      <p>The hero's birthday is {{ birthday | date:format }}</p>
      <button (click)="toggleFormat()">Toggle Format</button>
    `
  • src/app/hero-birthday2.component.ts (class)
  • export class HeroBirthday2Component {
      birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
      toggle = true; // start with true == shortDate
    
      get format()   { return this.toggle ? 'shortDate' : 'fullDate'; }
      toggleFormat() { this.toggle = !this.toggle; }
    }

点击 Toggle Format 按钮可以在 04/15/1988 和 Friday, April 15, 1988 之间切换日期格式。

范例:通过串联管道应用两种格式

可以对管道进行串联,以便一个管道的输出成为下一个管道的输入。

在下面的范例中,串联管道首先将格式应用于一个日期值,然后将格式化之后的日期转换为大写字符。 ​src/app/app.component.html​ 模板的第一个标签页把 ​DatePipe ​和 ​UpperCasePipe ​的串联起来,将其显示为 APR 15, 1988。​src/app/app.component.html​ 模板的第二个标签页在串联 ​uppercase ​之前,还把 ​fullDate ​参数传给了 ​date​,将其显示为 FRIDAY, APRIL 15, 1988

  • src/app/app.component.html (1)
  • The chained hero's birthday is
    {{ birthday | date | uppercase}}
  • src/app/app.component.html (2)
  • The chained hero's birthday is
    {{  birthday | date:'fullDate' | uppercase}}

为自定义数据转换创建管道

创建自定义管道来封装那些内置管道没有提供的转换。然后就可以在模板表达式中使用自定义管道了,像内置管道一样,把输入值转换成显示输出。

把一个类标记为一个管道

要把类标记为管道并提供配置元数据,请把 ​@Pipe​ 装饰器应用到这个类上。管道类名是 ​UpperCamelCase​(类名的一般约定),相应的 ​name ​字符串是 ​camelCase ​的。不要在 ​name ​中使用连字符。

在模板表达式中使用 ​name ​就像在内置管道中一样。

  • 把你的管道包含在 ​NgModule ​元数据的 ​declarations ​字段中,以便它能用于模板。请查看范例应用中的 ​app.module.ts​ 文件(现场演练 /  下载范例)。
  • 注册自定义管道。​Angular CLI​ 的 ​ng generate pipe​ 命令会自动注册该管道。

使用 PipeTransform 接口

在自定义管道类中实现 ​PipeTransform ​接口来执行转换。

Angular 调用 ​transform ​方法,该方法使用绑定的值作为第一个参数,把其它任何参数都以列表的形式作为第二个参数,并返回转换后的值。

范例:指数级转换

在游戏中,可能希望实现一种指数级转换,以指数级增加英雄的力量。例如,如果英雄的得分是 2,那么英雄的能量会指数级增长 10 次,最终得分为 1024。你可以使用自定义管道进行这种转换。

下列代码范例显示了两个组件定义:

  • exponential-strength.pipe.ts​ 通过一个执行转换的 ​transform ​方法定义了一个名为 ​exponentialStrength ​的自定义管道。它为传给管道的参数定义了 ​transform ​方法的一个参数(​exponent​)。
  • power-booster.component.ts​ 组件演示了如何使用该管道,指定了一个值( 2 )和一个 exponent 参数( 10 )。

  • src/app/exponential-strength.pipe.ts
  • 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 = 1): number {
        return Math.pow(value, exponent);
      }
    }
  • src/app/power-booster.component.ts
  • import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-power-booster',
      template: `
        <h2>Power Booster</h2>
        <p>Super power boost: {{2 | exponentialStrength: 10}}</p>
      `
    })
    export class PowerBoosterComponent { }

浏览器显示如下:

Power Booster

Superpower boost: 1024

要检查 ​exponentialStrength ​管道的行为,请查看现场演练 / 下载范例,并在模板中修改值和可选的指数参数。

通过管道中的数据绑定来检测变更

你可以通过带有管道的数据绑定来显示值并响应用户操作。如果是原始类型的输入值,比如 ​String ​或 ​Number ​,或者是对象引用型的输入值,比如 ​Date ​或 ​Array ​,那么每当 Angular 检测到输入值或引用有变化时,就会执行该输入管道。

比如,你可以修改前面的自定义管道范例,通过 ​ngModel ​的双向绑定来输入数量和提升因子,如下面的代码范例所示。

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

@Component({
  selector: 'app-power-boost-calculator',
  template: `
    <h2>Power Boost Calculator</h2>
    <label for="power-input">Normal power: </label>
    <input id="power-input" type="text" [(ngModel)]="power">
    <label for="boost-input">Boost factor: </label>
    <input id="boost-input" type="text" [(ngModel)]="factor">
    <p>
      Super Hero Power: {{power | exponentialStrength: factor}}
    </p>
  `,
  styles: ['input {margin: .5rem 0;}']
})
export class PowerBoostCalculatorComponent {
  power = 5;
  factor = 1;
}

每当用户改变 “normal power” 值或 “boost factor” 时,就会执行 ​exponentialStrength ​管道。

Angular 会检测每次变更,并立即运行该管道。对于原始输入值,这很好。但是,如果要在复合对象中更改某些内部值(例如日期中的月份、数组中的元素或对象中的属性),就需要了解变更检测的工作原理,以及如何使用 ​impure​(非纯)管道。

变更检测的工作原理

Angular 会在每次 DOM 事件(每次按键、鼠标移动、计时器滴答和服务器响应)之后运行的变更检测过程中查找对数据绑定值的更改。下面这段不使用管道的例子演示了 Angular 如何利用默认的变更检测策略来监控和更新 ​heroes ​数组中每个英雄的显示效果。范例显示如下:

  • 在 ​flying-heroes.component.html (v1)​ 模板中, ​*ngFor​ 会重复显示英雄的名字。
  • 与之相伴的组件类 ​flying-heroes.component.ts (v1)​ 提供了一些英雄,把这些英雄添加到数组中,并重置了该数组。

  • src/app/flying-heroes.component.html (v1)
  • <label for="hero-name">New hero name: </label>
    <input type="text" #box
           id="hero-name"
           (keyup.enter)="addHero(box.value); box.value=''"
           placeholder="hero name">
    <button (click)="reset()">Reset list of heroes</button>
      <div *ngFor="let hero of heroes">
        {{hero.name}}
      </div>
  • src/app/flying-heroes.component.ts (v1)
  • export class FlyingHeroesComponent {
      heroes: any[] = [];
      canFly = true;
      constructor() { this.reset(); }
    
      addHero(name: string) {
        name = name.trim();
        if (!name) { return; }
        const hero = {name, canFly: this.canFly};
        this.heroes.push(hero);
      }
    
      reset() { this.heroes = HEROES.slice(); }
    }

每次用户添加一个英雄时,Angular 都会更新显示内容。如果用户点击了 Reset 按钮,Angular 就会用原来这些英雄组成的新数组来替换 ​heroes ​,并更新显示。如果你添加删除或更改了某个英雄的能力,Angular 也会检测这些变化并更新显示。

然而,如果对于每次更改都执行一个管道来更新显示,就会降低你应用的性能。因此,Angular 会使用更快的变更检测算法来执行管道,如下一节所述。

检测原始类型和对象引用的纯变更

通过默认情况下,管道会定义成纯的(pure),这样 Angular 只有在检测到输入值发生了纯变更时才会执行该管道。纯变更是对原始输入值(比如 ​String​、​Number​、​Boolean ​或 ​Symbol ​)的变更,或是对对象引用的变更(比如 ​Date​、​Array​、​Function​、​Object​)。

纯管道必须使用纯函数,它能处理输入并返回没有副作用的值。换句话说,给定相同的输入,纯函数应该总是返回相同的输出。

使用纯管道,Angular 会忽略复合对象中的变化,例如往现有数组中新增的元素,因为检查原始值或对象引用比对对象中的差异进行深度检查要快得多。Angular 可以快速判断是否可以跳过执行该管道并更新视图。

但是,以数组作为输入的纯管道可能无法正常工作。为了演示这个问题,修改前面的例子来把英雄列表过滤成那些会飞的英雄。在 ​*ngFor​ 中使用 ​FlyingHeroesPipe ​,代码如下。这个例子的显示如下:

  • 带有新管道的模板(​flying-heroes.component.html (flyers)​)。
  • FlyingHeroesPipe ​自定义管道实现(​flying-heroes.pipe.ts​)。

  • src/app/flying-heroes.component.html (flyers)
  • <div *ngFor="let hero of (heroes | flyingHeroes)">
      {{hero.name}}
    </div>
  • src/app/flying-heroes.pipe.ts
  • import { Pipe, PipeTransform } from '@angular/core';
    
    import { Hero } from './heroes';
    
    @Pipe({ name: 'flyingHeroes' })
    export class FlyingHeroesPipe implements PipeTransform {
      transform(allHeroes: Hero[]) {
        return allHeroes.filter(hero => hero.canFly);
      }
    }

该应用现在展示了意想不到的行为:当用户添加了会飞的英雄时,它们都不会出现在 “Heroes who fly” 中。发生这种情况是因为添加英雄的代码会把它 push 到 ​heroes ​数组中:

this.heroes.push(hero);

而变更检测器会忽略对数组元素的更改,所以管道不会运行。

Angular 忽略了被改变的数组元素的原因是对数组的引用没有改变。由于 Angular 认为该数组仍是相同的,所以不会更新其显示。

获得所需行为的方法之一是更改对象引用本身。可以用一个包含新更改过的元素的新数组替换该数组,然后把这个新数组作为输入传给管道。在上面的例子中,你可以创建一个附加了新英雄的数组,并把它赋值给 ​heroes​。 Angular 检测到了这个数组引用的变化,并执行了该管道。

总结一下,如果修改了输入数组,纯管道就不会执行。如果替换了输入数组,就会执行该管道并更新显示。

上述例子演示了如何更改组件的代码来适应某个管道。

为了让你的组件更简单,独立于那些使用管道的 HTML,你可以用一个不纯的管道来检测复合对象(如数组)中的变化,如下一节所述。

检测复合对象中的非纯变更

要在复合对象内部进行更改后执行自定义管道(例如更改数组元素),就需要把管道定义为 ​impure ​以检测非纯的变更。每当按键或鼠标移动时,Angular 都会检测到一次变更,从而执行一个非纯管道。

虽然非纯管道很实用,但要小心使用。长时间运行非纯管道可能会大大降低你的应用速度。

通过把 ​pure ​标志设置为 ​false ​来把管道设置成非纯的:

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

下面的代码显示了 ​FlyingHeroesImpurePipe ​的完整实现,它扩展了 ​FlyingHeroesPipe ​以继承其特性。这个例子表明你不需要修改其他任何东西 - 唯一的区别就是在管道元数据中把 ​pure ​标志设置为 ​false ​。

  • src/app/flying-heroes.pipe.ts (FlyingHeroesImpurePipe)
  • @Pipe({
      name: 'flyingHeroesImpure',
      pure: false
    })
    export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
  • src/app/flying-heroes.pipe.ts (FlyingHeroesPipe)
  • import { Pipe, PipeTransform } from '@angular/core';
    
    import { Hero } from './heroes';
    
    @Pipe({ name: 'flyingHeroes' })
    export class FlyingHeroesPipe implements PipeTransform {
      transform(allHeroes: Hero[]) {
        return allHeroes.filter(hero => hero.canFly);
      }
    }

对于非纯管道,​FlyingHeroesImpurePipe ​是个不错的选择,因为它的 ​transform ​函数非常简单快捷:

return allHeroes.filter(hero => hero.canFly);

你可以从 ​FlyingHeroesComponent ​派生一个 ​FlyingHeroesImpureComponent​。如下面的代码所示,只有模板中的管道发生了变化。

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

要想确认是否在用户添加英雄时更新了显示,请参阅现场演练 / 下载范例

从一个可观察对象中解包数据

可观察对象能让你在应用的各个部分之间传递消息。建议在事件处理、异步编程以及处理多个值时使用这些可观察对象。可观察对象可以提供任意类型的单个或多个值,可以是同步的(作为一个函数为它的调用者提供一个值),也可以是异步的。

使用内置的 ​AsyncPipe ​接受一个可观察对象作为输入,并自动订阅输入。如果没有这个管道,你的组件代码就必须订阅这个可观察对象来使用它的值,提取已解析的值、把它们公开进行绑定,并在销毁这段可观察对象时取消订阅,以防止内存泄漏。 ​AsyncPipe ​是一个非纯管道,可以节省组件中的样板代码,以维护订阅,并在数据到达时持续从该可观察对象中提供值。

下列代码范例使用 ​async ​管道将带有消息字符串( ​message$​ )的可观察对象绑定到视图中。

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

import { Observable, interval } from 'rxjs';
import { map, take } from 'rxjs/operators';

@Component({
  selector: 'app-hero-async-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>;

  private messages = [
    'You are my hero!',
    'You are the best hero!',
    'Will you be my hero?'
  ];

  constructor() {
    this.message$ = this.getResendObservable();
  }

  resend() {
    this.message$ = this.getResendObservable();
  }

  private getResendObservable() {
    return interval(500).pipe(
      map(i => this.messages[i]),
      take(this.messages.length)
    );
  }
}

缓存 HTTP 请求

为了使用 HTTP 与后端服务进行通信,​HttpClient ​服务使用了可观察对象,并提供了 ​HttpClient.get()​ 方法来从服务器获取数据。这个异步方法会发送一个 HTTP 请求,并返回一个可观察对象,它会发出请求到的响应数据。

如 ​AsyncPipe ​所示,你可以使用非纯管道 ​AsyncPipe ​接受一个可观察对象作为输入,并自动订阅输入。你也可以创建一个非纯管道来建立和缓存 HTTP 请求。

每当组件运行变更检测时就会调用非纯管道,这可能每隔几毫秒就运行一次。为避免出现性能问题,只有当请求的 URL 发生变化时才会调用该服务器(如下例所示),并使用该管道缓存服务器的响应。显示如下:

  • fetch ​管道( ​fetch-json.pipe.ts​ )。
  • 一个用于演示该请求的挽具组件(​hero-list.component.ts​),它使用一个模板,该模板定义了两个到该管道的绑定,该管道会向 ​heroes.json​ 文件请求英雄数组。第二个绑定把 ​fetch ​管道与内置的 ​JsonPipe ​串联起来,以 JSON 格式显示同一份英雄数据。

  • src/app/fetch-json.pipe.ts
  • import { HttpClient } from '@angular/common/http';
    import { Pipe, PipeTransform } from '@angular/core';
    
    @Pipe({
      name: 'fetch',
      pure: false
    })
    export class FetchJsonPipe implements PipeTransform {
      private cachedData: any = null;
      private cachedUrl = '';
    
      constructor(private http: HttpClient) { }
    
      transform(url: string): any {
        if (url !== this.cachedUrl) {
          this.cachedData = null;
          this.cachedUrl = url;
          this.http.get(url).subscribe(result => this.cachedData = result);
        }
    
        return this.cachedData;
      }
    }
  • src/app/hero-list.component.ts
  • import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-hero-list',
      template: `
        <h2>Heroes from JSON File</h2>
    
        <div *ngFor="let hero of ('assets/heroes.json' | fetch) ">
          {{hero.name}}
        </div>
    
        <p>Heroes as JSON:
          {{'assets/heroes.json' | fetch | json}}
        </p>`
    })
    export class HeroListComponent { }

在上面的例子中,管道请求数据时的剖面展示了如下几点:

  • 每个绑定都有自己的管道实例。
  • 每个管道实例都会缓存自己的 URL 和数据,并且只调用一次服务器。

fetch ​和 ​fetch-json​ 管道会像这样在浏览器中显示英雄:

Heroes from JSON File

Windstorm
Bombasto
Magneto
Tornado

Heroes as JSON: [ { "name": "Windstorm", "canFly": true }, { "name": "Bombasto", "canFly": false }, { "name": "Magneto", "canFly": false }, { "name": "Tornado", "canFly": true } ]

内置的 ​JsonPipe ​提供了一种方法来诊断一个离奇失败的数据绑定,或用来检查一个对象是否能用于将来的绑定。

管道的优先级

管道操作符要比三目运算符(​?:​)的优先级高,这意味着 ​a ? b : c | x​ 会被解析成 ​a ? b : (c | x)​。

由于这种优先级设定,如果你要用管道处理三目元算符的结果,就要把整个表达式包裹在括号中,比如 ​(a ? b : c) | x​。

<!-- use parentheses in the third operand so the pipe applies to the whole expression -->
{{ (true ? 'true' : 'false') | uppercase }}