Angular 2 Beta, обучающий курс «Тур героев» часть 4

в 14:41, , рубрики: angular 2.0, angular2, AngularJS, javascript, tour of heroes, tutorial, Разработка веб-сайтов

Часть 1 Часть 2 Часть 3 Часть 4

Сервисы

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

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

Рефакторинг доступа к данным в отдельный сервис помогает не "раздувать" компоненты и направлен на поддержку отображения. Кроме того, использование мок-сервиса (мок — заглушка, позволяет не обращаться к реальным данным с сервера) облегчает модульное тестирование компонента (unit-тесты).

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

Запустить приложение, часть 4

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

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

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

Поддержка преобразования кода и выполнения приложения

Откройте окно терминала / консоли. Запустите компилятор TypeScript, который будет следить за изменениями, и сервер, введя команду:

    npm start

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

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

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

В данный момент AppComponent содержит фиктивные данные героев. У нас есть как минимум два возражения. Во-первых, определение данных героев не является обязанностью компонента. Во-вторых, мы не можем легко предоставить доступ к этим данным другим компонентам и представлениям.

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

Создание HeroService

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

Мы приняли конвенцию, в которой сервис именуется буквами в нижнем регистре с добавлением .service. Если имя сервиса состоит из нескольких слов, для именования используется "нижний регистр с тире". Например, сервис SpecialSuperHeroService будет определен именем файла special-super-hero.service.ts.

Назовем класс HeroService и пометим его экспортируемым, чтобы другие классы могли его импортировать.

hero.service.ts (экспортированный класс)

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

    @Injectable()
    export class HeroService {
    }

Внедрение сервиса

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

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

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

В данный момент HeroService не имеет никаких зависимостей. Добавьте декоратор в любом случае. Это пример "best practice" (хорошей практики) — применить декоратор @Injectable() с самого начала, чтобы показать его предназначение в перспективе.

Получение героев

Добавим метод-заглушку getHeroes.

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

    @Injectable()
    export class HeroService {
      getHeroes() {
      }
    }

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

Потребитель нашего сервиса не знает, каким образом сервис получает данные. Наш HeroService может получить данные Hero отовсюду. Он мог бы получить данные из веб-сервиса или локального запоминающего устройства или из мок-метода, возвращающего некие фиктивные данные.

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

Фиктивные данные героев

У нас уже есть фиктивные данные Hero, которые находятся в AppComponent. Но здесь им не место. Мы переместим эти фиктивные данные в отдельный файл.

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

mock-heroes.ts (Массив героев)

    import { Hero } from './hero';
    export var 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:

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

    heroes: Hero[];

Возвращение фиктивного списка героев

Вернувшись в HeroService мы импортируем нашу заглушку HEROES и возвращаем ее в методе getHeroes. Наш сервис HeroService выглядит следующим образом:

hero.service.ts

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

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

    @Injectable()
    export class HeroService {
      getHeroes() {
        return HEROES;
      }
    }

Использование сервиса героев

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

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

app.component.ts (import HeroService)

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

Импорт сервиса позволяет ссылаться на него в нашем коде. Как AppComponent должен получить конкретный экземпляр HeroService во время выполнения?

Должны ли мы использовать new HeroService? Ни в коем случае!

Мы могли бы создать новый экземпляр HeroService, используя new, например, вот так (не надо так делать!):

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

Это плохая идея по нескольким причинам.

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

Что, если… что, если… Эй, у нас есть работа, которую нужно сделать!

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

Внедрение HeroService

Две строки вместо одной, использовавшей new:

  1. Мы добавим конструктор.
  2. Мы добавим метаданные providers в компонент.

Вот конструктор:

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

    constructor(private heroService: HeroService) { }

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

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

Angular должен получить этот экземпляр откуда-то. Это функция Angular-ского Dependency Injector ("внедритель" зависимостей — далее Инъектор). Инъектор имеет контейнер предварительно созданных сервисов. Либо он находит и возвращает предварительно существующий экземпляр HeroService из контейнера, либо создает новый экземпляр, добавляет его в контейнер, и возвращает его в Angular.

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

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

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

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

app.component.ts (providing HeroService)

    providers: [HeroService]

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

Сервисы и дерево компонентов

Напомним, что AppComponent создает экземпляр HeroDetail, на основании тэга <my-hero-detail> в нижней части его шаблона. Компонент HeroDetail является потомком AppComponent.

Если компоненту HeroDetailComponent требуется HeroService родительского компонента, он попросит Angular внедрить сервис в его конструктор, который будет выглядеть так же, как для AppComponent:

hero-detail.component.ts (конструктор)

constructor(private heroService: HeroService) { }

Компонент HeroDetailComponent не должен повторять массив providers своего родителя! Подумайте, почему. В приложении ниже это будет рассмотрено более подробно.

AppComponent является компонентом самого высокого уровня нашего приложения. В приложении должен быть только один экземпляр этого компонента и только один экземпляр HeroService во всем нашем приложении.

getHeroes в AppComponent

У нас есть сервис в приватной переменной heroService. Давайте используем ее.

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

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

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

app.component.ts (getHeroes)

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

Хук ngOnInit жизненного цикла

AppComponent должен получать и отображать героев без лишней суеты. Где нам лучше вызвать getHeroes? В конструкторе? Не надо так делать!

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

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

