HTTP-клиент
Используйте HTTP-клиент для взаимодействия с удалённым сервером.
HTTP — основной протокол для обмена данными между браузером и сервером.
Протокол
WebSocket— ещё одна важная технология связи; он не рассматривается на этой странице.
Современные браузеры поддерживают два API, основанные на HTTP: XMLHttpRequest (XHR) и JSONP. Некоторые браузеры также поддерживают Fetch.
Библиотека Angular HTTP упрощает разработку приложений с использованием API XHR и JSONP.
Содержание
- Демонстрации
- Предоставление HTTP-сервисов
- Демонстрация клиента HTTP "Путешествие по героям"
- Получение данных с помощью
http.get() - Библиотека RxJS
- Обработка объекта ответа
- Отправка данных на сервер
- Запросы с разных доменов: пример из Википедии
- Неэффективное приложение
- Больше возможностей с Observables
- Защита от поддельных запросов с одного сайта
- Переопределение стандартных заголовков запроса (и других параметров)
- Приложение: in-memory web api для "Путешествия по героям"
Живой пример иллюстрирует эти темы.
Демонстрации
На этой странице описано взаимодействие с сервером с помощью следующих демонстраций:
- Демонстрация клиента HTTP "Путешествие по героям".
- Возврат к Promises.
- Запросы с разных доменов: пример из Википедии.
- Больше возможностей с Observables.
Основной AppComponent координирует эти демонстрации:
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<hero-list></hero-list>
<hero-list-promise></hero-list-promise>
<my-wiki></my-wiki>
<my-wiki-smart></my-wiki-smart>
`
})
export class AppComponent { }
Предоставление HTTP-сервисов
Сначала настройте приложение для использования средств взаимодействия с сервером.
Клиент Angular Http взаимодействует с сервером, используя знакомый протокол HTTP-запрос/ответ. Клиент Http — один из множества сервисов в библиотеке Angular HTTP.
При импорте из модуля
@angular/httpSystemJS знает, как загружать сервисы из библиотеки Angular HTTP, потому что файлsystemjs.config.jsсопоставляется с этим именем модуля.
Перед использованием клиента Http необходимо зарегистрировать его как поставщика в системе инъекции зависимостей.
Подробнее о поставщиках можно узнать на странице Инъекция зависимостей.
Регистрируйте поставщиков, импортируя другие NgModule в корневой NgModule в app.module.ts.
src/app/app.module.ts (v1)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule {
}
Начните с импорта необходимых элементов. Новые — HttpModule и JsonpModule из библиотеки Angular HTTP. Дополнительную информацию об импортах и связанной терминологии см. в справке MDN по инструкции import.
Чтобы добавить эти модули в приложение, передайте их в массив imports в корневом @NgModule.
Модуль
HttpModuleнеобходим для выполнения HTTP-запросов. Хотя модульJsonpModuleне нужен для обычных HTTP-запросов, в этой странице есть демонстрация JSONP. Загрузка его модуля сейчас сэкономит время.
Демонстрация клиента HTTP "Путешествие по героям"
Первая демонстрация — мини-версия приложения "Путешествие по героям" (ToH) из туториала. Эта версия получает героев с сервера, отображает их в списке, позволяет пользователю добавлять новых героев и сохраняет их на сервере. Приложение использует клиент Angular Http для взаимодействия через XMLHttpRequest (XHR).
Он работает так:
В этой демонстрации есть один компонент, HeroListComponent. Вот его шаблон:
src/app/toh/hero-list.component.html
<h1>Tour of Heroes ({{mode}})</h1>
<h3>Heroes:</h3>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<label>New hero name: <input #newHeroName /></label>
<button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button>
<p class="error" *ngIf="errorMessage">{{errorMessage}}</p>
Он представляет список героев с ngFor. Под списком находится поле ввода и кнопка Добавить героя, где вы можете ввести имена новых героев и добавить их в базу данных. С помощью переменной шаблона newHeroName, которая обращается к значению поля ввода, используется привязка событий (click). Когда пользователь нажимает кнопку, это значение передаётся в метод addHero компонента, а затем привязка события очищает его, чтобы подготовить его для нового имени героя.
Под кнопкой находится область для сообщения об ошибке.
Класс HeroListComponent
Вот класс компонента:
src/app/toh/hero-list.component.ts (класс)
export class HeroListComponent implements OnInit {
errorMessage: string;
heroes: Hero[];
mode = 'Observable';
constructor (private heroService: HeroService) {}
ngOnInit() { this.getHeroes(); }
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero(name: string) {
if (!name) { return; }
this.heroService.create(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
}
Angular вводит HeroService в конструктор, и компонент вызывает этот сервис для получения и сохранения данных.
Компонент не взаимодействует напрямую с клиентом Angular Http. Компонент не знает и не заботится о том, как он получает данные. Он делегирует это HeroService.
Золотое правило: всегда делегируйте доступ к данным вспомогательному классу сервиса.
Хотя во время выполнения компонент запрашивает героев сразу после создания, вы не вызываете метод get сервиса в конструкторе компонента. Вместо этого вызывайте его внутри цикла жизни ngOnInit метод жизненного цикла и полагайтесь на Angular, чтобы вызвать ngOnInit при создании этого компонента.
Это лучшая практика. Компоненты легче тестировать и отлаживать, когда их конструкторы простые, а вся реальная работа (особенно вызов удалённого сервера) выполняется в отдельном методе.
Методы getHeroes() и create() сервиса возвращают Observable данных героя, полученных клиентом Angular Http с сервера.
Представьте Observable как поток событий, публикуемый каким-либо источником. Чтобы прослушать события в этом потоке, подпишитесь на Observable. Эти подписки определяют действия, которые должны быть выполнены, когда веб-запрос генерирует событие успеха (с данными героя в полезной нагрузке события) или событие ошибки (с ошибкой в полезной нагрузке).
Теперь, с базовым пониманием компонента, вы готовы ознакомиться с содержимым HeroService.
Получение данных с помощью http.get()
Во многих предыдущих примерах приложение имитировало взаимодействие с сервером, возвращая тестовых героев в сервис, похожий на этот:
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}
}
Вы можете изменить этот HeroService для получения героев с сервера, используя сервис клиента Angular Http:
src/app/toh/hero.service.ts (переработанный)
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Hero } from './hero';
@Injectable()
export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web API
constructor (private http: Http) {}
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
Обратите внимание, что служба клиента Angular Http вводится в конструктор HeroService.
constructor (private http: Http) {}
Внимательно посмотрите, как вызывать http.get:
src/app/toh/hero.service.ts (getHeroes)
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
Вы передаёте URL ресурса в get , и он обращается к серверу, который возвращает героев.
Сервер возвращает героев, после того как вы настроили in-memory web api, описанный в приложении ниже. В качестве альтернативы, вы можете временно обратиться к файлу JSON, изменив URL конечной точки:
private heroesUrl = 'app/heroes.json'; // URL to JSON file
Если вы знакомы с асинхронными методами в современном JavaScript, вы, возможно, ожидали, что метод get вернёт promise. Вы ожидали бы цепочки вызова then() и извлечения героев. Вместо этого вы вызываете метод map(). Очевидно, это не promise.
На самом деле, метод http.get возвращает Observable HTTP-ответов (Observable<Response>) из библиотеки RxJS, а map() — один из операторов RxJS.
Библиотека RxJS
RxJS — библиотека сторонних разработчиков, одобренная Angular, которая реализует паттерн асинхронного Observable.
Все примеры Руководства разработчика установили пакет RxJS npm, потому что Observables широко используются в приложениях Angular. Это приложение нуждается в нём при работе с HTTP-клиентом. Но вы должны выполнить дополнительный важный шаг, чтобы сделать RxJS Observables применимыми: вы должны импортировать операторы RxJS по отдельности.
Включение операторов RxJS
Библиотека RxJS большая. Размер имеет значение при разработке производственного приложения и развертывании его на мобильных устройствах. Вы должны включить только необходимые функции.
Каждый файл кода должен добавлять необходимые операторы, импортируя их из библиотеки RxJS. Метод getHeroes() требует операторов map() и catch(), поэтому их импортируют так.
src/app/app.component.ts (import rxjs)
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
Обработка объекта ответа
Обратите внимание, что метод getHeroes() использовал вспомогательный метод extractData() для отображения объекта ответа http.get на героев:
src/app/toh/hero.service.ts (excerpt)
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
Объект response не содержит данные в формате, который приложение может использовать напрямую. Вы должны разобрать данные ответа в объект JSON.
Парсинг в JSON
Данные ответа находятся в формате JSON-строки. Приложение должно преобразовать эту строку в объекты JavaScript, вызвав response.json().
Это не собственный дизайн Angular. Клиент Angular HTTP следует спецификации Fetch для объекта ответа, возвращаемого функцией
Fetch. Эта спецификация определяет методjson(), который парсит тело ответа в объект JavaScript.
Не ожидайте, что декодированный JSON будет непосредственно массивом героев. Этот сервер всегда оборачивает результаты JSON в объект со свойством
data. Вам нужно его разобрать, чтобы получить героев. Это стандартное поведение веб-API, обусловленное особенностями безопасности.
Не делайте предположений о серверном API. Не все серверы возвращают объект со свойством
data.
Не возвращать объект ответа
Метод getHeroes() мог бы вернуть HTTP-ответ, но это не соответствовало бы лучшим практикам. Цель службы данных — скрыть детали взаимодействия с сервером от потребителей. Компонент, вызывающий HeroService, хочет только героев и отделен от их получения, кода, обрабатывающего их источник, и объекта ответа.
Метод http.get еще не отправляет запрос. Это Observable — холодное, что означает, что запрос не будет отправлен, пока к Observable не подпишется что-либо. Это что-то — HeroListComponent.
Всегда обрабатывать ошибки
Важная часть работы с вводом/выводом — прогнозирование ошибок, подготовка к их поимке и работе с ними. Один из способов обработки ошибок — передача сообщения об ошибке обратно в компонент для отображения пользователю, но только если оно содержит информацию, которую пользователь может понять и на основе которой может действовать.
Это простое приложение передает эту идею, хотя и несовершенно, способом обработки ошибки getHeroes.
src/app/toh/hero.service.ts (excerpt)
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
Оператор catch() передает объект ошибки из http в метод handleError(). Метод handleError преобразует ошибку в понятное для разработчика сообщение, регистрирует его в консоли и возвращает сообщение в новом, неудачном Observable через Observable.throw.
HeroListComponent обработка ошибок
Назад в HeroListComponent, в heroService.getHeroes(), функция subscribe имеет второй параметр функции для обработки сообщения об ошибке. Она устанавливает переменную errorMessage, которая условно связана в шаблоне HeroListComponent.
src/app/toh/hero-list.component.ts (getHeroes)
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
Хотите увидеть ошибку? В
HeroService, сбросьте конечную точку api на неверное значение. После этого не забудьте восстановить ее.
Отправка данных на сервер
До сих пор вы видели, как получать данные из удаленного расположения с помощью службы HTTP. Теперь вы добавите возможность создавать новых героев и сохранять их на сервере.
Вы напишете метод для вызова HeroListComponent, метод create(), который принимает только имя нового героя и возвращает Observable типа Observable, содержащего Hero. Он начинается так:
create(name: string): Observable<Hero> {
Чтобы его реализовать, вам необходимо знать API сервера для создания героев.
Сервер данных этого примера следует типичным REST-правилам. Он ожидает запрос POST по тому же адресу, что и GET для героев. Он ожидает, что новые данные героя будут в теле запроса, структурированные как сущность Hero, но без свойства id.
Тело запроса должно выглядеть так:
{ "name": "Windstorm" }
Сервер генерирует id и возвращает полное представление нового героя, включая сгенерированный идентификатор. Герой приходит вложенным в объект ответа со своим собственным свойством data.
Теперь, когда вы знаете, как работает API, реализуйте create() следующим образом:
src/app/toh/hero.service.ts (дополнительные импорты)
import { Headers, RequestOptions } from '@angular/http';
src/app/toh/hero.service.ts (create)
create(name: string): Observable<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.map(this.extractData)
.catch(this.handleError);
}
Заголовки
В объекте headers заголовок Content-Type указывает, что тело представляет JSON.
Далее, объект headers используется для настройки объекта options. Объект options — новый экземпляр RequestOptions, класса, который позволяет указывать определенные настройки при создании запроса. Таким образом, заголовки являются одним из RequestOptions.
В инструкции return options является третьим аргументом метода post().
Результаты JSON
Как и в случае с getHeroes(), используйте вспомогательный метод extractData() для извлечения данных из ответа.
Назад в HeroListComponent, его метод addHero() подписывается на Observable, возвращаемый методом службы create().
Когда данные приходят, он добавляет новый объект героя в массив heroes для отображения пользователю.
src/app/toh/hero-list.component.ts (addHero)
addHero(name: string) {
if (!name) { return; }
this.heroService.create(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
Возврат к promises
Хотя API клиента Angular http возвращает Observable, вы можете преобразовать его в Promise.
Хотя Promises могут быть более знакомы, Observable обладают многими преимуществами.
Вот сравнение использования HeroService с Promises и Observable, выделяя только части, которые отличаются.
getHeroes (): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
addHero (name: string): Promise<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Promise.reject(errMsg);
}
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
create(name: string): Observable<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
Вы можете следовать образцу Promise then(this.extractData).catch(this.handleError) как в этом примере.
В качестве альтернативы вы можете вызвать toPromise(success, fail). Обратный вызов Observable для успеха перемещается в первый параметр success, а для ошибки — во второй параметр fail в этом формате: .toPromise(this.extractData, this.handleError).
Метод errorHandler передает сообщение об ошибке как неудачный Observable вместо неудачного Promise.
Диагностический вывод в консоль — всего лишь еще один then() в цепочке Promise.
Вам нужно настроить вызывающий компонент для ожидания Promise вместо Observable:
getHeroes() {
this.heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) { return; }
this.heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero(name: string) {
if (!name) { return; }
this.heroService.create(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
Единственное очевидное различие состоит в том, что вы вызываете then() на возвращенном Promise вместо subscribe. Оба метода принимают те же функциональные аргументы.
Менее очевидное, но критическое различие заключается в том, что эти два метода возвращают совершенно разные результаты.
Основанный на Promise метод
then()возвращает еще один Promise. Вы можете продолжать цепочку вызововthen()иcatch(), получая новый Promise каждый раз.Метод
subscribe()возвращает Observable. Observable не является другим Promise. Это конец цепочки для Observable. Вы не можете вызватьmap()на нем или вызватьsubscribe()снова. Объект Observable имеет другую цель, обозначенную его основным методом,unsubscribe.Чтобы понять последствия и последствия подписок, посмотрите доклад Бена Леша об Observables или его видеокурс на egghead.io.
Запросы с разных доменных имен: пример из Википедии
Вы только что узнали, как делать XMLHttpRequests с помощью службы Angular Http. Это наиболее распространенный подход к взаимодействию с сервером, но он не работает во всех случаях.
По соображениям безопасности браузеры блокируют XHR-запросы удаленному серверу, чьё происхождение отличается от происхождения веб-страницы. Происхождение — это комбинация схемы URI, имени хоста и номера порта. Это называется политикой одинакового происхождения.
Современные браузеры разрешают
XHR-запросы серверам из разных источников, если сервер поддерживает протокол CORS. Если сервер требует учетных данных пользователя, включите их в заголовки запроса.
Некоторые серверы не поддерживают CORS, но поддерживают более старую, только для чтения альтернативу под названием JSONP. Википедия — один из таких серверов.
Этот ответ на Stack Overflow охватывает многие детали JSONP.
Поиск в Википедии
Вот простой поиск, который показывает предложения из Википедии по мере ввода текста в текстовом поле:
Википедия предлагает современный CORS API и устаревший JSONP API поиска. В этом примере используется последний.
Сервис Angular Jsonp расширяет Http сервис для JSONP и ограничивает вас GET запросами. Все остальные HTTP-методы генерируют ошибку, так как JSONP — это средство только для чтения.
Как всегда, оберните взаимодействие с клиентом доступа к данным Angular внутри специализированного сервиса, здесь названного WikipediaService.
src/app/wiki/wikipedia.service.ts
import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
let params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
// TODO: Add error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(response => <string[]> response.json()[1]);
}
}
Конструктор ожидает, что Angular внедрит его Jsonp сервис, который доступен, потому что JsonpModule находится в корневом @NgModule массиве imports в app.module.ts.
Параметры запроса
API Википедии "opensearch" ожидает четыре параметра (ключевых пары) в строке запроса URL-адреса. Ключи — search, action, format, и callback. Значение ключа search — это введённый пользователем поисковой запрос для поиска в Википедии. Три других параметра — это фиксированные значения "opensearch", "json" и "JSONP_CALLBACK" соответственно.
Техника
JSONPтребует, чтобы вы передали имя функции обратного вызова на сервер в строке запроса:callback=JSONP_CALLBACK. Сервер использует это имя для построения функции JavaScript-обёртки в своём ответе, которую Angular в конечном счёте вызовет для извлечения данных. Всё это происходит под капотом.
Если вы ищете статьи со словом "Angular", вы можете вручную создать строку запроса и вызвать jsonp так:
let queryString =
`?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`;
return this.jsonp
.get(wikiUrl + queryString)
.map(response => <string[]> response.json()[1]);
В более параметризованных примерах вы можете создать строку запроса с помощью помощника Angular URLSearchParams:
src/app/wiki/wikipedia.service.ts (параметры поиска)
let params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
В этот раз вы вызываете jsonp с двумя аргументами: wikiUrl и объектом параметров, чьё свойство search — объект params.
src/app/wiki/wikipedia.service.ts (вызов jsonp)
// TODO: Add error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(response => <string[]> response.json()[1]);
Jsonp сглаживает объект params в ту же строку запроса, которую вы видели ранее, отправляя запрос на сервер.
WikiComponent
Теперь, когда у вас есть сервис, который может выполнять запросы к API Википедии, обратите внимание на компонент (шаблон и класс), который получает пользовательский ввод и отображает результаты поиска.
src/app/wiki/wiki.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki',
template: `
<h1>Wikipedia Demo</h1>
<p>Search after each keystroke</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiComponent {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) { }
search (term: string) {
this.items = this.wikipediaService.search(term);
}
}
Шаблон представляет элемент <input> поисковой строки для сбора поисковых запросов от пользователя и вызывает метод search(term) после каждого события keyup.
Метод search(term) компонента делегирует задачу сервису WikipediaService, который возвращает массив Observable строк результатов (Observable<string[]>). Вместо подписки на Observable внутри компонента, как в HeroListComponent, приложение передаёт результат Observable в шаблон (через items), где пайп async в ngFor обрабатывает подписку. Подробнее о пайпах async см. на странице Пайпы.
Пайп async — хороший выбор в компонентах только для чтения, где компонент не нуждается во взаимодействии с данными.
HeroListComponentне может использовать пайп, потому чтоaddHero()добавляет недавно созданных героев в список.
Неэффективное приложение
Поиск в Википедии совершает слишком много обращений к серверу. Это неэффективно и потенциально дорого на мобильных устройствах с ограниченным объёмом данных.
1. Ожидание, пока пользователь перестанет вводить текст
В настоящее время код вызывает сервер после каждого нажатия клавиши. Он должен выполнять запросы только тогда, когда пользователь прекращает ввод. Вот как это будет работать после рефакторинга:
2. Поиск при изменении поискового запроса
Предположим, пользователь вводит слово angular в поле поиска и делает паузу. Приложение отправляет запрос на поиск angular.
Затем пользователь удаляет последние три буквы, lar, и сразу же вводит lar, прежде чем сделать ещё одну паузу. Поисковой запрос всё ещё angular. Приложение не должно отправлять ещё один запрос.
3. Обработка ответов, пришедших вне очереди
Пользователь вводит angular, делает паузу, очищает поле поиска и вводит http. Приложение отправляет два запроса на поиск, один для angular и один для http.
Какой ответ придёт первым? Это непредсказуемо. Когда несколько запросов находятся в очереди, приложение должно представить ответы в первоначальном порядке запросов. В этом примере приложение всегда должно отображать результаты поиска http независимо от того, какой ответ придёт первым.
Ещё больше возможностей с Observable
Вы можете внести изменения в WikipediaService, но для лучшего пользовательского опыта создайте копию WikiComponent и сделайте её умнее с помощью некоторых полезных операторов Observable.
Вот WikiSmartComponent, показанный рядом с оригинальным WikiComponent:
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';
import { Subject } from 'rxjs/Subject';
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki-smart',
template: `
<h1>Smarter Wikipedia Demo</h1>
<p>Search when typing stops</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiSmartComponent implements OnInit {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) {}
private searchTermStream = new Subject<string>();
search(term: string) { this.searchTermStream.next(term); }
ngOnInit() {
this.items = this.searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term: string) => this.wikipediaService.search(term));
}
}
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki',
template: `
<h1>Wikipedia Demo</h1>
<p>Search after each keystroke</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiComponent {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) { }
search (term: string) {
this.items = this.wikipediaService.search(term);
}
}
Хотя шаблоны почти идентичны, в «умной» версии используется гораздо больше RxJS, начиная с операторов debounceTime, distinctUntilChanged, и switchMap, импортированных, как описано выше.
Создание потока поисковых запросов
WikiComponent передаёт новый поисковой запрос напрямую в WikipediaService после каждого нажатия клавиши.
Класс WikiSmartComponent преобразует нажатия клавиш пользователя в Observable — поток поисковых запросов с помощью Subject, который вы импортируете из RxJS:
import { Subject } from 'rxjs/Subject';
Компонент создаёт searchTermStream как Subject типа string. Метод search() добавляет каждое новое значение поля поиска в этот поток через метод next() субъекта.
private searchTermStream = new Subject<string>();
search(term: string) { this.searchTermStream.next(term); }
Прослушивание поисковых запросов
WikiSmartComponent прослушивает поток поисковых запросов и обрабатывает этот поток перед вызовом сервиса.
this.items = this.searchTermStream .debounceTime(300) .distinctUntilChanged() .switchMap((term: string) => this.wikipediaService.search(term));
-
debounceTime ожидает, пока пользователь перестанет вводить текст в течение как минимум 300 миллисекунд.
-
distinctUntilChanged гарантирует, что сервис вызывается только тогда, когда новый поисковой запрос отличается от предыдущего.
-
Оператор switchMap вызывает
WikipediaServiceс новым отфильтрованным поисковым запросом и координирует поток(и) ответа сервиса.
Роль switchMap особенно важна. WikipediaService возвращает отдельный Observable массивов строк (Observable<string[]>) для каждого запроса на поиск. Пользователь может отправить несколько запросов, прежде чем медленный сервер успеет ответить, что означает, что в клиент могут прибыть очереди ответов-Observable в любой момент, в любом порядке.
switchMap возвращает собственный Observable, который объединяет все WikipediaService ответ-Observable, переупорядочивает их в исходном порядке запросов и доставляет подписчикам только самые последние результаты поиска.
Защита от подделки межсайтовых запросов
При подделке межсайтового запроса (CSRF или XSRF) злоумышленник обманывает пользователя, заставляя его посетить другую веб-страницу со вредоносным кодом, который тайно отправляет вредоносный запрос на веб-сервер вашего приложения.
Сервер и клиентское приложение должны работать вместе, чтобы предотвратить эту атаку. Клиент Angular Http выполняет свою часть, автоматически применяя стандартную CookieXSRFStrategy ко всем запросам.
CookieXSRFStrategy поддерживает распространённый метод борьбы с XSRF, в котором сервер отправляет случайно сгенерированный токен аутентификации в cookie с именем XSRF-TOKEN. Клиент HTTP добавляет заголовок X-XSRF-TOKEN со значением этого токена к последующим запросам. Сервер получает как cookie, так и заголовок, сравнивает их и обрабатывает запрос только в том случае, если cookie и заголовок совпадают.
Дополнительную информацию о XSRF и мерах противодействия XSRF в Angular см. в разделе «XSRF» на странице «Безопасность».
Переопределение стандартных параметров запроса
Параметры запроса (например, заголовки) объединяются в стандартные RequestOptions перед обработкой запроса. HttpModule предоставляет эти стандартные параметры через маркер RequestOptions.
Вы можете переопределить эти значения для своих потребностей, создав пользовательский подкласс RequestOptions, который устанавливает стандартные параметры для приложения.
В этом примере создаётся класс, который устанавливает стандартный заголовок Content-Type в JSON. Он экспортирует константу с необходимым провайдером RequestOptions для упрощения регистрации в AppModule.
src/app/default-request-options.service.ts
import { Injectable } from '@angular/core';
import { BaseRequestOptions, RequestOptions } from '@angular/http';
@Injectable()
export class DefaultRequestOptions extends BaseRequestOptions {
constructor() {
super();
// Set the default 'Content-Type' header
this.headers.set('Content-Type', 'application/json');
}
}
export const requestOptionsProvider = { provide: RequestOptions, useClass: DefaultRequestOptions };
Затем он регистрирует провайдера в корневом AppModule.
src/app/app.module.ts (provide default request header)
providers: [ requestOptionsProvider ],
Не забудьте включить этого поставщика во время настройки при тестировании приложений HTTP-сервисов.
После этого изменения настройка header в HeroService.create() больше не требуется.
src/app/toh/hero.service.ts (create)
create(name: string): Observable<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.map(this.extractData)
.catch(this.handleError);
}
Вы можете убедиться, что DefaultRequestOptions работает, проверив HTTP-запросы в панели разработчика браузера (вкладка «Сеть»). Если вы обходите вызов сервера, например, с помощью in-memory web api, попробуйте прокомментировать опцию заголовка create, установить точку останова на вызове POST и проследить обработку запроса, чтобы убедиться, что заголовок присутствует.
Индивидуальные параметры запросов, такие как этот, имеют приоритет над значениями по умолчанию RequestOptions. Возможно, будет разумно сохранить заголовок запроса create для дополнительной безопасности.
Приложение: Tour of Heroes in-memory web api
Если приложению нужно только извлечь данные, вы можете получить героев из файла heroes.json:
src/app/heroes.json
{
"data": [
{ "id": 1, "name": "Windstorm" },
{ "id": 2, "name": "Bombasto" },
{ "id": 3, "name": "Magneta" },
{ "id": 4, "name": "Tornado" }
]
}
Вы заключаете массив heroes в объект со свойством
dataпо той же причине, что и сервер данных: для снижения рисков безопасности, связанных с JSON-массивами верхнего уровня. Подробнее об этом риске.
Вы установите конечную точку для JSON-файла следующим образом:
src/app/toh/hero.service.ts
private heroesUrl = 'app/heroes.json'; // URL to JSON file
Сценарий получить героев будет работать, но поскольку приложение не может сохранять изменения в JSON-файле, ему нужен веб-API-сервер. Поскольку для этой демонстрации реального сервера нет, используется симулятор Angular in-memory web api вместо реального сервиса XHR.
In-memory web api не является частью основного Angular. Это необязательный сервис в собственной библиотеке angular-in-memory-web-api, установленной с помощью npm (см.
package.json).См. файл README для параметров конфигурации, поведения по умолчанию и ограничений.
In-memory web API получает данные из пользовательского класса приложения со свойством createDb(), которое возвращает карту, ключи которой — имена коллекций, а значения — массивы объектов в этих коллекциях.
Вот класс для этого примера, основанный на JSON-данных:
src/app/hero-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class HeroData implements InMemoryDbService {
createDb() {
let heroes = [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
];
return {heroes};
}
}
Убедитесь, что конечная точка HeroService указывает на веб-API:
src/app/toh/hero.service.ts
private heroesUrl = 'api/heroes'; // URL to web API
Наконец, перенаправьте запросы клиентов HTTP на in-memory web API, добавив InMemoryWebApiModule в список AppModule.imports. Одновременно вызовите метод конфигурации forRoot() с классом HeroData.
src/app/app.module.ts
InMemoryWebApiModule.forRoot(HeroData)
Как это работает
Сервис http Angular делегирует задачи обмена данными клиент/сервер вспомогательному сервису под названием XHRBackend.
Используя стандартные методы регистрации поставщиков Angular, InMemoryWebApiModule заменяет стандартный сервис XHRBackend на собственную альтернативу in-memory. Одновременно метод forRoot инициализирует in-memory web API с данными из шаблона данных героев.
Имя метода
forRoot()служит сильным напоминанием о том, что вы должны вызыватьInMemoryWebApiModuleтолько один раз при установке метаданных для корневогоAppModule. Не вызывайте его повторно.
Вот окончательная, переработанная версия src/app/app.module.ts, демонстрирующая эти шаги.
src/app/app.module.ts (excerpt)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { HeroData } from './hero-data';
import { requestOptionsProvider } from './default-request-options.service';
import { AppComponent } from './app.component';
import { HeroListComponent } from './toh/hero-list.component';
import { HeroListPromiseComponent } from './toh/hero-list.component.promise';
import { WikiComponent } from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule,
InMemoryWebApiModule.forRoot(HeroData)
],
declarations: [
AppComponent,
HeroListComponent,
HeroListPromiseComponent,
WikiComponent,
WikiSmartComponent
],
providers: [ requestOptionsProvider ],
bootstrap: [ AppComponent ]
})
export class AppModule {}
Импортируйте
InMemoryWebApiModuleпослеHttpModule, чтобы убедиться, что поставщикXHRBackendInMemoryWebApiModuleимеет приоритет над всеми другими.
См. полный исходный код в примере.
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/guide/server-communication.html