Эта статья является ответом на статью-перевод «Как сделать поиск пользователей по 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-компонентом нашего виджета, со следующим содержанием:
<input placeholder="GitHub username">
<style>
input {
font-size: 20px;
border: 1px solid black;
border-radius: 3px;
margin-bottom: 10px;
padding: 10px;
}
</style>
Здесь и далее, я использую стили из оригинальной статьи. Обратите внимание, что уже прямо сейчас они в scope, т.е. применяются только к данному компоненту и можно смело использовать имена тегов там где это актуально.
Стили писать буду прямо в компонентах, потому что SFC, а также потому что REPL не поддерживает вынос CSS/JS/HTML в разные файлы, хотя это легко делается с помощью препроцессоров Svelte.
Recompose
Отдыхаем…
Поточный компонент
… загораем…
Конфигурирование
… пьем кофе…
Recompose + RxJS
… пока другие…
Map
… работают.
Добавляем обработчик событий
Не совсем обработчик конечно, просто биндинг:
<input bind:value=username placeholder="GitHub username">
Ну и определим значение username по-умолчанию:
<script>
export default {
data() {
return {
username: ''
};
}
};
</script>
Теперь, если вы начнете вводить что-то в поле ввода, значение username будет меняться.
Проблема яйца и курицы
Ни куриц, ни яиц, ни проблем с другими животными, мы же не RxJS используем.
Связываем вместе
Все уже реактивно и связано. Так что дальше пьем кофе.
Компонент User
Этот компонент у нас будет отвечать за отображение пользователя, имя которого мы будет ему передавать. Он будет получать value из компонента App и переводить его в AJAX запрос.
«Воу-воу-воу, полегче» ©
У нас этот компонент будет тупым и просто отображать красивую карточку юзера по заранее известной модели. Мало ли откуда могут приходить данные и/или в каком месте интерфейса мы захотим показать эту карточку.
Примерно так будет выглядить компонент User.html:
<div class="github-card user-card">
<div class="header User" />
<a class="avatar" href="https://github.com/{login}">
<img src="{avatar_url}&s=80" alt={name}>
</a>
<div class="content">
<h1>{name || login}</h1>
<ul class="status">
<li>
<a href="https://github.com/{login}?tab=repositories">
<strong>{public_repos}</strong>Repos
</a>
</li>
<li>
<a href="https://gist.github.com/{login}">
<strong>{public_gists}</strong>Gists
</a>
</li>
<li>
<a href="https://github.com/{login}/followers">
<strong>{followers}</strong>Followers
</a>
</li>
</ul>
</div>
</div>
<style>
/* стили */
</style>
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:
<script>
import { getUserCard } from './api.js';
...
export default {
...
computed: {
user: ({ username }) => username && getUserCard(username)
}
};
</script>
Все, теперь при изменении имени пользователя будет отправляться запрос за получение данных по этому значению. Однако, так как биндинг реагирует на каждое изменение, то есть ввод в текстовое поле, запросов будет слишком много и мы быстро превысим все доступные лимиты.
В оригинальной статье эта проблема решается встроенной в RxJS функцией debounceTime, которая не дает нам слишком часто запрашивать данные. Для нашей же реализации можно воспользоваться standalone решением, типа debounce-promise или любым другим подходящим, благо есть из чего выбрать.
<script>
import debounce from 'debounce-promise';
import { getUserCard } from './api.js';
...
const getUser = debounce(getUserCard, 1000);
...
export default {
...
computed: {
user: ({ username }) => username && getUser(username)
}
};
</script>
Итак, эта либа создает debounce-версию переданной ей функции, которую мы потом используем в вычисляемом свойстве.
switchMap
ajax
RxJS предоставляет собственную реализацию ajax которая прекрасно работает со switchMap!
Так как мы не используем RxJS и тем более switchMap, мы можем использовать любую библиотеку для работы с ajax.
Я использую axios, потому что он удобный для меня, но вы можете использовать что угодно и сути дела это не меняет.
Пробуем
Для начала нам нужно зарегистрировать компонент User для использования его в качестве тега шаблона, так как сам по себе импорт не добавляет компонент в контекст шаблона:
<script>
import User from './User.html';
...
export default {
components: { User }
...
};
</script>
Несмотря на кажущуюся лишнюю писанину, это позволяет, в том числе, давать разные имена тегов инстансам одного и того же компонента, делая ваши шаблоны более семантически понятными.
Далее, значение user — это не сами данные, а промис на эти данные. Потому что мы халявщики и не хотим делать вообще никакой работы, только пить кофе с печеньками.
Для того, чтобы передать реальные данные в компонент User, мы можем воспользоваться специальной конструкцией для работы с промисами:
{#await user} <!-- тут это еще промис -->
{:then user} <!-- а здесь уже объект с данными. имя можно дать любое. -->
{#if user}
<User {...user} />
{/if}
{/await}
Имеет смысл проверить объект с данными на существование, прежде чем передавать его в компонент User. Spread-оператор здесь позволяет «расщепить» объект на отдельные пропсы при создании экземпляра компонента User.
Короче работинг.
Обработка ошибок
Попробуйте ввести несуществующее имя пользователя.
…
Наше приложение сломано.
Ваше наверное да, но наше точно нет))) Просто ничего не произойдет, хотя это конечно не дело.
catchError
Добавим дополнительный блок для обработки rejected-промиса:
{#await user}
{:then user}
{#if user}
<User {...user} />
{/if}
{:catch error}
<Error {...error} />
{/await}
Компонент Error
<div class="error">
<h2>Oops!</h2>
<b>{response.status}: {response.data.message}</b>
<p>Please try searching again.</p>
</div>
Сейчас наш UI выглядит гораздо лучше:
И не говорите, а главное никаких усилий.
Индикатор загрузки
Короче там дальше вообще ересь началась, со всякими там BehaviorSubject-ами и иже с ними. Мы же просто добавим индикатор загрузки и не будем доить слона:
{#await user}
<h3>Loading...</h3>
{:then user}
{#if user}
<User {...user} />
{/if}
{:catch error}
<Error {...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-бандла без вашего участия.
Автор: PaulMaly