Spec-Zone.ru › Angular 2

HTTP

Getting and saving data

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

На этой странице вы внесёте следующие улучшения.

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

Вы научите приложение выполнять соответствующие вызовы HTTP к веб-API удалённого сервера.

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

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

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

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

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

npm start

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

Вы можете продолжить разработку приложения Tour of Heroes, не прерывая перекомпиляцию или обновление браузера.

Предоставление служб HTTP

HttpModule не является основным модулем Angular. HttpModule — это необязательный подход Angular к доступу к веб-ресурсам. Он существует как отдельный дополнительный модуль под названием @angular/http и поставляется в отдельном файле скрипта как часть пакета Angular npm.

Вы готовы импортировать из @angular/http, потому что systemjs.config настроил SystemJS на загрузку этой библиотеки при необходимости.

Регистрация для служб HTTP

Приложение будет зависеть от службы Angular http, которая, в свою очередь, зависит от других вспомогательных служб. HttpModule из библиотеки @angular/http содержит провайдеры для полного набора служб HTTP.

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

src/app/app.module.ts (v1)

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppRoutingModule } from './app-routing.module';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService }          from './hero.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Обратите внимание, что вы также предоставляете HttpModule в качестве части массива imports в корневом NgModule AppModule.

Имитация веб-API

Рекомендуется регистрировать службы для всего приложения в корневом AppModule providers. Здесь вы регистрируете в main по особой причине.

Пока у вас нет веб-сервера, который может обрабатывать запросы данных героя, клиент HTTP будет извлекать и сохранять данные из имитационной службы — веб-API с кэшированием в памяти. Приложению не нужно знать об этом, поэтому вы можете поместить веб-API с кэшированием в памяти в конфигурацию выше AppComponent.

Обновите src/app/app.module.ts этой версией, которая использует имитационную службу:

src/app/app.module.ts (v2)

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppRoutingModule } from './app-routing.module';

// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService }          from './hero.service';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Вместо необходимости в реальном сервере API этот пример моделирует взаимодействие с удалённым сервером, добавив InMemoryWebApiModule в модуль imports, фактически заменив службу XHR клиента Http на альтернативу с кэшированием в памяти.

InMemoryWebApiModule.forRoot(InMemoryDataService),

Метод конфигурации forRoot() принимает класс InMemoryDataService, который инициализирует базу данных с кэшированием в памяти. Добавьте файл in-memory-data.service.ts в app со следующим содержимым:

src/app/in-memory-data.service.ts

import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    let heroes = [
      {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'}
    ];
    return {heroes};
  }
}

Этот файл заменяет mock-heroes.ts, который теперь можно удалить.

Веб-API с кэшированием в памяти полезен только на ранних этапах разработки и для демонстраций, таких как Tour of Heroes. Не беспокойтесь о деталях этой замены бэкенда; вы можете пропустить её, когда у вас будет реальный веб-сервер API.

Дополнительную информацию о веб-API с кэшированием в памяти см. в разделе «Приложение: веб-API с кэшированием в памяти Tour of Heroes» на странице «Клиент HTTP».

Герои и HTTP

В текущей реализации HeroService возвращается промис, разрешённый с помощью имитационных героев.

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

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

Теперь преобразуйте getHeroes() для использования HTTP.

src/app/hero.service.ts (обновлённые getHeroes и новые члены класса)

  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(private http: Http) { }

  getHeroes(): Promise<Hero[]> {
    return this.http.get(this.heroesUrl)
               .toPromise()
               .then(response => response.json().data as Hero[])
               .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); // for demo purposes only
    return Promise.reject(error.message || error);
  }

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

src/app/hero.service.ts (обновлённые импорты)

import { Injectable }    from '@angular/core';
import { Headers, Http } from '@angular/http';

import 'rxjs/add/operator/toPromise';

import { Hero } from './hero';

Обновите браузер. Данные героя должны успешно загружаться с имитационного сервера.

Промис HTTP

Сервис Angular http.get возвращает RxJS Observable. Наблюдаемые значения — это мощный способ управления асинхронными потоками данных. Вы узнаете о наблюдаемых значениях позже на этой странице.

Сейчас вы преобразовали Observable в Promise с помощью оператора toPromise.

.toPromise()

У службы Angular Observable нет оператора toPromise из коробки.

