Видеолекция этого сообщения находится здесь - https://www.youtube.com/watch?v=gIMMkX4VZ54&t=812s

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

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

Средняя скорость набора составляет 38–40 слов в минуту (слов в минуту), и многие люди могут печатать намного быстрее 100 слов в минуту! Если мы отправим запрос в наш API для каждого события input из поля поиска, мы можем легко перегрузить сервер.

Кроме того, большинство этих запросов являются избыточными. Допустим, я покупаю на сайте электронной коммерции и ищу варежки. К тому времени, когда я напишу MITT в поле поиска, меня больше не будут интересовать результаты для M, MI или MIT . Хуже того, если я сделаю опечатку и напишу M-I-T-T-A, а затем изменю запрос обратно на M-I-T-T, то я сделаю два ненужных запроса для результатов, которые у меня уже были.

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

Представьте, что я написал M-I-T. Скорее всего, в моей базе данных больше результатов для M -I, чем для MIT, что может сделать запрос MI медленнее, чем MIT запрос. Следовательно, я могу получить результаты для MIT,, а затем устаревшие результаты для MI. Предложения для автозаполнения теперь не синхронизированы с поиском запрос!

Введите наблюдаемые

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

Существует несколько библиотек для наблюдаемых объектов, но RxJS является наиболее популярным. Поскольку Angular полагается на RxJS, Google вносит большой вклад в проект с открытым исходным кодом RxJS. Кроме того, в ReactiveX (RxJS) есть библиотеки для нескольких языков, а не только для JavaScript. В этом пошаговом руководстве я буду использовать версию 6, выпущенную в апреле 2018 г. У меня будет следующий пост, в котором будут рассмотрены различия между v5 и v6.

Boilerplate - Github Repo: https://bit.ly/2nl4Cmr

Чтобы продемонстрировать возможности наблюдаемых, мы построим простой поиск с автозаполнением для имен пользователей Github. В нем будет поле поиска и место для добавления результатов.

Вот начальный HTML-код с тегом сценария RxJS v6:

<body>
  <input type="text" name="search" id="search"/>
  
  <ul id="results">
  </ul>
  <script src="https://unpkg.com/[email protected]/bundles/rxjs.umd.min.js"></script>
  <script src="main.js"></script>
</body>

А вот шаблонный код для нашего файла main.js:

const {
  fromEvent,
  from,
} = rxjs;
const {
  map, 
  filter,
  distinctUntilChanged,
  debounceTime,
  switchMap,
} = rxjs.operators;
let searchBox = document.getElementById('search');
let results = document.getElementById('results');
let searchGithub = (query) =>
  fetch(`https://api.github.com/search/users?q=${query}`) 
  .then(data => data.json());

Операторы

Это начало нашего наблюдателя:

let input$ = fromEvent(searchBox, 'input')
  .pipe();

Обратите внимание, что переменная, представляющая нашего наблюдателя, input $, заканчивается на «$»: это соглашение для наблюдаемых.

fromEvent () принимает элемент DOM в качестве первого аргумента и тип события в качестве второго аргумента. Наш код использует fromEvent () для создания потока данных всех событий input окна поиска, т. Е. Наблюдаемого.

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

.карта

Оператор .map () применяет функцию к каждому событию из наблюдаемого источника и возвращает поток преобразованных событий.

Для нашего автозаполнения мы наблюдаем каждое событие input из поля поиска, но нас интересует только одно значение в этом объекте события: event.target.value.

Вместо того, чтобы наблюдать за каждым событием input и затем генерировать весь объект события, с помощью .map мы теперь наблюдаем каждый объект события input и генерируем только input em> .target.value этих объектов событий.

let input$ = fromEvent(searchBox, 'input')
  .pipe(
    map(e => e.target.value)
  );

.фильтр

Предположим, что мы не хотим делать поисковый запрос в Github на предмет совпадения имени пользователя, если пользователь не ввел хотя бы две буквы. Если бы пользователь набрал только одну букву, результаты были бы бессмысленными из-за количества результатов.

На данный момент наш .map () передает только строки в. filter (). Таким образом, для нашего автозаполнения мы будем выдавать только те запросы, длина которых не менее 2.

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

let input$ = fromEvent(searchBox, 'input')
  .pipe(
    map(e => e.target.value),
    filter(query => query.length >= 2 || query.length === 0)
  );

.debounceTime

До сих пор мы не использовали оператор, который требует наблюдаемого. Если бы у нас был обычный прослушиватель событий input в нашем элементе searchBox, мы могли бы сделать оператор if для отправки нашего запроса, только если длина event.target. значение было не менее 2.

.debounceTime () - это когда наблюдение за событиями во времени становится актуальным. Единственный аргумент, который требуется .debounceTime () - это число (в миллисекундах).

