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

掌握Angular货币转换管道的技巧

2022-08-052.2k 阅读

什么是 Angular 货币转换管道

在 Angular 应用程序开发中,管道(Pipe)是一种非常有用的工具,它用于对数据进行转换和格式化。货币转换管道(Currency Pipe)是 Angular 内置管道之一,专门用于将数字格式化为货币字符串。这在处理财务数据,如价格、收入、支出等方面非常常见。

Angular 的货币转换管道基于国际标准 ISO 4217 货币代码进行工作。它允许开发人员轻松地将数字按照特定的货币格式显示,并且可以根据应用程序的需求和用户的区域设置进行定制。

使用基本的货币转换管道

使用货币转换管道非常简单。假设我们有一个组件,其中包含一个表示价格的数字属性,我们想要将其格式化为货币字符串并显示在模板中。

首先,在组件的 TypeScript 文件(例如 app.component.ts)中定义一个价格变量:

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  price: number = 1234.56;
}

然后,在模板文件(app.component.html)中使用货币转换管道:

<p>The price is: {{ price | currency }}</p>

上述代码中,price 是组件中的数字属性,| 是管道操作符,currency 是货币转换管道的名称。在浏览器中渲染时,会根据用户浏览器的区域设置将价格格式化为相应的货币字符串。例如,如果用户的区域设置是美国英语,可能会显示为 $1,234.56

自定义货币符号

默认情况下,货币转换管道会根据区域设置显示相应的货币符号。但有时我们可能需要自定义货币符号。可以通过在管道后传递货币代码来实现。

例如,要将价格显示为欧元格式:

<p>The price in Euros is: {{ price | currency:'EUR' }}</p>

这样会显示为类似 €1,234.56 的格式。

如果想要完全自定义货币符号,可以通过在货币代码后添加一个自定义符号。例如,假设我们正在开发一个游戏应用,有自己独特的游戏货币符号 G$

<p>The in - game price is: {{ price | currency:'G$' }}</p>

这将显示为 G$1,234.56

控制小数位数

货币转换管道还允许我们控制小数部分的显示位数。我们可以通过在管道后传递第二个参数来指定小数位数。

格式为 currency:'currencyCode':'symbolDisplay':'digitInfo'。其中 digitInfo 是一个字符串,格式为 minIntegerDigits.minFractionDigits - maxFractionDigits

例如,我们只想显示一位小数:

<p>The price with one decimal is: {{ price | currency:'USD':'symbol':'1.1 - 1' }}</p>

这将显示为 $1,234.6,因为我们指定了最小整数位数为 1,最小小数位数为 1,最大小数位数也为 1。

如果我们希望小数部分至少显示两位,即使小数部分为 0:

<p>The price with two decimals is: {{ price | currency:'USD':'symbol':'1.2 - 2' }}</p>

这样会显示为 $1,234.56,确保了小数部分始终有两位。

处理负数

货币转换管道对于负数也有默认的处理方式。默认情况下,负数会用括号括起来。

例如,如果我们的价格变为负数:

export class AppComponent {
  price: number = -1234.56;
}

在模板中:

<p>The negative price is: {{ price | currency }}</p>

可能会显示为 ($1,234.56),具体格式还是取决于区域设置。

如果我们想要自定义负数的显示格式,可以通过传递一个 CurrencyPipeConfig 对象来实现。首先,在组件中导入 CurrencyPipe

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  price: number = -1234.56;
  constructor(private currencyPipe: CurrencyPipe) {}

  getCustomNegativePrice() {
    const config: any = {
      display: 'code',
      negativePrefix: '-',
      negativeSuffix: ''
    };
    return this.currencyPipe.transform(this.price, 'USD', config);
  }
}

然后在模板中:

<p>The custom negative price is: {{ getCustomNegativePrice() }}</p>

这将显示为 -USD1,234.56,我们自定义了负数前缀为 -,并去掉了后缀。

与区域设置的交互

Angular 的货币转换管道会自动根据用户浏览器的区域设置来格式化货币。但有时我们可能需要根据应用程序的设置来覆盖浏览器的区域设置。

我们可以通过在模块的 providers 中提供 LOCALE_ID 来设置应用程序级别的区域设置。

例如,在 app.module.ts 中:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';

registerLocaleData(localeFr, 'fr');

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{ provide: 'LOCALE_ID', useValue: 'fr' }],
  bootstrap: [AppComponent]
})
export class AppModule {}

然后在模板中使用货币转换管道:

<p>The price in French format is: {{ price | currency }}</p>

这将按照法语区域设置来格式化价格,可能显示为 1 234,56 €

在服务中使用货币转换管道

