Так как это ответная статья, я решил построить ее в том же пошаговом формате, что и оригинал. Кроме того, статья также призвана сравнить оригинальную реализацию с описанной здесь. Поэтому в ней присутствует обилие отсылок и цитат из оригинала. Приступим.
Эта статья рассчитана на людей имеющих опыт работы с 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-бандла без вашего участия.
P/S
Читайте и другие статьи про Svelte, а также заглядывайте в русскоязычный телеграм канал SvelteJS. Будем рады вам!
ссылка на оригинал статьи https://habr.com/post/419653/

Добавить комментарий