Статьи

Angular Observables and Promises — как их использовать

Angular, как мы все знаем, это JavaScript-фреймворк, который облегчает создание веб-приложений. RxJS действительно сводит принципы функционального программирования к JavaScript.

Функциональное программирование в JavaScript?

Все началось с нарастания асинхронности или асинхронного JavaScript,  когда у нас есть данные, которые проходят через наше приложение из разных источников, например, API, веб-сокетов и событий и таймеров, инициируемых пользователем. У нас есть ситуация, когда мы не можем контролировать, когда какие-либо из этих данных отправляются через наше приложение. Мы также не можем определить порядок, в котором это происходит, поэтому наличие более функциональной парадигмы поможет решить эту проблему. Вы хотите, чтобы ваше приложение было более предсказуемым, но масштабирование приложений, выполняющих клиентскую часть, означает меньшую предсказуемость.

Затем наступает рост Обещаний, которые должны решать проблемы, которые у всех нас были с XHR, которые спасли нас от ада обратного вызова. Вы обнаружите, что чем больше вы используете Promises с их функциями then и catch, тем больше они снова выглядят как обратные вызовы.

Немного проблем с обещаниями 

  • Обещание определяется там, где создаются данные, а не там, где они используются. 
  • По мере того как ваше приложение становится больше, с Promises становится сложно управлять 
  • Что если я хочу повторить неудачный звонок? Теперь мы снова вернулись в адский колбэк.

Затем появились наблюдаемые

RxJS — это объединение идей Promises, обратных вызовов и потока данных и упрощение работы с ними.

Observable — это массив или последовательность событий во времени. У него есть как минимум два участника. Создатель (источник данных) и подписчик (подписка — где используются данные).

Например, пусть data = http.get (‘/ api.json’). В Angular данные будут Observable ответов, потому что метод HTTP.get возвращает Promise.

Поиск в YouTube API с использованием Angular и Observable

Чтобы продемонстрировать возможности Observable с Angular, мы создадим пример проекта, запрашивающего API YouTube. 

Создать новое угловое приложение

Откройте Angular IDE , нажмите «Файл» в верхнем меню, затем выберите «Новый», затем нажмите «Angular Project».

Название изображения

Это приведет нас к странице, где мы можем указать детали проекта, такие как имя, версия Angular-CLI и Node для использования. Теперь введите в youtube-searcherкачестве имени проекта 1.6.8 в качестве версии Angular CLI, 9.5.0 в качестве версии Node.js и 5.6.0 в качестве версии npm. Нажмите Далее, затем нажмите Готово.

Название изображения

Получить ключ API в консоли Google

Чтобы взаимодействовать с API YouTube, нам нужно получить ключ API из консоли Google, поэтому перейдите по этой ссылке и следуйте инструкциям, чтобы получить ключ API . Вам нужно будет создать приложение, чтобы получить для него учетные данные API. Это позволит нашему приложению отправлять запросы API в API YouTube. 

Название изображения

Нажмите, Create credentialsчтобы создать учетные данные для вашего приложения. 
Название изображенияПосле получения ключа API вернитесь к своему проекту в угловой среде IDE и создайте srcфайл TypeScript в папке приложения.

Название изображения

Вы можете назвать файл  api-key.ts, и это будет его содержимое:

const API_KEY = 'your-api-key-here'; export default API_KEY;

После этого вы импортируете ключ API в  app.component.tsфайл, в который мы напишем код для запроса API YouTube. Делай это так:

import API_KEY from './api-key';

Запросите API YouTube

API YouTube не возвращает прямой список результатов, а представляет собой группу метаданных, в которой есть одно свойство с именем Items, которое содержит список результатов. 

Далее, давайте создадим нашу форму в файле src / app / app.component.ts. Добавьте следующий код:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import API_KEY from './api-key';

const API_URL = 'https://www.googleapis.com/youtube/v3/search';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'Angular RxJs YouTube Searcher';
  searchForm: FormGroup;
  results: any;

  constructor(private formBuilder: FormBuilder, private http: HttpClient) {
    this.searchForm = this.formBuilder.group({
      search: ['', Validators.required]
    });

    this.searchForm.controls.search.valueChanges.subscribe(result => console.log(result));
  }
} 

Форма ввода поиска использует API угловых реактивных форм. Узнайте больше о реактивных формах здесь.

Поскольку valueChangesметод возвращает Observable, здесь в нашем примере он возвращает Observable символов, введенных во входном элементе. Вызов функции подписки подписывается на каждое значение, сохраняет их в переменной результата и отображает их в консоли браузера с console.log.

Мы также используем выражение функции стрелки ( =>) при вызове подписки, синтаксис которого короче, чем у выражения функции. Это очень эффективно, когда вы хотите написать короткие функции, такие как тип, который вы бы использовали в методе подписки. Узнайте больше о функции стрелки здесь. 

