Эта статья является ответом на статью-перевод «Как сделать поиск пользователей по GitHub используя React + RxJS 6 + Recompose», которая буквально вчера научила нас как надо использовать React, RxJS и Recompose вместе. Что ж, предлагаю теперь посмотреть, как это можно реализовать без оных инструментов.




Disclaimer
Многим может показаться, что данная статья содержит элементы троллинга, написана впопыхах и по фану… Так вот, это так.


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

Эта статья рассчитана на людей имеющих опыт работы с 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. Будем рады вам!