Часть 1 Часть 2 Часть 3 Часть 4 Тур героев развивается, и мы ожидаем добавление новых компонентов в ближайшем будущем. Нескольким компонентам нужен доступ к данным героев, и мы не хотим копировать и вставлять один и тот же код снова и снова. Вместо этого мы создадим один сервис передачи данных, который можно будет в дальнейшем повторно использовать, и научимся использовать его в компонентах, которые в нем нуждаются. Рефакторинг доступа к данным в отдельный сервис помогает не "раздувать" компоненты и направлен на поддержку отображения. Кроме того, использование мок-сервиса (мок — заглушка, позволяет не обращаться к реальным данным с сервера) облегчает модульное тестирование компонента (unit-тесты). Поскольку данные, получаемые сервисом всегда будут асинхронными, мы закончим эту главу версией сервиса, основанной на обещания (промисах, promise). Запустить приложение, часть 4 Прежде чем продолжить наш Тур героев, давайте проверим, что наш проект имеет следующую структуру. Если это не так, нужно будет вернуться к предыдущим главам. Откройте окно терминала / консоли. Запустите компилятор TypeScript, который будет следить за изменениями, и сервер, введя команду: Приложение запустится и будет обновляться автоматически, пока мы будем создавать Тур героев. Наши заказчики поделились своим видением нашего приложения. Они говорят, что хотят отображать героев по-разному на разных страницах. Мы уже можем выбрать героя из списка. Скоро мы добавим панель для отображения самых лучших героев, и создадим отдельное представление для редактирования деталей героя. Всем этим представлениям нужны данные героев. В данный момент AppComponent содержит фиктивные данные героев. У нас есть как минимум два возражения. Во-первых, определение данных героев не является обязанностью компонента. Во-вторых, мы не можем легко предоставить доступ к этим данным другим компонентам и представлениям. Мы можем отрефакторить логику получения данных о героях в один сервис, который будет предоставлять доступ к данным о героях и дать доступ к этому сервису тем компонентам, которым нужны герои. Создайте файл в папке app с именем hero.service.ts. Назовем класс HeroService и пометим его экспортируемым, чтобы другие классы могли его импортировать. hero.service.ts (экспортированный класс) Обратите внимание на то, что мы импортировали функцию Angular Injectable и использовали декоратор @Injectable(). TypeScript видит декоратор @Injectable() и делает доступными метаданные о нашем сервисе, те метаданные, которые Angular возможно потребуется внедрить в другие зависимости этого сервиса. В данный момент HeroService не имеет никаких зависимостей. Добавьте декоратор в любом случае. Это пример "best practice" (хорошей практики) — применить декоратор @Injectable() с самого начала, чтобы показать его предназначение в перспективе. Добавим метод-заглушку getHeroes. hero.service.ts (заглушка getHeroes) Немного повременим с реализацией, чтобы сделать важное замечание. Потребитель нашего сервиса не знает, каким образом сервис получает данные. Наш HeroService может получить данные Hero отовсюду. Он мог бы получить данные из веб-сервиса или локального запоминающего устройства или из мок-метода, возвращающего некие фиктивные данные. В этом прелесть вынесения доступа к данным из компонента. Мы можем менять реализации так часто, как нам нравится и когда это потребуется, не затрагивая компоненты, которые нуждаются в данных о героях. У нас уже есть фиктивные данные Hero, которые находятся в AppComponent. Но здесь им не место. Мы переместим эти фиктивные данные в отдельный файл. Вырежем массив HEROES из app.component.ts и вставим его в новый файл в папке app с названием heroes.ts. Кроме того, нужно скопировать объявление import {Hero} ..., потому что массив героев использует класс Hero. mock-heroes.ts (Массив героев) Мы экспортируем массив HEROES как константу, поэтому мы можем импортировать его в другом месте — например, как наш HeroService. Тем временем, в app.component.ts, откуда мы вырезали массив HEROES, мы оставляем неинициализированное свойство heroes: app.component.ts (свойство heroes) Вернувшись в HeroService мы импортируем нашу заглушку HEROES и возвращаем ее в методе getHeroes. Наш сервис HeroService выглядит следующим образом: hero.service.ts Мы готовы использовать HeroService в других компонентах. Начнем с AppComponent. В начале, как обычно, импортируем то, что хотим использовать, то есть HeroService. app.component.ts (import HeroService) Импорт сервиса позволяет ссылаться на него в нашем коде. Как AppComponent должен получить конкретный экземпляр HeroService во время выполнения? Мы могли бы создать новый экземпляр HeroService, используя new, например, вот так (не надо так делать!): Это плохая идея по нескольким причинам. Что, если… что, если… Эй, у нас есть работа, которую нужно сделать! Мы сделаем это. Избежать этих проблем так легко, что нет никаких оправданий для того, чтобы сделать это неправильно. Две строки вместо одной, использовавшей new: Вот конструктор: app.component.ts (конструктор) Сам по себе конструктор ничего не делает. Параметр одновременно определяет приватное свойство heroService и идентифицирует его как место внедрения HeroService. Теперь Angular будет знать, чтобы необходимо передать экземпляр HeroService, когда он создает новый AppComponent. Angular должен получить этот экземпляр откуда-то. Это функция Angular-ского Dependency Injector ("внедритель" зависимостей — далее Инъектор). Инъектор имеет контейнер предварительно созданных сервисов. Либо он находит и возвращает предварительно существующий экземпляр HeroService из контейнера, либо создает новый экземпляр, добавляет его в контейнер, и возвращает его в Angular. Инъектор еще не знает, как создать HeroService. Если мы запустим наш код сейчас, Angular выдаст ошибку: Мы должны научить инъектор, как ему создать HeroService путем регистрации поставщика (provider) HeroService. Чтобы это сделать, нужно добавить свойство массива providers в нижней части метаданных компонента в вызове @Component. app.component.ts (providing HeroService) Массив providers указывает Angular, что нужно создать новый экземпляр HeroService, когда он создает новый AppComponent. AppComponent может использовать этот сервис, чтобы получить героев, и кроме того, его может получить каждый потомок компонента в дереве этого компонентов. Напомним, что AppComponent создает экземпляр HeroDetail, на основании тэга <my-hero-detail> в нижней части его шаблона. Компонент HeroDetail является потомком AppComponent. Если компоненту HeroDetailComponent требуется HeroService родительского компонента, он попросит Angular внедрить сервис в его конструктор, который будет выглядеть так же, как для AppComponent: hero-detail.component.ts (конструктор) Компонент HeroDetailComponent не должен повторять массив providers своего родителя! Подумайте, почему. В приложении ниже это будет рассмотрено более подробно. AppComponent является компонентом самого высокого уровня нашего приложения. В приложении должен быть только один экземпляр этого компонента и только один экземпляр HeroService во всем нашем приложении. У нас есть сервис в приватной переменной heroService. Давайте используем ее. Сделаем паузу, чтобы подумать. Мы можем вызвать сервис и получить данные в одной строке. В самом деле, нам не нужен специальный метод, чтобы обернуть одну строку. Мы пишем это в любом случае: app.component.ts (getHeroes) AppComponent должен получать и отображать героев без лишней суеты. Где нам лучше вызвать getHeroes? В конструкторе? Не надо так делать! Многолетний опыт и горькие слезы научили нас выносить сложную логику из конструктора, особенно ту, что может использовать сервер как источник данных. Конструктор предназначен для простых инициализаций, вроде присвоения параметров конструктора к свойствам. Но не для тяжелых вещей. Мы должны иметь возможность создать компонент в тесте и не беспокоиться, что он может делать реальную работу — такую, как вызов сервера! — пока мы не скажем ему это сделать. Если нет конструктора, то кто-то должен сделать вызов метода getHeroes. Angular сделает это, если мы реализуем ngOnInit Lifecycle Hook (хук жизненного цикла ngOnInit). Angular предлагает ряд интерфейсов для того, чтобы участвовать в критических моментах жизненного цикла компонента: при создании, после каждого изменения, и при его окончательном уничтожении. Каждый интерфейс имеет один метод. Когда компонент реализует этот метод, Angular делает его вызов в соответствующее время. Вот краткое описание интерфейса OnInit: app.component.ts (протокол OnInit) Мы пишем метод ngOnInit с нашей логикой инициализации внутри и предоставляем Angular-у вызвать его в нужное время. В нашем случае, инициализация заключается в вызове getHeroes. app.component.ts (протокол OnInit) Наше приложение, как и ожидалось, показывает список героев и вид с детальной информацией о герое, когда мы нажимаем на имя героя. Мы становимся ближе. Но кое-что еще не совсем правильно. Наш HeroService возвращает список фиктивных героев сразу. Это сигнатура синхронного вызова getHeroes: Стоит запросить героев, и они есть в возвращаемом результате. В будущем мы собираемся получить героев с удаленного сервера. Пока что мы не будем использовать HTTP, но будем стремиться к этому в следующих главах. Когда мы делаем вызов, нам нужно дождаться ответа сервера, и мы не сможем блокировать пользовательский интерфейс, пока мы ждем, даже если мы захотим (хотя и не должны), так как браузер не заблокируешь. Нам придется использовать какой-то асинхронный метод получения данных, который изменит сигнаруту нашего метода getHeroes. Мы будем использовать обещания. Обещание… ну, это обещание сделать наш вызов позже, когда результаты будут готовы. Мы просим асинхронный сервис, чтобы он выполнил какую-то работу, и отдал функцию обратного вызова. Он делает эту работу (где-то) и в конце концов он вызывает нашу функцию с результатами работы или с ошибкой. Обновим HeroService, используя метод, возвращающий обещание getHeroes: hero.service.ts (getHeroes) Мы все еще используем фиктивные данные. Мы имитируем поведение сверхскоростного, с нулевой задержкой сервера, возвращая уже разрешенное (выполненное) обещание с фиктивным списком наших героев, как результат. Возвращаясь к компоненту AppComponent и методу getHeroes, мы видим, что он по-прежнему выглядит следующим образом: app.component.ts (getHeroes — старый вариант) В результате нашего изменения в HeroService, мы переделаем this.heroes на работу с обещанием, вместо массива героев. Мы должны изменить нашу реализацию, чтобы выполнить обещание, когда оно разрешено (выполнено). Когда обещание разрешено успешно, тогда у нас будут герои для отображения. Мы передаем нашу функцию обратного вызова как аргумент методу обещания then: app.component.ts (getHeroes — новый вариант) Наш обратный вызов присваивает свойству компонента heroes массив героев, который вернул сервис. Вот и все! Наше приложение должно по-прежнему показывать список героев, и отображать детальное представление о герое при выборе его из списка. Давайте проверим, что мы имеем следующую структуру после нашего замечательного рефакторинга в этой главе: Файлы кода, которые мы обсуждали в этой главе. Давайте подведем итоги того, что мы создали. Запустить приложение, часть 4 Наш Тур героев стал более пригоден для повторного использования с применением общих компонентов и сервисов. Мы хотим создать информационную панель, добавить ссылки меню, для маршрутизации между представлениями, а также форматировать данные в шаблоне. По мере развития нашего приложения, мы узнаем, как его спроектировать, чтобы упростить его расширение и поддержку. Мы узнаем о компоненте маршрутизации Angular и навигации между представлениями в следующей главе. Мы можем эмулировать медленное соединение. Импортируйте тип Hero и добавьте следующий метод getHeroesSlowly в HeroService hero.service.ts (getHeroesSlowy) Также, как и getHeroes, этот метод возвращает обещание. Но это обещание делает задержку в 2 секунды до разрешения с фиктивным списком героев. Вернувшись в AppComponent, замените heroService.getHeroes на heroService.getHeroesSlowly и посмотрите, как будет вести себя приложение. Мы говорили ранее, что если мы включаем родительский AppComponent HeroService в HeroDetailComponent, мы не должны добавлять массив провайдеров в метаданные HeroDetailComponent. Почему? Потому что этим мы скажем Angular, что нужно создать новый экземпляр HeroService на уровне HeroDetailComponent. Компоненту HeroDetailComponent не нужен свой собственный экземпляр сервиса; ему нужен экземпляр сервиса его родителя. Добавление массива providers создает новый экземпляр сервиса, который является клоном родительского экземпляра. Подумайте хорошенько о том, где и когда нужно регистрировать поставщика. Понять область видимости этой регистрации. Будьте осторожны, чтобы не создать новый экземпляр сервиса на неправильном уровне. Пока что при использовании старого синтаксиса появляется только предупреждение. Программист