Каждый раз, когда. debounceTime () получает событие, он ждет определенное количество времени, чтобы увидеть, не поступит ли другое событие по конвейеру. Если это так, он перезапускает свой таймер. По прошествии достаточного времени без потоковой передачи другого события он генерирует последнее событие.

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

let input$ = fromEvent(searchBox, 'input')
  .pipe(
    map(e => e.target.value),
    debounceTime(250),
    filter(query => query.length >= 2 || query.length === 0), 
    distinctUntilChanged(),
  );

.distinctUntilChanged

.distinctUntilChanged () - один из самых простых операторов. Возьмем пример, который я привел во введении: пользователь ищет «варежки», но совершает ошибку. Они набирают M-I-T-T-A и затем удаляют обратно в M-I-T-T. Предположим, у нас уже есть результаты для M-I-T-T. Поскольку запрос не изменился по сравнению с последними отправленными данными, .distinctUntilChanged () не пропускает событие.

let input$ = fromEvent(searchBox, 'input')
  .pipe(
    map(e => e.target.value), 
    debounceTime(250),
    filter(query => query.length >= 2 || query.length === 0),
    distinctUntilChanged()
  );

.switchMap

Последняя и самая сложная часть нашей конфигурации .pipe () - это .switchMap (). Он гарантирует, что мы выдадим результаты только из последнего размещенного запроса GET.

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

.switchMap(value => from(searchGithub(value)))

.switchMap () будет генерировать события, которые разрешаются из внутреннего наблюдаемого, т.е. он вернет разрешенное обещание.

Однако, если поступает новое значение запроса, .switchMap () отказывается от старого наблюдаемого и переключается на новый наблюдаемый. В нашем случае это новое обещание, сделанное из обновленного запроса. Следовательно, наш наблюдаемый объект будет выдавать значения только из самого нового запроса.

Как упоминалось в разделе о .filter (), если выдаваемая строка пуста, мы хотим, чтобы массив результатов был очищен. Поэтому вместо того, чтобы просто возвращать наблюдаемое напрямую, мы добавим тернарный оператор, чтобы проверить, является ли значение, которое передается в потоке, пустым. Если значение пустое, вместо отправки запроса мы просто преобразуем нашу наблюдаемую в пустой массив.

Запрос Github для имен пользователей возвращает объект, в котором нужная нам информация указана в .items, поэтому этот объект представлен в тернарном операторе ниже.

let input$ = fromEvent(searchBox, 'input')
  .pipe(
    map(e => e.target.value),
    debounceTime(250),
    filter(query => query.length >= 2 || query.length === 0),
    distinctUntilChanged(), 
    switchMap(value => value ?
      from(searchGithub(value)) : from(Promise.resolve({items: []}))
    )
  );

Подписывайся

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

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

input$.subscribe(data =>  {
  while (results.firstChild) {
    results.removeChild(results.firstChild);
  }
});

Как вы можете видеть выше, мы представляем генерируемое событие как данные и будем отображать массив результатов. В случае этого запроса, возвращенного Github, нас интересуют результаты data.items. Затем мы создадим ‹li› для каждого .login каждого результата в массиве.

input$.subscribe(data =>  {
  while (results.firstChild) {
    results.removeChild(results.firstChild);
  }
  data.items.map(user => {
    let newResult = document.createElement('li');
    newResult.textContent = user.login;
    results.appendChild(newResult);
  });
});

Сложите все это вместе, и у нас есть поиск с автозаполнением!

Заключение

RxJS имеет ТОННУ различных операторов, которые могут изменять поток данных, и наш автозаполнение просто использует их выборку. Вы можете увидеть все доступные операторы в их документации здесь: https://rxjs-dev.firebaseapp.com/api

Я также настоятельно рекомендую http://rxmarbles.com/ для визуального представления того, как операторы влияют на потоки данных. На каждой диаграмме есть интерактивный ползунок, чтобы увидеть, как влияют на события, и это очень здорово.

В RxJS есть не только множество операторов, но и множество различных встроенных наблюдателей. Для простоты я использовал встроенные наблюдатели fromEvent () и from (), но в библиотеку RxJS были включены все виды предварительно настроенных наблюдаемых объектов. Кроме того, вы можете создать наблюдаемое с нуля, которое будет иметь возможность наблюдать любые события, о которых вы только можете подумать!

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

В следующей статье мы рассмотрим, как использовать наблюдаемые объекты во фреймворке React и сопоставить создаваемый поток с реквизитами.

P.S. .debounceTime () должен быть первым оператором в нашем канале, но я написал его третьим для последовательности объяснения. Оба порядка работают, но если поставить сначала .debounceTime (), то больше событий не будет передаваться дальше по конвейеру.