Завершение саги….

С возвращением, товарищи читатели.

В этой заключительной главе саги о RxJS, состоящей из 3 частей, мы погружаемся в область продвинутых методов RxJS, где раскрываем истинный потенциал реактивного программирования. Откройте для себя искусство тестирования, связанное с RxJS, изучите сложные темы и используйте такие операции в контексте Angular.

Чтобы понять, что произошло в части 1 и части 2, пожалуйста, прочитайте предыдущие статьи, упомянутые ниже.



Reactive Ninja: использование RxJS для освоения Angular — часть 1
Все, что вам нужно знать о RxJS в Angularpriyank-bhardwaj.medium.com





Давайте продолжим и подробно рассмотрим остальные темы

Тестирование

Введение в тестирование кода RxJS

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

Одним из полезных инструментов является TestScheduler, который позволяет нам контролировать время и моделировать операции, основанные на времени. Это помогает нам обрабатывать асинхронное поведение и тестировать пограничные случаи, манипулируя виртуальным временем.

При тестировании в RxJS важно учитывать ожидаемые результаты и возможные сценарии ошибок. Использование фреймворков тестирования, таких как Jasmine или Jest, вместе с утилитами тестирования Angular позволяет вам писать комплексные тесты, охватывающие различные сценарии и обеспечивающие надежность кода.

Написание модульных тестов для наблюдаемых и операторов

Тестирование наблюдаемых объектов в RxJS включает проверку выдаваемых значений, завершение и возможные ошибки.

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

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

function fetchData(http: HttpClient): Observable<string[]> {
  return http.get<string[]>('https://api.sample.com/data');
}

Чтобы протестировать этот наблюдаемый объект, мы можем использовать HttpClientTestingModule, предоставляемый утилитами тестирования Angular. Вот пример тестового примера с использованием Jasmine:

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { fetchData } from './data.service';

describe('fetchData', () => {
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });

    httpTestingController = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should fetch data from the API', () => {
    const testData: string[] = ['data1', 'data2', 'data3'];

    fetchData(TestBed.inject(HttpClient)).subscribe((data) => {
      expect(data).toEqual(testData);
    });

    const req = httpTestingController.expectOne('https://api.sample.com/data');
    expect(req.request.method).toBe('GET');

    req.flush(testData);
  });
});

Мы используем HttpClientTestingModule и HttpTestingController Angular для имитации и проверки HTTP-запросов. Подписавшись на функцию fetchData и предоставив фиктивный экземпляр HttpClient, мы можем сравнить сгенерированные данные с нашими тестовыми данными.

Используя httpTestingController, мы проверяем метод запроса и предоставляем имитированные данные ответа, используя метод req.flush. С помощью утилит тестирования Angular и модуля тестирования HTTP мы можем имитировать HTTP-запросы, тестировать наблюдаемые объекты, которые зависят от таких взаимодействий, и обеспечивать правильную выборку данных и обработку ответов.

Использование мраморной диаграммы для тестирования

Мраморное тестирование — это метод тестирования, используемый в RxJS для проверки наблюдаемых последовательностей и операторов. Он обеспечивает визуальный и декларативный способ представления выбросов и времени значений в потоке.

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

--a---b-c---d---|

a, b, c и d представляют значения, испускаемые наблюдаемым, а тире "-" обозначают течение времени. Вертикальные полосы «|» указывают на завершение наблюдаемой.

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

Теперь мы рассмотрим тестирование оператора с помощью мраморной диаграммы.

Операторы тестирования. Для этого мы возьмем пользовательский оператор «filterGraterThan», который отфильтровывает значения, превышающие указанный порог.

import { TestScheduler } from 'rxjs/testing';
import { filterGreaterThan } from './custom-operators';

describe('filterGreaterThan', () => {
  let scheduler: TestScheduler;

  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      // Custom assertion logic
      expect(actual).toEqual(expected);
    });
  });

  it('should filter out values greater than the threshold', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const values = { a: 10, b: 5, c: 15, d: 8 };
      const threshold = 10;
      const source = cold('a-b-c-d|', values);
      const expected = '---b---|';

      const result = source.pipe(filterGreaterThan(threshold));
      expectObservable(result).toBe(expected, values);
    });
  });
});

Расширенные темы

Наблюдаемые более высокого порядка

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

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

import { of, merge } from 'rxjs';

// Individual data source observables
const stockPrices$ = of('Stock prices');
const weatherUpdates$ = of('Weather updates');
const newsFeeds$ = of('News feeds');

// Higher-order observable combining the individual streams
const combined$ = merge(stockPrices$, weatherUpdates$, newsFeeds$);

combined$.subscribe(update => {
  console.log('Received update:', update);
});

Операторы, основанные на времени

