Перейти к содержанию

Преобразование данных с помощью пайпов

📅 28.02.2022

Используйте пайпы для преобразования строк, сумм валют, дат и других данных для отображения на экране. Пайпы — это простые функции, которые можно использовать в шаблонных выражениях для приема входного значения и возврата преобразованного значения. Пайпы полезны тем, что их можно использовать во всем приложении, объявляя каждый пайп только один раз. Например, с помощью пайпа можно отобразить дату в виде April 15, 1988, а не в виде необработанной строки.

Пример приложения, используемый в данной теме, приведен в живом примере.

Angular предоставляет встроенные пайпы для типичных преобразований данных, включая преобразования для интернационализации (i18n), которые используют информацию о локали для форматирования данных. Ниже перечислены часто используемые встроенные пайпы для форматирования данных:

Трубы Подробнее
DatePipe Формирование значения даты в соответствии с правилами локали.
UpperCasePipe Преобразование текста в верхний регистр.
LowerCasePipe Преобразование текста во все строчные регистры.
CurrencyPipe Преобразование числа в строку валюты, отформатированную в соответствии с правилами локали.
DecimalPipe Преобразование числа в строку с десятичной точкой, отформатированную в соответствии с правилами локали.
PercentPipe Преобразование числа в строку с процентами, оформленную в соответствии с правилами локали.

Создание пайпов для инкапсуляции пользовательских преобразований и использование пользовательских пайпов в шаблонных выражениях.

Предварительные условия

Для использования пайпов необходимо иметь базовое представление о следующем:

Использование пайпа в шаблоне

Чтобы применить пайп, используйте символ пайпа (|) в выражении шаблона, как показано в следующем примере кода, вместе с именем пайпа, которым является date для встроенного DatePipe. Вкладки в примере выглядят следующим образом:

Файлы Подробности
app.component.html Использует date в отдельном шаблоне для отображения дня рождения.
hero-birthday1.component.ts Использует тот же пайп как часть встроенного шаблона в компоненте, который также устанавливает значение дня рождения.
1
<p>The hero's birthday is {{ birthday | date }}</p>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Component } from '@angular/core';

@Component({
    selector: 'app-hero-birthday',
    template:
        "<p>The hero's birthday is {{ birthday | date }}</p>",
})
export class HeroBirthdayComponent {
    birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
}

Значение birthday компонента через оператор пайпа, |, попадает в функцию date.

Преобразование данных с помощью параметров и цепочек пайпов

Используйте необязательные параметры для точной настройки выходных данных пайпа. Например, используйте CurrencyPipe с кодом страны, например EUR, в качестве параметра. Шаблонное выражение {{ amount | currency:'EUR' }} преобразует amount в валюту в евро. После имени пайпа (currency) следует символ двоеточия (:) и значение параметра ('EUR').

Если пайп принимает несколько параметров, разделяйте их значения двоеточиями. Например, {{ amount | currency:'EUR':'Euros'}} добавляет второй параметр, строковый литерал 'Euros', в выходную строку. В качестве параметра можно использовать любое допустимое выражение шаблона, например, строковый литерал или свойство компонента.

Некоторые пайпы требуют как минимум один параметр и допускают большее количество необязательных параметров, например SlicePipe. Например, {{ slice:1:5 }} создает новый массив или строку, содержащую подмножество элементов, начиная с элемента 1 и заканчивая элементом 5.

Пример: Форматирование даты

На вкладках в следующем примере демонстрируется переключение между двумя различными форматами ('shortDate' и 'fullDate'):

  • Шаблон app.component.html использует параметр формата для DatePipe (с именем date), чтобы показать дату как 04/15/88.
  • Компонент hero-birthday2.component.ts привязывает параметр формата пайпа к свойству format компонента в секции template и добавляет кнопку для события click, привязанную к методу toggleFormat() компонента.
  • Метод toggleFormat() компонента hero-birthday2.component.ts переключает свойство format компонента между короткой формой ('shortDate') и более длинной ('fullDate').
1
2
3
<p>
    The hero's birthday is {{ birthday | date:"MM/dd/yy" }}
</p>
1
2
3
4
template: `
<p>The hero's birthday is {{ birthday | date:format }}</p>
<button type="button" (click)="toggleFormat()">Toggle Format</button>
`;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export class HeroBirthday2Component {
    birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
    toggle = true; // start with true == shortDate

    get format() {
        return this.toggle ? 'shortDate' : 'fullDate';
    }
    toggleFormat() {
        this.toggle = !this.toggle;
    }
}

