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

Angular管道在数据显示中的重要作用

2022-12-085.6k 阅读

Angular管道基础概念

在Angular应用开发中,管道是一种非常有用的工具,它主要用于对数据进行转换和格式化,以便在视图中更合适地显示。简单来说,管道就像是一个数据处理器,接收输入的数据,然后按照特定的规则对其进行处理,并输出处理后的数据。

管道的语法非常直观,在模板中使用|符号来应用管道。例如,假设有一个组件中有一个name属性,想要将其首字母大写后显示,就可以使用UpperCasePipe管道:

<p>{{ name | uppercase }}</p>

这里,name是输入的数据,|后面的uppercase就是管道名称,经过管道处理后,name的值会以全大写形式显示在<p>标签内。

Angular自带了一些常用的管道,比如UpperCasePipeLowerCasePipeDatePipeNumberPipe等。这些管道在日常开发中能帮助开发者快速处理各种类型的数据显示问题。

DatePipe为例,它用于格式化日期。假设在组件中有一个date属性表示日期:

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

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

在模板中,可以这样使用DatePipe

<p>{{ date | date:'short' }}</p>

上述代码中,date属性的值会按照short格式(例如“22年11月18日 下午4:15”)进行格式化显示。

自定义管道

虽然Angular自带的管道能满足很多常见需求,但在实际项目中,开发者可能会遇到一些特殊的数据处理需求,这时候就需要自定义管道。

自定义管道需要创建一个类,并实现PipePipeTransform接口。以下是一个简单的自定义管道示例,这个管道用于将字符串截断到指定长度,并在截断处添加省略号。

首先,使用Angular CLI生成一个管道:

ng generate pipe truncate

这会在项目中生成一个truncate.pipe.ts文件,内容如下:

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

@Pipe({
  name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 10): string {
    if (value.length <= limit) {
      return value;
    }
    return value.slice(0, limit) + '...';
  }
}

在上述代码中,@Pipe装饰器用于定义管道的名称为truncatePipeTransform接口中有一个transform方法,这个方法接收两个参数:value是要处理的数据,limit是截断的长度,默认值为10。在transform方法中,首先判断字符串长度是否小于等于limit,如果是则直接返回原字符串,否则截取前limit个字符并添加省略号。

在模板中使用这个自定义管道:

<p>{{ '这是一个很长很长很长很长很长很长的字符串' | truncate:15 }}</p>

上述代码会将字符串截断为15个字符,并添加省略号,显示为“这是一个很长很长很长... ”。

管道在数据显示中的作用

  1. 数据格式化:正如前面提到的DatePipeNumberPipe,管道能将原始数据转换为更易读的格式。例如,在显示货币金额时,可以使用NumberPipe进行格式化。假设组件中有一个amount属性表示金额:
import { Component } from '@angular/core';

@Component({
  selector: 'app-currency-example',
  templateUrl: './currency-example.component.html',
  styleUrls: ['./currency-example.component.css']
})
export class CurrencyExampleComponent {
  amount = 12345.678;
}

在模板中使用NumberPipe

<p>{{ amount | number:'1.2-2':'en-US': '$' }}</p>

上述代码中,number管道的参数'1.2-2'表示最少整数部分为1位,小数部分固定为2位,'en-US'表示使用美国英语格式,'$'表示添加美元符号。最终会显示为“$12,345.68”。

  1. 数据过滤:虽然Angular没有像一些其他框架那样专门的过滤管道,但可以通过自定义管道实现数据过滤功能。例如,假设有一个数组包含一些用户对象,每个用户对象有nameage属性,现在想要在视图中只显示年龄大于18岁的用户。

首先,创建一个自定义的FilterUsersPipe

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

@Pipe({
  name: 'filterUsers'
})
export class FilterUsersPipe implements PipeTransform {
  transform(users: any[], ageLimit: number = 18): any[] {
    return users.filter(user => user.age > ageLimit);
  }
}

在组件中定义用户数组:

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

@Component({
  selector: 'app-user-filter-example',
  templateUrl: './user-filter-example.component.html',
  styleUrls: ['./user-filter-example.component.css']
})
export class UserFilterExampleComponent {
  users = [
    { name: 'Alice', age: 16 },
    { name: 'Bob', age: 20 },
    { name: 'Charlie', age: 22 }
  ];
}

在模板中使用自定义管道:

<ul>
  <li *ngFor="let user of users | filterUsers:18">
    {{ user.name }} - {{ user.age }}
  </li>
</ul>

上述代码会在<ul>中只显示年龄大于18岁的用户。

  1. 提高代码复用性:通过自定义管道,可以将一些通用的数据处理逻辑封装起来,在多个组件的视图中复用。例如,前面提到的truncate管道,如果在多个地方都需要对字符串进行截断处理,就可以直接使用这个管道,而不需要在每个地方都编写相同的截断逻辑。这不仅减少了代码量,也使得代码的维护更加容易。如果需要修改截断逻辑,只需要在管道的transform方法中进行修改,所有使用该管道的地方都会自动应用新的逻辑。

  2. 保持视图逻辑清晰:将数据处理逻辑放在管道中,使得模板代码更加简洁和易读。模板主要负责展示数据,而管道负责数据的转换和格式化,这样的分离有助于开发者更好地理解和维护代码。例如,在一个复杂的表格中,如果对每列的数据都有不同的格式化需求,使用管道可以让表格的模板代码保持清晰,不会因为过多的数据处理逻辑而变得混乱。

管道的链式调用

在Angular中,管道支持链式调用,即可以将多个管道依次应用到同一个数据上。这在处理复杂的数据显示需求时非常有用。

例如,假设有一个日期对象,想要先将其格式化为特定的日期格式,然后再转换为全大写形式显示。可以这样实现:

<p>{{ new Date() | date:'yyyy-MM-dd' | uppercase }}</p>

在上述代码中,首先使用date管道将日期格式化为yyyy-MM-dd的形式,然后再使用uppercase管道将格式化后的日期字符串转换为全大写。

再比如,有一个包含数字的数组,想要先过滤掉小于10的数字,然后对剩下的数字进行求和,并格式化为货币金额显示。可以通过自定义管道和内置管道的链式调用来实现。

首先,创建一个FilterNumbersPipe用于过滤数字:

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

@Pipe({
  name: 'filterNumbers'
})
export class FilterNumbersPipe implements PipeTransform {
  transform(numbers: number[], minValue: number = 10): number[] {
    return numbers.filter(number => number >= minValue);
  }
}

然后,创建一个SumPipe用于对数组中的数字求和:

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

@Pipe({
  name:'sum'
})
export class SumPipe implements PipeTransform {
  transform(numbers: number[]): number {
    return numbers.reduce((acc, num) => acc + num, 0);
  }
}

在组件中定义数字数组:

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

@Component({
  selector: 'app-number-chain-example',
  templateUrl: './number-chain-example.component.html',
  styleUrls: ['./number-chain-example.component.css']
})
export class NumberChainExampleComponent {
  numbers = [5, 12, 8, 15, 20];
}

在模板中使用管道链式调用:

<p>{{ numbers | filterNumbers:10 | sum | number:'1.2-2':'en-US': '$' }}</p>

上述代码首先使用filterNumbers管道过滤掉小于10的数字,然后使用sum管道对剩下的数字求和,最后使用number管道将求和结果格式化为货币金额显示。

管道的纯与非纯

在Angular中,管道分为纯管道和非纯管道。

  1. 纯管道:纯管道是Angular默认的管道类型。纯管道只会在输入值发生“纯”变化时才会重新执行transform方法。所谓“纯”变化,对于基本数据类型(如字符串、数字、布尔值),是指值的变化;对于对象和数组,是指引用的变化。例如,在组件中有一个字符串属性text,使用UpperCasePipe(纯管道):
import { Component } from '@angular/core';

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

在模板中:

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

当点击按钮调用changeText方法时,text的值发生了变化,UpperCasePipe会重新执行transform方法,显示的文本会更新为“WORLD”。

但如果是对象或数组,只有当对象或数组的引用发生变化时,纯管道才会重新执行。例如:

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

@Component({
  selector: 'app-pure-array-pipe-example',
  templateUrl: './pure-array-pipe-example.component.html',
  styleUrls: ['./pure-array-pipe-example.component.css']
})
export class PureArrayPipeExampleComponent {
  numbers = [1, 2, 3];
  addNumber() {
    this.numbers.push(4);
  }
}

在模板中使用一个自定义的纯管道SquarePipe(假设已定义):

<p>{{ numbers | square }}</p>
<button (click)="addNumber()">Add Number</button>