Существует множество операторов, таких как toPromise, которые расширяют Observable полезными возможностями. Чтобы использовать эти возможности, вам нужно добавить сами операторы. Это легко, как импорт из библиотеки RxJS, например так:

import 'rxjs/add/operator/toPromise';

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

Извлечение данных в обратном вызове then

В обратном вызове Promise then() вы вызываете метод json службы HTTP Response для извлечения данных в ответе.

.then(response => response.json().data as Hero[])

Ответ JSON содержит единственное свойство data, которое содержит массив героев, которые требуются вызывающей стороне. Поэтому вы захватываете этот массив и возвращаете его в качестве значения разрешённого Promise.

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

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

Обработка ошибок

В конце getHeroes(), вы catch ошибки сервера и передаёте их обработчику ошибок.

.catch(this.handleError);

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

private handleError(error: any): Promise<any> {
  console.error('An error occurred', error); // for demo purposes only
  return Promise.reject(error.message || error);
}

В этой демо-службе вы записываете ошибку в консоль; в реальной жизни вы бы обрабатывали ошибку в коде. Для демонстрации это работает.

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

Получение героя по ID

Когда HeroDetailComponent просит HeroService извлечь героя, HeroService в настоящее время извлекает всех героев и фильтрует, чтобы найти того, у кого соответствует id. Это хорошо для имитации, но это расточительно запрашивать у реального сервера всех героев, когда вам нужен только один. Большинство веб-API поддерживают запрос получение по ID в форме api/hero/:id (например, api/hero/11).

Обновите метод HeroService.getHero для выполнения запроса получение по ID:

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

getHero(id: number): Promise<Hero> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.get(url)
    .toPromise()
    .then(response => response.json().data as Hero)
    .catch(this.handleError);
}

Этот запрос почти такой же, как getHeroes. Идентификатор героя в URL указывает, какого героя должен обновить сервер.

Кроме того, data в ответе — это один объект героя, а не массив.

Неизменённый API getHeroes

Хотя вы внесли существенные внутренние изменения в getHeroes() и getHero(), публичные сигнатуры не изменились. Вы по-прежнему возвращаете Promise из обоих методов. Вам не придётся обновлять ни один из компонентов, которые их вызывают.

Теперь пришло время добавить возможность создания и удаления героев.

Обновление подробностей о герое

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

Ранее изменения не терялись. Что изменилось? Когда приложение использовало список имитационных героев, обновления применялись непосредственно к объектам героев в едином общеприменимом общем списке. Теперь, когда вы получаете данные с сервера, если вы хотите, чтобы изменения сохранялись, вы должны записать их обратно на сервер.

Добавление возможности сохранения подробностей о герое

В конце шаблона представления подробностей о герое добавьте кнопку «Сохранить» со связью события click , которая вызывает новый метод компонента под названием save.

src/app/hero-detail.component.html (save)

<button (click)="save()">Save</button>

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

src/app/hero-detail.component.ts (save)

save(): void {
  this.heroService.update(this.hero)
    .then(() => this.goBack());
}

Добавление метода службы героев update

Общая структура метода update аналогична структуре метода getHeroes, но она использует HTTP put() для сохранения изменений на стороне сервера.

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

private headers = new Headers({'Content-Type': 'application/json'});

update(hero: Hero): Promise<Hero> {
  const url = `${this.heroesUrl}/${hero.id}`;
  return this.http
    .put(url, JSON.stringify(hero), {headers: this.headers})
    .toPromise()
    .then(() => hero)
    .catch(this.handleError);
}

Для идентификации героя, который должен обновиться на сервере, идентификатор героя id кодируется в URL. Тело put() — это строка JSON, кодирующая героя, полученная путём вызова JSON.stringify. Тип содержимого тела (application/json ) идентифицируется в заголовке запроса.

Обновите браузер, измените имя героя, сохраните изменения и нажмите кнопку «Назад» в браузере. Изменения должны сохраниться.

Добавление возможности добавлять героев

Чтобы добавить героя, приложению потребуется имя героя. Вы можете использовать элемент input вместе с кнопкой «Добавить».

Вставьте следующее в HTML-компонент heroes, сразу после заголовка:

src/app/heroes.component.html (add)

<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>

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

src/app/heroes.component.ts (add)

add(name: string): void {
  name = name.trim();
  if (!name) { return; }
  this.heroService.create(name)
    .then(hero => {
      this.heroes.push(hero);
      this.selectedHero = null;
    });
}

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

Реализуйте метод create в классе HeroService.

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

