Spec-Zone.ru › Angular 2

Сервисы

Создайте переиспользуемый сервис для управления вызовами данных героя.

По мере развития приложения «Тур по героям» вы будете добавлять больше компонентов, которым нужен доступ к данным героев.

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

Поскольку сервисы данных неизбежно асинхронны, вы закончите страницу версией сервиса данных на основе Promise.

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

Где вы остановились

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

angular-tour-of-heroes
  src
    app
      app.component.ts
      app.module.ts
      hero.ts
      hero-detail.component.ts
    main.ts
    index.html
    styles.css
    systemjs.config.js
    tsconfig.json
  node_modules ...
  package.json

Поддерживайте транспиляцию и работу приложения

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

npm start

Эта команда запускает компилятор TypeScript в режиме «наблюдения», автоматически перекомпилируя код при его изменении. Команда одновременно запускает приложение в браузере и обновляет браузер при изменении кода.

Вы можете продолжать создавать «Тур по героям», не прерывая процесс перекомпиляции или обновления браузера.

Создание сервиса героя

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

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

Создайте HeroService

Создайте файл в папке app с именем hero.service.ts.

Конвенция именования файлов сервисов — название сервиса в нижнем регистре, за которым следует .service. Для многословного названия сервиса используйте нижний кемализированный регистр. Например, имя файла для SpecialSuperHeroService — special-super-hero.service.ts.

Назовите класс HeroService и экспортируйте его для импорта другими.

src/app/hero.service.ts (начало)

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

@Injectable()
export class HeroService {
}

Внедряемые сервисы

Обратите внимание, что вы импортировали функцию Angular Injectable и применили эту функцию как декоратор @Injectable().

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

Декоратор @Injectable() сообщает TypeScript о том, что сервис может содержать метаданные. Метаданные указывают, что Angular может потребоваться внедрить другие зависимости в этот сервис.

Хотя у HeroService на данный момент нет зависимостей, применение декоратора @Injectable() с самого начала обеспечивает согласованность и будущее устойчивость.

Получение данных героев

Добавьте заглушку метода getHeroes().

src/app/hero.service.ts (заглушка getHeroes)

@Injectable()
export class HeroService {
  getHeroes(): void {} // stub
}

Сервис HeroService может получить данные Hero из любого источника — веб-сервиса, локального хранилища или источника эмуляционных данных. Удаление доступа к данным из компонента означает, что вы можете изменить реализацию в любое время без изменения компонентов, которым нужны данные героев.

Перемещение эмуляционных данных героев

Вырежьте массив HEROES из app.component.ts и вставьте его в новый файл в папке app с именем mock-heroes.ts. Кроме того, скопируйте оператор import {Hero} ..., так как массив heroes использует класс Hero.

src/app/mock-heroes.ts

import { Hero } from './hero';

export const HEROES: Hero[] = [
  {id: 11, name: 'Mr. Nice'},
  {id: 12, name: 'Narco'},
  {id: 13, name: 'Bombasto'},
  {id: 14, name: 'Celeritas'},
  {id: 15, name: 'Magneta'},
  {id: 16, name: 'RubberMan'},
  {id: 17, name: 'Dynama'},
  {id: 18, name: 'Dr IQ'},
  {id: 19, name: 'Magma'},
  {id: 20, name: 'Tornado'}
];

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

В app.component.ts, где вы вырезали массив HEROES, добавьте неинициализированное свойство heroes:

src/app/app.component.ts (свойство heroes)

heroes: Hero[];

Возврат эмуляционных данных героев

Вернитесь в HeroService, импортируйте эмуляционные данные HEROES и верните их из метода getHeroes(). Сервис HeroService выглядит следующим образом:

src/app/hero.service.ts

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

import { Hero } from './hero';
import { HEROES } from './mock-heroes';

@Injectable()
export class HeroService {
  getHeroes(): Hero[] {
    return HEROES;
  }
}

Импорт сервиса героя

Вы готовы использовать HeroService в других компонентах, начиная с AppComponent.

Импортируйте HeroService для ссылки на него в коде.

src/app/app.component.ts (импорт hero-service)

import { HeroService } from './hero.service';

Не используйте new с HeroService

Как компонент должен получить экземпляр конкретного AppComponent в runtime?

Вы можете создать новый экземпляр HeroService с помощью new так:

heroService = new HeroService(); // don't do this

