Легенда о Фреймворке Всевластия +15


В последнее время набирает популярность тренд «исчезающих фреймворков», локомотивом которого, без сомнения, можно считать SvelteJS — buildtime-фреймворк и компилятор в ванильный javascript.

Несмотря на то, что концептуально Svelte весьма прост, а в использовании еще проще, многие разработчики задаются вопросом, в чем же killer-фича данного фреймворка, да и подхода в целом? Почему это не «yet another javascript framework»?

В этой статье я расскажу про одну из множества суперспособностей Svelte, которые могут всерьез облегчить вам жизнь.

Давайте разберемся, но сначала я расскажу вам одну легенду…



Легенда о Фреймворке Всевластия


Одни фреймворки были созданы ребятами из Google и Facebook, другие — крутыми чуваками, но все под под пристальным «вниманием» Рича Харриса.

Девять фреймворков были созданы для людей, семь, по всей видимости, для гномов. Ещё три фреймворка (react, vue, angular) были для эльфов.

После создания фреймворков и их имплементации в тысячи проектов, Рич Харрис самолично и тайно создал один фреймворк…

One framework to rule them all,
One framework to find them,
One framework to bring them all
And together bind them.

— The Lord of the Frameworks

Проблема


Уверен, многие из вас, кто всерьез и долго занимается фронтенд-разработкой, не раз сталкивались с проблемой выбора инструментов для вашего текущего и/или следующего проекта.
Многообразие всевозможных пакетов, утилит, китов, фреймворков, библиотек и других решений, зашкаливает как никогда ранее. А главное вся эта движуха продолжает ускоряться.

Все это, так или иначе, касается и выбора фреймворка. Наверное не ошибусь, если предположу, что лишь немногие современные команды и компании, начинают новые проекты без использования какого-либо js фреймворка. Конечно если речь идет о современных веб-приложениях, а не просто о веб-сайтах. И все бы ничего, если бы от этого не зависело так многое в вашем проекте.


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

Но реальные проблемы начинаются, если где-нибудь в середине проекта, вы понимаете, что сделали не совсем верный выбор. Что-то не срослось и не закрутилось. Фреймворк потребовал немного больше времени на освоение, немного большей команды, оказался немного менее быстрым, немного не подошел вашим целям или стилю разработки и т.д. А самое главное теперь ваш проект на 100% завязан на этом фреймворке и вы не можете просто взять и переписать его на чем-то другом.

Еще более обидно, когда все же закончив проект успехом, вы понимаете, что в целом не очень то и довольны. И наверное, не хотели бы писать следующий проект на этом же фреймворке. А значит, все те «переиспользуемые» решения, к которым мы с вами стремимся, можно выкинуть в трубу.

Реально, черт с ним с бизнес-кодом, реализующим конкретную бизнес-задачу, работает и ладно. Но ведь вы написали такой классный "%вставить свое% с блекджеком и девушками с пониженной социальной ответственностью", и так хотели использовать его в следующем своем проекте, но эта зараза тесно завязана на текущем фреймворке от одного вида которого вас уже воротит.

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

А тут еще ваше замечательное руководство, решило составить конкуренцию Google Material Design и отправить вас в крестовый поход на разношерстные интерфейсы ваших проектов дабы привести их к общему знаменателю. Ушлые дизайнеры уже рисуют новые кнопки и селекторы и строчат тысячи страниц гайдлайнов для нового единого UI-kit'а ваших компонентов. Ура товарищи!

Не жизнь, а сказка, правда? Осталось только придумать как бы так натянуть все эти новые компоненты на все те проекты, которые вы уже успели понаписать на всех возможных фреймворках. Если времени и денег реально много и есть эстетическое желание, а главное вера, в то что «все надо унифицировать», то можно посадить пару тройку десятков команд переписать все это снова, например на React. Это и правильно, потому что то унылое говно, на котором вы писали последние 2-3 года, уже морально устарело, а вот React будет вечен. Ну-ну)

Есть и другой путь. Можно написать чудесный новый UI-kit на одном фреймворке, создать как бы библиотеку переиспользуемых компонентов, а потом просто использовать этот UI-kit во всех своих проектах. Прикольно звучит? Конечно, но остается одна проблема — рантайм.

Если у вас проект написан на Angular (~500Kb), а UI-kit вы решили писать на React (~98Kb), то тащить в каждый проект на одном фреймворке, другой фреймворк, да еще с кучей зависимостей и сам UI-kit, прям скажем не выглядит оптимальным.

