Как сделать поиск пользователей по GitHub без React + RxJS 6 + Recompose
Эта статья является ответом на статью-перевод «Как сделать поиск пользователей по GitHub используя React + RxJS 6 + Recompose», которая буквально вчера научила нас как надо использовать React, RxJS и Recompose вместе. Что ж, предлагаю теперь посмотреть, как это можно реализовать без оных инструментов.
Многим может показаться, что данная статья содержит элементы троллинга, написана впопыхах и по фану… Так вот, это так.
Так как это ответная статья, я решил построить ее в том же пошаговом формате, что и оригинал. Кроме того, статья также призвана сравнить оригинальную реализацию с описанной здесь. Поэтому в ней присутствует обилие отсылок и цитат из оригинала. Приступим.
Эта статья рассчитана на людей имеющих опыт работы с React и RxJS. Я всего лишь делюсь шаблонами, которые я посчитал полезными для создания такого UI.
Эта статья рассчитана на людей имеющих опыт работы с Javascript (ES6), HTML и CSS. Кроме того, в своей реализации я буду использовать «исчезающий» фреймворк SvelteJS, но он настолько прост, что вам не обязательно иметь опыт его использования, чтобы понимать код.
Делаем мы все ту же штуку:
Без классов, работы с жизненным циклом или setState.
Да, без классов, работы с жизненным циклом или setState. А также без React, ReactDOM, RxJS, Recompose. И кроме того без componentFromStream, createEventHandler, combineLatest, map, startWith, setObservableConfig, BehaviorSubject, merge, of, catchError, delay, filter, map, pluck, switchMap, tap, {another bullshit}… Короче вы поняли.
Подготовка
Все что нужно лежит в моем REPL примере на сайте SvelteJS. Можете потыркать там, либо локально, скачав исходники оттуда (кнопка с характерной иконкой).
Для начала создадим простой файлик App.html, который будет являться root-компонентом нашего виджета, со следующим содержанием:
Здесь и далее, я использую стили из оригинальной статьи. Обратите внимание, что уже прямо сейчас они в scope, т.е. применяются только к данному компоненту и можно смело использовать имена тегов там где это актуально.
Стили писать буду прямо в компонентах, потому что SFC, а также потому что REPL не поддерживает вынос CSS/JS/HTML в разные файлы, хотя это легко делается с помощью препроцессоров Svelte.
Recompose
Отдыхаем…
Поточный компонент
… загораем…
Конфигурирование
… пьем кофе…
Recompose + RxJS
… пока другие…
Map
… работают.
Добавляем обработчик событий
Не совсем обработчик конечно, просто биндинг:
Ну и определим значение username по-умолчанию:
Теперь, если вы начнете вводить что-то в поле ввода, значение username будет меняться.
Проблема яйца и курицы
Ни куриц, ни яиц, ни проблем с другими животными, мы же не RxJS используем.
Связываем вместе
Все уже реактивно и связано. Так что дальше пьем кофе.
Компонент User
Этот компонент у нас будет отвечать за отображение пользователя, имя которого мы будет ему передавать. Он будет получать value из компонента App и переводить его в AJAX запрос.
«Воу-воу-воу, полегче» ©
У нас этот компонент будет тупым и просто отображать красивую карточку юзера по заранее известной модели. Мало ли откуда могут приходить данные и/или в каком месте интерфейса мы захотим показать эту карточку.
Примерно так будет выглядить компонент User.html:
JSX/CSS
CSS просто добавили в компонент. Вместо JSX у нас HTMLx встроенный в Svelte.
Контейнер
Контейнером выступает любой родительский компонент для компонента User. В данном случае это компонент App.
debounceTime
Дебоунсить ввод текста не имеет смысла, если операция лишь перезаписывает значение в модели данных. Так что в самом биндинге нам это не нужно.
pluck
filter
map
Подключаем
Вернёмся в App.html и импортируем компонент User:
import User from './User.html';
Запрос данных
GitHub предоставляет API для получения информации о пользователе:
Для демонстрации, просто напишем маленький файлик api.js, который будет абстрагировать получение данных и экспортировать соответствующую функцию:
import axios from 'axios';
export function getUserCard(username) {
return axios.get(`https://api.github.com/users/${username}`)
.then(res => res.data);
}
И точно также импортируем эту функцию в App.html.
Теперь сформулируем задачу более предметным языком: нам нужно при изменении одного значения в модели данных (username) изменять другое значение. Назовем его соответственно user — данные о юзере, которые мы получаем из API. Реактивность во всей красе.
Для этого, напишем вычисляемое свойство Svelte, используя следующую конструкцию в App.html:
Все, теперь при изменении имени пользователя будет отправляться запрос за получение данных по этому значению. Однако, так как биндинг реагирует на каждое изменение, то есть ввод в текстовое поле, запросов будет слишком много и мы быстро превысим все доступные лимиты.
В оригинальной статье эта проблема решается встроенной в RxJS функцией debounceTime, которая не дает нам слишком часто запрашивать данные. Для нашей же реализации можно воспользоваться standalone решением, типа debounce-promise или любым другим подходящим, благо есть из чего выбрать.
Итак, эта либа создает debounce-версию переданной ей функции, которую мы потом используем в вычисляемом свойстве.
switchMap
ajax
RxJS предоставляет собственную реализацию ajax которая прекрасно работает со switchMap!
Так как мы не используем RxJS и тем более switchMap, мы можем использовать любую библиотеку для работы с ajax.
Я использую axios, потому что он удобный для меня, но вы можете использовать что угодно и сути дела это не меняет.
Пробуем
Для начала нам нужно зарегистрировать компонент User для использования его в качестве тега шаблона, так как сам по себе импорт не добавляет компонент в контекст шаблона:
Несмотря на кажущуюся лишнюю писанину, это позволяет, в том числе, давать разные имена тегов инстансам одного и того же компонента, делая ваши шаблоны более семантически понятными.
Далее, значение user — это не сами данные, а промис на эти данные. Потому что мы халявщики и не хотим делать вообще никакой работы, только пить кофе с печеньками.
Для того, чтобы передать реальные данные в компонент User, мы можем воспользоваться специальной конструкцией для работы с промисами:
{#await user}
{:then user}
{#if user}
{/if}
{/await}
Имеет смысл проверить объект с данными на существование, прежде чем передавать его в компонент User. Spread-оператор здесь позволяет «расщепить» объект на отдельные пропсы при создании экземпляра компонента User.
Короче работинг.
Обработка ошибок
Попробуйте ввести несуществующее имя пользователя.
…
Наше приложение сломано.
Ваше наверное да, но наше точно нет))) Просто ничего не произойдет, хотя это конечно не дело.
catchError
Добавим дополнительный блок для обработки rejected-промиса:
{#await user}
{:then user}
{#if user}
{/if}
{:catch error}
{/await}
Компонент Error
Oops!
{response.status}: {response.data.message}
Please try searching again.
Сейчас наш UI выглядит гораздо лучше:
И не говорите, а главное никаких усилий.
Индикатор загрузки
Короче там дальше вообще ересь началась, со всякими там BehaviorSubject-ами и иже с ними. Мы же просто добавим индикатор загрузки и не будем доить слона:
{#await user}
Loading...
{:then user}
{#if user}
{/if}
{:catch error}
{/await}
Результат?
Два крошечных logic-less компонента (User и Error) и один управляющий компонент (App), где наиболее сложная бизнес-логика описана в одну строку — создание вычисляемого свойства. Никаких обмазываний observable-объектами с ног до головы и подключений +100500 инструментов, которые вам не нужны.
→ Интерактивная демка
Пишите простой и понятный код. Пишите меньше кода, а значит меньше работайте и проводите больше времени с семьей. Живите!
Всем счастья и здоровья!
Все:)
FYI
Если вы уже посмотрели пример в REPL, то наверное обратили внимание на тост с warning’ом слева внизу:
Compiled, but with 1 warning — check the console for details
Если вы не поленитесь открыть консоль, то увидите вот такое сообщение:
Unused CSS selector
.user-card .Organization {
background-position: top right;
}
Статический анализатор Svelte сообщает нам, что некоторые стили компонентов не используются. Кроме того, по вашему желанию или повелению дефолтных настроек компилятора, неиспользуемые стили буду удалены из итогового css-бандла без вашего участия.