RxJS — популярная библиотека, доступная для TypeScript и JavaScript.

Он предоставляет API для создания приложений и библиотек с использованием асинхронных потоков данных и реактивных методов. Это одна из базовых библиотек Angular.

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

Многие из операторов являются низкоуровневыми, и, объединяя их с помощью метода pipe, они создают мощный способ работы с данными.

Создание пользовательских операторов для домена

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

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

Можно создать два типа операторов — MonoTypeOperatorFunction и OperatorFunction, и все операторы должны делать две вещи:

  • Возвращает функцию, которая принимает в качестве параметра источник из предыдущего наблюдаемого значения в потоке.
  • Вернуть значение того же типа для MonoTypeOperatorFunction или другого типа для OperatorFunction, используя исходное значение с pipe

Ниже у нас будет пример каждого из них, но сначала для поддержки создания операторов нам понадобится некоторый код для упрощения:

import { from } from 'rxjs';
import { map, tap } from 'rxjs/operators';
// Create a cold source that will emit each number
const source$ = from([1, 2, 3, 4, 5]);
// Create a cold source that multiplies each number by `5`
const multiplyByFive$ = source$.pipe(map(value => value * 5));
// Create a cold source that multiplies each number by `10`
const multiplyByTen$ = source$.pipe(map(value => value * 10));
// Subscribe to the sources and console.log the output
multiplyByFive$.pipe(tap(console.log)).subscribe();
// Output: `5, 10, 15, 20, 25`
multiplyByTen$.pipe(tap(console.log)).subscribe();
// Output: `10, 20, 30, 40, 50`

Создание MonoTypeOperatorFunction для одиночных типов

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

Глядя на наш код, мы можем определить две одинаковые операции умножения в нашем коде. Чтобы превратить это в оператор, функция будет выглядеть так:

import { MonoTypeOperatorFunction } from 'rxjs';
import { map } from 'rxjs/operators';
export function multiply(factor: number): MonoTypeOperatorFunction<number> {
  return (source) => source.pipe(map(value => value * factor))
}

Здесь мы возвращаем стрелочную функцию, которая принимает предыдущий источник, который должен быть Observable<number>. Источник передается на карту, ​​что позволяет преобразовать исходное значение в новое значение, в нашем случае мы умножаем на factor

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

Написание мраморного теста

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

Я лично предпочитаю писать эти тесты в Jest, используя rxjs-marbles и jest-marbles, но есть и другие доступные библиотеки для написания этих тестов.

Используя шарики, мы можем настроить фиктивный источник, который будет выдавать 5 чисел в указанных кадрах.

Результат теста содержит две вещи:

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

В этом тесте мы передадим источник чисел и умножим на 10

import { marbles } from "rxjs-marbles/jest";
import { map } from "rxjs/operators";
import { multiply } from './multiply'
describe("multiply", () => {
  it("should multiply by 10", marbles(m => {
    const input = m.hot('-a-b-c-d-e-|', {a: 2, b: 3, c: 4, d: 5, e: 6});
    const subs = '^----------!';
    const expected = m.cold('-a-b-c-d-e-|', {a: 20, b: 30, c: 40, d: 50, e: 60});
    m.expect(input.pipe(mul(10))).toBeObservable(expected);
    m.expect(input).toHaveSubscriptions(subs);
  }));
});

Обновить код

Теперь оператор создан, его можно использовать в существующем коде сверху — в идеале оператор должен быть частью общей библиотеки кода:

import { from } from 'rxjs';
import { multiply } from '@myorg/rxjs-library'
const source$ = from([1, 2, 3, 4, 5]);
const multiplyByFive$ = source$.pipe(multiply(5));
const multiplyByTen$ = source$.pipe(multiply(10));

Уже намного читабельнее! Наш код объясняет наши намерения, но на самом деле мы не уменьшили дублирование наших источников.

Изменение API с помощью OperatorFunction

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

Это приведет к изменению API, но при наличии надлежащих тестов мы сможем легко перенести наш код.

Для нашего исходного значения это по-прежнему одно числовое значение, но в API мы изменили:

  • Вход factor может быть одним значением или массивом значений.
  • Возвращаемое значение теперь представляет собой массив значений, независимо от ввода.

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

import { OperatorFunction } from 'rxjs';
import { map } from 'rxjs/operators';
export function multiply(factor: number | number[]): OperatorFunction<number, number[]> {
  return source => source.pipe(map(value => (Array.isArray(factor) ? factor : [factor]).map(f => value * f)))
}

Обновление тестов

Во-первых, нам нужно обновить существующий тест — здесь нам нужно только изменить значения в нашем expected Observable — теперь мы ожидаем массив чисел независимо от ввода — но с одним значением длина нашего массива будет 1

it("should multiply by 10", marbles(m => {
  const input = m.hot('-a-b-c-d-e-|', {a: 2, b: 3, c: 4, d: 5, e: 6});
  const subs = '^----------!';
  const expected = m.cold('-a-b-c-d-e-|', {a: [20], b: [30], c: [40], d: [50], e: [60]});
  m.expect(input.pipe(mul(10))).toBeObservable(expected);
  m.expect(input).toHaveSubscriptions(subs);
}));

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

it("should multiply by by 5 and 10", marbles(m => {
  const input = m.hot('-a-b-c-d-e-|', {a: 2, b: 3, c: 4, d: 5, e: 6});
  const subs = '^----------!';
  const expected = m.cold('-a-b-c-d-e-|', {a: [10, 20], b: [15, 30], c: [20, 40], d: [25, 50], e: [30, 60]});
  m.expect(input.pipe(mul([5, 10]))).toBeObservable(expected);
  m.expect(input).toHaveSubscriptions(subs);
}));

Обновить код

Теперь мы можем обновить код дальше — теперь мы можем удалить два дополнительных холодных Observable и создать один, используя наш новый оператор multiply, передав ему массив, содержащий факторы out:

import { from } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { multiply } from '@myorg/rxjs-library'
const source$ = from([1, 2, 3, 4, 5]);
const multiplyValues$ = source$.pipe(multiply([5, 10]));

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

multiplyValues$.pipe(tap(console.log)).subscribe();
// Output: `[5, 10], [10, 20], [15, 30], [20, 40], [25, 50]`

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

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

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

Коллекция готовых операторов для ваших проектов

Теперь о беспардонной затычке — моя собственная библиотека — RxJS Ninja — это набор из более чем 130 операторов для работы с различными типами данных (такими как массивы или числа) и потоки, позволяющие модифицировать, фильтровать и запрос данных.

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

Вы можете проверить исходный код на GitHub. Там же вы можете найти стартовый проект для создания собственных библиотек TypeScript, подобных этому.