- PVSM.RU - https://www.pvsm.ru -
Часть 1 [1] Часть 2 [2] Часть 3 [3] Часть 4 [4]
Тур героев развивается, и мы ожидаем добавление новых компонентов в ближайшем будущем.
Нескольким компонентам нужен доступ к данным героев, и мы не хотим копировать и вставлять один и тот же код снова и снова. Вместо этого мы создадим один сервис передачи данных, который можно будет в дальнейшем повторно использовать, и научимся использовать его в компонентах, которые в нем нуждаются.
Рефакторинг доступа к данным в отдельный сервис помогает не "раздувать" компоненты и направлен на поддержку отображения. Кроме того, использование мок-сервиса (мок — заглушка, позволяет не обращаться к реальным данным с сервера) облегчает модульное тестирование компонента (unit-тесты).
Поскольку данные, получаемые сервисом всегда будут асинхронными, мы закончим эту главу версией сервиса, основанной на обещания (промисах, promise).
Запустить приложение, часть 4 [5]
Прежде чем продолжить наш Тур героев, давайте проверим, что наш проект имеет следующую структуру. Если это не так, нужно будет вернуться к предыдущим главам.
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
содержит фиктивные данные героев. У нас есть как минимум два возражения. Во-первых, определение данных героев не является обязанностью компонента. Во-вторых, мы не можем легко предоставить доступ к этим данным другим компонентам и представлениям.
Мы можем отрефакторить логику получения данных о героях в один сервис, который будет предоставлять доступ к данным о героях и дать доступ к этому сервису тем компонентам, которым нужны герои.
Создайте файл в папке 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
во время выполнения?
Мы могли бы создать новый экземпляр HeroService, используя new
, например, вот так (не надо так делать!):
heroService = new HeroService(); // don't do this
Это плохая идея по нескольким причинам.
HeroService
. Если мы когда-нибудь изменим конструктор HeroService
, мы должны найти каждое место, в котором создается сервис и внести правки. Такие правки могут привести к появлению ошибок, и добавляют дополнительную нагрузку по тестированию.AppComponent
мы привязываемся к конкретной реализации HeroService
. Будет трудно переключать реализации для различных сценариев. Можем ли мы работать оффлайн? Нужны ли будут разные версии фиктивных реализаций при тестировании? Это не просто.Что, если… что, если… Эй, у нас есть работа, которую нужно сделать!
Мы сделаем это. Избежать этих проблем так легко, что нет никаких оправданий для того, чтобы сделать это неправильно.
Две строки вместо одной, использовавшей new:
providers
в компонент.Вот конструктор:
app.component.ts (конструктор)
constructor(private heroService: HeroService) { }
Сам по себе конструктор ничего не делает. Параметр одновременно определяет приватное свойство heroService
и идентифицирует его как место внедрения HeroService
.
Теперь Angular будет знать, чтобы необходимо передать экземпляр HeroService
, когда он создает новый AppComponent
.
Angular должен получить этот экземпляр откуда-то. Это функция Angular-ского Dependency Injector ("внедритель" зависимостей — далее Инъектор). Инъектор имеет контейнер предварительно созданных сервисов. Либо он находит и возвращает предварительно существующий экземпляр HeroService
из контейнера, либо создает новый экземпляр, добавляет его в контейнер, и возвращает его в Angular.
Подробнее о внедрении зависимостей в главе Внедрение зависимостей [6].
Инъектор еще не знает, как создать 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
во всем нашем приложении.
У нас есть сервис в приватной переменной heroService
. Давайте используем ее.
Сделаем паузу, чтобы подумать. Мы можем вызвать сервис и получить данные в одной строке.
this.heroes = this.heroService.getHeroes();
В самом деле, нам не нужен специальный метод, чтобы обернуть одну строку. Мы пишем это в любом случае:
app.component.ts (getHeroes)
getHeroes() {
this.heroes = this.heroService.getHeroes();
}
AppComponent
должен получать и отображать героев без лишней суеты. Где нам лучше вызвать getHeroes
? В конструкторе? Не надо так делать!
Многолетний опыт и горькие слезы научили нас выносить сложную логику из конструктора, особенно ту, что может использовать сервер как источник данных.
Конструктор предназначен для простых инициализаций, вроде присвоения параметров конструктора к свойствам. Но не для тяжелых вещей. Мы должны иметь возможность создать компонент в тесте и не беспокоиться, что он может делать реальную работу — такую, как вызов сервера! — пока мы не скажем ему это сделать.
Если нет конструктора, то кто-то должен сделать вызов метода getHeroes
.
Angular сделает это, если мы реализуем ngOnInit Lifecycle Hook (хук жизненного цикла ngOnInit). Angular предлагает ряд интерфейсов для того, чтобы участвовать в критических моментах жизненного цикла компонента: при создании, после каждого изменения, и при его окончательном уничтожении.
Каждый интерфейс имеет один метод. Когда компонент реализует этот метод, Angular делает его вызов в соответствующее время.
Подробнее о жизненном циклом хуков в главе Жизненный цикл хуков [7].
Вот краткое описание интерфейса 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();
}
Наше приложение, как и ожидалось, показывает список героев и вид с детальной информацией о герое, когда мы нажимаем на имя героя.
Мы становимся ближе. Но кое-что еще не совсем правильно.
Наш HeroService
возвращает список фиктивных героев сразу. Это сигнатура синхронного вызова getHeroes
:
this.heroes = this.heroService.getHeroes();
Стоит запросить героев, и они есть в возвращаемом результате.
В будущем мы собираемся получить героев с удаленного сервера. Пока что мы не будем использовать HTTP, но будем стремиться к этому в следующих главах.
Когда мы делаем вызов, нам нужно дождаться ответа сервера, и мы не сможем блокировать пользовательский интерфейс, пока мы ждем, даже если мы захотим (хотя и не должны), так как браузер не заблокируешь.
Нам придется использовать какой-то асинхронный метод получения данных, который изменит сигнаруту нашего метода getHeroes
.
Мы будем использовать обещания.
Обещание… ну, это обещание сделать наш вызов позже, когда результаты будут готовы. Мы просим асинхронный сервис, чтобы он выполнил какую-то работу, и отдал функцию обратного вызова. Он делает эту работу (где-то) и в конце концов он вызывает нашу функцию с результатами работы или с ошибкой.
Это упрощенное описание. Узнать о ES2015 обещаниях можно здесь [8] и в других местах в Интернете.
Обновим 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 [9] в обратном вызове является более емкой, чем использование эквивалентной функции, и изящно работает с 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
Файлы кода, которые мы обсуждали в этой главе.
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
);
}
}
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; }
}
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 [5]
Наш Тур героев стал более пригоден для повторного использования с применением общих компонентов и сервисов. Мы хотим создать информационную панель, добавить ссылки меню, для маршрутизации между представлениями, а также форматировать данные в шаблоне. По мере развития нашего приложения, мы узнаем, как его спроектировать, чтобы упростить его расширение и поддержку.
Мы узнаем о компоненте маршрутизации Angular и навигации между представлениями в следующей главе [10].
Мы можем эмулировать медленное соединение.
Импортируйте тип 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
и посмотрите, как будет вести себя приложение.
Мы говорили ранее, что если мы включаем родительский AppComponent
HeroService
в HeroDetailComponent
, мы не должны добавлять массив провайдеров в метаданные HeroDetailComponent
.
Почему? Потому что этим мы скажем Angular, что нужно создать новый экземпляр HeroService
на уровне HeroDetailComponent
. Компоненту HeroDetailComponent
не нужен свой собственный экземпляр сервиса; ему нужен экземпляр сервиса его родителя. Добавление массива providers
создает новый экземпляр сервиса, который является клоном родительского экземпляра.
Подумайте хорошенько о том, где и когда нужно регистрировать поставщика. Понять область видимости этой регистрации. Будьте осторожны, чтобы не создать новый экземпляр сервиса на неправильном уровне.
Важное замечание. С выходом очередного релиза в Angular произошли изменения в директиве ngFor.
Вместо, например,*ngFor="#hero of heroes"
нужно использовать новый синтаксис*ngFor="let hero of heroes"
.
Пока что при использовании старого синтаксиса появляется только предупреждение.
Автор: illian
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/120681
Ссылки в тексте:
[1] Часть 1: https://habrahabr.ru/post/281190/
[2] Часть 2: https://habrahabr.ru/post/281727/
[3] Часть 3: https://habrahabr.ru/post/282634/
[4] Часть 4: https://habrahabr.ru/post/283556/
[5] Запустить приложение, часть 4: https://angular.io/resources/live-examples/toh-4/ts/plnkr.html
[6] Внедрение зависимостей: https://angular.io/docs/ts/latest/guide/dependency-injection.html
[7] Жизненный цикл хуков: https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
[8] здесь: http://exploringjs.com/es6/ch_promises.html
[9] Функция стрелки ES2015: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
[10] следующей главе: https://angular.io/docs/ts/latest/tutorial/toh-pt5.html
[11] Источник: https://habrahabr.ru/post/283556/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.