MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

自定义Angular管道提升数据展示效果

2024-06-055.3k 阅读

Angular 管道基础回顾

在深入探讨自定义 Angular 管道之前,我们先来回顾一下 Angular 管道的基本概念和用途。管道是 Angular 框架中一个强大的功能,它主要用于对数据进行转换和格式化,以便在视图中更好地展示。例如,我们经常使用的 DatePipe 可以将日期对象转换为指定格式的字符串,UpperCasePipe 可以将字符串转换为大写形式。

在模板中使用管道非常简单,通过 | 符号来应用管道。比如,假设我们有一个日期对象 myDate,要将其格式化为 yyyy-MM-dd 的形式,可以这样写:

{{ myDate | date: 'yyyy-MM-dd' }}

这里,date 就是 Angular 内置的管道,'yyyy-MM-dd' 是管道的参数,用于指定日期的格式化样式。

Angular 内置了许多实用的管道,像 CurrencyPipe 用于货币格式化,SlicePipe 用于截取数组或字符串的一部分等。这些内置管道能满足大部分常见的数据转换需求,但在实际项目中,我们往往会遇到一些特殊的、内置管道无法解决的数据处理场景,这时候就需要自定义管道了。

自定义 Angular 管道的必要性

应对特殊数据格式需求

在许多业务场景中,后端返回的数据格式可能并不直接适用于前端展示。例如,在一个医疗项目中,后端返回的患者病历时间可能是 Unix 时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到指定时间的毫秒数),而前端需要展示为 “X 天前” 或者 “X 小时前” 这样更人性化的时间格式。这种特殊的时间格式化需求,Angular 内置的 DatePipe 无法直接满足,就需要我们自定义管道来实现。

提升代码复用性

假设在一个电商应用中,商品价格需要根据不同的用户角色(普通用户、会员用户、高级会员用户)进行不同的折扣计算并展示。如果每次都在模板中编写复杂的价格计算逻辑,不仅模板代码会变得冗长且难以维护,而且如果折扣规则发生变化,需要在多个地方进行修改。通过自定义管道,我们可以将价格计算逻辑封装在管道中,在不同的模板中复用,提高代码的可维护性和复用性。

创建自定义 Angular 管道

创建管道类

要创建自定义管道,首先需要创建一个管道类。在 Angular 中,管道类是一个带有 @Pipe 装饰器的类,并且要实现 PipeTransform 接口。我们以一个简单的将字符串首字母大写的自定义管道为例。

在 Angular 项目中,使用 Angular CLI 可以快速生成管道类。在终端中执行以下命令:

ng generate pipe capitalize

这会在 src/app 目录下生成一个名为 capitalize.pipe.ts 的文件,内容如下:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalize'
})
export class CapitalizePipe implements PipeTransform {

  transform(value: string, ...args: unknown[]): string {
    return value;
  }

}

这里,@Pipe 装饰器的 name 属性指定了管道在模板中使用的名称,即 capitalizePipeTransform 接口只有一个 transform 方法,这个方法就是管道的核心逻辑所在,它接收要转换的值 value 和可选的参数 args,并返回转换后的值。

实现转换逻辑

对于我们的 CapitalizePipe,要实现将字符串首字母大写的功能,修改 transform 方法如下:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'capitalize'
})
export class CapitalizePipe implements PipeTransform {

  transform(value: string, ...args: unknown[]): string {
    if (!value) return '';
    return value.charAt(0).toUpperCase() + value.slice(1);
  }

}

在这个实现中,首先检查 value 是否为空,如果为空则返回空字符串。否则,使用 charAt(0) 获取字符串的第一个字符并转换为大写,然后使用 slice(1) 获取从第二个字符开始的剩余字符串,最后将两部分拼接起来返回。

在模板中使用自定义管道

创建好自定义管道后,就可以在模板中使用了。假设在一个组件的模板中有一个字符串变量 myString

@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.css']
})
export class ExampleComponent {
  myString = 'hello world';
}

example.component.html 中可以这样使用 CapitalizePipe

<p>{{ myString | capitalize }}</p>

这样,myString 的值 hello world 经过 capitalize 管道处理后,会在页面上显示为 Hello world

带参数的自定义管道

参数的作用

在实际应用中,很多时候我们需要根据不同的条件对数据进行不同的转换,这就需要给管道传递参数。以日期格式化为例,我们可能有时候需要显示完整的日期时间,有时候只需要显示日期部分,通过传递不同的参数可以让管道实现不同的格式化效果。

实现带参数管道

我们创建一个自定义的日期管道 CustomDatePipe,它可以根据传入的参数来决定日期的显示格式。首先使用 Angular CLI 创建管道类:

ng generate pipe custom-date

custom - date.pipe.ts 中编写如下代码:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'customDate'
})
export class CustomDatePipe implements PipeTransform {