有时,我们可能需要在服务中使用货币转换管道,而不仅仅是在模板中。例如,我们可能有一个服务负责生成报表,需要将数字格式化为货币字符串。

首先,在服务中注入 CurrencyPipe

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

@Injectable({
  providedIn: 'root'
})
export class ReportService {
  constructor(private currencyPipe: CurrencyPipe) {}

  formatPriceAsCurrency(price: number, currencyCode: string) {
    return this.currencyPipe.transform(price, currencyCode);
  }
}

然后在组件中使用该服务:

import { Component } from '@angular/core';
import { ReportService } from './report.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  price: number = 1234.56;
  constructor(private reportService: ReportService) {}

  getFormattedPrice() {
    return this.reportService.formatPriceAsCurrency(this.price, 'USD');
  }
}

在模板中:

<p>The price from service is: {{ getFormattedPrice() }}</p>

这样我们就可以在服务中灵活地使用货币转换管道来格式化数据。

性能考虑

在使用货币转换管道时,性能也是一个需要考虑的因素。尤其是在数据频繁更新的场景下,过多地使用管道可能会影响应用程序的性能。

Angular 的管道默认是纯管道(pure pipe),这意味着只有当输入值发生纯变化(例如对象引用变化、基本类型值变化)时,管道才会重新执行。对于货币转换管道,如果价格值没有变化,管道不会重新计算。

但是,如果我们在模板中频繁地传递不同的参数给货币转换管道,例如不同的货币代码或小数位数设置,管道可能会频繁重新执行。

为了优化性能,可以尽量减少在模板中动态改变管道参数的情况。如果确实需要动态改变货币格式,可以考虑在组件中提前计算好不同格式的值,然后在模板中直接使用。

例如,假设我们需要根据用户选择的货币来显示价格:

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  price: number = 1234.56;
  selectedCurrency: string = 'USD';
  formattedPrices: { [key: string]: string } = {};

  constructor(private currencyPipe: CurrencyPipe) {}

  ngOnInit() {
    this.updateFormattedPrices();
  }

  onCurrencyChange() {
    this.updateFormattedPrices();
  }

  updateFormattedPrices() {
    const currencies = ['USD', 'EUR', 'GBP'];
    currencies.forEach(currency => {
      this.formattedPrices[currency] = this.currencyPipe.transform(this.price, currency);
    });
  }
}

在模板中:

<select [(ngModel)]="selectedCurrency" (change)="onCurrencyChange()">
  <option value="USD">USD</option>
  <option value="EUR">EUR</option>
  <option value="GBP">GBP</option>
</select>
<p>The price in {{ selectedCurrency }} is: {{ formattedPrices[selectedCurrency] }}</p>

这样,我们通过在组件中预先计算不同货币格式的值,减少了模板中管道的动态计算,从而提高了性能。

与国际化(i18n)的集成

在全球化的应用程序中,国际化是非常重要的。Angular 提供了强大的国际化支持,货币转换管道可以很好地与国际化功能集成。

首先,我们需要提取应用程序中的文本,包括货币转换管道中使用的货币符号和格式相关的文本。可以使用 Angular CLI 的 ng xi18n 命令来提取。

例如,在模板中:

<p i18n>The price is: {{ price | currency }}</p>

运行 ng xi18n 后,会生成一个 messages.xlf 文件,其中包含了需要翻译的文本。

然后,我们可以根据不同的语言环境创建相应的 messages.[language code].xlf 文件,并进行翻译。

在构建应用程序时,通过指定目标语言环境,Angular 会根据相应的翻译文件来显示货币格式。例如,对于法语环境,货币格式会根据法语的习惯进行显示,包括货币符号的位置和小数分隔符等。

错误处理

在使用货币转换管道时,也可能会遇到一些错误情况。例如,如果传递给管道的不是一个有效的数字,可能会导致异常。

为了避免这种情况,可以在使用管道前进行数据验证。在组件中,可以使用 isNaN() 函数来检查价格是否为有效数字。

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  price: any = 'not a number';

  getValidPrice() {
    return isNaN(this.price)? 0 : this.price;
  }
}

在模板中:

<p>The price is: {{ getValidPrice() | currency }}</p>

这样,当 price 不是有效数字时,会显示为货币格式的 0,而不会导致管道抛出异常。

另外,如果传递了不支持的货币代码,货币转换管道可能也会出现问题。虽然 Angular 对常见的货币代码有很好的支持,但对于一些罕见或自定义的货币代码,可能需要额外的处理。在这种情况下,可以通过在组件中维护一个支持的货币代码列表,并在使用管道前进行检查,以避免出现错误。

高级自定义