Если нет конструктора, то кто-то должен сделать вызов метода getHeroes.

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

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

Подробнее о жизненном циклом хуков в главе Жизненный цикл хуков.

Вот краткое описание интерфейса OnInit:

app.component.ts (протокол OnInit)

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

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

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

app.component.ts (протокол OnInit)

      ngOnInit() {
    this.getHeroes();
  }

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

Мы становимся ближе. Но кое-что еще не совсем правильно.

Асинхронные сервисы и обещания (promises)

Наш HeroService возвращает список фиктивных героев сразу. Это сигнатура синхронного вызова getHeroes:

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

Стоит запросить героев, и они есть в возвращаемом результате.

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

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

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

Мы будем использовать обещания.

Сервис героев дает обещание

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

Это упрощенное описание. Узнать о ES2015 обещаниях можно здесь и в других местах в Интернете.

Обновим HeroService, используя метод, возвращающий обещание getHeroes:

hero.service.ts (getHeroes)

    getHeroes() {
      return Promise.resolve(HEROES);
    }

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

Выполнение обещания

Возвращаясь к компоненту AppComponent и методу getHeroes, мы видим, что он по-прежнему выглядит следующим образом:

app.component.ts (getHeroes — старый вариант)

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

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

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

Мы передаем нашу функцию обратного вызова как аргумент методу обещания then:

app.component.ts (getHeroes — новый вариант)

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

Функция стрелки ES2015 в обратном вызове является более емкой, чем использование эквивалентной функции, и изящно работает с this.

Наш обратный вызов присваивает свойству компонента heroes массив героев, который вернул сервис. Вот и все!

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

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

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

Давайте проверим, что мы имеем следующую структуру после нашего замечательного рефакторинга в этой главе:

    angular2-tour-of-heroes
        app
            app.component.ts
            hero.ts
            hero-detail.component.ts
            hero.service.ts
            main.ts
            mock-heroes.ts
        node_modules ...
        typings ...
        index.html
        package.json
        tsconfig.json
        typings.json

Файлы кода, которые мы обсуждали в этой главе.

app/hero.service.ts
    import { Injectable } from '@angular/core';
    import { Hero } from './hero';
    import { HEROES } from './mock-heroes';
    @Injectable()
    export class HeroService {
      getHeroes() {
        return Promise.resolve(HEROES);
      }
      // See the "Take it slow" appendix
      getHeroesSlowly() {
        return new Promise<Hero[]>(resolve =>
          setTimeout(()=>resolve(HEROES), 2000) // 2 seconds
        );
      }
    }

app/app.component.ts
    import { Component, OnInit } from '@angular/core';
    import { Hero } from './hero';
    import { HeroDetailComponent } from './hero-detail.component';
    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>
        <my-hero-detail [hero]="selectedHero"></my-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;
        }
      `],
      directives: [HeroDetailComponent],
      providers: [HeroService]
    })
    export class AppComponent implements OnInit {
      title = 'Tour of Heroes';
      heroes: Hero[];
      selectedHero: Hero;
      constructor(private heroService: HeroService) { }
      getHeroes() {
        this.heroService.getHeroes().then(heroes => this.heroes = heroes);
      }
      ngOnInit() {
        this.getHeroes();
      }
      onSelect(hero: Hero) { this.selectedHero = hero; }
    }

app/mock-heroes.ts
    import { Hero } from './hero';
    export var 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.
  • Мы создали фиктивные данные героев и импортировали их в наш сервис.
  • Мы произвели разработку нашего сервиса так, чтобы он вернул обещание, и наш компонент, чтобы он получил наши данные из обещания.

Запустить приложение, часть 4

Предстоящий путь

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

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

Приложение: Получи это медленно

Мы можем эмулировать медленное соединение.

Импортируйте тип Hero и добавьте следующий метод getHeroesSlowly в HeroService

hero.service.ts (getHeroesSlowy)

    getHeroesSlowly() {
      return new Promise<Hero[]>(resolve =>
        setTimeout(()=>resolve(HEROES), 2000) // 2 seconds
      );
    }

Также, как и getHeroes, этот метод возвращает обещание. Но это обещание делает задержку в 2 секунды до разрешения с фиктивным списком героев.

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

Приложение: Теневое клонирование (shadowing) родительского сервиса

Мы говорили ранее, что если мы включаем родительский AppComponent HeroService в HeroDetailComponent, мы не должны добавлять массив провайдеров в метаданные HeroDetailComponent.

Почему? Потому что этим мы скажем Angular, что нужно создать новый экземпляр HeroService на уровне HeroDetailComponent. Компоненту HeroDetailComponent не нужен свой собственный экземпляр сервиса; ему нужен экземпляр сервиса его родителя. Добавление массива providers создает новый экземпляр сервиса, который является клоном родительского экземпляра.

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

Важное замечание. С выходом очередного релиза в Angular произошли изменения в директиве ngFor.
Вместо, например, *ngFor="#hero of heroes" нужно использовать новый синтаксис *ngFor="let hero of heroes".

Пока что при использовании старого синтаксиса появляется только предупреждение.

Автор: illian

Источник

Поделиться новостью

* - обязательные к заполнению поля