create(name: string): Promise<Hero> {
  return this.http
    .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
    .toPromise()
    .then(res => res.json().data as Hero)
    .catch(this.handleError);
}

Обновите браузер и создайте некоторых героев.

Добавление возможности удаления героя

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

Добавьте следующий элемент кнопки в HTML-компонент heroes, после имени героя в повторяющемся <li> теге.

<button class="delete"
  (click)="delete(hero); $event.stopPropagation()">x</button>

Элемент <li> теперь должен выглядеть так:

src/app/heroes.component.html (li-element)

  <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
      [class.selected]="hero === selectedHero">
    <span class="badge">{{hero.id}}</span>
    <span>{{hero.name}}</span>
    <button class="delete"
      (click)="delete(hero); $event.stopPropagation()">x</button>
  </li>

Помимо вызова метода delete() компонента, код обработчика клика кнопки удаления останавливает распространение события клика — вы не хотите, чтобы срабатывал обработчик клика <li>, так как это приведет к выбору героя, которого пользователь хочет удалить.

Логика обработчика delete() немного сложнее:

src/app/heroes.component.ts (delete)

delete(hero: Hero): void {
  this.heroService
      .delete(hero.id)
      .then(() => {
        this.heroes = this.heroes.filter(h => h !== hero);
        if (this.selectedHero === hero) { this.selectedHero = null; }
      });
}

Конечно, вы делегируете удаление героя службе героев, но компонент по-прежнему отвечает за обновление отображения: он удаляет удалённого героя из массива и, при необходимости, сбрасывает выбранного героя.

Чтобы разместить кнопку удаления в правом конце записи о герое, добавьте следующий дополнительный CSS:

src/app/heroes.component.css (additions)

button.delete {
  float:right;
  margin-top: 2px;
  margin-right: .8em;
  background-color: gray !important;
  color:white;
}

Метод службы героев delete()

Добавьте метод службы героев delete(), который использует метод delete() HTTP для удаления героя с сервера:

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

delete(id: number): Promise<void> {
  const url = `${this.heroesUrl}/${id}`;
  return this.http.delete(url, {headers: this.headers})
    .toPromise()
    .then(() => null)
    .catch(this.handleError);
}

Обновите браузер и попробуйте новую функциональность удаления.

Наблюдаемые значения

Каждый метод службы Http возвращает Observable объектов HTTP Response.

HeroService преобразует этот Observable в Promise и возвращает обещание вызывающей стороне. Этот раздел показывает, как, когда и почему возвращать Observable напрямую.

Описание

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

Основной Angular поддерживает наблюдаемые значения. Разработчики дополняют эту поддержку операторами и расширениями из библиотеки RxJS. Вы скоро увидите это.

Вспомните, что HeroService применил оператор toPromise к результату Observable http.get(). Этот оператор преобразовал Observable в Promise и вы передали это обещание обратно вызывающей стороне.

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

Но запросы не всегда выполняются только один раз. Вы можете начать один запрос, отменить его и сделать другой запрос, прежде чем сервер ответит на первый запрос. Последовательность «запрос — отмена — новый запрос» сложно реализовать с функциями Promise, но легко с Observable.

Добавление возможности поиска по имени

Вы собираетесь добавить функцию поиска героев в Tour of Heroes. По мере ввода пользователем имени в поле поиска вы будете повторять HTTP-запросы для героев, отфильтрованных по этому имени.

Начните с создания HeroSearchService, которая отправляет запросы поиска на веб-API сервера.

src/app/hero-search.service.ts

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

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

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`app/heroes/?name=${term}`)
               .map(response => response.json().data as Hero[]);
  }
}

Вызов http.get() в HeroSearchService похож на вызов в HeroService, хотя URL теперь содержит строку запроса.

Что еще важнее, вы больше не вызываете toPromise(). Вместо этого вы возвращаете наблюдаемое значение из htttp.get(), после применения к нему другого оператора RxJS, map(), для извлечения героев из данных ответа.

Цепочка операторов RxJS упрощает и делает читабельнее обработку ответов. См. ниже обсуждение операторов.

HeroSearchComponent

Создайте компонент HeroSearchComponent, который вызывает новый HeroSearchService.

Шаблон компонента простой — просто поле ввода текста и список совпадающих результатов поиска.

src/app/hero-search.component.html

<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>

Также добавьте стили для нового компонента.

src/app/hero-search.component.css

