Angular 18+ 高级教程 – Component 组件 の Pipe 管道

介绍

Pipe 类似于 Template Syntax,它的用途是 transform value for display。

 

参考: 

Docs – Understanding Pipes

 

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,有好几个选择:

  1. 'code' 表示显示 currency code

  2. 'symbol' 表示显示符号就够了。Angular 会把 code 转成对应的 symbol,比如 USD -> $

  3. 'symbol-narrow' 也是显示 symbol 但是是 "narrow" 版,马来西亚的 symbol-narrow 是 'RM' Ringgit Malaysia 的缩写

  4. '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 常用于处理 PromiseRxJS 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

View Code

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 😊💻

 

posted @ 2023-04-06 16:52  兴杰  阅读(700)  评论(0编辑  收藏  举报