Решение


На помощью нам приходят те самые «исчезающие» фреймворки, без рантайма. Главное условие тут, чтобы они были максимально изолированные в плане своей экосистемы и имели механизмы внешней интеграции и соответствующие API.

Прекрасный пример такого фреймворка — SvelteJS, про который уже написано не мало статьей на Хабре.

Итак, представим ситуацию, что у нас есть приложение на React. Возможно он нам надоел, и мы хотим от него избавиться, но переписать все разом непозволительная роскошь. А может быть, некоторые части приложения требуют улучшения или рефакторинга. Ну или мы решили делать единую библиотеку компонентов, и поэтому теперь все компоненты будем писать на Svelte и использовать во всех проектах. Представили? Да конечно нет, ни у кого нет такой фантазии. Давайте лучше разберем на реальном примере.


Disclaimer
Сразу хочу обратить ваше внимание, что я не являюсь React-разработчиком и последний раз «щупал» React в далеком 2015 году. Поэтому, предполагаю, что то, как я написал часть примера на React может задеть чувства верующих реактоводов. Прощу не судить строго, тем более что смысл статьи от этого не меняется.


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

Код этого компонента можно посмотреть в REPL, а код примера из данной статьи в репозиторий.

Create React App


Для начала создадим новый React проект, воспользовавшись де-факто стандартной тулзой — create-react-app:

npx create-react-app my-app
cd my-app
npm start

Ок, если зайти на 3000-й порт, вроде бы работает.

Настраиваем Svelte


Если вы ничего не знаете о Svelte, то скажу так, в контексте задачи Svelte — это всего лишь еще один шаг вашего сборщика (webpack/rollup/gupl/grunt/etc), который позволит вам писать компоненты в формате SFC и компилировать их в vanilla javascript.

В сообществе Svelte больше предпочитают Rollup, что не мудрено, так как у них один автор — Рич Харрис. Однако, так как CRA использует webpack, то мы настроим Svelte через него. Для этого, сначала надо вынести конфиги webpack из react-scripts в проект, чтобы мы могли менять их. Делается это с помощью встроенной команды:

npm run eject


Насколько я знаю, это не кошерный подход, но для примера это наиболее удобный вариант.

Теперь, когда конфиги webpack в корне проекта, можно ставить Svelte:

npm i --save-dev svelte svelte-loader


Обратите внимание на флажок --save-dev, помните да, что рантайма-то нету.))))

Последний штрих, нужно подключить соответствующий лоадер в конфиги:

  {
    test: /\.svelte$/,
      use: {
        loader: 'svelte-loader',
      }
   },


Вообще, в сообществе Svelte принято писать файлы компонентов с расширением .html, потому что компонент Svelte — это валидный HTML файл. Однако, чтобы избежать вероятных коллизий, в некоторых случаях, лучше использовать кастомный формат файла .svelte.

Так мы и сделали, теперь все файлы .svelte подключаемые в проект будут перехватываться этим лоадером и компилироваться Svelte.

Пишем компонент Svelte


Сперва лучше настроить редактор кода, например, чтобы он применял подсветку html-синтаксиса к файлам с соответствующим расширением. Примерно так это делается в VS Code:

  "files.associations": {
    "*.svelte": "html"
  }

Теперь создадим папочку ./src/svelte_components/ и там папку самого компонента. После просто переносим все файлы из REPL примера в эту папку, попутно давая им новое расширение .svelte, а файл App.html назовем Widget.svelte.

В итоге должно получиться, что-то вроде этого:


Там же создаем файл index.js, в котором у нас будет располагаться код интеграции Svelte и React.

Интегрируем


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

Серьезно, теперь мы можем использовать компоненты Svelte в нашем React приложении как совершенно обычные JS конструкторы, а значит код интеграции со Svelte ничем не будет отличаться от интеграции с любой другой standalone либой. Документация React даже содержит раздел посвященный этому: Integrating with Other Libraries .

Код интеграции может выглядеть например так:

import React, { PureComponent } from 'react';

import Widget from './Widget.svelte';

export default class extends PureComponent {

  componentDidMount() {
    
    const { username } = this.props;
    
    this.widget = new Widget({
      target: this.el,
      data: { username }
    });

  }
  