当点击按钮调用addNumber方法时,虽然数组numbers的内容发生了变化,但引用没有变化,SquarePipe不会重新执行transform方法,显示的内容不会更新。要使纯管道重新执行,需要改变数组的引用,例如:

addNumber() {
  this.numbers = [...this.numbers, 4];
}
  1. 非纯管道:非纯管道会在每次Angular运行变更检测时执行transform方法,无论输入值是否发生变化。这意味着非纯管道会更频繁地执行,可能会影响性能。非纯管道适用于一些需要实时响应数据变化的场景,比如监听浏览器窗口大小变化等。

要创建一个非纯管道,只需要在@Pipe装饰器中设置pure: false

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

@Pipe({
  name: 'nonPurePipe',
  pure: false
})
export class NonPurePipe implements PipeTransform {
  transform(value: any): any {
    // 数据处理逻辑
    return value;
  }
}

例如,假设有一个非纯管道RandomNumberPipe,它每次执行都会返回一个随机数:

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

@Pipe({
  name: 'randomNumber',
  pure: false
})
export class RandomNumberPipe implements PipeTransform {
  transform(): number {
    return Math.random();
  }
}

在模板中:

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

每次Angular运行变更检测时,randomNumber管道都会重新执行,显示的随机数也会更新。

管道与服务的区别

在Angular开发中,管道和服务都用于处理数据相关的逻辑,但它们有明显的区别。

  1. 功能侧重点

    • 管道:主要侧重于数据的转换和格式化,以便在视图中更好地显示。管道的输入是数据,输出也是经过处理的数据,其主要作用域是视图层。例如,DatePipe将日期对象转换为特定格式的字符串,方便在模板中显示。
    • 服务:服务更侧重于提供应用程序级别的功能和数据访问。它可以处理业务逻辑、与后端服务器进行通信、管理应用程序状态等。例如,一个UserService可以负责从服务器获取用户数据,进行用户认证等操作。
  2. 使用场景

    • 管道:在模板中,当需要对数据进行简单的转换、格式化或过滤时使用管道。比如在表格中对列数据进行格式化,在列表中过滤特定的数据等。管道的使用使得模板代码简洁明了,专注于数据的展示。
    • 服务:在组件类中,当需要执行复杂的业务逻辑、共享数据或与外部资源交互时使用服务。例如,多个组件可能需要共享用户登录状态,就可以创建一个AuthService来管理和提供这个状态。
  3. 生命周期和调用频率

    • 管道:纯管道只有在输入值发生“纯”变化时才会执行,非纯管道在每次变更检测时执行。管道的执行频率相对较低,并且主要在视图渲染时起作用。
    • 服务:服务通常在应用程序启动时创建(如果是单例服务),并且可以在组件的生命周期内随时被调用。服务的调用频率取决于业务需求,可能会频繁地被调用以获取或更新数据。
  4. 代码结构

    • 管道:管道类实现PipeTransform接口,通过@Pipe装饰器定义名称。其核心方法是transform,用于处理数据。管道代码相对简单,专注于数据处理逻辑。
    • 服务:服务类通常使用@Injectable装饰器,它可以包含多个方法和属性,用于实现各种业务功能。服务代码可以更复杂,可能涉及到依赖注入、异步操作等。

管道在大型项目中的应用实践

在大型Angular项目中,管道的合理应用可以极大地提高开发效率和代码的可维护性。

  1. 多语言支持:在国际化的大型项目中,DatePipeNumberPipe等管道可以根据不同的语言和地区设置进行数据格式化。例如,使用DatePipe时,可以根据用户的语言偏好显示不同格式的日期。假设项目中使用@angular/localize进行多语言支持,在组件中设置语言:
import { Component } from '@angular/core';
import { LOCALE_ID } from '@angular/core';
import { registerLocaleData } from '@angular/common';
import localeEn from '@angular/common/locales/en';
import localeFr from '@angular/common/locales/fr';

@Component({
  selector: 'app-i18n-example',
  templateUrl: './i18n-example.component.html',
  styleUrls: ['./i18n-example.component.css'],
  providers: [
    { provide: LOCALE_ID, useValue: 'en' }
  ]
})
export class I18nExampleComponent {
  date = new Date();

  setFrench() {
    registerLocaleData(localeFr);
    this.localeId = 'fr';
  }