除了前面提到的基本自定义选项外,我们还可以进行更高级的自定义。例如,我们可以创建一个自定义管道,继承自 CurrencyPipe,并根据应用程序的特定需求重写一些方法。

首先,创建一个自定义管道类:

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

@Pipe({
  name: 'customCurrency'
})
export class CustomCurrencyPipe implements PipeTransform {
  constructor(private currencyPipe: CurrencyPipe) {}

  transform(value: number, currencyCode: string = 'USD', display: string = 'code', digitsInfo: string = '1.2 - 2'): string {
    // 在这里可以进行自定义逻辑,例如添加额外的前缀或后缀
    let result = this.currencyPipe.transform(value, currencyCode, display, digitsInfo);
    return `CustomPrefix ${result} CustomSuffix`;
  }
}

然后在模块中声明这个自定义管道:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';
import { CustomCurrencyPipe } from './custom - currency.pipe';

@NgModule({
  declarations: [AppComponent, CustomCurrencyPipe],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

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

<p>The custom - formatted price is: {{ price | customCurrency:'EUR' }}</p>

这将显示为类似 CustomPrefix €1,234.56 CustomSuffix 的格式,通过继承和重写 CurrencyPipetransform 方法,我们实现了更高级的自定义功能。

与响应式编程的结合

在现代 Angular 开发中,响应式编程是非常常见的。我们经常会处理 Observable 数据,例如从 API 获取的价格数据。货币转换管道同样可以与响应式编程很好地结合。

假设我们有一个服务,通过 HttpClient 从 API 获取价格数据:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class PriceService {
  constructor(private http: HttpClient) {}

  getPrice(): Observable<number> {
    return this.http.get<number>('/api/price');
  }
}

在组件中订阅这个 Observable 并使用货币转换管道:

import { Component, OnInit } from '@angular/core';
import { PriceService } from './price.service';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  price: number | null = null;
  constructor(private priceService: PriceService, private currencyPipe: CurrencyPipe) {}

  ngOnInit() {
    this.priceService.getPrice().subscribe(price => {
      this.price = price;
    });
  }

  getFormattedPrice() {
    return this.price? this.currencyPipe.transform(this.price, 'USD') : '';
  }
}

在模板中:

<p>The price from API is: {{ getFormattedPrice() }}</p>

这样,当从 API 获取到价格数据时,会自动使用货币转换管道进行格式化并显示。

另外,如果我们需要对多个 Observable 数据进行合并,并对合并后的数据进行货币格式化,可以使用 combineLatest 等操作符。例如,假设我们同时获取价格和货币代码:

import { Component, OnInit } from '@angular/core';
import { PriceService } from './price.service';
import { CurrencyService } from './currency.service';
import { combineLatest } from 'rxjs';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  price: number | null = null;
  currencyCode: string | null = null;
  constructor(private priceService: PriceService, private currencyService: CurrencyService, private currencyPipe: CurrencyPipe) {}

  ngOnInit() {
    combineLatest([this.priceService.getPrice(), this.currencyService.getCurrencyCode()]).subscribe(([price, currencyCode]) => {
      this.price = price;
      this.currencyCode = currencyCode;
    });
  }

  getFormattedPrice() {
    return this.price && this.currencyCode? this.currencyPipe.transform(this.price, this.currencyCode) : '';
  }
}

在模板中:

<p>The price with dynamic currency is: {{ getFormattedPrice() }}</p>

通过这种方式,我们可以将货币转换管道与响应式编程的各种操作符结合,灵活处理动态数据。

跨平台应用中的货币转换管道

当开发跨平台的 Angular 应用(例如同时支持 Web、移动和桌面应用)时,货币转换管道同样适用。但需要注意不同平台可能存在的区域设置差异。

在移动应用中,特别是原生移动应用(通过 Cordova 或 Ionic 等框架开发),区域设置可能与设备的系统设置相关。而在桌面应用(例如使用 Electron 开发)中,区域设置可能又有所不同。

为了确保一致性,我们可以在应用启动时检测设备的区域设置,并根据检测结果设置应用程序级别的区域设置。例如,在 Ionic 应用中,可以使用 @ionic - native/diagnostic 插件来获取设备的区域设置:

import { Component, OnInit } from '@angular/core';
import { Diagnostic } from '@ionic - native/diagnostic/ngx';
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeEn from '@angular/common/locales/en';
import { LOCALE_ID, Inject } from '@angular/core';