При нажатии на кнопку Toggle Format формат даты чередуется между 04/15/1988 и Friday, April 15, 1988.

Параметры формата пайпа date см. в разделе DatePipe.

Пример: Применение двух форматов с помощью цепочки пайпов

Цепочка пайпов позволяет сделать так, чтобы выход одного пайпа стал входом для следующего.

В следующем примере цепочки пайпов сначала применяют формат к значению даты, а затем преобразуют отформатированную дату в заглавные символы. Первая вкладка шаблона src/app/app.component.html связывает DatePipe и UpperCasePipe для отображения дня рождения как APR 15, 1988. На второй вкладке шаблона src/app/app.component.html параметр fullDate передается в date перед цепочкой в uppercase, в результате чего получается FRIDAY, APRIL 15, 1988.

1
2
The chained hero's birthday is {{ birthday | date |
uppercase}}
1
2
The chained hero's birthday is {{ birthday | date:'fullDate'
| uppercase}}

Создание пайпов для пользовательских преобразований данных

Создайте пользовательские пайпы для инкапсуляции преобразований, которые не предусмотрены встроенными пайпами. Затем используйте пользовательские пайпы в шаблонных выражениях так же, как и встроенные пайпы — для преобразования входных значений в выходные значения для отображения.

Пометка класса как пайпа

Чтобы пометить класс как пайп и снабдить его конфигурационными метаданными, примените к классу @Pipe decorator. Используйте UpperCamelCase (общее соглашение для имен классов) для имени класса пайпа и camelCase для соответствующей строки name. Не используйте дефисы в строке name. Подробности и примеры см. в разделе Имена пайпов.

Используйте name в шаблонных выражениях так же, как и для встроенного пайпа.

  • Включите свой пайп в поле declarations метаданных NgModule для того, чтобы он был доступен шаблону.

    См. файл app.module.ts в примере приложения (живой пример).

    Подробнее см. раздел NgModules.

  • Зарегистрируйте пользовательские пайпы.

    Команда Angular CLI ng generate pipe регистрирует пайп автоматически.

Использование интерфейса PipeTransform

Реализуйте интерфейс PipeTransform в своем пользовательском классе пайпа для выполнения трансформации.

Angular вызывает метод transform со значением привязки в качестве первого аргумента и любыми параметрами в качестве второго аргумента в виде списка и возвращает преобразованное значение.

Пример: Экспоненциальное преобразование значения

В игре может потребоваться реализовать преобразование, которое экспоненциально увеличивает значение для повышения силы героя. Например, если количество очков героя равно 2, то при экспоненциальном увеличении силы героя на 10 количество очков будет равно 1024. Для этого преобразования используйте пользовательский пайп.

В следующем примере кода показаны два определения компонентов:

Файлы Подробности
exponential-strength.pipe.ts Определяет пользовательский пайп с именем exponentialStrength и методом transform, выполняющим преобразование. Определяет аргумент метода transform (exponent) для параметра, передаваемого в пайп.
power-booster.component.ts Демонстрирует использование пайпа с указанием значения (2) и параметра экспоненты (10).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Pipe, PipeTransform } from '@angular/core';
/*
* Raise the value exponentially
* Takes an exponent argument that defaults to 1.
* Usage:
*   value | exponentialStrength:exponent
* Example:
*   {{ 2 | exponentialStrength:10 }}
*   formats to: 1024
*/
@Pipe({ name: 'exponentialStrength' })
export class ExponentialStrengthPipe
    implements PipeTransform {
    transform(value: number, exponent = 1): number {
        return Math.pow(value, exponent);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Component } from '@angular/core';

@Component({
    selector: 'app-power-booster',
    template: `
        <h2>Power Booster</h2>
        <p>
            Super power boost:
            {{ 2 | exponentialStrength: 10 }}
        </p>
    `,
})
export class PowerBoosterComponent {}

В браузере отображается следующее:

1
2
3
Power Booster

Superpower boost: 1024

Чтобы исследовать поведение пайпа exponentialStrength в живом примере, измените значение и необязательную экспоненту в шаблоне.

Обнаружение изменений с помощью привязки данных в пайпах

Для отображения значений и реагирования на действия пользователя в пайпе используется привязка данных. Если в качестве данных используется примитивное входное значение, например String или Number, или ссылка на объект, например Date или Array, Angular выполняет пайп всякий раз, когда обнаруживает изменение входного значения или ссылки.

Например, можно изменить предыдущий пример пользовательского пайпа, чтобы использовать двустороннее связывание данных с ngModel для ввода суммы и повышающего коэффициента, как показано в следующем примере кода.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Component } from '@angular/core';

