Получение данных с сервера¶
28.02.2022
В этом руководстве добавлены следующие возможности сохранения данных с помощью HttpClient
от Angular.
- Сервис
HeroService
получает данные о героях с помощью HTTP-запросов. - Пользователи могут добавлять, редактировать и удалять героев и сохранять эти изменения по HTTP
- Пользователи могут искать героев по имени
Пример приложения, которое описывается на этой странице, см.:
Включите HTTP-сервисы¶
HttpClient
— это механизм Angular для связи с удаленным сервером по HTTP.
Сделайте HttpClient
доступным везде в приложении в два шага. Во-первых, добавьте его в корневой AppModule
, импортировав его:
1 |
|
Далее, все еще в AppModule
, добавьте HttpClientModule
в массив imports
:
1 2 3 4 5 |
|
Имитация сервера данных¶
Этот учебный пример имитирует связь с удаленным сервером данных с помощью модуля In-memory Web API.
После установки модуля приложение делает запросы к HttpClient
и получает ответы от него. Приложение не знает, что In-memory Web API перехватывает эти запросы, применяет их к хранилищу данных в памяти и возвращает имитированные ответы.
Используя In-memory Web API, вам не придется настраивать сервер, чтобы узнать о HttpClient
.
Модуль In-memory Web API не имеет никакого отношения к HTTP в Angular.
Если вы читаете этот учебник, чтобы узнать о HttpClient
, то можете пропустить этот шаг. Если же вы кодите вместе с этим руководством, оставайтесь здесь и добавьте In-memory Web API прямо сейчас.
Установите пакет In-memory Web API из npm с помощью следующей команды:
1 |
|
Сгенерируйте класс src/app/in-memory-data.service.ts
с помощью следующей команды:
1 |
|
Замените стандартное содержимое in-memory-data.service.ts
на следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
В AppModule
импортируйте HttpClientInMemoryWebApiModule
и класс InMemoryDataService
, который вы создадите следующим.
1 2 |
|
После HttpClientModule
добавьте HttpClientInMemoryWebApiModule
в массив импортов
AppModule
и сконфигурируйте его с InMemoryDataService
.
1 2 3 4 5 6 7 8 |
|
Метод конфигурации forRoot()
принимает класс InMemoryDataService
, который заправляет базу данных in-memory.
Файл in-memory-data.service.ts
берет на себя функцию mock-heroes.ts
. Пока не удаляйте mock-heroes.ts
. Он еще понадобится вам для нескольких шагов этого руководства.
После того как сервер будет готов, отсоедините In-memory Web API, чтобы запросы приложения могли проходить через сервер.
Герои и HTTP¶
В HeroService
импортируйте HttpClient
и HttpHeaders
:
1 2 3 4 |
|
Все еще в HeroService
, инжектируйте HttpClient
в конструктор в частное свойство http
.
1 2 3 |
|
Обратите внимание, что вы продолжаете инжектировать MessageService
, но поскольку ваше приложение вызывает его так часто, оберните его в частный метод log()
:
1 2 3 4 |
|
Определите heroesUrl
вида :base/:collectionName
с адресом ресурса heroes на сервере. Здесь base
— это ресурс, к которому делаются запросы, а collectionName
— это объект данных heroes в in-memory-data-service.ts
.
1 |
|
Получение героев с помощью HttpClient
¶
Текущий HeroService.getHeroes()
использует функцию RxJS of()
для возврата массива подражаемых героев в виде Observable<Hero[]>
.
1 2 3 4 |
|
Преобразуйте этот метод для использования HttpClient
следующим образом:
1 2 3 4 |
|
Обновите браузер. Данные о героях должны успешно загрузиться с имитационного сервера.
Вы поменяли of()
на http.get()
, и приложение продолжает работать без каких-либо других изменений, потому что обе функции возвращают Observable<Hero[]>
.
Методы HttpClient
возвращают одно значение¶
Все методы HttpClient
возвращают RxJS Observable
чего-либо.
HTTP — это протокол запроса/ответа. Вы делаете запрос, он возвращает один ответ.
В общем, наблюдаемая может возвращать более одного значения в течение времени. Наблюдаемая HttpClient
всегда выдает одно значение и затем завершается, чтобы больше никогда не выдавать его.
Этот конкретный вызов HttpClient.get()
возвращает Observable<Hero[]>
, который является наблюдаемым массивом героев. На практике он возвращает только один массив героев.
HttpClient.get()
возвращает данные ответа¶
По умолчанию HttpClient.get()
возвращает тело ответа в виде нетипизированного объекта JSON. Применение дополнительного спецификатора типа, <Hero[]>
, добавляет возможности TypeScript, которые уменьшают количество ошибок во время компиляции.
API данных сервера определяет форму данных JSON. API данных Tour of Heroes возвращает данные о героях в виде массива.
В других API нужные вам данные могут быть спрятаны внутри объекта. Возможно, вам придется выкапывать эти данные, обрабатывая результат Observable
с помощью оператора RxJS map()
.
Хотя здесь это не рассматривается, пример использования map()
есть в методе getHeroNo404()
, включенном в исходный код примера.
Обработка ошибок¶
Все идет не так, особенно когда вы получаете данные с удаленного сервера. Метод HeroService.getHeroes()
должен отлавливать ошибки и делать что-то соответствующее.
Чтобы отлавливать ошибки, вы "передаете" наблюдаемый результат из http.get()
через оператор RxJS catchError()
.
Импортируйте символ catchError
из rxjs/operators
, а также некоторые другие операторы, которые будут использоваться позже.
1 |
|
Теперь расширьте наблюдаемый результат методом pipe()
и дайте ему оператор catchError()
.
1 2 3 4 5 6 |
|
Оператор catchError()
перехватывает `Observable``, который потерпел неудачу. Затем оператор передает ошибку в функцию обработки ошибок.
Следующий метод handleError()
сообщает об ошибке, а затем возвращает безобидный результат, чтобы приложение продолжало работать.
handleError
¶
Следующий метод handleError()
может быть общим для многих методов HeroService
, поэтому он обобщен для удовлетворения их различных потребностей.
Вместо того, чтобы обрабатывать ошибку напрямую, она возвращает функцию обработчика ошибок catchError
. Эта функция настраивается как на имя операции, которая завершилась неудачей, так и на безопасное возвращаемое значение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
После сообщения об ошибке в консоль обработчик строит дружественное сообщение и возвращает безопасное значение, чтобы приложение могло продолжать работу.
Поскольку каждый метод сервиса возвращает результат типа Observable
, handleError()
принимает параметр типа, чтобы вернуть безопасное значение в том виде, который ожидает приложение.
Tap into the Observable¶
Метод getHeros()
подключается к потоку наблюдаемых значений и отправляет сообщение, используя метод log()
, в область сообщений в нижней части страницы.
Оператор RxJS tap()
обеспечивает эту возможность, просматривая наблюдаемые значения, делая что-то с этими значениями и передавая их. Обратный вызов tap()
не обращается к самим значениям.
Вот окончательная версия getHeroes()
с tap()
, который регистрирует операцию.
1 2 3 4 5 6 7 8 |
|
Получение героя по идентификатору¶
Большинство веб-интерфейсов поддерживают запрос get by id в форме :baseURL/:id
.
Здесь базовый URL — это heroesURL
, определенный в разделе Heroes and HTTP в api/heroes
, а id — это номер героя, который вы хотите получить. Например, api/heroes/11
.
Обновите метод HeroService
getHero()
следующим образом, чтобы сделать этот запрос:
1 2 3 4 5 6 7 8 |
|
getHero()
имеет три существенных отличия от getHeroes()
:
getHero()
конструирует URL запроса с идентификатором нужного героя.- Сервер должен ответить одним героем, а не массивом героев
getHero()
возвращаетObservable<Hero>
, который является наблюдаемым изHero
объектов, а не наблюдаемым изHero
массивов.
Обновление героев¶
Редактирование имени героя в представлении подробной информации о герое. По мере ввода имя героя обновляет заголовок в верхней части страницы, однако когда вы нажимаете Назад, ваши изменения теряются.
Если вы хотите, чтобы изменения сохранялись, вы должны записать их обратно на сервер.
В конце шаблона детализации героя добавьте кнопку сохранения с привязкой события click
, которая вызывает новый метод компонента с именем save()
.
1 |
|
В класс компонента HeroDetail
добавьте следующий метод save()
, который сохраняет изменения имени героя с помощью метода сервиса hero updateHero()
и затем переходит к предыдущему представлению.
1 2 3 4 5 6 |
|
Добавьте HeroService.updateHero()
¶
Структура метода updateHero()
похожа на структуру метода getHeroes()
, но он использует http.put()
для сохранения измененного героя на сервере. Добавьте следующее в HeroService
.
1 2 3 4 5 6 7 |
|
Метод HttpClient.put()
принимает три параметра:
- URL
- Данные для обновления, которые в данном случае являются измененным героем
- Параметры
URL остается неизменным. Веб-интерфейс heroes знает, какого героя нужно обновить, глядя на id
героя.
Веб-интерфейс heroes ожидает специальный заголовок в HTTP-запросах на сохранение. Этот заголовок находится в константе httpOptions
, определенной в HeroService
.
Добавьте следующее в класс HeroService
.
1 2 3 4 5 |
|
Обновите браузер, измените имя героя и сохраните изменения. Метод save()
в HeroDetailComponent
осуществляет переход к предыдущему виду.
Теперь герой отображается в списке с измененным именем.
Добавление нового героя¶
Для добавления героя этому приложению требуется только его имя. Для этого можно использовать элемент <input>
в паре с кнопкой добавления.
Вставьте в шаблон HeroesComponent
после заголовка следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
В ответ на событие щелчка вызовите обработчик щелчка компонента add()
, а затем очистите поле ввода, чтобы оно было готово для другого имени. Добавьте следующее в класс HeroesComponent
:
1 2 3 4 5 6 7 8 |
|
Если заданное имя не является пустым, обработчик создает объект на основе имени героя. Обработчик передает имя объекта методу сервиса addHero()
.
Когда addHero()
создает новый объект, обратный вызов subscribe()
получает нового героя и помещает его в список heroes
для отображения.
Добавьте следующий метод addHero()
в класс HeroService
.
1 2 3 4 5 6 7 |
|
addHero()
отличается от updateHero()
двумя особенностями:
- вызывает
HttpClient.post()
вместоput()
. - ожидает, что сервер создаст идентификатор для нового героя, который он возвращает в
Observable<Hero>
вызывающей стороне.
Обновите браузер и добавьте несколько героев.
Удалить героя¶
Каждый герой в списке героев должен иметь кнопку удаления.
Добавьте в шаблон HeroesComponent
следующий элемент button после имени героя в повторяющемся элементе <li>
.
1 2 3 4 5 6 7 8 |
|
HTML для списка героев должен выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Чтобы расположить кнопку удаления в крайнем правом углу записи героя, добавьте некоторые CSS из финального кода обзора в heroes.component.css
.
Добавьте обработчик delete()
в класс компонента.
1 2 3 4 |
|
Хотя компонент делегирует удаление героев HeroService
, он остается ответственным за обновление своего собственного списка героев. Метод компонента delete()
немедленно удаляет героя, подлежащего удалению из этого списка, ожидая, что HeroService
преуспеет на сервере.
Компоненту действительно нечего делать с Observable
, возвращаемым heroService.deleteHero()
но он должен подписаться в любом случае.
Далее, добавьте метод deleteHero()
к HeroService
следующим образом.
1 2 3 4 5 6 7 8 9 |
|
Обратите внимание на следующие ключевые моменты:
deleteHero()
вызываетHttpClient.delete()
.- URL — это URL ресурса героев плюс
id
героя, которого нужно удалить. - Вы не отправляете данные, как это было с
put()
иpost()
. - Вы по-прежнему отправляете
httpOptions
.
Обновите браузер и попробуйте новую возможность удаления.
Если вы пренебрежете subscribe()
, сервис не сможет отправить запрос на удаление на сервер. Как правило, Observable
ничего не делает, пока на него не подпишутся.
Убедитесь в этом сами, временно удалив subscribe()
, нажав Dashboard, затем Heroes. Это снова покажет полный список героев.
Поиск по имени¶
В последнем упражнении вы научитесь объединять операторы Observable
в цепочки, чтобы уменьшить количество одинаковых HTTP-запросов для экономного расходования пропускной способности сети.
Добавьте функцию поиска героев в приборную панель¶
Когда пользователь вводит имя в поле поиска, ваше приложение делает повторные HTTP-запросы на героев, отфильтрованных по этому имени. Ваша цель — сделать только столько запросов, сколько необходимо.
HeroService.searchHeroes()
¶
Начните с добавления метода searchHeroes()
к HeroService
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Метод возвращает сразу пустой массив, если нет поискового запроса. В остальном он очень похож на getHeroes()
, единственным существенным отличием является URL, который включает строку запроса с поисковым запросом.
Добавление поиска в приборную панель¶
Откройте шаблон DashboardComponent
и добавьте элемент поиска героев, <app-hero-search>
, в нижнюю часть разметки.
1 2 3 4 5 6 7 8 9 10 11 |
|
Этот шаблон очень похож на повторитель *ngFor
в шаблоне HeroesComponent
.
Чтобы это работало, следующим шагом будет добавление компонента с селектором, который соответствует <app-hero-search>
.
Создайте HeroSearchComponent
¶
Запустите ng generate
для создания HeroSearchComponent
.
1 |
|
ng generate
создает три файла HeroSearchComponent
и добавляет компонент в декларации AppModule
.
Замените шаблон HeroSearchComponent
на <input>
и список подходящих результатов поиска, как показано ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Добавьте частные CSS стили в hero-search.component.css
, как указано в финальном обзоре кода ниже.
Когда пользователь набирает текст в поле поиска, привязка события ввода вызывает метод компонента search()
с новым значением поля поиска.
AsyncPipe
¶
Функция *ngFor
повторяет объекты героев. Обратите внимание, что *ngFor
итерирует список heroes$
, а не heroes
.
Символ $
— это соглашение, указывающее, что heroes$
— это Observable
, а не массив.
1 |
|
Поскольку *ngFor
не может ничего сделать с Observable
, используйте символ пайпы |
, за которым следует async
. Это идентифицирует AsyncPipe
от Angular и подписывается на Observable
автоматически, так что вам не придется делать это в классе компонента.
Отредактируйте класс HeroSearchComponent
¶
Замените класс HeroSearchComponent
и метаданные следующим образом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Обратите внимание на объявление heroes$
как Observable
:
1 |
|
Установите это в ngOnInit()
. Прежде чем это сделать, обратите внимание на определение searchTerms
.
Объект searchTerms
RxJS¶
Свойство searchTerms
является RxJS Subject
.
1 2 3 4 5 6 |
|
Объект Subject
является как источником наблюдаемых значений, так и самой Observable
. Вы можете подписаться на Subject
, как и на любую Observable
.
Вы также можете добавлять значения в эту Observable
, вызывая ее метод next(value)
, как это делает метод search()
.
Привязка к событию input
текстового поля вызывает метод search()
.
1 2 3 4 5 |
|
Каждый раз, когда пользователь набирает текст в текстовом поле, привязка вызывает search()
со значением текстового поля в качестве термина поиска. searchTerms
становится Observable
, испускающим постоянный поток поисковых терминов.
Цепочка операторов RxJS¶
Передача нового поискового запроса непосредственно в searchHeroes()
после каждого нажатия клавиши пользователем создает чрезмерное количество HTTP-запросов, что нагружает ресурсы сервера и сжигает тарифные планы.
Вместо этого метод ngOnInit()
передает наблюдаемую searchTerms
через последовательность операторов RxJS, которые уменьшают количество обращений к searchHeroes()
. В конечном итоге, возвращается наблюдаемая таблица результатов поиска своевременных героев, каждый из которых является Hero[]
.
Вот более подробный взгляд на код.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Каждый оператор работает следующим образом:
-
debounceTime(300)
ждет, пока поток новых строковых событий не приостановится на 300 миллисекунд, прежде чем передать последнюю строку.Запросы вряд ли будут происходить чаще, чем 300 мс.
-
Функция
distinctUntilChanged()
гарантирует, что запрос будет отправлен только в том случае, если текст фильтра изменился. -
switchMap()
вызывает службу поиска для каждого поискового термина, который проходит черезdebounce()
иdistinctUntilChanged()
.Он отменяет и отбрасывает предыдущие наблюдения за поиском, возвращая только последнее наблюдение за службой поиска.
С помощью оператора switchMap
каждое определяющее ключевое событие может вызвать вызов метода HttpClient.get()
. Даже с паузой в 300 мс между запросами, у вас может быть много HTTP-запросов в полете, и они могут возвращаться не в том порядке, в котором были отправлены.
Функция switchMap()
сохраняет исходный порядок запросов, возвращая только наблюдаемую из самого последнего вызова метода HTTP. Результаты предыдущих вызовов отменяются и отбрасываются.
Отмена предыдущей наблюдаемой searchHeroes()
на самом деле не отменяет ожидающий HTTP-запрос. Нежелательные результаты отбрасываются до того, как они достигнут кода вашего приложения.
Помните, что компонент class не подписывается на наблюдаемую героев$
. Это работа AsyncPipe
в шаблоне.
Попробуйте¶
Запустите приложение снова. В Панели введите текст в поле поиска.
Введите символы, которые совпадают с любыми существующими именами героев, и ищите что-то вроде этого.
Окончательный обзор кода¶
Вот файлы кода, рассмотренные на этой странице. Они находятся в директории src/app/
.
HeroService
, InMemoryDataService
, AppModule
¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
HeroesComponent
¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
|
HeroDetailComponent
¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
DashboardComponent
¶
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
HeroSearchComponent
¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
Резюме¶
Вы находитесь в конце своего пути и многого достигли.
- Вы добавили необходимые зависимости для использования HTTP в приложении
- Вы рефакторизовали
HeroService
для загрузки героев из веб-API - Вы расширили
HeroService
для поддержки методовpost()
,put()
иdelete()
. - Вы обновили компоненты, чтобы позволить добавлять, редактировать и удалять героев
- Вы настроили веб-интерфейс API in-memory
- Вы узнали, как использовать наблюдаемые объекты
На этом мы завершаем учебник "Тур по героям". Вы готовы узнать больше о разработке Angular в разделе "Основы", начиная с руководства Architecture.