@Component({
  selector: 'app - home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {
  constructor(private diagnostic: Diagnostic, @Inject(LOCALE_ID) private locale: string) {}

  ngOnInit() {
    this.diagnostic.getDeviceLocale().then(locale => {
      if (locale.startsWith('fr')) {
        registerLocaleData(localeFr, 'fr');
        // 设置应用程序的区域设置为法语
        this.locale = 'fr';
      } else {
        registerLocaleData(localeEn, 'en');
        // 设置应用程序的区域设置为英语
        this.locale = 'en';
      }
    });
  }
}

这样,在不同平台上,货币转换管道都能根据设备的区域设置进行正确的格式化,提供一致的用户体验。

货币转换管道在大型项目中的架构考虑

在大型 Angular 项目中,合理使用货币转换管道需要进行一些架构上的考虑。

首先,为了保持一致性,建议将货币转换的相关逻辑进行集中管理。可以创建一个专门的服务,例如 CurrencyFormatService,来封装货币转换管道的使用。这个服务可以提供一些通用的方法,如根据不同业务场景格式化价格。

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

@Injectable({
  providedIn: 'root'
})
export class CurrencyFormatService {
  constructor(private currencyPipe: CurrencyPipe) {}

  formatProductPrice(price: number) {
    return this.currencyPipe.transform(price, 'USD', 'code', '1.2 - 2');
  }

  formatRevenue(price: number) {
    return this.currencyPipe.transform(price, 'USD', 'code', '1.0 - 0');
  }
}

在各个组件中,只需要注入这个服务并调用相应的方法,而不是在每个组件中都直接使用货币转换管道。这样可以方便维护和修改货币格式的逻辑。

其次,在处理大量数据时,例如在数据表格中显示大量价格数据,需要考虑性能优化。可以采用分页、虚拟滚动等技术,同时结合前面提到的性能优化方法,如减少管道参数的动态变化。

另外,在大型项目中,可能会涉及多个团队协作开发。为了避免不一致的货币格式,应该制定统一的规范,明确在不同业务场景下如何使用货币转换管道,包括货币代码的选择、小数位数的设置等。这样可以确保整个项目中货币格式的一致性,提高代码的可维护性和可读性。

货币转换管道与单元测试

在 Angular 开发中,单元测试是保证代码质量的重要环节。对于使用货币转换管道的组件和服务,我们也需要编写相应的单元测试。

首先,测试组件中使用货币转换管道的功能。假设我们有一个 PriceComponent,它使用货币转换管道来显示价格:

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

@Component({
  selector: 'app - price',
  templateUrl: './price.component.html'
})
export class PriceComponent {
  price: number = 1234.56;
}

price.component.html 中:

<p>The price is: {{ price | currency }}</p>

编写单元测试:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PriceComponent } from './price.component';
import { CurrencyPipe } from '@angular/common';

describe('PriceComponent', () => {
  let component: PriceComponent;
  let fixture: ComponentFixture<PriceComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [PriceComponent],
      providers: [CurrencyPipe]
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(PriceComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should display the price in currency format', () => {
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.textContent).toContain('$');
  });
});

上述测试中,我们通过 TestBed 配置了 PriceComponentCurrencyPipe,然后检查组件渲染后是否包含货币符号。

对于在服务中使用货币转换管道的情况,也可以进行类似的测试。假设我们有一个 ReportService,它使用 CurrencyPipe 来格式化价格:

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

@Injectable({
  providedIn: 'root'
})
export class ReportService {
  constructor(private currencyPipe: CurrencyPipe) {}

  formatPriceAsCurrency(price: number, currencyCode: string) {
    return this.currencyPipe.transform(price, currencyCode);
  }
}

编写单元测试:

import { TestBed } from '@angular/core/testing';
import { ReportService } from './report.service';
import { CurrencyPipe } from '@angular/common';

describe('ReportService', () => {
  let service: ReportService;
  let currencyPipe: CurrencyPipe;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [ReportService, CurrencyPipe]
    });
    service = TestBed.inject(ReportService);
    currencyPipe = TestBed.inject(CurrencyPipe);
  });

  it('should format price as currency', () => {
    const price = 1234.56;
    const currencyCode = 'USD';
    const spy = spyOn(currencyPipe, 'transform').and.returnValue('$1,234.56');
    const result = service.formatPriceAsCurrency(price, currencyCode);
    expect(result).toBe('$1,234.56');
    expect(spy).toHaveBeenCalledWith(price, currencyCode);
  });
});

在这个测试中,我们通过 spyOn 方法来模拟 CurrencyPipetransform 方法,并验证 ReportServiceformatPriceAsCurrency 方法是否正确调用了 CurrencyPipe 并返回预期结果。

通过编写这些单元测试,可以确保货币转换管道在组件和服务中的正确使用,提高代码的稳定性和可靠性。