.search-result{
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
}

.search-result:hover {
  color: #eee;
  background-color: #607D8B;
}

#search-box{
  width: 200px;
  height: 20px;
}

По мере ввода пользователем текста в поле поиска привязка события keyup вызывает метод компонента search() со значением нового поля поиска.

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

Но, как вы скоро увидите, свойство heroes теперь является наблюдаемым значением массивов героев, а не просто массивом героев. Компонент *ngFor ничего не может сделать с наблюдаемым значением, пока вы не передадите его через трубу async (AsyncPipe). Труба async подписывается на наблюдаемое значение Observable и создаёт массив героев для *ngFor.

Создайте класс HeroSearchComponent и метаданные.

src/app/hero-search.component.ts

import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';

@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: [ './hero-search.component.css' ],
  providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
  heroes: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(
    private heroSearchService: HeroSearchService,
    private router: Router) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes = this.searchTerms
      .debounceTime(300)        // wait 300ms after each keystroke before considering the term
      .distinctUntilChanged()   // ignore if next search term is same as previous
      .switchMap(term => term   // switch to new observable each time the term changes
        // return the http search observable
        ? this.heroSearchService.search(term)
        // or the observable of empty heroes if there was no search term
        : Observable.of<Hero[]>([]))
      .catch(error => {
        // TODO: add real error handling
        console.log(error);
        return Observable.of<Hero[]>([]);
      });
  }

  gotoDetail(hero: Hero): void {
    let link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}

Критерии поиска

Сфокусируйтесь на searchTerms:

private searchTerms = new Subject<string>();

// Push a search term into the observable stream.
search(term: string): void {
  this.searchTerms.next(term);
}

Subject — это генератор потока событий наблюдаемого значения; searchTerms производит наблюдаемое значение строк, критериев фильтрации для поиска по имени.

Каждый вызов search() помещает новую строку в поток наблюдаемого значения этого субъекта, вызывая next().

Инициализация свойства heroes (ngOnInit)

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

heroes: Observable<Hero[]>;

ngOnInit(): void {
  this.heroes = this.searchTerms
    .debounceTime(300)        // wait 300ms after each keystroke before considering the term
    .distinctUntilChanged()   // ignore if next search term is same as previous
    .switchMap(term => term   // switch to new observable each time the term changes
      // return the http search observable
      ? this.heroSearchService.search(term)
      // or the observable of empty heroes if there was no search term
      : Observable.of<Hero[]>([]))
    .catch(error => {
      // TODO: add real error handling
      console.log(error);
      return Observable.of<Hero[]>([]);
    });
}

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

Вместо этого вы можете связать операторы RxJS, которые уменьшат поток запросов до строки Observable. Вы будете делать меньше вызовов к HeroSearchService и всё равно получите своевременные результаты. Вот как:

  • debounceTime(300) ожидает, пока поток новых событий строк не остановится на 300 миллисекунд, прежде чем передать последнюю строку. Вы никогда не будете отправлять запросы чаще, чем каждые 300 мс.
  • distinctUntilChanged гарантирует, что запрос отправляется только в том случае, если текст фильтрации изменился.
  • switchMap() вызывает службу поиска для каждого поискового запроса, который проходит через debounce и distinctUntilChanged. Она отменяет и отбрасывает предыдущие наблюдаемые значения поиска, возвращая только последнее наблюдаемое значение службы поиска.

С оператором switchMap (ранее известным как flatMapLatest) каждый соответствующий ввод пользователя может вызывать метод http(). Даже с 300-миллисекундной задержкой между запросами у вас могут быть несколько HTTP-запросов в процессе, и они могут не возвращаться в том порядке, в котором были отправлены.

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

Если текст поиска пустой, вызов метода http() также «замыкается», и возвращается наблюдаемое значение с пустым массивом.

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

  • catch перехватывает неисправное наблюдаемое значение. В простом примере ошибка выводится в консоль; в реальном приложении это было бы лучше. Затем, чтобы очистить результат поиска, вы возвращаете наблюдаемое значение с пустым массивом.

Импорт операторов RxJS

Большинство операторов RxJS не включены в базовый Angular Observable реализации. В базовой реализации включены только необходимые Angular операторы.

Когда вам нужны дополнительные возможности RxJS, расширьте Observable путём импорта библиотек, в которых они определены. Вот все импорты RxJS, которые нужны этому компоненту:

src/app/hero-search.component.ts (rxjs imports)

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

Синтаксис import 'rxjs/add/...' может быть незнакомым. Ему не хватает обычного списка символов между фигурными скобками: {...}.

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

Добавление компонента поиска на панель управления

Добавьте элемент HTML поиска героев в конец шаблона DashboardComponent.

src/app/dashboard.component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes"  [routerLink]="['/detail', hero.id]"  class="col-1-4">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>
<hero-search></hero-search>

Наконец, импортируйте HeroSearchComponent из hero-search.component.ts и добавьте его в массив declarations.

src/app/app.module.ts (search)

  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
    HeroSearchComponent
  ],

Запустите приложение снова. На панели управления введите текст в поле поиска. Если вы введёте символы, соответствующие именам существующих героев, вы увидите что-то подобное.

Hero Search Component

Структура приложения и код

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

angular-tour-of-heroes
  src
    app
      app.component.ts
      app.component.css
      app.module.ts
      app-routing.module.ts
      dashboard.component.css
      dashboard.component.html
      dashboard.component.ts
      hero.ts
      hero-detail.component.css
      hero-detail.component.html
      hero-detail.component.ts
      hero-search.component.html (new)
      hero-search.component.css (new)
      hero-search.component.ts (new)
      hero-search.service.ts (new)
      hero.service.ts
      heroes.component.css
      heroes.component.html
      heroes.component.ts
      in-memory-data.service.ts (new)
    main.ts
    index.html
    styles.css
    systemjs.config.js
    tsconfig.json
  node_modules ...
  package.json

Финишная прямая

Вы находитесь в конце пути и добились многого.

  • Вы добавили необходимые зависимости для использования HTTP в приложении.
  • Вы переработали HeroService для загрузки героев из веб-API.
  • Вы расширили HeroService для поддержки методов post(), put(), и delete().
  • Вы обновили компоненты, чтобы позволить добавлять, редактировать и удалять героев.
  • Вы настроились на веб-API в оперативной памяти.
  • Вы узнали, как использовать Observable.

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

app.comp...ts
import { Component }          from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>{{title}}</h1>
    <nav>
      <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Tour of Heroes';
}
app.mod...ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppRoutingModule } from './app-routing.module';

// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard.component';
import { HeroesComponent }      from './heroes.component';
import { HeroDetailComponent }  from './hero-detail.component';
import { HeroService }          from './hero.service';
import { HeroSearchComponent }  from './hero-search.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
    HeroSearchComponent
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
heroes.comp...ts
import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

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

@Component({
  selector: 'my-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: [ './heroes.component.css' ]
})
export class HeroesComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;

  constructor(
    private heroService: HeroService,
    private router: Router) { }

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

  add(name: string): void {
    name = name.trim();
    if (!name) { return; }
    this.heroService.create(name)
      .then(hero => {
        this.heroes.push(hero);
        this.selectedHero = null;
      });
  }

  delete(hero: Hero): void {
    this.heroService
        .delete(hero.id)
        .then(() => {
          this.heroes = this.heroes.filter(h => h !== hero);
          if (this.selectedHero === hero) { this.selectedHero = null; }
        });
  }

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

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }

  gotoDetail(): void {
    this.router.navigate(['/detail', this.selectedHero.id]);
  }
}
heroes.comp...html
<h2>My Heroes</h2>
<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''">
    Add
  </button>
</div>
<ul class="heroes">
  <li *ngFor="let hero of heroes" (click)="onSelect(hero)"
      [class.selected]="hero === selectedHero">
    <span class="badge">{{hero.id}}</span>
    <span>{{hero.name}}</span>
    <button class="delete"
      (click)="delete(hero); $event.stopPropagation()">x</button>
  </li>
</ul>
<div *ngIf="selectedHero">
  <h2>
    {{selectedHero.name | uppercase}} is my hero
  </h2>
  <button (click)="gotoDetail()">View Details</button>
</div>
heroes.comp...css
.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:hover {
  color: #607D8B;
  background-color: #DDD;
  left: .1em;
}
.heroes li.selected:hover {
  background-color: #BBD8DC !important;
  color: white;
}
.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;
}
button {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button.delete {
  float:right;
  margin-top: 2px;
  margin-right: .8em;
  background-color: gray !important;
  color:white;
}
hero-detail.comp...ts
import 'rxjs/add/operator/switchMap';
import { Component, OnInit }      from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Location }               from '@angular/common';

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