Эти операторы позволяют нам управлять синхронизацией выбросов от наблюдаемых на основе временных интервалов. Назвать несколько:

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

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

Мы можем использовать оператор debounceTime, чтобы ввести задержку перед отправкой запроса. Скажем, задержка в 400 миллисекунд после каждого нажатия клавиши перед отправкой последнего значения.
Это гарантирует, что HTTP-запрос будет отправлен только после того, как пользователь закончит печатать или, скажем, сделает паузу в течение короткого периода времени.

import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

// input element with the id 'searchInput' where the user is typing
const searchInput = document.getElementById('searchInput');

fromEvent(searchedInput, 'input')
  .pipe(
    map((event: any) => event.target.value),
    debounceTime(400)
  )
  .subscribe((searchWord: string) => {
    // Make HTTP request or perform search operation
    // using the searchWord
    console.log('Searched Word:', searchWord);
  });

оператор ThrottleTime будет контролировать выбросы от наблюдаемого объекта, устанавливая минимальный временной интервал между последовательными выбросами. В отличие от debounceTime, этот оператор гарантирует, что первое отправленное значение в течение указанного интервала времени будет распространяться, а последующие значения будут игнорироваться. Пример ?

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

Мы можем использовать ThrottleTime для наблюдаемого потока нажатий кнопок и, таким образом, ввести задержку между последовательными нажатиями. Только первый щелчок в течение указанного интервала времени вызовет операцию, а последующие щелчки в течение этого интервала будут игнорироваться.

import { fromEvent } from 'rxjs';
import { throttleTime } from 'rxjs/operators';

// A button element with the id 'submitButton'
const submitButton = document.getElementById('submitButton');

fromEvent(submitButton, 'click')
  .pipe(throttleTime(1000))
  .subscribe(() => {
    // Perform the expensive operation here
    console.log('Button clicked');
  });

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

Пользовательские операторы

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

Эти операторы обычно создаются с использованием шаблона оператора pipeable и могут выполнять широкий спектр задач, таких как фильтрация, сопоставление, агрегирование или изменение выдаваемых значений. Пользовательские операторы предоставляют мощный способ адаптировать наблюдаемые объекты к потребностям вашего приложения и работать со сложными асинхронными потоками данных. Давайте рассмотрим это на примере, где у нас есть поток пользовательских событий, таких как щелчки мышью, нажатия клавиш и сенсорные события, и мы хотим создать собственный оператор, который должен отфильтровывать события на основе таких критериев, как тип события. Назовите пользовательский оператор как «filterEventType»:

import { Observable, OperatorFunction } from 'rxjs';

function filterEventType<T>(eventType: string): OperatorFunction<T, T> {
  return (source: Observable<T>) => {
    return new Observable<T>(subscriber => {
      source.subscribe({
        next(value) {
          if (value instanceof Event && value.type === eventType) {
            subscriber.next(value);
          }
        },
        error(error) {
          subscriber.error(error);
        },
        complete() {
          subscriber.complete();
        }
      });
    });
  };
}

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

import { fromEvent } from 'rxjs';

const userEvents$ = fromEvent(document, 'click').pipe(
  filterEventType('mousedown')
);

userEvents$.subscribe(event => {
  console.log('Mouse down event:', event);
});

Пользовательское наблюдаемое создание

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

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

import { Observable } from 'rxjs';

function createMessageObservable(): Observable<string> {
//returns a custom observable
  return new Observable<string>(subscriber => { 
    const socket = new WebSocket('wss://sample.com/chat');
    //listen for incoming messages from web socket
    socket.addEventListener('message', event => {
      const message = event.data;
      subscriber.next(message);
    });

    socket.addEventListener('close', () => {
      subscriber.complete();
    });

    socket.addEventListener('error', error => {
      subscriber.error(error);
    });
    // close the websocket connection when observable is unsubscribed
    return () => {
      socket.close();
    };
  });
}

const messageObservable$ = createMessageObservable();

messageObservable$.subscribe(message => {
  console.log('New message:', message);
});

RxJS в угловом

Вот несколько примеров некоторых распространенных вариантов использования Angular.

Использование RxJS с HttpClient Angular

RxJS обычно используется с модулем Angular HttpClient для обработки асинхронных операций, таких как выполнение HTTP-запросов и обработка ответов. Используя мощные операторы и функции RxJS, вы можете оптимизировать свои HTTP-запросы и более эффективно обрабатывать сложные сценарии. Например, использование catchError для обработки ошибок в HTTP-запросе.

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';

@Component({
  selector: 'app-example',
  template: `
    <button (click)="getRandomeUsername()">Get Random User</button>
    <div>{{ username }}</div>
  `
})
export class ExampleComponent {
  username: string;