  transform(value: Date, format: string = 'yyyy-MM-dd'): string {
    if (!(value instanceof Date)) return '';
    const year = value.getFullYear();
    const month = (value.getMonth() + 1).toString().padStart(2, '0');
    const day = value.getDate().toString().padStart(2, '0');
    if (format === 'yyyy-MM-dd') {
      return `${year}-${month}-${day}`;
    } else if (format === 'MM/dd/yyyy') {
      return `${month}/${day}/${year}`;
    }
    return '';
  }

}

在这个管道中,transform 方法接收两个参数,value 是要转换的日期对象,format 是日期的格式化字符串,默认值为 'yyyy-MM-dd'。根据不同的 format 值,返回不同格式的日期字符串。

在模板中使用带参数管道

假设在组件中有一个日期对象 myDate

@Component({
  selector: 'app-date-example',
  templateUrl: './date - example.component.html',
  styleUrls: ['./date - example.component.css']
})
export class DateExampleComponent {
  myDate = new Date();
}

date - example.component.html 中可以这样使用 CustomDatePipe

<p>{{ myDate | customDate:'yyyy-MM-dd' }}</p>
<p>{{ myDate | customDate:'MM/dd/yyyy' }}</p>

这样,第一个 <p> 标签会以 yyyy-MM-dd 的格式显示日期,第二个 <p> 标签会以 MM/dd/yyyy 的格式显示日期。

管道的纯与不纯

纯管道

在 Angular 中,默认情况下,管道是纯的。纯管道只在输入值发生 “纯变化” 时才会重新计算。所谓 “纯变化”,对于基本数据类型(如字符串、数字、布尔值),只要值本身改变就算是纯变化;对于对象和数组,只有当它们的引用发生改变时才算纯变化。

以我们之前创建的 CapitalizePipe 为例,它就是一个纯管道。假设在组件中有一个字符串变量 text

@Component({
  selector: 'app - pure - pipe - example',
  templateUrl: './pure - pipe - example.component.html',
  styleUrls: ['./pure - pipe - example.component.css']
})
export class PurePipeExampleComponent {
  text = 'initial value';

  changeText() {
    this.text = 'new value';
  }
}

pure - pipe - example.component.html 中:

<p>{{ text | capitalize }}</p>
<button (click)="changeText()">Change Text</button>

当点击按钮调用 changeText 方法时,text 的值发生了变化,capitalize 管道会重新计算并更新视图。因为字符串是基本数据类型,其值的变化属于纯变化。

但如果是对象或数组,情况就不同了。假设我们有一个包含对象的数组 items

@Component({
  selector: 'app - object - array - pure - pipe',
  templateUrl: './object - array - pure - pipe.component.html',
  styleUrls: ['./object - array - pure - pipe.component.css']
})
export class ObjectArrayPurePipeComponent {
  items = [
    { name: 'item1', value: 1 },
    { name: 'item2', value: 2 }
  ];

  updateItem() {
    this.items[0].value = 3;
  }
}

如果我们在模板中使用一个纯管道来处理 items,比如:

<p>{{ items | somePurePipe }}</p>
<button (click)="updateItem()">Update Item</button>

即使调用 updateItem 方法修改了 items 数组中对象的属性值,但由于数组的引用没有改变,纯管道不会重新计算,视图也不会更新。

不纯管道

不纯管道会在每次 Angular 运行变更检测时都重新计算,无论输入值是否发生纯变化。要创建一个不纯管道,需要在 @Pipe 装饰器中设置 pure: false

例如,我们创建一个获取当前时间的不纯管道 CurrentTimePipe

ng generate pipe current - time

current - time.pipe.ts 中:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'currentTime',
  pure: false
})
export class CurrentTimePipe implements PipeTransform {

  transform(): string {
    return new Date().toLocaleTimeString();
  }

}

在模板中使用这个管道:

<p>{{ '' | currentTime }}</p>

由于这是一个不纯管道,Angular 每次运行变更检测时,都会调用 transform 方法,所以页面上显示的时间会实时更新。

需要注意的是,虽然不纯管道能实现一些特殊需求,但由于它会频繁重新计算,可能会对性能产生一定影响,所以在使用时要谨慎评估。

自定义管道与依赖注入

依赖注入的概念

依赖注入(Dependency Injection,简称 DI)是一种设计模式,它允许我们将一个对象的依赖(即该对象所需要的其他对象)传递给它,而不是让对象自己去创建这些依赖。在 Angular 中,依赖注入是一个核心特性,它使得代码更易于测试、维护和复用。

在自定义管道中使用依赖注入

假设我们有一个自定义管道 LoggerPipe,它在转换数据的同时,需要记录一些日志信息。我们可以通过依赖注入来注入一个日志服务 LoggerService

首先创建日志服务:

ng generate service logger

logger.service.ts 中:

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

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  log(message: string) {
    console.log(`[LoggerService] ${message}`);
  }
}

然后创建 LoggerPipe

ng generate pipe logger

logger.pipe.ts 中:

import { Pipe, PipeTransform } from '@angular/core';
import { LoggerService } from './logger.service';

@Pipe({
  name: 'logger'
})
export class LoggerPipe implements PipeTransform {

  constructor(private loggerService: LoggerService) {}