@Component({
  selector: 'hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
  hero: Hero;

  constructor(
    private heroService: HeroService,
    private route: ActivatedRoute,
    private location: Location
  ) {}

  ngOnInit(): void {
    this.route.params
      .switchMap((params: Params) => this.heroService.getHero(+params['id']))
      .subscribe(hero => this.hero = hero);
  }

  save(): void {
    this.heroService.update(this.hero)
      .then(() => this.goBack());
  }

  goBack(): void {
    this.location.back();
  }
}
hero-detail.comp...html
<div *ngIf="hero">
  <h2>{{hero.name}} details!</h2>
  <div>
    <label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="hero.name" placeholder="name" />
   </div>
  <button (click)="goBack()">Back</button>
  <button (click)="save()">Save</button>
</div>
hero.service.ts
import { Injectable }    from '@angular/core';
import { Headers, Http } from '@angular/http';

import 'rxjs/add/operator/toPromise';

import { Hero } from './hero';

@Injectable()
export class HeroService {

  private headers = new Headers({'Content-Type': 'application/json'});
  private heroesUrl = 'api/heroes';  // URL to web api

  constructor(private http: Http) { }

  getHeroes(): Promise<Hero[]> {
    return this.http.get(this.heroesUrl)
               .toPromise()
               .then(response => response.json().data as Hero[])
               .catch(this.handleError);
  }


  getHero(id: number): Promise<Hero> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.get(url)
      .toPromise()
      .then(response => response.json().data as Hero)
      .catch(this.handleError);
  }

  delete(id: number): Promise<void> {
    const url = `${this.heroesUrl}/${id}`;
    return this.http.delete(url, {headers: this.headers})
      .toPromise()
      .then(() => null)
      .catch(this.handleError);
  }

  create(name: string): Promise<Hero> {
    return this.http
      .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
      .toPromise()
      .then(res => res.json().data as Hero)
      .catch(this.handleError);
  }

  update(hero: Hero): Promise<Hero> {
    const url = `${this.heroesUrl}/${hero.id}`;
    return this.http
      .put(url, JSON.stringify(hero), {headers: this.headers})
      .toPromise()
      .then(() => hero)
      .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); // for demo purposes only
    return Promise.reject(error.message || error);
  }
}
in-memory-data.service.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
  createDb() {
    let heroes = [
      {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'}
    ];
    return {heroes};
  }
}
hero-search.service.ts
import { Injectable } from '@angular/core';
import { Http }       from '@angular/http';

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

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> {
    return this.http
               .get(`app/heroes/?name=${term}`)
               .map(response => response.json().data as Hero[]);
  }
}
hero-search.component.ts
import { Component, OnInit } from '@angular/core';
import { Router }            from '@angular/router';

import { Observable }        from 'rxjs/Observable';
import { Subject }           from 'rxjs/Subject';

// Observable class extensions
import 'rxjs/add/observable/of';

// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';

@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: [ './hero-search.component.css' ],
  providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
  heroes: Observable<Hero[]>;
  private searchTerms = new Subject<string>();

  constructor(
    private heroSearchService: HeroSearchService,
    private router: Router) {}

  // Push a search term into the observable stream.
  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnInit(): void {
    this.heroes = this.searchTerms
      .debounceTime(300)        // wait 300ms after each keystroke before considering the term
      .distinctUntilChanged()   // ignore if next search term is same as previous
      .switchMap(term => term   // switch to new observable each time the term changes
        // return the http search observable
        ? this.heroSearchService.search(term)
        // or the observable of empty heroes if there was no search term
        : Observable.of<Hero[]>([]))
      .catch(error => {
        // TODO: add real error handling
        console.log(error);
        return Observable.of<Hero[]>([]);
      });
  }

  gotoDetail(hero: Hero): void {
    let link = ['/detail', hero.id];
    this.router.navigate(link);
  }
}
hero-search.component.html
<div id="search-component">
  <h4>Hero Search</h4>
  <input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
  <div>
    <div *ngFor="let hero of heroes | async"
         (click)="gotoDetail(hero)" class="search-result" >
      {{hero.name}}
    </div>
  </div>
</div>
hero-search.component.css
.search-result{
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
}

.search-result:hover {
  color: #eee;
  background-color: #607D8B;
}

#search-box{
  width: 200px;
  height: 20px;
}

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

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

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

Spec-Zone.ru

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