Использование API Angular Reactive Form

Чтобы начать использовать Reactive Forms API и HTTP-клиент, нам нужно импортировать модули в  app.module.ts файл, поэтому обновите модуль следующим импортом и добавьте модули в массив импорта:

 import { ReactiveFormsModule } from '@angular/forms';
    import { HttpClientModule } from '@angular/common/http';

    .....

    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule
    ],

    ......

И в файле шаблона src / app / app.component.html добавьте следующий код:

<h3>{{ title }}</h3>

<form [formGroup]="searchForm">
<label for="search">Search YouTube</label>
<br />
<input formControlName="search" id="search" />
</form>

Также обратите внимание, что searchFormи search и search соответствуют именам, указанным в шаблоне, потому что именно так Angular знает, как подключить HTML-код шаблона к коду компонента TypeScript.

Запуск приложения Angular

Давайте запустим приложение Angular через представление сервера в Angular IDE. Чтобы открыть его, выберите «Окно», затем «Показать представление», затем «Серверы». Щелкните правой кнопкой мыши  youtube-searcherи выберите «Запустить сервер». Angular IDE по умолчанию обслуживает приложение на локальном порте 4200, поэтому откройте в браузере, чтобы увидеть запущенное приложение. 

Название изображения

Мы ничего не будем делать с нашим поиском, сейчас нам просто нужно убедиться, что форма работает. Чтобы открыть консоль, щелкните правой кнопкой мыши в любом месте страницы браузера, нажмите «Проверить» и выберите вкладку «Консоль».

Название изображения

Как видите, все, что вводится в форму ввода, выходит из системы — это первый пример, показывающий, как Angular использует мощь метода Observable.

Затем нам нужно передать это значение и использовать его для запроса результатов в API YouTube. Давайте сделаем это дальше. Обновите раздел поиска в файле, чтобы он выглядел следующим образом (мы укажем console.log значение, которое мы получаем из API YouTube).

                            this.searchForm.controls.search.valueChanges.subscribe(searchTerm => {
      this.http.get<any>(
         `${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)
          .subscribe(result => {
        console.log(result);
   });
});

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

Название изображения

Улучшение и рефакторинг кода

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

Во-первых, давайте переместим всю функцию поиска в отдельный метод. Давайте назовем это search. Затем мы будем вызывать его внутри,  ngOnInit а не в конструкторе, как мы сейчас делаем. В больших приложениях Angular плохой практикой является вызов функций, особенно функций Angular, внутри конструктора. Вы не хотите вызывать какие-либо ваши функции Angular внутри конструктора, потому что Angular не имеет никакого контроля над тем, когда конструктор вызывается или инициируется при загрузке страницы. Вы можете прочитать больше здесь о разнице между ngOnInit и конструктором.

После рефакторинга кода результат выглядит следующим образом. Обратите внимание на импорт  OnInitиз  @angular/coreстроки 1 и его реализацию в строке 20. После этого обновления поведение вашего приложения должно оставаться прежним. Давай и попробуй.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import API_KEY from './api-key';

const API_URL = 'https://www.googleapis.com/youtube/v3/search';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'Angular RxJs YouTube Searcher';
  searchForm: FormGroup;
  results: any;

  constructor(private formBuilder: FormBuilder, private http: HttpClient) {
    this.searchForm = this.formBuilder.group({
      search: ['', Validators.required]
    });
  }

  ngOnInit() {
    this.search();
  }

  search() {
     this.searchForm.controls.search.valueChanges.subscribe(searchTerm => {
      this.http.get<any>(
         `${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)
         .subscribe(result => {
        console.log(result);
      });
    });
  }
}

Итак, возвращаясь к получению желаемого результата, наша первая проблема связана с вложенной подпиской, которая часто встречается при использовании Promises. Этот вид вложения называется пирамидой гибели , что является одной из основных причин, по которой мы не хотим использовать * a * Promise. Мы * хотим * избежать этого любой ценой *, и для этого Observable предоставляет оператор Pipeable Operator, который позволяет вам * делать * эту вложенность операций более эффективным способом.

Таким образом, мы проведем рефакторинг нашего кода, чтобы использовать канал. Первый оператор, который мы будем использовать, это switchMapоператор.

Понимание операторов в RxJS

switchMap отменит предыдущее событие и просто использует значение в последнем событии. При  switchMapкаждом возврате нового Observable он отписывается от предыдущего и подписывается на последний. По сути, он переключается с одного Observable на последнее Observable, в то время как  mergeMapвсе Observable исчезнут (он не отменит старые — здесь вы можете получить условия гонки, что приведет к странному поведению, когда результат первое событие может быть возвращено вместо желаемого). Узнайте больше об условиях гонки и о различных типах наблюдаемых карт .

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

