每个应用程序都以一个简单的任务开始:获取数据,转换它们,并将它们展示给用户。 获取数据可以像创建本地变量一样简单,也可以像通过WebSocket传输流数据一样复杂。
一旦数据到达,您可以将其原始的toString值直接推送到视图中,但这很少能提供良好的用户体验。 例如,在大多数使用情况下,用户更喜欢以1988年4月15日这样的简单格式查看日期,而不是原始字符串格式Fri Apr 15 1988 00:00:00 GMT-0700(太平洋夏令时)。
显然,一些值可以从一些编辑中受益。 您可能会注意到,您希望在许多应用程序内部和许多应用程序中重复执行许多相同的转换。 你几乎可以把它们想象成风格。 事实上,您可能会喜欢将它们应用到HTML模板中,就像样式一样。
介绍Angular管道,这是一种编写显示值转换的方法,您可以在HTML中声明这些转换。 尝试一下实例(查看源代码)。
使用管道
管道将数据作为输入并将其转换为所需的输出。 在此页面中,您将使用管道将组件的生日属性转换为人性化的日期。
lib/src/hero_birthday1_component.dart
import 'package:angular/angular.dart';
@Component(
selector: 'hero-birthday',
template: "<p>The hero's birthday is {{ birthday | date }}</p>",
pipes: const [COMMON_PIPES],
)
class HeroBirthdayComponent {
DateTime birthday = new DateTime(1988, 4, 15); // April 15, 1988
}
关注组件的模板。
<p>The hero's birthday is {{ birthday | date }}</p>
在插值表达式中,通过管道运算符(|)将组件的生日值传递给右侧的日期管道函数。 所有管道都是这样工作的。
Date(日期)和Currency(货币)管道需要ECMAScript国际化API。 Safari和其他旧版浏览器不支持它。 您可以使用polyfill添加支持。
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>
内置管道
Angular附带一系列管道,如DatePipe,UpperCasePipe,LowerCasePipe,CurrencyPipe,PercentPipe。它们都可用于任何模板。
在API参考的管道主题中了解更多关于这些和许多其他内置管道的信息; 过滤包含单词“管道”的条目。
由于本页附录中解释了Angular没有FilterPipe或OrderByPipe的原因。
参数化管道
管道可以接受任意数量的可选参数来微调其输出。 要向管道添加参数,请使用冒号(:)跟随管道名称,然后使用参数值(例如currency:"EUR")。 如果管道接受多个参数,请使用冒号分隔值(如slice:1:5)
修改生日模板以给日期管道一个格式参数。 格式化英雄的生日后,它呈现为04/15/88:
<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>
参数值可以是任何有效的模板表达式(请参阅模板语法页面的模板表达式部分),例如字符串文字或组件属性。 换句话说,您可以通过绑定来控制格式,就像您通过绑定控制生日值一样。
编写第二个组件,将管道的格式参数绑定到组件的format属性。 这是该组件的模板:
lib/src/hero_birthday2_component.dart (template)
template: '''
<p>The hero's birthday is {{ birthday | date:format }}</p>
<button (click)="toggleFormat()">Toggle Format</button>
''',
您还向模板添加了一个按钮,并将其单击事件绑定到组件的toggleFormat()方法。 该方法在短格式("shortDate")和较长格式("fullDate")之间切换组件的format属性。
lib/src/hero_birthday2_component.dart (class)
class HeroBirthday2Component {
DateTime birthday = new DateTime(1988, 4, 15); // April 15, 1988
bool toggle = true;
get format => toggle ? 'shortDate' : 'fullDate';
void toggleFormat() {
toggle = !toggle;
}
}
当您点击该按钮时,显示的日期在“04/15/1988”和“Friday, April 15, 1988”之间交替。
在Date Pipe API Reference页面阅读有关DatePipe格式选项的更多信息。
链接管道
您可以将管道连接成可能有用的组合。 在以下示例中,要以大写形式显示生日,生日将链接到DatePipe并连接到UpperCasePipe。 生日显示为APR 15, 1988。
The chained hero's birthday is
{{ birthday | date | uppercase}}
这个例子显示了FRIDAY, APRIL 15, 1988,它链接上面的相同管道,但是还传递了一个参数。
The chained hero's birthday is
{{ birthday | date:'fullDate' | uppercase}}
自定义管道
您可以编写自己的自定义管道。 这是一个名为ExponentialStrengthPipe的自定义管道,可以提升英雄的力量:
lib/src/exponential_strength_pipe.dart
import 'dart:math' as math;
import 'package:angular/angular.dart';
/*
* Raise the value exponentially
* Takes an exponent argument that defaults to 1.
* Usage:
* value | exponentialStrength:exponent
* Example:
* {{ 2 | exponentialStrength:10}}
* formats to: 1024
*/
@Pipe('exponentialStrength')
class ExponentialStrengthPipe extends PipeTransform {
num transform(num value, num exponent) => math.pow(value ?? 0, exponent ?? 1);
}
这个管道定义揭示了以下关键点:
- 管道是用@Pipe元数据注解的类。
- 管道类实现了PipeTransform接口的transform方法,该方法接受一个输入值,后跟一个可选参数并返回转换后的值。
- 对于传递给管道的每个参数,transform方法都会有一个额外的参数。 你的管道有一个这样的参数:exponent。
- 为了告诉Angular这是一个管道,应用从主Angular库导入的@Pipe注解。
- @Pipe注解允许您定义将在模板表达式中使用的管道名称。 它必须是有效的Dart标识符。 你的管道名称是exponentialStrength。
PipeTransform接口
transform方法对于管道是必不可少的。 PipeTransform接口定义该方法并指导工具和编译器。 从技术上讲,这是可选的; 无论角度如何,Angular都会查找并执行transform方法。
现在您需要一个组件来演示管道。
lib/src/power_booster_component.dart
import 'package:angular/angular.dart';
import 'exponential_strength_pipe.dart';
@Component(
selector: 'power-booster',
template: '''
<h2>Power Booster</h2>
<p>Super power boost: {{2 | exponentialStrength: 10}}</p>
''',
pipes: const [ExponentialStrengthPipe])
class PowerBoosterComponent {}
请注意以下几点:
- 您可以像使用内置管道一样使用自定义管道。
- 您必须将自定义管道包含在@Component的pipes列表中。
记住管道列表
您必须手动注册自定义管道。 如果您不这样做,Angular会报告错误。 在前面的例子中,你没有列出DatePipe,因为所有的Angular内置管道都是预先注册的。
要在实例中查看行为(查看源代码),请更改模板中的值和可选的指数。
功率提升计算器
更新模板以测试自定义管道并不是很有趣。 将示例升级到“Power Boost Calculator”,它使用ngModel将您的管道和双向数据绑定相结合。
lib/src/power_boost_calculator_component.dart
import 'package:angular/angular.dart';
import 'package:angular_forms/angular_forms.dart';
import 'exponential_strength_pipe.dart';
@Component(
selector: 'power-boost-calculator',
template: '''
<h2>Power Boost Calculator</h2>
<div>Normal power: <input type="number" [(ngModel)]="power"/></div>
<div>Boost factor: <input type="number" [(ngModel)]="factor"/></div>
<p>
Super Hero Power: {{power | exponentialStrength: factor}}
</p>
''',
directives: const [CORE_DIRECTIVES, formDirectives],
pipes: const [ExponentialStrengthPipe],
)
class PowerBoostCalculatorComponent {
num power = 5;
num factor = 1;
}
管道和变化检测
Angular通过在每个DOM事件之后运行的更改检测过程查找数据绑定值的更改:每次击键,鼠标移动,计时器滴答和服务器响应。 这可能是昂贵的。 Angular努力尽可能降低成本并适当。
当您使用管道时,Angular会选择更简单,更快速的变更检测算法。
不使用管道
在下一个示例中,组件使用默认的积极变化检测策略来监控并更新其hero列表中每个英雄的显示。 这是模板:
lib/src/flying_heroes_component.html (v1)
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>
伴随组件类提供英雄,将英雄添加到列表中,并可以重置列表。
lib/src/flying_heroes_component.dart (v1)
class FlyingHeroesComponent {
List<Hero> heroes;
bool canFly = true;
FlyingHeroesComponent() {
reset();
}
void addHero(String name) {
name = name.trim();
if (name.isEmpty) return;
var hero = new Hero(name, canFly);
heroes.add(hero);
}
void reset() {
heroes = new List<Hero>.from(mockHeroes);
}
}
你可以添加英雄和Angular更新显示。 如果你点击reset按钮,Angular用原有英雄的新列表替换heroes并更新显示。 如果您添加了删除或更改英雄的功能,Angular会检测这些更改并更新显示。
飞行英雄管道
将一个FlyingHeroesPipe添加到*ngFor迭代器,该迭代器将英雄列表过滤到只能飞行的英雄。
lib/src/flying_heroes_component.html (flyers)
<div *ngFor="let hero of (heroes | flyingHeroes)">
{{hero.name}}
</div>
这是FlyingHeroesPipe实现,它遵循前面描述的自定义管道模式。
lib/src/flying_heroes_pipe.dart (pure)
import 'package:angular/angular.dart';
import 'heroes.dart';
@Pipe('flyingHeroes')
class FlyingHeroesPipe extends PipeTransform {
List<Hero> transform(List<Hero> value) =>
value.where((hero) => hero.canFly).toList();
}
请注意实例中的奇怪行为(查看源代码):添加飞行英雄时,它们都不会显示在“飞翔的英雄”下。
虽然你没有得到你想要的行为,但Angular并没有被破坏。 它只是使用不同的变更检测算法,忽略对列表或其任何项目的更改。
注意如何添加一个英雄:
heroes.add(hero);
您将英雄添加到英雄列表中。 对列表的引用没有改变。 这是同一个列表。 这都是Angular关心的。 从它的角度来看,同样的列表,没有变化,没有显示更新。
为了解决这个问题,创建一个新的英雄列表并将其分配给heroes。 这次Angular检测到列表引用已经改变。 它执行管道并用新的列表更新显示,其中包括新的飞行英雄。
如果您更改列表,则不会调用管道,并且不会更新显示; 如果您替换列表,管道将执行并更新显示。 Flying Heroes应用程序通过复选框开关和附加显示扩展代码,以帮助您体验这些效果。
替换列表是发信号通知Angular更新显示的有效方式。 你什么时候更换清单? 数据发生变化时。 在这个例子中,这是一个简单的规则,其中更改数据的唯一方法是添加一个英雄。
更常见的情况是,您不知道数据何时发生变化,特别是在以多种方式变异数据的应用程序中,可能在远离应用程序的位置。 这样的应用程序中的组件通常无法了解这些更改。 此外,篡改组件设计以适应管道是不明智的。 努力保持组件类独立于HTML。 组件应该不知道管道。
为了过滤飞行英雄,请考虑一个不纯的管道。
纯净和不纯的管道
有两类管道:纯净和不纯。 管道默认是纯净的。 到目前为止,你看到的每个管道都是纯净的。 通过将pure设置为false,可以使管道不纯。 你可以让FlyingHeroesPipe不纯像这样:
@Pipe('flyingHeroes', pure: false)
在此之前,先了解纯净和不纯的区别,从纯净的管道开始。
纯净的管道
仅当Angular检测到对输入值的纯粹更改时才执行纯管道。 在AngularDart中,纯粹的改变仅仅来自对象引用的改变(假设所有东西都是Dart中的对象)。
Angular忽略(复合)对象内的更改。 如果您更改输入月份,添加到输入列表或更新输入对象属性,它将不会调用纯管道。
这看起来很有限制,但速度也很快。 对象引用检查的速度比深入检查差异要快得多 - 所以Angular可以快速确定它是否可以跳过管道执行和视图更新。
出于这个原因,如果您可以接受变更检测策略,则最好使用纯净的管道。 当你不能时,你可以使用不纯的管道。
或者你可能根本不使用管道。 用组件的属性来追求管道的目的可能会更好,这点在本页稍后会讨论。
不纯的管道
Angular在每个组件更改检测周期执行不纯管道。 经常调用不纯的管道,就像每次按键或鼠标移动一样。
考虑到这一点,谨慎使用不纯管道。 昂贵的,长期运行的管道可能会破坏用户体验。
不纯的FlyingHeroesPipe
翻转开关将FlyingHeroesPipe变成FlyingHeroesImpurePipe。 完整的实现如下:
lib/src/flying_heroes_pipe.dart (impure)
@Pipe('flyingHeroes', pure: false)
class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
lib/src/flying_heroes_pipe.dart (pure)
import 'package:angular/angular.dart';
import 'heroes.dart';
@Pipe('flyingHeroes')
class FlyingHeroesPipe extends PipeTransform {
List<Hero> transform(List<Hero> value) =>
value.where((hero) => hero.canFly).toList();
}
您从FlyingHeroesPipe继承以证明内部没有任何变化。 唯一的区别是管道元数据中的纯标志。
对于不纯的管道来说,这是一个很好的选择,因为转换函数很简单快捷。
List<Hero> transform(List<Hero> value) =>
value.where((hero) => hero.canFly).toList();
您可以从FlyingHeroesComponent派生FlyingHeroesImpureComponent。
lib/src/flying_heroes_component.dart (impure component)
@Component(
selector: 'flying-heroes-impure',
templateUrl: 'flying_heroes_component.html',
pipes: const [FlyingHeroesImpurePipe],
directives: const [CORE_DIRECTIVES, formDirectives],
)
class FlyingHeroesImpureComponent extends FlyingHeroesComponent {
FlyingHeroesImpureComponent() {
title = 'Flying Heroes (impure pipe)';
}
}
唯一的实质性变化是模板中的管道。 您可以在实例(查看源代码)中确认,当您添加英雄时,即使您变更heroes列表,飞行英雄也会显示更新。
不纯的AsyncPipe
Angular AsyncPipe是一个不纯管道的有趣例子。 AsyncPipe接受Future或Stream作为输入并自动订阅输入,最终返回发出的值。
AsyncPipe也是有状态的。 管道保持对输入Stream的订阅,并在到达时保持该Stream的值。
下一个示例使用异步管道将消息字符串(message)Stream绑定到视图。
lib/src/hero_async_message_component.dart
import 'dart:async';
import 'package:angular/angular.dart';
@Component(
selector: 'hero-message',
template: '''
<h2>Async Hero Message and AsyncPipe</h2>
<p>Message: {{ message | async }}</p>
<button (click)="resend()">Resend</button>
''',
pipes: const [COMMON_PIPES],
)
class HeroAsyncMessageComponent {
static const _msgEventDelay = const Duration(milliseconds: 500);
Stream<String> message;
HeroAsyncMessageComponent() {
resend();
}
void resend() {
message =
new Stream.periodic(_msgEventDelay, (i) => _msgs[i]).take(_msgs.length);
}
List<String> _msgs = <String>[
'You are my hero!',
'You are the best hero!',
'Will you be my hero?'
];
}
异步管道将样板文件保存在组件代码中。 该组件不必订阅异步数据源,提取已解析的值并将其公开以进行绑定,并且必须在其销毁时取消订阅(内存泄漏的有效来源)。
不纯的缓存管道
再写一个不纯的管道,一个发出HTTP请求的管道。
请记住,每隔几毫秒就会调用不纯的管道。 如果你不注意,这个管道将用请求折腾服务器。
在以下代码中,管道只在请求URL发生更改和缓存服务器响应时调用服务器。
lib/src/fetch_json_pipe.dart
import 'dart:convert';
import 'dart:html';
import 'package:angular/angular.dart';
@Pipe('fetch', pure: false)
class FetchJsonPipe extends PipeTransform {
dynamic _cachedData;
String _cachedUrl;
dynamic transform(String url) {
if (url != _cachedUrl) {
_cachedUrl = url;
_cachedData = null;
HttpRequest.getString(url).then((s) {
_cachedData = JSON.decode(s);
});
}
return _cachedData;
}
}
现在在一个线束组件中演示它,该组件的模板定义了对这个管道的两个绑定,都请求heroes.json文件中的heroes。
lib/src/hero_list_component.dart
import 'package:angular/angular.dart';
import 'fetch_json_pipe.dart';
@Component(
selector: 'hero-list',
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>
''',
directives: const [CORE_DIRECTIVES],
pipes: const [COMMON_PIPES, FetchJsonPipe])
class HeroListComponent {}
该组件呈现如下:
管道的数据请求断点显示如下:
- 每个绑定都有自己的管道实例。
- 每个管道实例都缓存自己的URL和数据。
- 每个管道实例只调用一次服务器。
JsonPipe
在前面的代码示例中,第二个提取管道绑定显示了更多的管道链接。 它通过链接到内置的JsonPipe以JSON格式显示相同的英雄数据。
使用JsonPipe进行调试:JsonPipe提供了一种简单的方法来诊断离奇失败的数据绑定,或者检查未来绑定的对象。
纯净的管道和纯粹的功能
纯管道使用纯功能。 纯函数处理输入并返回值,但没有可检测到的副作用。 给定相同的输入,他们应该总是返回相同的输出。
本页前面讨论的管道是用纯函数实现的。 内置的DatePipe是一个纯函数实现的纯管道。 ExponentialStrengthPipe和FlyingHeroesPipe也是如此。 回过头来,你回顾了FlyingHeroesImpurePipe--一个纯粹功能的不纯管道。
总是要实现一个纯函数的纯管道。 否则,你会看到很多关于表达式被检查后改变的控制台错误。
下一步
管道是封装和共享常见显示值转换的好方法。 像样式一样使用它们,将它们放入模板表达式中,以丰富视图的吸引力和可用性。
在API参考中探索Angular的内置管道库。 尝试编写一个自定义管道,并可能将其贡献给社区。
附录:无FilterPipe或OrderByPipe
Angular不提供过滤或排序列表的管道。 熟悉Angular 1的开发人员将这些知识视为filter和orderBy。 Angular中没有等价物。
这不是一个疏忽。 Angular不提供这样的管道,因为它们表现不佳,并且避免操控性变弱。 filter和orderBy都需要引用对象属性的参数。 在本页面的前面,您了解到这些管道必须是不纯的,并且Angular在几乎每个变更检测周期都会调用不纯的管道。
过滤和特殊分类是昂贵的操作。 当Angular每秒钟多次调用这些管道方法时,即使是中等大小的列表,用户体验也会严重降级。 filter和orderBy经常被滥用在Angular 1应用程序中,导致投诉Angular本身很慢。从间接的意义上说,Angular 1通过首先提供filter和orderBy来准备这个性能陷阱是公平的。
如果不那么明显,缩小危险也是令人信服的。 想象一下,排序管道应用于英雄列表。 该列表可能按以下方式按英雄name和planet属性排序:
<!-- NOT REAL CODE! -->
<div *ngFor="let hero of heroes | orderBy:'name,planet'"></div>
您通过文本字符串来识别排序字段,期望管道通过索引引用属性值(如hero ["name"])。不幸的是,主动减少操纵Hero属性名称让Hero.name和Hero.planet成为Hero.a和Hero.b. 显然 hero[”name“] 不起作用。
虽然有些人可能并不在意这种积极的态度,但Angular的产品不应该阻止任何人积极贬低。 因此,Angular团队决定Angular提供的所有内容都将安全地缩小。
Angular团队和许多经验丰富的Angular开发人员强烈建议将过滤和排序逻辑移植到组件本身中。 该组件可以公开一个filteredHeroes或sortedHeroes属性,并控制执行支持逻辑的时间和频率。 您可以在管道中放置并在应用程序中共享的任何功能都可以写入过滤/排序服务并注入到组件中。
如果这些性能和缩小比例考虑不适用于您,您可以随时创建自己的这种管道(类似于FlyingHeroesPipe)或在社区中找到它们。