@Component({
    selector: 'app-power-boost-calculator',
    template: `
        <h2>Power Boost Calculator</h2>
        <label for="power-input">Normal power: </label>
        <input
            id="power-input"
            type="text"
            [(ngModel)]="power"
        />
        <label for="boost-input">Boost factor: </label>
        <input
            id="boost-input"
            type="text"
            [(ngModel)]="factor"
        />
        <p>
            Super Hero Power:
            {{ power | exponentialStrength: factor }}
        </p>
    `,
    styles: ['input {margin: .5rem 0;}'],
})
export class PowerBoostCalculatorComponent {
    power = 5;
    factor = 1;
}

Пайп exponentialStrength выполняется каждый раз, когда пользователь изменяет значение "нормальной мощности" или "коэффициента усиления".

Angular обнаруживает каждое изменение и немедленно запускает пайп. Это хорошо подходит для примитивных входных значений. Однако если вы изменяете что-то внутри составного объекта (например, месяц даты, элемент массива или свойство объекта), вам необходимо понять, как работает обнаружение изменений и как использовать нечистый пайп.

Как работает обнаружение изменений

Angular ищет изменения в значениях, связанных с данными, в процессе change detection, который запускается после каждого события DOM: каждого нажатия клавиши, перемещения мыши, тиканья таймера и ответа сервера. Следующий пример, в котором не используется пайп, демонстрирует, как Angular использует свою стандартную стратегию обнаружения изменений для отслеживания и обновления отображения каждого героя в массиве heroes. На вкладках примера показано следующее:

Файлы Подробности
flying-heroes.component.html (v1) Повторитель *ngFor отображает имена героев.
flying-heroes.component.ts (v1) Предоставляет героев, добавляет героев в массив и сбрасывает массив.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<label for="hero-name">New hero name: </label>
<input
    type="text"
    #box
    id="hero-name"
    (keyup.enter)="addHero(box.value); box.value=''"
    placeholder="hero name"
/>
<button type="button" (click)="reset()">
    Reset list of heroes
</button>
<div *ngFor="let hero of heroes">{{hero.name}}</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export class FlyingHeroesComponent {
    heroes: any[] = [];
    canFly = true;
    constructor() {
        this.reset();
    }

    addHero(name: string) {
        name = name.trim();
        if (!name) {
            return;
        }
        const hero = { name, canFly: this.canFly };
        this.heroes.push(hero);
    }

    reset() {
        this.heroes = HEROES.slice();
    }
}

Angular обновляет отображение каждый раз, когда пользователь добавляет героя. Если пользователь нажимает кнопку Reset, Angular заменяет heroes новым массивом исходных героев и обновляет отображение. Если добавить возможность удаления или изменения героя, то Angular обнаружит эти изменения и также обновит отображение.

Однако выполнение пайпа для обновления дисплея при каждом изменении приведет к снижению производительности приложения. Поэтому Angular использует более быстрый алгоритм обнаружения изменений для выполнения пайпа, как описано в следующем разделе.

Обнаружение чистых изменений примитивов и ссылок на объекты

По умолчанию пайпы определяются как чистые, поэтому Angular выполняет пайп только тогда, когда обнаруживает чистое изменение входного значения. Чистое изменение — это либо изменение примитивного входного значения (такого как String, Number, Boolean или Symbol), либо изменение объектной ссылки (такой как Date, Array, Function или Object).

Чистый пайп должен использовать чистую функцию, то есть функцию, которая обрабатывает входные данные и возвращает значения без побочных эффектов. Другими словами, при одинаковых входных данных чистая функция всегда должна возвращать одинаковые выходные данные.

При использовании чистого пайпа Angular игнорирует изменения внутри составных объектов, например, вновь добавленный элемент существующего массива, поскольку проверка примитивного значения или ссылки на объект выполняется гораздо быстрее, чем глубокая проверка различий внутри объектов. Angular может быстро определить, можно ли пропустить выполнение пайпа и обновление представления.

Однако чистый пайп с массивом в качестве входных данных может работать не так, как вы хотите. Чтобы продемонстрировать эту проблему, изменим предыдущий пример так, чтобы отфильтровать список героев только по тем героям, которые умеют летать. Используйте FlyingHeroesPipe в ретрансляторе *ngFor, как показано в следующем коде. Вкладки для примера выглядят следующим образом:

  • Шаблон (flying-heroes.component.html (flyers)) с новым пайпом
  • Реализация пользовательского пайпа FlyingHeroesPipe (flying-heroes.pipe.ts)
