Spec-Zone.ru › Angular 2

Трубы

Трубы преобразуют отображаемые значения в шаблоне.

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

После получения данных вы можете напрямую передать их необработанные toString значения в представление, но это редко приводит к хорошему пользовательскому опыту. Например, в большинстве случаев пользователи предпочитают видеть дату в простом формате, например, 15 апреля 1988 года, а не в формате исходной строки Пт апр 15 1988 00:00:00 GMT-0700 (Pacific Daylight Time).

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

Представляем Angular-трубы — способ написания преобразований отображаемых значений, которые вы можете объявлять в своём HTML.

Вы можете запустить пример в реальном времени в Plunker и скачать код оттуда.

Использование труб

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

src/app/hero-birthday1.component.ts

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

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

Обратите внимание на шаблон компонента.

<p>The hero's birthday is {{ birthday | date }}</p>

Внутри выражения интерполяции вы передаёте значение компонента birthday через оператор трубы ( | ) в функцию трубы Date Pipe справа. Все трубы работают таким образом.

Трубам Date и Currency необходим API интернационализации ECMAScript. Safari и другие старые браузеры его не поддерживают. Вы можете добавить поддержку с помощью полифила.

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>

Встроенные трубы

Angular поставляется с набором труб, таких как DatePipe, UpperCasePipe, LowerCasePipe, CurrencyPipe, и PercentPipe. Все они доступны для использования в любом шаблоне.

Узнайте больше об этих и многих других встроенных трубах в темах по трубам Справочника по API; отфильтруйте записи, содержащие слово "pipe".

Angular не имеет трубы FilterPipe или OrderByPipe по причинам, объяснённым в Приложении к этой странице.

Параметризация трубы

Труба может принимать любое количество необязательных параметров для тонкой настройки вывода. Чтобы добавить параметры к трубе, укажите имя трубы, двоеточие ( : ) и затем значение параметра (например, currency:'EUR'). Если труба принимает несколько параметров, разделяйте значения двоеточиями (например, slice:1:5).

Измените шаблон дня рождения, чтобы передать параметр формата трубе Date. После форматирования дня рождения героя 15 апреля, он отображается как 15.04.88:

<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>

Значение параметра может быть любым допустимым выражением шаблона (см. раздел Выражения шаблона страницы Синтаксис шаблонов), например, строковая литерал или свойство компонента. Другими словами, вы можете управлять форматом через привязку так же, как вы управляете значением дня рождения через привязку.

Напишите второй компонент, который привязывает параметр формата трубы к свойству компонента format. Вот шаблон для этого компонента:

src/app/hero-birthday2.component.ts (шаблон)

template: `
  <p>The hero's birthday is {{ birthday | date:format }}</p>
  <button (click)="toggleFormat()">Toggle Format</button>
`

Вы также добавили кнопку в шаблон и связали её событие клика с методом компонента toggleFormat(). Этот метод переключает свойство компонента format между коротким форматом ('shortDate') и длинным форматом ('fullDate').

src/app/hero-birthday2.component.ts (класс)

export class HeroBirthday2Component {
  birthday = new Date(1988, 3, 15); // April 15, 1988
  toggle = true; // start with true == shortDate

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

При нажатии кнопки отображаемая дата переключается между "15.04.1988" и "Пятница, 15 апреля 1988 года".

Date Format Toggle

Узнайте больше о параметрах формата DatePipe на странице справки API Date Pipe.

Цепочечные трубы

Вы можете объединять трубы вместе в потенциально полезных комбинациях. В следующем примере, чтобы отобразить день рождения заглавными буквами, день рождения цепляется к DatePipe и далее к UpperCasePipe . День рождения отображается как АПР 15, 1988.

The chained hero's birthday is
{{ birthday | date | uppercase}}

В этом примере, который отображает ПЯТНИЦА, АПРЕЛЬ 15, 1988, используются те же трубы, что и выше, но передаётся параметр в date.

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

Пользовательские трубы

Вы можете писать свои собственные пользовательские трубы. Вот пользовательская труба с именем ExponentialStrengthPipe, которая может усилить способности героя:

src/app/exponential-strength.pipe.ts

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: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

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

  • Труба — это класс, помеченный метаданными трубы.
  • Класс трубы реализует метод PipeTransform интерфейса transform, который принимает входное значение, за которым следуют необязательные параметры, и возвращает преобразованное значение.
  • Для каждого параметра, переданного трубе, в метод transform будет передано ещё одно дополнительное аргумент. У вашей трубы есть один такой параметр: exponent.
  • Чтобы сообщить Angular, что это труба, вы применяете декоратор @Pipe, который импортируется из основной библиотеки Angular.
  • Декоратор @Pipe позволяет определить имя трубы, которое вы будете использовать в выражениях шаблона. Оно должно быть допустимым идентификатором JavaScript. Имя вашей трубы — exponentialStrength.

Интерфейс PipeTransform

Метод transform является важным элементом трубы. Интерфейс PipeTransform определяет этот метод и направляет как инструменты, так и компилятор. Технически он необязателен; Angular ищет и выполняет метод transform независимо.

Теперь вам нужен компонент для демонстрации трубы.

src/app/power-booster.component.ts

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

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

Обратите внимание на следующее:

  • Вы используете свою пользовательскую трубу так же, как и встроенные трубы.
  • Вы должны включить свою трубу в массив declarations массива AppModule.
Помните массив declarations

Вы должны вручную регистрировать пользовательские трубы. В противном случае Angular выдаёт ошибку. В предыдущем примере вы не указывали DatePipe , потому что все встроенные трубы Angular предварительно зарегистрированы.

Для проверки поведения в примере в реальном времени измените значение и необязательный показатель степени в шаблоне.

Калькулятор усиления мощности

Обновление шаблона для тестирования пользовательской трубы не очень интересно. Обновите пример до "Калькулятора усиления мощности", который объединяет вашу трубу и двунаправленную привязку данных с ngModel.

src/app/power-boost-calculator.component.ts

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

@Component({
  selector: 'power-boost-calculator',
  template: `
    <h2>Power Boost Calculator</h2>
    <div>Normal power: <input [(ngModel)]="power"></div>
    <div>Boost factor: <input [(ngModel)]="factor"></div>
    <p>
      Super Hero Power: {{power | exponentialStrength: factor}}
    </p>
  `
})
export class PowerBoostCalculatorComponent {
  power = 5;
  factor = 1;
}
Power Boost Calculator

Обнаружение изменений

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

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

Без трубы

В следующем примере компонент использует стандартную агрессивную стратегию обнаружения изменений для отслеживания и обновления отображения каждого героя в массиве heroes . Вот шаблон:

src/app/flying-heroes.component.html (v1)

New hero:
  <input type="text" #box
          (keyup.enter)="addHero(box.value); box.value=''"
          placeholder="hero name">
  <button (click)="reset()">Reset</button>
  <div *ngFor="let hero of heroes">
    {{hero.name}}
  </div>

Сопутствующий класс компонента предоставляет героев, добавляет героев в массив и может сбросить массив.

src/app/flying-heroes.component.ts (v1)

export class FlyingHeroesComponent {
  heroes: any[] = [];
  canFly = true;
  constructor() { this.reset(); }

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

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

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

Труба Flying-heroes

Добавьте трубу FlyingHeroesPipe к циклу повторения *ngFor , которая фильтрует список героев только для тех, кто может летать.

src/app/flying-heroes.component.html (flyers)

<div *ngFor="let hero of (heroes | flyingHeroes)">
  {{hero.name}}
</div>

Вот реализация FlyingHeroesPipe, которая следует шаблону пользовательских труб, описанному ранее.

src/app/flying-heroes.pipe.ts

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

import { Flyer } from './heroes';

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

Обратите внимание на странное поведение в примере в реальном времени: когда вы добавляете летящих героев, ни один из них не отображается в разделе "Герои, которые летают".

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

Обратите внимание на добавление героя:

this.heroes.push(hero);

Вы добавляете героя в массив heroes . Ссылка на массив не изменилась. Это тот же массив. Это всё, что интересует Angular. С его точки зрения, тот же массив, никаких изменений, никакого обновления отображения.

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

Если вы модифицируете массив, никакая труба не вызывается, и отображение не обновляется; если вы заменяете массив, труба выполняется, и отображение обновляется. Приложение Flying Heroes расширяет код с переключателями флажков и дополнительными отображениями, чтобы помочь вам испытать эти эффекты.

Flying Heroes

Замена массива — это эффективный способ сигнализировать Angular об обновлении отображения. Когда вы заменяете массив? Когда данные меняются. Это лёгкое правило, которому следует следовать в этом примере, где единственный способ изменить данные — добавить героя.

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

Для фильтрации летящих героев рассмотрите использование нечистого конвейера.

Чистые и нечистые конвейеры

Существует две категории конвейеров: чистые и нечистые. Конвейеры по умолчанию чистые. Каждый конвейер, который вы видели до сих пор, был чистым. Вы делаете конвейер нечистым, установив его флаг pure в значение false. Вы можете сделать FlyingHeroesPipe нечистым следующим образом:

@Pipe({
  name: 'flyingHeroesImpure',
  pure: false
})

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

Чистые конвейеры

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

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

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

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

Или вы вообще можете не использовать конвейер. Возможно, лучше преследовать цель конвейера с помощью свойства компонента, о чём будет сказано позже на этой странице.

Нечистые конвейеры

Angular выполняет нечистый конвейер во время каждого цикла обнаружения изменений компонента. Нечистый конвейер вызывается часто, так же часто, как каждый нажатие клавиши или движение мыши.

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

Нечистый FlyingHeroesPipe

Переключение выключателя превращает FlyingHeroesPipe в FlyingHeroesImpurePipe. Полная реализация приведена ниже:

FlyingHeroesImpurePipe
@Pipe({
  name: 'flyingHeroesImpure',
  pure: false
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
FlyingHeroesPipe
import { Pipe, PipeTransform } from '@angular/core';

import { Flyer } from './heroes';

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

Вы наследуете от FlyingHeroesPipe , чтобы доказать, что ничего не изменилось внутри. Единственное различие — флаг pure в метаданных конвейера.

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

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

Вы можете получить FlyingHeroesImpureComponent от FlyingHeroesComponent.

src/app/flying-heroes-impure.component.html (выдержка)

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

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

Нечистый AsyncPipe

Angular AsyncPipe — интересный пример нечистого конвейера. AsyncPipe принимает Promise или Observable в качестве входных данных и автоматически подписывается на вход, в конечном итоге возвращая выпущенные значения.

AsyncPipe также имеет состояние. Конвейер поддерживает подписку на входной Observable и продолжает предоставлять значения из этого Observable по мере их поступления.

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

src/app/hero-async-message.component.ts

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

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';

@Component({
  selector: 'hero-message',
  template: `
    <h2>Async Hero Message and AsyncPipe</h2>
    <p>Message: {{ message$ | async }}</p>
    <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.resend(); }

  resend() {
    this.message$ = Observable.interval(500)
      .map(i => this.messages[i])
      .take(this.messages.length);
  }
}

Async-конвейер экономит код в компоненте. Компоненту не нужно подписываться на асинхронный источник данных, извлекать разрешённые значения и предоставлять их для привязки, а также нужно отписываться при уничтожении (сильный источник утечек памяти).

Нечистый кэширующий конвейер

Напишите ещё один нечистый конвейер, конвейер, который выполняет HTTP-запрос.

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

В приведенном ниже коде конвейер вызывает сервер только при изменении URL запроса и кэширует ответ сервера. Код использует клиент Angular http для получения данных:

src/app/fetch-json.pipe.ts

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

import 'rxjs/add/operator/map';

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

  constructor(private http: Http) { }

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

    return this.cachedData;
  }
}

Теперь продемонстрируйте это в компоненте-инструменте, шаблон которого определяет две привязки к этому конвейеру, оба запрашивающие героев из файла heroes.json.

src/app/hero-list.component.ts

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

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

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

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

Компонент отображается следующим образом:

Hero List

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

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

JsonPipe

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

Отладка с помощью конвейера json

Конвейер JsonPipe предоставляет удобный способ диагностирования загадочно сбоя привязки данных или инспекции объекта для будущей привязки.

Чистые конвейеры и чистые функции

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

Конвейеры, обсуждавшиеся ранее на этой странице, реализуются с использованием чистых функций. Встроенный DatePipe — это чистый конвейер с реализацией чистой функции. Также ExponentialStrengthPipe и FlyingHeroesPipe. Несколько шагов назад вы рассмотрели FlyingHeroesImpurePipe — нечистый конвейер с чистой функцией.

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

Следующие шаги

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

Изучите встроенные конвейеры Angular в Справочнике по API. Попробуйте написать собственный конвейер и, возможно, внести его в сообщество.

Приложение: Отсутствует FilterPipe или OrderByPipe

Angular не предоставляет конвейеры для фильтрации или сортировки списков. Разработчики, знакомые с AngularJS, знают их как filter и orderBy. В Angular нет эквивалентов.

Это не ошибка. Angular не предлагает таких конвейеров, потому что они работают плохо и препятствуют агрессивной минификации. Как filter , так и orderBy требуют параметров, ссылающихся на свойства объекта. Ранее на этой странице вы узнали, что такие конвейеры должны быть нечистыми, и что Angular вызывает нечистые конвейеры практически в каждом цикле обнаружения изменений.

Фильтрация, а особенно сортировка — это дорогостоящие операции. Пользовательский опыт может серьёзно ухудшиться даже для списков умеренного размера, когда Angular вызывает эти методы конвейера много раз в секунду. filter и orderBy часто использовались в приложениях AngularJS, что приводило к жалобам на то, что сам Angular медленный. Это справедливое обвинение в косвенном смысле, так как AngularJS создал эту ловушку производительности, предложив filter и orderBy в первую очередь.

Опасность минификации также является убедительной, хотя и менее очевидной. Представьте себе конвейер сортировки, применяемый к списку героев. Список может быть отсортирован по свойствам героя name и planet происхождения следующим образом:

<!-- NOT REAL CODE! -->
<div *ngFor="let hero of heroes | orderBy:'name,planet'"></div>

Вы определяете поля сортировки текстовыми строками, ожидая, что конвейер будет ссылаться на значение свойства по индексу (например, hero['name']). К сожалению, агрессивная минификация изменяет имена свойств Hero так, что Hero.name и Hero.planet становятся чем-то вроде Hero.a и Hero.b соответственно. Очевидно, что hero['name'] не работает.

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

Команда Angular и многие опытные разработчики Angular настоятельно рекомендуют перемещать логику фильтрации и сортировки непосредственно в компонент. Компонент может предоставить свойство filteredHeroes или sortedHeroes и контролировать, когда и как часто выполнять вспомогательную логику. Любые возможности, которые вы бы поместили в конвейер и поделились бы ими по всему приложению, можно записать в службу фильтрации/сортировки и ввести в компонент.

Если эти соображения по производительности и минификации не относятся к вам, вы всегда можете создать свои собственные такие конвейеры (подобно FlyingHeroesPipe) или найти их в сообществе.

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/guide/pipes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API