管道(Pipe)是Angular中用于数据转换和格式化的功能,类似于其他框架中的过滤器(Filter)。管道可以将输入数据转换为更友好的显示格式。
{{ value | pipeName:arg1:arg2 }}
Angular提供了多种内置管道来处理常见的数据格式化需求。
<!-- 内置管道使用示例 -->
<div class="container">
<!-- 字符串管道 -->
<p>{{ 'angular pipe' | uppercase }}</p>
<p>{{ 'ANGULAR PIPE' | lowercase }}</p>
<p>{{ 'angular pipe' | titlecase }}</p>
<!-- 数字管道 -->
<p>{{ 123456.789 | number:'1.2-2' }}</p>
<p>{{ 0.1234 | percent:'1.2-2' }}</p>
<!-- 货币管道 -->
<p>{{ 1234.56 | currency:'USD':'symbol':'1.2-2' }}</p>
<p>{{ 1234.56 | currency:'CNY':'symbol-narrow':'1.2-2' }}</p>
<p>{{ 1234.56 | currency:'EUR':'symbol':'4.2-2' }}</p>
<!-- 日期管道 -->
<p>{{ currentDate | date:'yyyy-MM-dd' }}</p>
<p>{{ currentDate | date:'medium' }}</p>
<p>{{ currentDate | date:'fullDate' }}</p>
<p>{{ currentDate | date:'yyyy-MM-dd HH:mm:ss' }}</p>
<!-- JSON管道(调试用) -->
<pre>{{ user | json }}</pre>
<!-- Slice管道 -->
<p>{{ 'Hello Angular' | slice:0:5 }}</p>
<p>{{ [1,2,3,4,5] | slice:1:3 }}</p>
<!-- I18nSelect管道 -->
<p>{{ gender | i18nSelect:genderMapping }}</p>
<!-- I18nPlural管道 -->
<p>{{ items.length | i18nPlural:messageMapping }}</p>
<!-- KeyValue管道 -->
<div *ngFor="let item of object | keyvalue">
{{item.key}}: {{item.value}}
</div>
</div>
// 组件中的相关数据
import { Component } from '@angular/core';
@Component({
selector: 'app-pipe-demo',
templateUrl: './pipe-demo.component.html'
})
export class PipeDemoComponent {
currentDate = new Date();
user = {
name: '张三',
age: 30,
email: 'zhangsan@example.com'
};
gender = 'male';
genderMapping = {
'male': '先生',
'female': '女士',
'other': '其他'
};
items = ['苹果', '香蕉', '橙子'];
messageMapping = {
'=0': '没有商品',
'=1': '一件商品',
'other': '#件商品'
};
object = {
name: '张三',
age: 30,
city: '北京'
};
}
<!-- 管道参数 -->
<p>{{ price | currency:'CNY':'¥':'1.2-2' }}</p>
<p>{{ date | date:'yyyy年MM月dd日 HH:mm:ss' }}</p>
<p>{{ percentage | percent:'1.2-2' }}</p>
<!-- 链式管道(管道可以串联) -->
<p>{{ 'hello world' | uppercase | slice:0:5 }}</p>
<p>{{ user.birthday | date:'yyyy-MM-dd' | uppercase }}</p>
<p>{{ amount | currency:'USD' | lowercase }}</p>
<!-- 带参数的链式管道 -->
<p>{{ longText | slice:0:100 | lowercase | titlecase }}</p>
// reverse.pipe.ts - 反转字符串管道
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'reverse'
})
export class ReversePipe implements PipeTransform {
transform(value: string): string {
if (!value) return '';
// 反转字符串
return value.split('').reverse().join('');
}
}
// truncate.pipe.ts - 截断文本管道(带参数)
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate'
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 50, suffix: string = '...'): string {
if (!value) return '';
// 如果文本长度小于等于限制,直接返回
if (value.length <= limit) {
return value;
}
// 截断文本并添加后缀
return value.substring(0, limit) + suffix;
}
}
// filter.pipe.ts - 数组过滤管道
import { Pipe, PipeTransform } from '@angular/core';
interface Product {
id: number;
name: string;
category: string;
price: number;
inStock: boolean;
}
@Pipe({
name: 'filter'
})
export class FilterPipe implements PipeTransform {
transform(items: Product[], filterBy: string): Product[] {
if (!items || !filterBy) {
return items;
}
// 将过滤器转换为小写以进行不区分大小写的比较
filterBy = filterBy.toLowerCase();
// 过滤数组
return items.filter(item =>
item.name.toLowerCase().includes(filterBy) ||
item.category.toLowerCase().includes(filterBy)
);
}
}
// shared.module.ts 或相关模块
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReversePipe } from './pipes/reverse.pipe';
import { TruncatePipe } from './pipes/truncate.pipe';
import { FilterPipe } from './pipes/filter.pipe';
@NgModule({
declarations: [
ReversePipe,
TruncatePipe,
FilterPipe
],
imports: [
CommonModule
],
exports: [
ReversePipe,
TruncatePipe,
FilterPipe
]
})
export class SharedModule { }
<!-- 使用自定义管道 -->
<div class="demo">
<!-- 反转字符串 -->
<p>原始: {{ 'Hello Angular' }}</p>
<p>反转: {{ 'Hello Angular' | reverse }}</p>
<!-- 截断文本 -->
<p>{{ longText | truncate:100:'...' }}</p>
<p>{{ shortText | truncate:50 }}</p>
<!-- 数组过滤 -->
<input type="text" [(ngModel)]="searchText" placeholder="搜索...">
<ul>
<li *ngFor="let product of products | filter:searchText">
{{product.name}} - {{product.price | currency}}
</li>
</ul>
</div>
异步管道用于处理Promise和Observable,自动订阅并在数据到达时更新视图。
// async-demo.component.ts
import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
@Component({
selector: 'app-async-demo',
templateUrl: './async-demo.component.html'
})
export class AsyncDemoComponent implements OnInit {
// Promise示例
promiseData: Promise<string>;
// Observable示例
observableData$: Observable<string>;
observableArray$: Observable<number[]>;
observableUser$: Observable<{name: string, age: number}>;
// 定时器Observable
timer$: Observable<number>;
ngOnInit(): void {
// Promise示例
this.promiseData = new Promise((resolve) => {
setTimeout(() => {
resolve('Promise数据已加载!');
}, 2000);
});
// Observable示例
this.observableData$ = of('Observable数据已加载!').pipe(
delay(1500)
);
this.observableArray$ = of([1, 2, 3, 4, 5]);
this.observableUser$ = of({
name: '张三',
age: 30
});
// 定时器
this.timer$ = new Observable(observer => {
let count = 0;
const interval = setInterval(() => {
observer.next(count++);
}, 1000);
// 清理函数
return () => {
clearInterval(interval);
};
});
}
}
<!-- async-demo.component.html -->
<div class="async-demo">
<h4>异步管道演示</h4>
<!-- Promise -->
<div class="mb-3">
<h5>Promise数据:</h5>
<p>{{ promiseData | async }}</p>
<small class="text-muted">等待2秒后显示</small>
</div>
<!-- Observable -->
<div class="mb-3">
<h5>Observable数据:</h5>
<p>{{ observableData$ | async }}</p>
<p>{{ observableArray$ | async | json }}</p>
<p *ngIf="observableUser$ | async as user">
用户: {{user.name}}, 年龄: {{user.age}}
</p>
</div>
<!-- 定时器 -->
<div class="mb-3">
<h5>定时器:</h5>
<p>计数: {{ timer$ | async }}</p>
</div>
<!-- 条件渲染 -->
<div class="mb-3">
<h5>条件渲染:</h5>
<ng-container *ngIf="observableData$ | async as data">
<p>数据已加载: {{data}}</p>
</ng-container>
<div *ngIf="!(observableData$ | async)">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">加载中...</span>
</div>
数据加载中...
</div>
</div>
</div>
// 纯管道(默认)- 只有当输入值变化时才执行
@Pipe({
name: 'purePipe',
pure: true // 默认值,可省略
})
export class PurePipe implements PipeTransform {
transform(value: any): any {
console.log('纯管道执行');
return value;
}
}
// 非纯管道 - 每次变更检测都会执行
@Pipe({
name: 'impurePipe',
pure: false // 设置为非纯
})
export class ImpurePipe implements PipeTransform {
transform(value: any): any {
console.log('非纯管道执行');
return value;
}
}
// 非纯管道示例:实时计算数组长度
@Pipe({
name: 'arrayLength',
pure: false
})
export class ArrayLengthPipe implements PipeTransform {
transform(array: any[]): number {
return array ? array.length : 0;
}
}
// sort-by.pipe.ts - 数组排序管道
import { Pipe, PipeTransform } from '@angular/core';
interface SortConfig {
property: string;
direction: 'asc' | 'desc';
}
@Pipe({
name: 'sortBy'
})
export class SortByPipe implements PipeTransform {
transform(array: any[], config: SortConfig): any[] {
if (!array || !config || !config.property) {
return array;
}
// 复制数组以避免修改原数组
const result = [...array];
// 排序逻辑
result.sort((a, b) => {
const aValue = this.getPropertyValue(a, config.property);
const bValue = this.getPropertyValue(b, config.property);
if (aValue < bValue) {
return config.direction === 'asc' ? -1 : 1;
}
if (aValue > bValue) {
return config.direction === 'asc' ? 1 : -1;
}
return 0;
});
return result;
}
private getPropertyValue(obj: any, propertyPath: string): any {
return propertyPath.split('.').reduce((o, i) => o ? o[i] : null, obj);
}
}
// safe-html.pipe.ts - 安全HTML管道
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Pipe({
name: 'safeHtml'
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string): SafeHtml {
if (!value) return '';
// 将HTML标记为安全
return this.sanitizer.bypassSecurityTrustHtml(value);
}
}
<!-- 使用复杂管道 -->
<div class="complex-pipes">
<!-- 排序管道 -->
<div *ngFor="let item of items | sortBy:{property: 'name', direction: 'asc'}">
{{item.name}} - {{item.price}}
</div>
<!-- 安全HTML管道 -->
<div [innerHTML]="unsafeHtml | safeHtml"></div>
<!-- 多个参数的管道 -->
<p>{{ text | truncate:limit:suffix }}</p>
<!-- 管道链 -->
<p>{{ items | filter:searchText | sortBy:{property: 'price', direction: 'desc'} | slice:0:5 }}</p>
</div>
trackBy与*ngFor结合使用过滤管道OnPush优化// 性能优化示例
@Component({
selector: 'app-optimized',
template: `
<!-- 使用trackBy优化ngFor性能 -->
<div *ngFor="let item of filteredItems; trackBy: trackById">
{{item.name}}
</div>
<!-- 在组件中预处理数据,而不是使用非纯管道 -->
<div>商品总数: {{totalItems}}</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent implements OnInit {
items: Item[] = [];
filteredItems: Item[] = [];
totalItems: number = 0;
constructor(private itemService: ItemService) {}
ngOnInit(): void {
this.itemService.getItems().subscribe(items => {
this.items = items;
this.filteredItems = this.filterItems(items, 'filter');
this.totalItems = this.calculateTotal(items);
});
}
// 组件内过滤,避免使用非纯管道
filterItems(items: Item[], filter: string): Item[] {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}
// 计算总值
calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// trackBy函数
trackById(index: number, item: Item): number {
return item.id;
}
}
// reverse.pipe.spec.ts - 管道测试
import { ReversePipe } from './reverse.pipe';
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => {
pipe = new ReversePipe();
});
it('应该创建管道', () => {
expect(pipe).toBeTruthy();
});
it('应该反转字符串', () => {
expect(pipe.transform('hello')).toBe('olleh');
expect(pipe.transform('angular')).toBe('ralugna');
expect(pipe.transform('12345')).toBe('54321');
});
it('应该处理空字符串', () => {
expect(pipe.transform('')).toBe('');
expect(pipe.transform(null as any)).toBe('');
expect(pipe.transform(undefined as any)).toBe('');
});
it('应该保持空格', () => {
expect(pipe.transform('hello world')).toBe('dlrow olleh');
expect(pipe.transform(' angular ')).toBe(' ralugna ');
});
});
// truncate.pipe.spec.ts
import { TruncatePipe } from './truncate.pipe';
describe('TruncatePipe', () => {
let pipe: TruncatePipe;
beforeEach(() => {
pipe = new TruncatePipe();
});
it('应该截断超长的文本', () => {
const longText = '这是一个非常长的文本,需要被截断显示';
const result = pipe.transform(longText, 10);
expect(result).toBe('这是一个非常长的文...');
});
it('不应该截断短文本', () => {
const shortText = '短文本';
expect(pipe.transform(shortText, 10)).toBe(shortText);
});
it('应该使用自定义后缀', () => {
const text = '这是一个测试文本';
expect(pipe.transform(text, 5, ' [更多]')).toBe('这是一个测 [更多]');
});
it('应该使用默认参数', () => {
const pipe = new TruncatePipe();
expect(pipe.transform('测试文本')).toBe('测试文本'); // 默认limit 50
});
});
// 实际应用中的管道示例集合
// phone-format.pipe.ts - 手机号格式化
@Pipe({ name: 'phoneFormat' })
export class PhoneFormatPipe implements PipeTransform {
transform(phone: string): string {
if (!phone) return '';
// 移除所有非数字字符
const cleaned = phone.replace(/\D/g, '');
// 格式化为 123-4567-8900
if (cleaned.length === 11) {
return `${cleaned.substr(0, 3)}-${cleaned.substr(3, 4)}-${cleaned.substr(7)}`;
}
return phone;
}
}
// file-size.pipe.ts - 文件大小格式化
@Pipe({ name: 'fileSize' })
export class FileSizePipe implements PipeTransform {
transform(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
}
// time-ago.pipe.ts - 时间距离现在多久
@Pipe({ name: 'timeAgo', pure: false })
export class TimeAgoPipe implements PipeTransform {
transform(value: Date): string {
if (!value) return '';
const seconds = Math.floor((+new Date() - +new Date(value)) / 1000);
const intervals = {
'年': 31536000,
'月': 2592000,
'周': 604800,
'天': 86400,
'小时': 3600,
'分钟': 60,
'秒': 1
};
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit);
if (interval >= 1) {
return `${interval}${unit}前`;
}
}
return '刚刚';
}
}
// highlight.pipe.ts - 搜索关键词高亮
@Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform {
transform(text: string, search: string): string {
if (!search || !text) return text;
const pattern = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${pattern})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
}
<!-- 应用中的管道使用 -->
<div class="real-world-examples">
<!-- 手机号格式化 -->
<p>电话: {{ user.phone | phoneFormat }}</p>
<!-- 文件大小 -->
<p>文件大小: {{ file.size | fileSize }}</p>
<p>文件大小: {{ file.size | fileSize:1 }}</p>
<!-- 时间显示 -->
<p>发布时间: {{ article.createdAt | timeAgo }}</p>
<p>更新时间: {{ article.updatedAt | date:'yyyy-MM-dd' }}</p>
<!-- 搜索高亮 -->
<div [innerHTML]="content | highlight:searchTerm | safeHtml"></div>
<!-- 综合示例 -->
<div *ngFor="let file of files">
{{ file.name }} ({{ file.size | fileSize }}) -
上传于 {{ file.uploadedAt | timeAgo }}
</div>
</div>