  setEnglish() {
    registerLocaleData(localeEn);
    this.localeId = 'en';
  }
}

在模板中:

<p>{{ date | date:'fullDate':undefined:localeId }}</p>
<button (click)="setFrench()">Set French</button>
<button (click)="setEnglish()">Set English</button>

上述代码中,根据用户点击按钮切换语言,DatePipe会根据不同的localeId显示不同语言格式的完整日期。

  1. 数据展示优化:在大型项目中,数据量往往较大,对数据的展示优化非常重要。通过自定义管道,可以对复杂数据结构进行处理,提高视图渲染效率。例如,在一个电商项目中,商品列表可能包含大量商品信息,每个商品有多个属性。可以创建自定义管道来处理商品图片路径、价格格式化等。

假设商品对象结构如下:

interface Product {
  id: number;
  name: string;
  price: number;
  imageUrl: string;
}

创建一个FormatProductImagePipe用于处理图片路径:

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

@Pipe({
  name: 'formatProductImage'
})
export class FormatProductImagePipe implements PipeTransform {
  transform(imageUrl: string): string {
    // 假设需要在图片路径前添加CDN地址
    return 'https://cdn.example.com' + imageUrl;
  }
}

在模板中显示商品列表:

<ul>
  <li *ngFor="let product of products">
    <img [src]="product.imageUrl | formatProductImage" alt="{{ product.name }}">
    <p>{{ product.name }} - {{ product.price | number:'1.2-2':'en-US': '$' }}</p>
  </li>
</ul>

这样可以将商品图片路径处理和价格格式化逻辑封装在管道中,使得模板代码简洁,同时也方便维护和复用。

  1. 数据安全与隐私保护:在涉及用户敏感信息的大型项目中,管道可以用于对数据进行脱敏处理。例如,在显示用户手机号码时,可以创建一个自定义管道将号码中间几位替换为星号。
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name:'maskPhone'
})
export class MaskPhonePipe implements PipeTransform {
  transform(phone: string): string {
    if (!phone) return '';
    return phone.slice(0, 3) + '****' + phone.slice(7);
  }
}

在模板中:

<p>{{ user.phone | maskPhone }}</p>

通过这种方式,在视图中显示用户手机号码时进行了脱敏处理,保护了用户的隐私。

管道的性能优化

虽然管道在数据显示中非常有用,但如果使用不当,可能会影响应用程序的性能。以下是一些管道性能优化的建议。

  1. 尽量使用纯管道:纯管道只有在输入值发生“纯”变化时才会执行,相比非纯管道,执行频率较低,性能更好。在大多数情况下,应优先考虑使用纯管道来处理数据。例如,对于简单的数据格式化,如日期格式化、字符串大小写转换等,纯管道完全可以满足需求。

  2. 避免在非纯管道中执行复杂计算:由于非纯管道在每次变更检测时都会执行,在其中执行复杂计算会导致性能问题。如果确实需要执行复杂计算,考虑将其放在服务中,并在适当的时候调用服务方法,而不是在非纯管道中直接执行。例如,对于一些需要大量数据处理和计算的逻辑,如复杂的数据分析等,不应放在非纯管道中。

  3. 缓存管道结果:如果管道的计算结果在一定时间内不会改变,可以考虑缓存结果。对于纯管道,如果输入值没有变化,管道不会重新执行,但对于非纯管道,可以手动实现缓存机制。例如,可以在管道类中添加一个属性来存储上一次的计算结果,并在每次执行transform方法时先检查输入值是否变化,如果没有变化则直接返回缓存结果。

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

@Pipe({
  name: 'cachedPipe',
  pure: false
})
export class CachedPipe implements PipeTransform {
  private lastValue: any;
  private lastResult: any;

  transform(value: any): any {
    if (value === this.lastValue) {
      return this.lastResult;
    }
    // 数据处理逻辑
    const result = value.toUpperCase();
    this.lastValue = value;
    this.lastResult = result;
    return result;
  }
}
  1. 减少管道链式调用的深度:虽然管道链式调用很方便,但过多的管道链式调用会增加性能开销。尽量将复杂的处理逻辑分解为更简单的管道,并合理安排调用顺序,避免不必要的深度链式调用。例如,如果可以通过一个自定义管道完成多个步骤的处理,就不需要使用多个管道进行链式调用。

  2. 使用异步管道处理异步数据:在处理异步数据(如ObservablePromise)时,应使用Angular的异步管道(async)。异步管道会自动订阅Observable或等待Promise的解析,并在数据可用时更新视图。同时,它会在组件销毁时自动取消订阅,避免内存泄漏。