1
2
3
<div *ngFor="let hero of (heroes | flyingHeroes)">
    {{hero.name}}
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Pipe, PipeTransform } from '@angular/core';

import { Hero } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
    transform(allHeroes: Hero[]) {
        return allHeroes.filter((hero) => hero.canFly);
    }
}

Теперь приложение демонстрирует неожиданное поведение: Когда пользователь добавляет летающих героев, ни один из них не появляется в разделе "Heroes who fly". Это происходит потому, что код, добавляющий героя, заталкивает его в массив heroes:

1
this.heroes.push(hero);

Детектор изменений игнорирует изменения элементов массива, поэтому пайп не выполняется.

Причина, по которой Angular игнорирует измененный элемент массива, заключается в том, что ссылка на массив не изменилась. Поскольку массив остался прежним, Angular не обновляет отображение.

Один из способов добиться желаемого поведения — изменить саму ссылку на объект. Замените массив новым массивом, содержащим новые измененные элементы, а затем введите новый массив в пайп. В предыдущем примере создайте массив с добавлением нового героя и присвойте его heroes. Angular обнаружит изменение в ссылке на массив и выполнит пайп.

Подведем итог: если вы мутируете входной массив, то чистый пайп не выполняется. Если же входной массив заменить, то пайп будет выполнен, и отображение будет обновлено.

Приведенный пример демонстрирует изменение кода компонента для реализации пайпа.

Чтобы сохранить независимость компонента от HTML-шаблонов, использующих пайпы, в качестве альтернативы можно использовать нечистый пайп для обнаружения изменений в составных объектах, таких как массивы, как описано в следующем разделе.

Обнаружение нечистых изменений внутри составных объектов

Чтобы выполнить пользовательский пайп после изменения внутри составного объекта, например, изменения элемента массива, необходимо определить пайп как impure для обнаружения нечистых изменений. Angular выполняет нечистый пайп каждый раз, когда обнаруживает изменение при каждом нажатии клавиши или движении мыши.

Хотя нечистый пайп может быть полезен, использовать его следует с осторожностью. Долго работающий нечистый пайп может значительно замедлить работу приложения.

Сделать пайп нечистым, установив его флаг pure в значение false:

1
2
3
4
@Pipe({
  name: 'flyingHeroesImpure',
  pure: false
})

В приведенном ниже коде показана полная реализация FlyingHeroesImpurePipe, которая расширяет FlyingHeroesPipe для наследования его характеристик. Из примера видно, что больше ничего менять не нужно — единственное отличие — установка флага pure как false в метаданных пайпа.

1
2
3
4
5
@Pipe({
    name: 'flyingHeroesImpure',
    pure: false,
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Pipe, PipeTransform } from '@angular/core';

import { Hero } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
    transform(allHeroes: Hero[]) {
        return allHeroes.filter((hero) => hero.canFly);
    }
}

FlyingHeroesImpurePipe является хорошим кандидатом на роль нечистого пайпа, поскольку функция transform является тривиальной и быстрой:

1
return allHeroes.filter((hero) => hero.canFly);

От FlyingHeroesImpureComponent можно получить FlyingHeroesComponent. Как показано в следующем коде, изменяется только пайп в шаблоне.

1
2
3
<div *ngFor="let hero of (heroes | flyingHeroesImpure)">
    {{hero.name}}
</div>

Чтобы убедиться в том, что при добавлении героев на экране происходит обновление, посмотрите живой пример.

Развертывание данных из наблюдаемого объекта

Observables позволяют передавать сообщения между частями приложения. Обсерватории рекомендуются для обработки событий, асинхронного программирования и работы с несколькими значениями. Наблюдаемые могут передавать одиночные или множественные значения любого типа как синхронно (как функция передает значение вызывающему ее пользователю), так и асинхронно по расписанию.

Подробности и примеры использования observables см. в Observables Overview.

Используйте встроенный AsyncPipe для приема наблюдаемого объекта в качестве входного и автоматической подписки на него. Без этого пайпа коду компонента пришлось бы подписываться на наблюдаемую, чтобы потреблять ее значения, извлекать разрешенные значения, выставлять их для связывания и отписываться от них при уничтожении наблюдаемой, чтобы избежать утечек памяти. AsyncPipe — это нечистый пайп, который избавляет ваш компонент от необходимости поддерживать подписку и продолжать передавать значения из наблюдаемой таблицы по мере их поступления.

Следующий пример кода привязывает наблюдаемое хранилище строк сообщений (message$) к представлению с помощью пайпа async.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Component } from '@angular/core';