  componentWillUnmount() {
    this.widget.destroy();
  }
  
  render() {
    return (
      <div ref={el => this.el = el}></div>
    );
  }
}

Итого, мы просто обернули код нашего сложного Svelte компонента в очень простой компонент React, который просто создает новый экземпляр Svelte компонента, передавая при его создании элемент для монтирования и данные из пропсов. Кроме того, мы не забываем демонтировать Svelte компонент в хуке componentWillUnmount.

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

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

  componentDidMount() {   
    ...
    this.widget.on('state', ({ current: { username }, changed }) => {
        if (changed.username) {
          this.props.onChange({ username });
        } 
    });
  }

  componentWillReceiveProps({ username }) {
    this.widget.set({ username });
  }

Здесь мы использовали встроенное событие state, которое срабатывает каждый раз, когда стейт Svelte компонента меняется. В коллбек передается объект содержащий текущий стейт компонента (current), предыдущий стейт (previous) и список измененных свойcтв (changed). Соответственно, мы просто проверяем был ли изменен username и вызываем коллбек onChange, если это так.

В хуке componentWillReceiveProps мы устанавливаем новое значение username с помощью встроенного метода set().

Кроме встроенных, компоненты Svelte могут имплементировать кастомные события и методы. Именно эти приятные возможности позволяют описать интерфейс компонента и довольно удобно организовать коммуникацию с «внешним миром».

Используем


Теперь попробуем использовать наш виджет непосредственно в React приложении. Для этого отредактируем App.js файл, сгенерированный стартером:

import React, { Component } from 'react';
import './App.css';

import GithubWidget from './svelte_components/GithubWidget';

class App extends Component {

  constructor() {
    super();
    this.state = {
      username: ''
    };
  }

  handleChange = (state) => {
    this.setState({ ...state });   
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <h1>Github Widget for: {this.state.username}</h1>
          <GithubWidget
            onChange={this.handleChange} 
            username={this.state.username}
          />
        </header>
      </div>
    );
  }
}

export default App;

Короче используем как обычный React компонент. И в результате получаем:


Уже не плохо, правда?) Обратите внимание, значение username, которое мы вводим в текстовое поле виджета сразу же пробрасывается наверх в React приложение.

Доработаем


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

Во-первых, нужно создать новый компонент Repo.svelte, который как раз и будет отрисовывать карточку репозитория. Для простоты примера, я просто скопипастил шаблон и стили из User.svelte и адаптировал под данные репозитория. Однако, теоретически это отдельный компонент.

Далее, нужно научить управляющий компонент Widget.svelte переключать эти два вида карточек на лету. Кроме того, нужно научить его дергать разные запросы для пользователя и репозитория.

Мы будем использовать одно поле для ввода и определять тип данных по наличию "/" в значении. То есть, если нужно поискать пользователя, вводим его username, а если репозиторий, тогда вводим username пользователя, далее "/" и имя репозитория.

На первый взгляд выглядит довольно заморочено, но на Svelte решение займет буквально 5-6 строк кода. Для начала подключим новый компонент и метод API, который обернем в debounce:

...
import Repo from './Repo.svelte';
...
import { getUserCard, getRepoCard } from './api.js';
...
const getRepo = debounce(getRepoCard, 1000);

Далее, создадим вычисляемое свойство, которое будет определять какой тип карточки мы должны показывать:

computed: {
  ...
  repo: ({ username }) => username.includes('/'),
  ...
}

Теперь добавим переключалку запросов к API:

computed: {
  ...
  card: ({ username, repo }) => username && (repo ? getRepo : getUser)(username),
  ...
}

И напоследок, переключалка компонентов карточки в зависимости от типа:

computed: {
  ...
  Card: ({ repo }) => repo ? Repo : User,
  ...
}

Кроме того, чтобы динамически подменять компоненты, нам необходимо использовать специальный тэг Svelte, который отрисовывает тот компонент, значение которого передано в аттрибут this:

<svelte:component this={Card} {...card} />


Работает. Обратили внимание? Мы уже пишем на Svelte внутри React приложения! )))

Теперь давайте, научим наш виджет скрывать поле для ввода и попробуем вводить username не внутри виджета, а внутри React приложения. Мало ли откуда наша бизнес-логика будет получать это значение.

Введем новое свойство search, значение по-умолчания которого будет false. В зависимости от этого свойства, будет отображаться или не отображаться поле для ввода. По-умолчанию, соответственно, поля не будет.

