Angular 18+ 高级教程 – Component 组件 の Pipe 管道
介绍
Pipe 类似于 Template Syntax,它的用途是 transform value for display。
参考:
DatePipe
一个简单的例子。
我有一个 JavaScript 的 Date 对象,我要 display 给用户看,那你不可能直接 .toString() 就显示嘛,很丑丫。
所以呢,它就需要一个 transformation。而按照职责划分 (Thinking in Angular Way),这个任务显然是属于 view 的 logic。
所以应该在 HTML 去表达,于是就有了 Pipe 写法。
<h1>{{ today }}</h1> <h1>{{ today | date : 'dd MMM yyyy' }}</h1> <h1>{{ today | date : 'fullDate' }}</h1>
pipe 的语法是这样的 {{ value | pipeName : paramter1 : paramter2 }}
你可以把它看作是一个 function call。
比如现在我们使用的 DatePipe,就是 const displayDate = date(today , 'dd MMM yyyy');
today 是 component property,是一个 new Date()
| pipe 就是启动 pipe transform。
date 是 Angular build-in 的 DatePipe,Angular build-in 了许多 pipe,每一个负责不同的 transform,顾名思义 DatePipe 自然是用于 transform date value。
注:要使用 Angular build-in 的 Pipe,必须在 Component metadata imports CommonModule 哦。
: 分号表示要输入 paramters
'dd MMM yyyy' 则是输入的 parameter,声明想 transform to 什么 date format。(p.s. 想知道所有 Angular 支持的 format, 参考这里)
效果
About time zone
默认情况下,DatePipe 会使用游览器设置的 time zone,比如上面的 GMT+0800。
这个我们是可以改的。
<p>{{ now | date : 'full' }}</p> <p>{{ now | date : 'full' : '+00:00' }}</p>
效果
注意看它俩的 "小时" 是不同的,一个是 1:33 PM,另一个是 5:33 AM,相差了 8 小时。
这是因为它俩都是 now,完整的时间肯定是一样的,只是由于 time zone 不同,所以 "小时" 在显示上才会差了 8 小时。
总之,看时间必须看完整,包括它的 time zone,这样才是准确的时间。
提醒:DatePipe 只允许设置 time zone offset,而 time zone offset 其实并不一定等价于 time zone (详解看这篇),所以用的时候要特别留意哦。
UpperCasePipe, LowerCasePipe, TitleCase
<h1>{{ 'Hello World' | uppercase }}</h1> <h1>{{ 'Hello World' | lowercase }}</h1> <h1>{{ 'hello world' | titlecase }}</h1>
效果
DecimalPipe
在 JavaScript, 我们可以用下划线来分割 number 让它好看一点
<h1>{{ 1_000_000 }}</h1>
但当渲染 HTML 的时候,它会被 number.toString(),结果变成
DecimalPipe 可用于解决这种问题。
<h1>{{ 1_000_000 | number }}</h1>
注:虽然它叫 DecimalPipe,但使用时是 | number 哦。
效果
除此之外,number 还支持一个 digit setting,
它可以控制小数点前后号码,最少最多几个数字。
语法是
{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}
for example '2.2-4' 表示小数点前最少要 2 digit,小数点后最少 2 最多 4 个 digit。
不够 digit 时它会用零补上,超过 digit 时它会四舍五入进位.
<h1>{{ 1.10 | number : '2.2-4' }}</h1> <!--01.10 前面补上了一个 0 --> <h1>{{ 0.1 | number : '.2-4' }}</h1> <!--0.10 后面补上了一个 0--> <h1>{{ .123 | number : '1.2-4' }}</h1> <!--0.123 前面补上了一个 0--> <h1>{{ .12345 | number : '1.2-4' }}</h1> <!--0.1235 后面四舍五入进位-->
注:它的 default 是 '1.0-3'。另外,小数点前只能控制最少 digit,不能控制最多。
PercentPipe
PercentPipe 的作用时把 0.57 变成 '57%'
<h1>{{ 0.57 | percent }}</h1>
效果
它也支持 digit setting
<h1>{{ 0.575 | percent : '.0' }}</h1> <!--58%-->
p.s. '.0' 相等于 '1.0-0',best practice 是写完整的 '1.0-0'。
CurrencyPipe
CurrentPipe 会 transform number 变成带有 current code。
<h1>{{ 100 | currency : 'USD' }}</h1> <!-- $100.00 默认是显示 symbol 哦 --> <h1>{{ 100 | currency : 'USD' : 'code' }}</h1> <!-- USD100.00 --> <h1>{{ 100 | currency : 'USD' : 'symbol' }}</h1> <!-- $100.00 --> <h1>{{ 100 | currency : 'MYR' : 'symbol-narrow' }}</h1> <!-- RM100.00 --> <h1>{{ 100 | currency : 'MYR' : 'M$ ' }}</h1> <!-- M$100.00 -->
第一个参数是 currency code ISO 4217
第二个参数是 display mode,有好几个选择:
-
'code' 表示显示 currency code
-
'symbol' 表示显示符号就够了。Angular 会把 code 转成对应的 symbol,比如 USD -> $
-
'symbol-narrow' 也是显示 symbol 但是是 "narrow" 版,马来西亚的 symbol-narrow 是 'RM' Ringgit Malaysia 的缩写
-
'whatever string',如果 code, symbol, symbol-narrow 都不是你要的,那可以提供一个 string 作为 display。
第三个参数是 digit setting,默认是 '1.2-2'。
Global override default currency
default currency 是 USD,我们可以通过 Dependancy Injection 的方式提供一个全局 currency 替换掉 default currency。
import { DEFAULT_CURRENCY_CODE, type ApplicationConfig } from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [{ provide: DEFAULT_CURRENCY_CODE, useValue: 'MYR' }], };
JsonPipe, SlicePipe, AsyncPipe
其它 build-in pipes.
<h1>{{ { name: 'Derrick' } | json }}</h1> <!-- { "name": "Derrick" } --> <h1>{{ ['a', 'b', 'c', 'd'] | slice : 1 : -1 }}</h1> <!-- ['b', 'c'] --> <h1>{{ value$ | async }}</h1> <!-- value1---value2---value3 -->
async pipe 常用于处理 Promise、RxJS Stream。
它内部会 subscribe 和自动 unsubscribe Observable 非常方便。
当 Observable 处于未发布的时候,async pipe 会先返回 null 作为初始值。
Global Configuration
上面例子中有些 Pipe 使用时需要传入 parameters。比如 | currency : 'USD', | date : 'dd MMM yyyy'。
许多时候整个项目的 currency、date format 都是一致的。这时就需要一个 global config。
Pipe 的 global config 是利用 DI 来实现的。
如果想设置全局,可以到 app.config.ts 写入 provider 就可以了。
import { DATE_PIPE_DEFAULT_OPTIONS, DatePipeConfig } from '@angular/common'; import { ApplicationConfig, DEFAULT_CURRENCY_CODE } from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [ { provide: DEFAULT_CURRENCY_CODE, useValue: 'USD' }, { provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: { dateFormat: 'dd MMM yyyy', timezone: '+0800', // if empty string, then Angular will use end-user's local system timezone } satisfies DatePipeConfig, }, ], };
DEFAULT_CURRENCY_CODE 和 DATE_PIPE_DEFAULT_OPTIONS 是 InjectionToken。
Use Pipe Transform in Any Where
大部分 Angular pipe 都提供了底层方法,所以可以用在任何地方
import { formatDate, formatCurrency, formatNumber, formatPercent} from '@angular/common'; console.log(formatDate(new Date(), 'dd MMM yyyy', 'en-US')); // 06 Apr 2023 console.log(formatCurrency(100, 'en-US', 'RM', 'MYR')); // RM100.00
当然,它们不在 DI 的 cover 范围,也就无法使用 global config 了。
如果想在组件内使用 pipe,可以通过 DI
DatePipe by default 是没有在 DI 中的,所以我们需要 provide。
可以在 app.config.ts provide,也可以通过 Component metadata provide(区别是什么,我会在接下来章节讲解,这里不展开)
然后注入就可以使用了。这样它就能使用到 global config 了。
Custom Pipe
上面提及的都是 Angular build-in 的 pipe。虽然挺多的,但在真实项目中依然会不够用。
幸好,Angular 允许我们自定义 Pipe。(Angular build-in 的 pipe 也是用同一种方式添加进项目的哦)
我们尝试做一个 AgoPipe
CLI command
ng g p ago
p for pipe
一开始长这样
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'ago', standalone: true }) export class AgoPipe implements PipeTransform { transform(value: unknown, ...args: unknown[]): unknown { return null; } }
接着我们加入逻辑
import { InjectionToken, Pipe, PipeTransform, inject } from '@angular/core'; // 如果有需要 global config export interface AgoPipeConfig {} export const AGO_PIPE_CONFIG_TOKEN = new InjectionToken<AgoPipeConfig>( 'AgoPipeConfig' ); // 底层方法 export function formatAgo( date: Date, param1?: string, config: AgoPipeConfig | null = null ) { console.log([date, param1, config]); // value, parameter, config // do any transformation (这里我省略掉具体实现方法) return '7 days ago'; } @Pipe({ name: 'ago', standalone: true, }) export class AgoPipe implements PipeTransform { private agoPipeConfig = inject(AGO_PIPE_CONFIG_TOKEN, { optional: true }); // 注入 global config transform(date: Date, param1?: string): string { // 调用底层方法 return formatAgo(date, param1, this.agoPipeConfig); } }
看注释理解,里面包含了 global config 和底层方法。类似 Angular 的 formatCurrency 和 DEFAULT_CURRENCY_CODE
在 app.config.ts 提供 global config
import { ApplicationConfig } from '@angular/core'; import { AgoPipeConfig, AGO_PIPE_CONFIG_TOKEN } from './ago.pipe'; export const appConfig: ApplicationConfig = { providers: [ { provide: AGO_PIPE_CONFIG_TOKEN, useValue: {} satisfies AgoPipeConfig }, ], };
在组件 imports AgoPipe
@Component({ selector: 'app-root', standalone: true, imports: [CommonModule, AgoPipe], // imports AgoPipe templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent { constructor() {} today = new Date(); }
BTW, Angular build-in 的 pipe 是封装在 CommonModule 里的。
最后,在 template 使用
<h1>{{ today | ago }}</h1>
这样就可以了。
Pure
@Pipe({ name: 'ago', standalone: true, pure: false })
pure 是 Pipe 的一个 config。默认是 true。它表示只有当 value 改变时才需要重新运行 transform 方法。
这个规则在某一些情况是不对的,比如 transform 依赖 config 的时候。因为不只是 value 改变会影响 transform 的结果,config 改变也可能影响 transform 的结果。
所以在这个情况下,我们需要把 pure 设置成 false。设置成 false 以后,不仅仅是当 value 改变,只要当前的组件 re-render(DoCheck 执行不代表 re-render 哦,要 dirty 才会 re-render),transform 就会执行。
至于这个 re-render 是什么概念,我会在后面 Change Detection 章节详细讲解。
Override Built-in Pipe
built-in pipe 很好,但也不是万能的,遇到不合适的场景还是需要魔改一下。这里提供一些魔改方案。
让 CurrencyPipe 支持 Big.js
CurrencyPipe 只支持 number | string | null | undefined
假如我的 value 类型是 Big.js 的 Big 对象,那就不能直接使用了。
最直接的解决方法就是调用 Big.toNumber 把 Big 对象转换成 number 类型。
<h1>{{ price.toNumber() | currency : 'MYR' : 'symbol-narrow' : '.2-2' }}</h1>
不过每次都要调用就很烦啊,我们能不能让 current pipe 支持 Big 对象,在调用 toNumber 封装起来?
可以,直接继承 CurrencyPipe 然后 override transform 方法
@Pipe({ name: 'currency', standalone: true, }) export class MyCurrencyPipe extends CurrencyPipe implements PipeTransform { override transform( value: number | Decimal | string, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string, ): string | null; override transform( value: null | Decimal | undefined, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string, ): null; override transform( value: number | Decimal | string | null | undefined, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string, ): string | null; override transform( value: number | Decimal | string | null | undefined, currencyCode?: string, display?: 'code' | 'symbol' | 'symbol-narrow' | string | boolean, digitsInfo?: string, locale?: string, ): string | null { const convertedValue = value instanceof Decimal ? value.toNumber() : value; return super.transform(convertedValue, currencyCode, display, digitsInfo, locale); } }
transform 方法本来就有很多 overload,所以我们得 override 每一个才行,真麻烦😔,
接着 imports 就可以了。
效果
没有报错了。
注:Pipe name 用回 'currency' 也不要紧,不会撞名字的。Angular compiler 会优先选择我们定义的 Pipe。
让 DatePipe 支持 Temporal
再一个简单的例子,让 DatePipe 支持 Temporal。
首先是 date-util.ts
import { Temporal } from 'temporal-polyfill'; export type TemporalObject = | Temporal.Instant | Temporal.PlainDate | Temporal.PlainTime | Temporal.PlainDateTime | Temporal.ZonedDateTime; export function isTemporalObject(value: unknown): value is TemporalObject { return ( value instanceof Temporal.Instant || value instanceof Temporal.PlainDate || value instanceof Temporal.PlainTime || value instanceof Temporal.PlainDateTime || value instanceof Temporal.ZonedDateTime ); } export function temporalToDate(temporalObject: TemporalObject): Date { if (temporalObject instanceof Temporal.PlainDate) { const { year, month, day } = temporalObject; return new Date(year, month - 1, day); } if (temporalObject instanceof Temporal.ZonedDateTime || temporalObject instanceof Temporal.Instant) { return new Date(temporalObject.epochMilliseconds); } if (temporalObject instanceof Temporal.PlainDateTime) { const { year, month, day, hour, minute, second, millisecond } = temporalObject; return new Date(year, month - 1, day, hour, minute, second, millisecond); } if (temporalObject instanceof Temporal.PlainTime) { const { hour, minute, second, millisecond } = temporalObject; return new Date(1970, 0, 1, hour, minute, second, millisecond); } console.error('temporalObject is not TemporalObject', temporalObject); throw new Error('temporalObject is not TemporalObject'); }
接着是 MyDatePipe
@Pipe({ name: 'date', standalone: true, }) export class MyDatePipe extends DatePipe implements PipeTransform { override transform( value: Date | TemporalObject | string | number, format?: string, timezone?: string, locale?: string, ): string | null; override transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null; override transform( value: Date | TemporalObject | string | number | null | undefined, format?: string, timezone?: string, locale?: string, ): string | null; override transform( value: Date | TemporalObject | string | number | null | undefined, format?: string, timezone?: string, locale?: string, ): string | null { return super.transform(isTemporalObject(value) ? temporalToDate(value) : value, format, timezone, locale); } }
也是一样,extends + override Angular DatePipe 就可以了。
最后,还有 myFormatDate 函数
export function myFormatDate( value: string | number | Date | TemporalObject, format: string, locale: string, timezone?: string, ): string { value = isTemporalObject(value) ? temporalToDate(value) : value; return formatDate(value, format, locale, timezone); // call Angular formatDate 函数 }
目录
上一篇 Angular 18+ 高级教程 – Component 组件 の Attribute Directives 属性型指令
下一篇 Angular 18+ 高级教程 – Change Detection & Ivy rendering engine
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