import { Observable, interval } from 'rxjs';
import { map, take } from 'rxjs/operators';

@Component({
    selector: 'app-hero-async-message',
    template: ` <h2>Async Hero Message and AsyncPipe</h2>
        <p>Message: {{ message$ | async }}</p>
        <button type="button" (click)="resend()">
            Resend
        </button>`,
})
export class HeroAsyncMessageComponent {
    message$: Observable<string>;

    private messages = [
        'You are my hero!',
        'You are the best hero!',
        'Will you be my hero?',
    ];

    constructor() {
        this.message$ = this.getResendObservable();
    }

    resend() {
        this.message$ = this.getResendObservable();
    }

    private getResendObservable() {
        return interval(500).pipe(
            map((i) => this.messages[i]),
            take(this.messages.length)
        );
    }
}

Кэширование HTTP-запросов

Для [взаимодействия с внутренними сервисами по протоколу HTTP] (understanding-communicating-with-http.md 'Communicating with backend services using HTTP') сервис HttpClient использует наблюдаемые и предлагает метод HttpClient.get() для получения данных с сервера. Асинхронный метод посылает HTTP-запрос и возвращает наблюдаемую, которая выдает в ответ запрошенные данные.

Как было показано в предыдущем разделе, используйте нечистую трубу AsyncPipe для приема наблюдаемого объекта в качестве входного и автоматической подписки на него. Вы также можете создать нечистый пайп для выполнения и кэширования HTTP-запроса.

Нечистые пайпы вызываются каждый раз, когда для компонента выполняется обнаружение изменений, что может происходить как раз в несколько миллисекунд. Чтобы избежать проблем с производительностью, вызывайте сервер только при изменении запрашиваемого URL, как показано в следующем примере, и используйте пайп для кэширования ответа сервера. На вкладках показано следующее:

  • Пайп fetch (fetch-json.pipe.ts).
  • Компонент harness (hero-list.component.ts) для демонстрации запроса, использующий шаблон, определяющий две привязки к пайпу, запрашивающему героев из файла heroes.json.

    Вторая привязка связывает пайп fetch со встроенным JsonPipe для отображения тех же данных о героях в формате JSON.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { HttpClient } from '@angular/common/http';
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'fetch',
    pure: false,
})
export class FetchJsonPipe implements PipeTransform {
    private cachedData: any = null;
    private cachedUrl = '';

    constructor(private http: HttpClient) {}

    transform(url: string): any {
        if (url !== this.cachedUrl) {
            this.cachedData = null;
            this.cachedUrl = url;
            this.http
                .get(url)
                .subscribe(
                    (result) => (this.cachedData = result)
                );
        }

        return this.cachedData;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { Component } from '@angular/core';

@Component({
    selector: 'app-hero-list',
    template: ` <h2>Heroes from JSON File</h2>

        <div
            *ngFor="
                let hero of 'assets/heroes.json' | fetch
            "
        >
            {{ hero.name }}
        </div>

        <p>
            Heroes as JSON:
            {{ 'assets/heroes.json' | fetch | json }}
        </p>`,
})
export class HeroListComponent {}

В предыдущем примере точка останова на запросе пайпа на получение данных показывает следующее:

  • Каждое связывание получает свой собственный экземпляр пайпа.
  • Каждый экземпляр пайпа кэширует свой URL и данные и обращается к серверу только один раз.

Пайпы fetch и fetch-json отображают героев в браузере следующим образом:

1
2
3
4
5
6
7
8
Heroes from JSON File

Windstorm
Bombasto
Magneto
Tornado

Heroes as JSON: [ { "name": "Windstorm", "canFly": true }, { "name": "Bombasto", "canFly": false }, { "name": "Magneto", "canFly": false }, { "name": "Tornado", "canFly": true } ]

Встроенная функция JsonPipe предоставляет возможность диагностировать загадочную неудачную привязку данных или проверить объект на предмет будущей привязки.

Пайпы и старшинство

Оператор пайп имеет более высокий приоритет, чем тернарный оператор (?:), поэтому a ? b : c | x будет разобран как a ? b : (c | x). Оператор пайп не может быть использован без круглых скобок в первом и втором операндах ?:.

В силу старшинства, если вы хотите, чтобы пайп применялся к результату тернарного оператора, оберните все выражение круглыми скобками; например, (a ? b : c) | x.

1
2
<!-- use parentheses in the third operand so the pipe applies to the whole expression -->
{{ (true ? 'true' : 'false') | uppercase }}

Ссылки

Комментарии