{#if search}
<input bind:value=username placeholder="username or username/repo">
{/if}
...
<script>
  export default {
    ...
    data() {
      return {
        username: '',
	search: false
      };
    },
    ...
  };
</script>

Теперь в App.js создадим поле для ввода на стороне React приложения и напишем соответствующую обработку события ввода:

  ...
  handleUsername = (e) => {
    this.setState({ username: e.target.value });   
  }
  ...
          <h1>Github Widget for: {this.state.username}</h1>
          <input 
            value={this.state.username}
            onChange={this.handleUsername}
            className="Username" 
            placeholder="username or username/repo" 
          />

А еще копипастим в папку с виджетом вот такой вот svg спиннер на Svelte:

<svg
  height={size}
  width={size}
  style="animation-duration:{speed}ms;"
  class="svelte-spinner"
  viewbox="0 0 32 32"
>
  <circle
    role="presentation"
    cx="16"
    cy="16"
    r={radius}
    stroke={color}
    fill="none"
    stroke-width={thickness}
    stroke-dasharray="{dash},100"
    stroke-linecap="round"
  />
</svg>

<script>
  export default {
    data() {
      return {
        size: 25,
        speed: 750,
        color: 'rgba(0,0,0,0.4)',
        thickness: 2,
        gap: 40,
        radius: 10
      };
    },
    computed: {
      dash: ({radius, gap}) => 2 * Math.PI * radius * (100 - gap) / 100
    }
  };
</script>

<style>
    .svelte-spinner {
        transition-property: transform;
        animation-name: svelte-spinner_infinite-spin;
        animation-iteration-count: infinite;
        animation-timing-function: linear;
    }
    @keyframes svelte-spinner_infinite-spin {
        from { transform: rotate(0deg); }
        to { transform: rotate(360deg); }
    }
</style>

И применим его в виджете, чтобы было совсем красиво:

...
{#await card}
  <Spinner size="50" speed="750" color="#38b0ee" thickness="2" gap="40" />
{:then card}
...

По-моему, получилось очень даже не плохо:


Верхняя шапка с черным фоном и полем для ввода — это React приложение, белый блок снизу — Svelte виджет. Вот такие пироги. )))

> Репозиторий

Выводы


Svelte — отличный инструмент для разработки современных веб-приложений, основанных на компонентном подходе. Кроме того, на нем можно быстро и удобно писать переиспользуемые standalone UI компоненты и виджеты, которые могут быть использованы в любых веб-приложениях, даже совместно с другими фреймворками. Еще он прекрасно подходит для микрофронтендов.

Svelte прекрасно подойдет вам если:


  1. Вы хотите начать новый проект и не знаете какой фреймворк выбрать для этого.
  2. У вас есть проект, он работает и его лучше не трогать. Новые компоненты и модули, вы можете писать на Svelte и бесшовно интегрировать в существующий код.
  3. У вас уже есть проект, но он устарел частично или полностью и/или требует серьезного рефакторинга, вплоть до полного переписывания. Вы можете начать переписывать его по частям. При этом вам не нужно придумывать сложные конфигурации. Вы просто берете какой-то компонент, переписываете его на Svelte и оборачиваете новый компонент посредством старого. При этом остальные части приложения даже не догадываются об изменениях.
  4. У вас несколько проектов на разных кодовых базах и при этом, вам бы хотелось иметь единый UI-kit и использовать его в любом из этих проектов. Пишите UI-kit на Svelte и используйте его где угодно. Это приятно.

Хотите узнать больше интересных кейсов? Присоединяйтесь к нашему Telegram-каналу!

UPDATE: спасибо justboris за правильный вопрос. Продолжая пример:

import React, { PureComponent } from 'react';

import Widget from './Widget.svelte';

export default class extends PureComponent {

  componentDidMount() {
    ...    

    this.widget = new Widget({
      target: this.el,
      data: { username },
      slots: {
        default: this.slot
      }
    });

    ...
  }
  ...
  render() {
    return (
      <div ref={el => this.el = el}>
        <div ref={el => this.slot = el}>
          {this.props.children}
        </div>
      </div>
    );
  }
}


<GithubWidget onChange={this.handleChange}  username={this.state.username}>
  <p>Hello world</p>
</GithubWidget>




К сожалению, не доступен сервер mySQL