<p>{{ myObservable | async }}</p>

通过以上性能优化方法,可以在充分利用管道功能的同时,确保应用程序的性能不受影响。

管道在不同场景下的应用案例

  1. 表格数据处理:在表格中显示数据时,经常需要对每列数据进行格式化。例如,在一个员工信息表格中,有入职日期列、薪资列等。
import { Component } from '@angular/core';

interface Employee {
  name: string;
  hireDate: Date;
  salary: number;
}

@Component({
  selector: 'app-employee-table',
  templateUrl: './employee-table.component.html',
  styleUrls: ['./employee-table.component.css']
})
export class EmployeeTableComponent {
  employees: Employee[] = [
    { name: 'Alice', hireDate: new Date('2020-01-01'), salary: 5000 },
    { name: 'Bob', hireDate: new Date('2021-03-15'), salary: 6000 }
  ];
}

在模板中:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Hire Date</th>
      <th>Salary</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let employee of employees">
      <td>{{ employee.name }}</td>
      <td>{{ employee.hireDate | date:'yyyy-MM-dd' }}</td>
      <td>{{ employee.salary | number:'1.2-2':'en-US': '$' }}</td>
    </tr>
  </tbody>
</table>

通过DatePipeNumberPipe对日期和薪资进行格式化,使表格数据更易读。

  1. 搜索和过滤功能:在一个搜索框和列表结合的场景中,可以使用自定义管道实现搜索和过滤功能。假设列表中是一些书籍对象,每个书籍对象有titleauthor属性。
import { Component } from '@angular/core';

interface Book {
  title: string;
  author: string;
}

@Component({
  selector: 'app-book-search',
  templateUrl: './book-search.component.html',
  styleUrls: ['./book-search.component.css']
})
export class BookSearchComponent {
  books: Book[] = [
    { title: 'Angular in Action', author: 'John Doe' },
    { title: 'JavaScript for Dummies', author: 'Jane Smith' }
  ];
  searchText = '';
}

创建一个SearchBooksPipe

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

@Pipe({
  name:'searchBooks'
})
export class SearchBooksPipe implements PipeTransform {
  transform(books: Book[], searchText: string): Book[] {
    if (!searchText) return books;
    return books.filter(book =>
      book.title.toLowerCase().includes(searchText.toLowerCase()) ||
      book.author.toLowerCase().includes(searchText.toLowerCase())
    );
  }
}

在模板中:

<input [(ngModel)]="searchText" placeholder="Search books">
<ul>
  <li *ngFor="let book of books | searchBooks:searchText">
    {{ book.title }} - {{ book.author }}
  </li>
</ul>

用户在搜索框输入内容时,通过SearchBooksPipe过滤显示相关书籍。

  1. 状态显示转换:在一些应用中,会使用数字或枚举值来表示状态,但在视图中需要显示更友好的文本。例如,在一个任务管理系统中,任务状态用数字表示(0表示未开始,1表示进行中,2表示已完成)。
import { Component } from '@angular/core';

interface Task {
  title: string;
  status: number;
}

@Component({
  selector: 'app-task-status',
  templateUrl: './task-status.component.html',
  styleUrls: ['./task-status.component.css']
})
export class TaskStatusComponent {
  tasks: Task[] = [
    { title: 'Finish project', status: 1 },
    { title: 'Review code', status: 0 }
  ];
}

创建一个TaskStatusPipe

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

@Pipe({
  name: 'taskStatus'
})
export class TaskStatusPipe implements PipeTransform {
  transform(status: number): string {
    switch (status) {
      case 0:
        return 'Not started';
      case 1:
        return 'In progress';
      case 2:
        return 'Completed';
      default:
        return 'Unknown';
    }
  }
}

在模板中:

<ul>
  <li *ngFor="let task of tasks">
    {{ task.title }} - {{ task.status | taskStatus }}
  </li>
</ul>

通过TaskStatusPipe将数字状态转换为友好的文本显示。

通过以上不同场景下的应用案例,可以看到管道在Angular应用开发中对于数据显示处理的灵活性和实用性,能够满足各种复杂的数据展示需求。