Однако этот вариант не идеален по следующим причинам:

  • Компонент должен знать, как создать HeroService. Если вы измените конструктор HeroService, вам нужно найти и обновить каждое место, где вы создали сервис. Исправление кода в нескольких местах подвержено ошибкам и увеличивает нагрузку на тестирование.
  • Вы создаете сервис каждый раз, когда используете new. Что, если сервис кеширует героев и делится этим кешем с другими? Вы не сможете этого сделать.
  • Если AppComponent заблокирован за определённой реализацией HeroService, то переключение реализаций для различных сценариев, например, работы оффлайн или использования различных эмуляционных версий для тестирования, будет затруднено.

Внедрите HeroService

Вместо использования строки new, вы добавите две строки.

  • Добавьте конструктор, который также определяет частное свойство.
  • Добавьте в метаданные компонента providers.

Добавьте конструктор:

src/app/app.component.ts (конструктор)

constructor(private heroService: HeroService) { }

Сам конструктор ничего не делает. Параметр одновременно определяет частное свойство heroService и идентифицирует его как точку внедрения HeroService.

Теперь Angular знает, что должен предоставить экземпляр HeroService при создании AppComponent.

Подробнее о внедрении зависимостей см. на странице Внедрение зависимостей.

Инжектор пока не знает, как создать HeroService. Если вы запустите код сейчас, Angular выдаст следующую ошибку:

EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)

Чтобы научить инжектор создавать HeroService, добавьте следующее свойство массива providers в конец метаданных компонента в вызове @Component.

src/app/app.component.ts (providers)

providers: [HeroService]

Массив providers сообщает Angular о том, что нужно создавать новый экземпляр HeroService при создании AppComponent. Компонент AppComponent, а также его дочерние компоненты, могут использовать этот сервис для получения данных героев.

getHeroes() в AppComponent

Сервис находится в heroService частной переменной.

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

this.heroes = this.heroService.getHeroes();

Вам действительно не нужен отдельный метод для обертки одной строки. Напишите его всё равно:

src/app/app.component.ts (getHeroes)

  getHeroes(): void {
    this.heroes = this.heroService.getHeroes();
  }

Цикл жизни ngOnInit

AppComponent должен получать и отображать данные героев без проблем.

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

Чтобы Angular вызвал getHeroes(), вы можете реализовать интерфейс жизненного цикла Angular ngOnInit. Angular предоставляет интерфейсы для доступа к важнейшим моментам жизненного цикла компонента: при создании, после каждого изменения и при его окончательном уничтожении.

Подробнее о жизненных циклах см. на странице Жизненные циклы.

Вот основной план для интерфейса OnInit (не копируйте это в свой код):

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

export class AppComponent implements OnInit {
  ngOnInit(): void {
  }
}

Добавьте реализацию интерфейса OnInit в ваш оператор экспорта:

export class AppComponent implements OnInit {}

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

app/app.component.ts (ng-on-init)

  ngOnInit(): void {
    this.getHeroes();
  }

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

Асинхронные сервисы и Promises

HeroService возвращает список эмуляционных героев немедленно; его getHeroes() сигнатура синхронна.

this.heroes = this.heroService.getHeroes();

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

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

Сервис героя создаёт Promise

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

Это упрощенное объяснение. Подробнее об обещаниях ES2015 в асинхронном программировании читайте на странице Обещания для асинхронного программирования в документации Exploring ES6.

Обновите HeroService с помощью этого возвращающего обещание getHeroes() метода:

src/app/hero.service.ts (excerpt)

getHeroes(): Promise<Hero[]> {
  return Promise.resolve(HEROES);
}

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

Действие по обещанию

В результате изменения в HeroService, this.heroes теперь установлено в обещание, а не в массив героев.

src/app/app.component.ts (getHeroes - old)

  getHeroes(): void {
    this.heroes = this.heroService.getHeroes();
  }

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

Передайте функцию обратного вызова в качестве аргумента методу обещания then():

src/app/app.component.ts (getHeroes - revised)

getHeroes(): void {
  this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}

Как описано в стрелочных функциях, стрелочная функция ES2015 в обратном вызове более лаконична, чем эквивалентное выражение функции и элегантно обрабатывает this.

Обратный вызов устанавливает свойство компонента heroes в массив героев, возвращенный сервисом.

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

В конце этой страницы Приложение: медленное выполнение описывает, как приложение может вести себя при плохом соединении.

Обзор структуры приложения

Убедитесь, что у вас есть следующая структура после всех рефакторингов:

angular-tour-of-heroes
  src
    app
      app.component.ts
      app.module.ts
      hero.ts
      hero-detail.component.ts
      hero.service.ts
      mock-heroes.ts
    main.ts
    index.html
    styles.css
    systemjs.config.js
    tsconfig.json
  node_modules ...
  package.json

Вот файлы кода, обсуждаемые на этой странице.

src/app/hero.service.ts
import { Injectable } from '@angular/core';

import { Hero } from './hero';
import { HEROES } from './mock-heroes';

@Injectable()
export class HeroService {
  getHeroes(): Promise<Hero[]> {
    return Promise.resolve(HEROES);
  }
}
src/app/app.component.ts
import { Component, OnInit } from '@angular/core';

import { Hero } from './hero';
import { HeroService } from './hero.service';

@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ngFor="let hero of heroes"
        [class.selected]="hero === selectedHero"
        (click)="onSelect(hero)">
        <span class="badge">{{hero.id}}</span> {{hero.name}}
      </li>
    </ul>
    <hero-detail [hero]="selectedHero"></hero-detail>
  `,
  styles: [`
    .selected {
      background-color: #CFD8DC !important;
      color: white;
    }
    .heroes {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 15em;
    }
    .heroes li {
      cursor: pointer;
      position: relative;
      left: 0;
      background-color: #EEE;
      margin: .5em;
      padding: .3em 0;
      height: 1.6em;
      border-radius: 4px;
    }
    .heroes li.selected:hover {
      background-color: #BBD8DC !important;
      color: white;
    }
    .heroes li:hover {
      color: #607D8B;
      background-color: #DDD;
      left: .1em;
    }
    .heroes .text {
      position: relative;
      top: -3px;
    }
    .heroes .badge {
      display: inline-block;
      font-size: small;
      color: white;
      padding: 0.8em 0.7em 0 0.7em;
      background-color: #607D8B;
      line-height: 1em;
      position: relative;
      left: -1px;
      top: -4px;
      height: 1.8em;
      margin-right: .8em;
      border-radius: 4px 0 0 4px;
    }
  `],
  providers: [HeroService]
})
export class AppComponent implements OnInit {
  title = 'Tour of Heroes';
  heroes: Hero[];
  selectedHero: Hero;

  constructor(private heroService: HeroService) { }

  getHeroes(): void {
    this.heroService.getHeroes().then(heroes => this.heroes = heroes);
  }

  ngOnInit(): void {
    this.getHeroes();
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }
}
src/app/mock-heroes.ts
import { Hero } from './hero';

export const HEROES: Hero[] = [
  {id: 11, name: 'Mr. Nice'},
  {id: 12, name: 'Narco'},
  {id: 13, name: 'Bombasto'},
  {id: 14, name: 'Celeritas'},
  {id: 15, name: 'Magneta'},
  {id: 16, name: 'RubberMan'},
  {id: 17, name: 'Dynama'},
  {id: 18, name: 'Dr IQ'},
  {id: 19, name: 'Magma'},
  {id: 20, name: 'Tornado'}
];

Путь, который вы прошли

Вот что вы сделали на этой странице:

  • Вы создали класс сервиса, который может быть использован многими компонентами.
  • Вы использовали обработчик жизненного цикла ngOnInit для получения данных о героях при активации AppComponent.
  • Вы определили HeroService как поставщика для AppComponent.
  • Вы создали имитационные данные героев и импортировали их в сервис.
  • Вы спроектировали сервис для возврата обещания, а компонент для получения данных из обещания.

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

Путь вперёд

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

Узнайте об Angular компонентном роутере и навигации между представлениями на странице следующего руководства.

Приложение: медленное выполнение

Чтобы смоделировать медленное соединение, импортируйте символ Hero и добавьте следующий метод getHeroesSlowly() в HeroService.

app/hero.service.ts (getHeroesSlowly)

getHeroesSlowly(): Promise<Hero[]> {
  return new Promise(resolve => {
    // Simulate server latency with 2 second delay
    setTimeout(() => resolve(this.getHeroes()), 2000);
  });
}

Как и getHeroes(), он также возвращает обещание. Но это обещание ожидает две секунды, прежде чем выполнить обещание с имитированными героями.

Вернувшись в AppComponent, замените getHeroes() на getHeroesSlowly() и посмотрите, как ведет себя приложение.

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

Маршрутизация

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

Spec-Zone.ru

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