
Когда я только начал осваивать Angular, мне не было до конца понятно, что это за зверь такой – Pipe, и зачем вообще его использовать. Официальная документация гласит следующее (в переводе от metanit.com):
Pipes представляют специальные инструменты, которые позволяют форматировать отображаемые значения
Да, это действительно объясняет, что делают Pipes. Но в тоже время не отвечает на вопрос, а зачем их использовать? Почему нельзя воспользоваться обычным методом класса? Pipes для меня какое-то время был тёмной лошадкой. Но потом я узнал «страшный секрет» о Pipes, который всё расставил на свои места…
Ладно, я излишне накручиваю интригу. Главный секрет Pipes заключается в том, что они кэшируют результат трансформации. Для опытного разработчика это никакой не секрет. Но для меня это было неочевидно. Да и сегодня замечаю, что не все мои коллеги в курсе этой особенности. Не понимаю, почему в документации кэширование так вскользь затронуто. Может быть, разработчики не особо хотят говорить на неудобные темы (Change Detection)? Так или иначе, кэширование в Pipes меняет всё, и я хотел бы рассказать в этой небольшой статейке, почему использовать Pipes – важно
⚠️В чем проблема
Для начала хотелось бы затронуть тему Change Detection в Angular, ведь именно этот механизм мотивирует использовать Pipes. Говоря кратко, на каждый чих Angular запускает проверку изменений. Она сравнивает все значения в компоненте, в том числе дёргает getter’ы и вызывает функции, если они есть в шаблоне. И в этом заключается суть проблемы. Допустим, у вас есть следующий код:
<ul>
@for (fruit of fruits.split(', '); track fruit) {
<li>{{ fruit }}</li>
}
</ul>
Он кажется безобидным, но заключает в себе серьезную проблему – при каждый проверке изменений, будет выполняться split(‘, ’)! В приложении может быть очень много триггеров на проверку изменения состояния. Например, в проекте над которым я сейчас работаю, за секунду может триггернуться 100-200 проверок (привет, ChangeDetectionStrategy.Default). И каждый раз выполнять split? Это накладно. Но тут на помощь приходят пайпы
?Как использовать Pipes
Создадим сам пайп через консольную команду npx ng generate pipe split
@Pipe({ name: 'split' })
export class SplitPipe implements PipeTransform {
transform(value: string): string[] {
return value.split(', ');
}
}
И отрефакторим шаблон следующим образом:
<ul>
@for (fruit of fruits | split:', '; track fruit) {
<li>{{ fruit }}</li>
}
</ul>
Теперь трансформация вызовется всего один раз. Все благодаря тому, что мы написали чистый Pipe, который будет выдавать закэшированный результат трансформации до тех пор, пока передаваемое в него значение не изменится (или ссылка в случае, если в Pipe кладется объект)
Поведение Pipe можно изменить, если установить в директиве @Pipe({ ..., pure: false }). Тогда Pipe будет вести себя как обычный сервис и осуществлять новую трансформацию при каждой проверке изменений (но зачем это делать?)
В Angular уже встроены наиболее распространенные Pipes. Например, мне пригождались SlicePipe, DatePipe, LowerCasePipe и UpperCasePipe из стандартной библиотеки. Подробнее о них можно узнать тут: https://angular.dev/guide/templates/pipes#built-in-pipes
Но их не так много. Поэтому, скорее всего вам придется написать свои Pipes, либо воспользоваться какой-либо существующей библиотекой. Например, @nglrx/pipes ( https://github.com/nglrx/pipes ). В ней реализованы десятки стандартных операций для строк, чисел и массивов, в том числе и split из примера
✅️Когда использовать Pipes
Механизм проверки состояний всегда был слабым местом фреймворка Angular. И очевидно, Pipes были придуманы, чтобы «закостылить» случаи, когда всё становится совсем плохо. К счастью, избегать такие ситуации довольно легко, особенно с Signals. Но для себя я всё же выделил следующие три кейса, когда Pipes могут быть полезны:
1️⃣Преобразование внутри @for
В идеале мы передаем в @for уже полностью обработанный массив. На практике же это не всегда удобно и проще вызвать преобразование внутри @for
<ul>
@for (fruit of fruitList; track fruit) {
<li>{{ fruit | lowercase }}</li>
}
</ul>
2️⃣Тривиальные, но ресурсоемкие преобразования
Вам вряд ли захочется реализовать самостоятельно кэшировние ради одного лишь slice или toUpperCase. К счастью, для этого можно воспользоваться Pipes. Еще раз упомяну @nglrx/pipes здесь
<ul>
@for (item of fruits | combine: vegetables; track item) {
<li>{{ item }}</li>
}
</ul>
3️⃣Реально сложные вычисления
Сомнительный кейс с точки зрения юнит-тестирования. Но если у вас есть сервис, который маппит DTO в модель, то почему бы не отрефакторить его с использованием PipeTransform?
<ul>
@for (item of apiResponse | innerModels; track item) {
<li>{{ item }}</li>
}
</ul>
@Pipe({ name: 'innerModels' })
export class InnerModelPipe implements PipeTransform {
transform(value: ResponseDto): InnerModels {
// some transformations here ...
}
}
?Заключение
Поначалу Pipes могут выглядеть неудобными и бесполезными. Но при большем погружении в Angular они начинают казаться всё более привлекательными. В то же время ни в коем случае не призываю искушаться и обмазывать всю кодовую базу пайпами:
❌ Обилие преобразований в шаблоне усложняет написание юнит-тестов
❌ Создание своих Pipes требует бойлерплейта
А вот здоровое использование своей / сторонней библиотеки пайпов-утилит я бы точно порекомендовал. Это может существенно облегчить вашу жизнь и улучшить производительность приложения. Я бы руководствовался следующим небольшим чеклистом в вопросе переноса функционала в Pipes:
✔️ Ресурсоемкая операция, особенно если она выделяет память
✔️ Имеет потенциал для переиспользования (или можно свести до какого-то переиспользуемого примитива, в духе
sliceилиsplit)
P.S. Надеюсь, моя статья не продублировала уже существующую на хабре. Беглым поиском нашел только англоязычную статью