  constructor(private http: HttpClient) {}

  getRandomeUsername() {
    this.http.get<any>('https://api.sample.com/user/random').pipe(
      catchError(error => {
        console.error('Error:', error);
        return [];
      })
    ).subscribe(response => {
      this.username = response.username;
    });
  }
}

Использование RxJS с модулем Angular Forms

Модуль Angular Forms предоставляет мощные функции для работы с формами в приложениях Angular. RxJS может улучшить функциональность форм Angular, предоставляя реактивные и декларативные подходы к проверке форм, синхронизации данных и управлению состоянием формы.

Ниже у нас есть компонент с полем ввода, которое привязано к FormControl с именем searchControl, свойство valueChanges которого выдает наблюдаемое значение всякий раз, когда изменяется входное значение. Мы будем использовать такие операторы, как debounceTime и DifferentUntilChanged, чтобы контролировать выброс значений из наблюдаемого объекта. Это гарантирует, что операция поиска запускается только после определенного времени устранения отказов и когда условие поиска действительно изменилось.

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Component({
  selector: 'app-example',
  template: `
    <input type="text" [formControl]="searchControl" placeholder="Search">
  `
})
export class ExampleComponent {
  searchControl = new FormControl();

  constructor() {
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged()
    ).subscribe(searchTerm => {
      // Perform search operation based on the entered search term
      this.search(searchTerm);
    });
  }

  search(searchTerm: string) {
    // Perform search operation and update UI accordingly
    console.log('Searching for:', searchTerm);
  }
}

Использование RxJS с модулем Router Angular

RxJS также можно использовать с модулем Router Angular для управления маршрутизацией и навигацией в приложениях Angular. Наблюдаемые объекты RxJS позволяют нам прослушивать изменения маршрута и выполнять действия на основе текущего маршрута, параметров запроса и параметров маршрута.

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

import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';

@Component({
  selector: 'app-notification',
  template: `
    <div *ngIf="showNotification" class="notification">
      <h4>Limited Time Offer!!</h4>
      <p>Don't miss out.</p>
    </div>
  `,
  styles: [`
    .notification {
      background-color: yellow;
      padding: 10px;
      text-align: center;
    }
  `]
})
export class NotificationComponent implements OnInit {
  showNotification = false;
  private destroy$ = new Subject<void>();

  constructor(private router: Router) { }

  ngOnInit() {
    this.router.events
      .pipe(
        filter(event => event instanceof NavigationEnd),
        takeUntil(this.destroy$)
      )
      .subscribe((event: NavigationEnd) => {
        // Check if the current route has a special offer
        if (event.url.includes('/special-offers')) {
          this.showNotification = true;
        } else {
          this.showNotification = false;
        }
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Мы применяем такие операторы RxJS, как filter, чтобы прослушивать только события NavigationEnd, и takeUntil, чтобы отписаться от наблюдаемого при уничтожении компонента.

Заключение

Обзор основных концепций и функций

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

Лучшие практики использования RxJS

Завершая эту серию статей, важно выделить некоторые передовые методы эффективного использования RxJS. Во-первых, стремитесь к реактивному мышлению и примите асинхронный характер наблюдаемых объектов. Используйте операторы с умом для манипулирования потоками данных и следите за тем, чтобы ваш код оставался читабельным и удобным в сопровождении. Избегайте вложенности и корректно обрабатывайте ошибки с помощью операторов обработки ошибок. Не забудьте отказаться от подписки на наблюдаемые объекты, чтобы предотвратить утечку памяти.

Ресурсы для дальнейшего обучения

RxJS — обширная библиотека с безграничными возможностями. Чтобы продолжить обучение, я рекомендую изучить официальную документацию по RxJS, которая содержит исчерпывающие пояснения, примеры и ссылки на API. Кроме того, существует множество онлайн-руководств, блогов и видеокурсов, которые позволяют глубже погрузиться в конкретные темы RxJS. Оставайтесь на связи с активным сообществом RxJS, участвуйте в обсуждениях и делитесь своими знаниями с другими. Назвать несколько:

  1. Официальная документация RxJS: Документация RxJS
  2. Репозиторий RxJS GitHub: Репозиторий RxJS GitHub
  3. Курс RxJS Essentials от Ultimate Courses: Курс RxJS Essentials
  4. Курсы Egghead.io RxJS: Курсы Egghead.io RxJS
  5. Шаблоны RxJS от Андре Штальтца: Шаблоны RxJS
  6. ReactiveX.io: ReactiveX.io

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

Продолжайте изучать, экспериментировать и применять RxJS в своих проектах. Примите реактивное мышление и позвольте RxJS стать вашим союзником в создании передовых приложений Angular. Удачного кодирования!