  transform(value: string): string {
    this.loggerService.log(`Transforming value: ${value}`);
    return value.toUpperCase();
  }

}

在这个管道中,通过构造函数注入了 LoggerService,在 transform 方法中,先调用 LoggerServicelog 方法记录日志,然后将输入的字符串转换为大写并返回。

在模板中使用 LoggerPipe

<p>{{ 'hello' | logger }}</p>

在控制台中会看到日志输出 [LoggerService] Transforming value: hello,同时页面上会显示 HELLO

通过依赖注入,我们可以将管道的业务逻辑和一些辅助功能(如日志记录)分离,提高代码的可维护性和可测试性。

自定义管道的测试

测试的重要性

对自定义管道进行测试是确保管道功能正确性和稳定性的重要环节。通过测试,我们可以在开发过程中及时发现问题,避免将错误引入到生产环境中。同时,良好的测试覆盖率也有助于提高代码的质量和可维护性。

测试自定义管道

我们以 CapitalizePipe 为例来编写测试用例。在 Angular 项目中,测试文件默认与组件或管道文件同名,后缀为 .spec.ts。在 capitalize.pipe.spec.ts 中编写如下测试代码:

import { TestBed } from '@angular/core/testing';
import { CapitalizePipe } from './capitalize.pipe';

describe('CapitalizePipe', () => {
  let pipe: CapitalizePipe;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    pipe = new CapitalizePipe();
  });

  it('create an instance', () => {
    expect(pipe).toBeTruthy();
  });

  it('should capitalize the first letter', () => {
    const result = pipe.transform('hello world');
    expect(result).toBe('Hello world');
  });

  it('should return empty string for null value', () => {
    const result = pipe.transform(null);
    expect(result).toBe('');
  });

  it('should return empty string for undefined value', () => {
    const result = pipe.transform(undefined);
    expect(result).toBe('');
  });
});

在这个测试文件中,使用了 Angular 的测试工具 TestBedbeforeEach 钩子函数在每个测试用例执行前都会执行,这里创建了 CapitalizePipe 的实例。

第一个测试用例 it('create an instance', () => {... }) 检查管道实例是否创建成功。

第二个测试用例 it('should capitalize the first letter', () => {... }) 验证管道是否能正确将字符串首字母大写。

后面两个测试用例分别验证管道在输入为 nullundefined 时是否返回空字符串。

通过这样的测试用例,可以全面验证自定义管道的功能是否符合预期。

自定义管道在实际项目中的应用场景

数据过滤与筛选

在一个大型的电商产品列表页面中,后端可能返回所有的产品数据,但前端需要根据不同的条件进行过滤展示。例如,根据产品类别、价格范围等条件进行筛选。我们可以创建一个自定义管道 ProductFilterPipe,通过传递不同的参数来实现不同的过滤逻辑。

import { Pipe, PipeTransform } from '@angular/core';
import { Product } from './product.model';

@Pipe({
  name: 'productFilter'
})
export class ProductFilterPipe implements PipeTransform {

  transform(products: Product[], category: string, minPrice: number, maxPrice: number): Product[] {
    return products.filter(product => {
      const categoryMatch = category === '' || product.category === category;
      const priceMatch = product.price >= minPrice && product.price <= maxPrice;
      return categoryMatch && priceMatch;
    });
  }

}

在模板中可以这样使用:

<ul>
  <li *ngFor="let product of products | productFilter:selectedCategory:minPrice:maxPrice">
    {{ product.name }} - {{ product.price }}
  </li>
</ul>

这样,通过在模板中传递不同的 selectedCategoryminPricemaxPrice 参数,就可以实现对产品列表的动态过滤。

数据加密与解密展示

在一些涉及用户敏感信息的应用中,如银行应用展示用户账号信息时,可能需要对账号进行加密展示,只有在特定操作(如用户确认查看)时才进行解密。我们可以创建 EncryptPipeDecryptPipe 来实现这个功能。

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'encrypt'
})
export class EncryptPipe implements PipeTransform {

  transform(value: string): string {
    // 简单的加密示例,实际应用中应使用更安全的加密算法
    return value.split('').map(char => char.charCodeAt(0) + 1).join('');
  }

}

@Pipe({
  name: 'decrypt'
})
export class DecryptPipe implements PipeTransform {

  transform(value: string): string {
    return value.split('').map(char => String.fromCharCode(parseInt(char) - 1)).join('');
  }

}

在模板中:

<p>{{ accountNumber | encrypt }}</p>
<button (click)="showDecrypted =!showDecrypted">Toggle Decrypt</button>
<p *ngIf="showDecrypted">{{ accountNumber | decrypt }}</p>

通过这种方式,既保证了敏感信息的安全展示,又满足了用户查看完整信息的需求。

通过以上对自定义 Angular 管道的详细介绍,从基础概念到实际应用场景,涵盖了创建、使用、测试等多个方面,希望能帮助开发者在前端开发中更好地利用自定义管道来提升数据展示效果,打造更优质的用户体验。