import { switchMap } from 'rxjs/operators';
search() {
     this.searchForm.controls.search.valueChanges.pipe(
      switchMap(searchTerm => this.http.get<any>(
        `${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`))
     ).subscribe(result => console.log(result));
}

Но что-то все еще кажется выключенным. Если вы работаете в быстрой сети, вы заметите, что по крайней мере два объекта все еще возвращаются, что означает, что подписка не дождалась, пока вы закончили вводить запрос, прежде чем запрос был отправлен. Мы можем немного отложить отправку запроса, добавив еще один оператор, называемый debounceTime , действительно полезный оператор, который будет отбрасывать события. Он будет отбрасывать излучаемые значения, которые занимают меньше указанного времени между выходами.

import { switchMap, debounceTime } from 'rxjs/operators';
search() {
     this.searchForm.controls.search.valueChanges.pipe(
      debounceTime(500),
      switchMap(searchTerm => this.http.get<any>(
        `${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`))
     ).subscribe(result => console.log(result));
}

Поэтому switchMapотменил предыдущий запрос, но только на уровне браузера. Как только эти сетевые запросы отправляются, они все еще попадают на сервер, что означает, что нежелательный запрос все еще может быть отправлен. Теперь debounce заставит запрос ждать 500 миллисекунд, пока события не будут запущены (пока запросы не будут отправлены на сервер) — события не будут отправлены на сервер, пока вы не закончите вводить запрос. Это означает, что только один вызов API / запрос пойдет на сервер.

Мы также можем добавить другие операторы, такие как filter и differentUntilChanged . Фильтр испускает только те элементы из Observable, которые проходят предикатный тест, но distinctUntilChangedтолько тогда, когда текущее значение отличается от последнего.

Мы также должны применить оператор map для отображения значений, возвращаемых из API, потому что запрос возвращает большой объект с другими метаданными, и единственный объект, который нам нужен, это объект Items.

Окончательный код выглядит так:

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import API_KEY from './api-key';

const API_URL = 'https://www.googleapis.com/youtube/v3/search';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'Angular RxJs YouTube Searcher';
  searchForm: FormGroup;
  results: Observable<any>;

  constructor(private formBuilder: FormBuilder, private http: HttpClient) {
    this.searchForm = this.formBuilder.group({
      search: ['', Validators.required]
    });
  }

  ngOnInit() {
    this.search();
  }

  search() {
     this.results = this.searchForm.controls.search.valueChanges.pipe(
       debounceTime(500),
       filter(value => value.length > 3),
       distinctUntilChanged(),
       switchMap(searchTerm => this.http.get<any>(`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)),
       map(response => response.items)
     );
  }
}

HTTP-метод Angular возвращает Observable, а не Promise. Затем на эту Observable необходимо подписаться, чтобы ее можно было использовать. С помощью оператора pipeable в подписке мы можем преобразовать возвращаемую подписку, и теперь нам нужно использовать асинхронный канал, чтобы использовать подписку на стороне шаблона.

Чтобы сделать Angular и RxJS лучше вместе, Angular создал асинхронный канал, используемый в шаблоне.

While it’s possible to do a standard subscription on the component side, it can also be done in the declarative template style, which is generally recommended by Angular. Async will automatically subscribe to the Observable for you, and it will automatically unsubscribe for you as well when you navigate out of the page or component.

And in the HTML template we have this:

<h3>{{ title }}</h3>

<form [formGroup]="searchForm">
<label for="search">Search YouTube</label>
<br />
<input formControlName="search" id="search" />
<div *ngFor="let result of results | async">
<a [href]="'https://www.youtube.com/watch?v=' + result.id.videoId" target="_blank">
{{result.snippet.title}}
</a>
<p>{{result.snippet.description}}</p>
<img [src]="result.snippet.thumbnails.default.url" style="width: 100%; max-width: 250px;" />
</div>
</form>

What the async pipe does depends on whether you give it a Promise or an Observable. It creates and unwraps the subscription or Promise, and displays the data when the component is loaded, when the template is running, and then automatically unloads and unsubscribes when the component is unloaded, for example when you navigate to another page with a new component. This manages the whole life cycle of subscription to Observables, so you don’t have to be managing any of that yourself.

Your result should now look like this:
Название изображения

Points to take note of:

  • With a Promise you can only handle one event.
  • With an Observable you can handle multiple events.
  • .subscribe() is similar to .then(). 
  • An Observable can do everything that a Promise can do, plus more.
  • Use Angular’s HttpClient to handle API calls.
  • The Angular framework uses a lot of RxJS.
  • Many Angular APIs use Observables – we can take advantage of this to build some really cool stuff.

Conclusion

RxJS is a really important part of your Angular toolbox – even though it doesn’t solve every problem, it will definitely make you a better Angular developer. Asynchronous experiences are the norm for today’s websites. With the combined power of Angular and RxJS, you’re well on your way to delivering this experience to your users too.