Довольно странно, но до сих пор на Хабре нет ни одной статьи об этом, как минимум, очень интересном инструменте разработки. Возможно это связано с тем, что данный фреймворк довольно молод, также как и идея, лежащая в его основе. А может быть все дело в постоянном хайпе разжигаем вокруг «большой тройки» фронтенда и закрывающем обзор альтернативных решений. Точно не знаю, но постараюсь исправить данный просчет в этой статье. Не переключайтесь.
You can't write serious applications in vanilla JavaScript without hitting a complexity wall. But a compiler can do it for you.
— Rich Harris, 2016
Пролог
Зайду издалека. Примерно с 2013 года и до сих пор, я активно использую в своей работе RactiveJS — не слишком популярный, но развивающийся реактивный MVVM-фреймворк. В тот момент, когда Ractive появился, как мне кажется, он был весьма инновационен и до сих пор имеет наиболее удобный, на мой взгляд, API. Однако, как и любой другой инструмент, Ractive имеет свои минусы. Основными минусами для меня является его размер (~200Кб minified), а также не слишком высокая, по сравнению с конкурентами, скорость работы.
Относительно недавно, возникла необходимость написать довольно интерактивный виджет для сайтов (аля callback-виджет). Основные требования к виджету понятны: он должен весить мало и работать быстро. Учитывая минусы, который я описал выше, Ractive, а также большинство других JS фреймворков (Vue ~70Кб / React ~150Кб / etc.), не слишком хорошо подходят для этой задачи. Поэтому в какой-то момент даже возникла мысль писать виджет на ванильном JS, вообще без каких-либо фреймворков и библиотек. Однако мне повезло.
The magical disappearing UI framework
Итак, SvelteJS — JS фреймворк, который компилирует свой код в vanilla javascript на этапе сборки (AoT). В связи с чем у нас полностью исчезают накладные расходы связанные с самим фреймворком. Его создателем и основным мейнтейнером является Рич Харрис (Rich Harris), автор таких прикольных штук как Rollup, Buble, Roadtrip, а также того самого Ractive. В 2016 году Рич прекратил активную работу над Ractive, оставив его другим мейнтейнерам, ради того, чтобы развивать Svelte и в том же году написал вводную статью о своей новой разработке.
В своем новом «детище» Рич попытался переосмыслить то, зачем мы вообще используем фреймворки. Почему мы готовы отправлять в браузер пользователя сотни и тысячи килобайт javascript'а, облепившись кучей абстракций над нативным кодом, по сути, убивая производительность. Если вы не считаете это проблемой, почитайте блог Алекса Рассела (Alex Russell), там довольно много интересных мыслей и примеров.
Так вот, Рич, пришел к выводу, что основной задачей, которую решают фреймворки, является не уменьшение сложности вашего собственного кода, путем вынесения излишней сложности в код фреймворка, а структурирование ваших мыслей и путей решения задач. Иными словами, фреймворк как таковой нам не нужен. Нам нужен лишь способ писать приложения, руководствуясь каким-то подходом, например, компонентным. И нам нужен удобный API для этого. Так что, если фреймворк будет лишь средством писать код, а не самим кодом приложения?
Почему же Svelte идеально подошел для описанной задачи?
Когда вы пишете на React, Vue или Ractive, у вас есть код вашего приложения, а также код самого фреймворка, на котором основан код вашего приложения, без которого данный код работать априори не может. Итого мы имеем код фреймворка (в среднем 100кб) + код приложения, который собственно решает задачи приложения. Однако когда вы пишете на Svelte у вас есть только код вашего приложения, потому что Svelte — это прежде всего компилятор, который создает vanilla JS из кода, написанного с использованием Svelte API. Скомпилированный код решает только задачи вашего приложения и не имеет накладных расходов. Соответственно, JS бандл весит ровно столько, сколько весит код самого приложения.
Далее, Svelte быстр. На самом деле он быстр ровно настолько, насколько быстр сам Javascript. И это очевидно, потому что Svelte — это и есть vanilla javascript, который вам не нужно писать.
Короче говоря Svelte — это средство, писать vanilla JS без необходимости писать на vanilla JS. Вот такая вот тавтология. )))))
Возможности Svelte
Итак, на первый взгляд, все звучит довольно аппетитно, но нужно взглянуть поближе. Сделать это проще простого!
Svelte имеет наверное самую пологую кривую обучения из всех фреймворков, на которых мне доводилось писать. В моем случае, не понадобилось даже обучения, потому что API Svelte практически идентичен API Ractive, за исключением того, что Ractive все же более наворочен. Однако Svelte не отстает и имеет в арсенале пару классных фич, о которых я расскажу ниже.
Вся документация по Svelte располагается буквально на одной странице (15 min reading), также имеется готовый к использованию REPL с интерактивными примерами кода (включая 7GUIs).
Основа приложения на Svelte — это компоненты (surprise surprise). По своему виду и способу написания svelte-компоненты очень похожи на однофайловые компоненты Vue или теги Riot, да и вообще на то, что мы давно уже имеем практически во всех фреймворках, даже в Ractive. Обычно все это хозяйство основано на загрузчике (loader) для Webpack или Rollup. Со Svelte такая же история, есть svelte-loader и rollup-plugin-svelte. Также всегда можно скомпилировать компоненты в чистый JS с помощью svelte-cli.
Итак Svelte-компоненты — это просто html файл, который может иметь следующую структуру:
<!-- любая html разметка в качестве шаблона -->
<h1>Hello {{name}}!</h1>
<!-- component scoped стили -->
<style>
h1 { color: red; }
</style>
<!-- собственное код компонента, аля ES6 модули -->
<script>
export default {
};
</script>
Ни один из элементов не является обязательным. Поэтому простой stepper-компонент можно написать так, вообще без script-тега:
<!-- stepper.html -->
<button on:click="set({ count: count - 1 })">-</button>
<input bind:value="count" readonly />
<button on:click="set({ count: count + 1 })">+</button>
Сам по себе Svelte крайне минималистичный. Однако при этом в нем фактически присутствует все необходимое для разработки веб-приложений.
Components & composition
Императивное создание инстанса компонента выглядит так:
import MyComponent from './MyComponent.html';
const component = new MyComponent({
target: document.querySelector( 'main' ),
data: {}
});
Естественно есть композиция компонентов и передача значений через аттрибуты:
<div class='widget-container'>
<Widget foo="static" bar="{{dynamic}}" />
</div>
<script>
import Widget from './Widget.html';
export default {
data () {
return {
dynamic: 'this can change'
}
},
components: {
Widget
}
};
</script>
Также имеется инъекция разметки с помощью слотов (привет Vue):
// Box component
<div class='box'>
<slot><!-- content is injected here --></slot>
</div>
// parent component
<Box>
<h2>Hello!</h2>
<p>This is a box. It can contain anything.</p>
</Box>
Data & computed props
Svelte-компоненты имеют внутренний стейт, который описывается в проперти «data» и должен быть POJO-объектом. Также как Ractive или Vue, Svelte-компонент может иметь вычисляемые свойства (computed props) зависящие от других реактивных данных:
<p>
The time is
<strong>{{hours}}:{{minutes}}:{{seconds}}</strong>
</p>
<script>
export default {
data () {
return {
time: new Date()
};
},
computed: {
hours: time => time.getHours(),
minutes: time => time.getMinutes(),
seconds: time => time.getSeconds()
}
};
</script>
Единственный минус вычисляемых свойств в Svelte — отсутствие возможности задать setter для них, т.е. вычисляемые свойства работают только на получение значения.
Для работы со стейтом инстанс компонента Svelte имеет встроенные методы:
component.set({ time: new Date() });
component.get('time');
При изменении данных компонента с помощью метода set(), изменение реактивно распространяется на все зависящие вычисляемые свойства и DOM.
Observers & Lifecycle hooks
Как я уже говорил, Svelte минималистичен, поэтому предоставляет всего 2 lifecycle хука: oncreate и ondestroy. Изменение данных, мы можем отслеживать с помощью наблюдателей:
<script>
export default {
oncreate () {
const observer = this.observe('foo', (newVal, oldVal) => {});
this.on('destroy', () => observer.cancel());
}
};
</script>
Events
Svelte имеет полноценную систему прокси-событий (DOM Events proxy) и кастомных событий (Custom Events), которые также могут быть применены к компонентам:
<!-- CategoryChooser.html -->
<p>Select a category:</p>
{{#each categories as category}}
<button on:click="fire('select', { category })">select {{category}}</button>
{{/each}}
<script>
export default {
data() {
return {
categories: [
'animal',
'vegetable',
'mineral'
]
}
}
};
<!-- parent component -->
<CategoryChooser on:select="doSomething(event.category)"/>
</script>
Здесь мы подписываемся на клик по кнопке, преобразуем DOM событие клика в кастомное событие компонента «onselect». Далее, родительский компонент может подписываться на событие «onselect» данного компонента. Короче говоря кастомные события также умеют «всплывать» (bubbling). Все как обычно.)))
API для работы с событиями также минималистично:
const listener = component.on( 'thingHappened', event => {
console.log( `A thing happened: ${event.thing}` );
});
// some time later..
listener.cancel();
component.fire( 'thingHappened', {
thing: 'this event was fired'
});
Templating & directives
Синтаксис шаблонов Svelte очень напоминает «усы», однако по факту ими не является:
<!-- значения могут использоваться как в виде текста, так и внутри аттрибутов -->
<h1 style="color: {{color}};">{{color}}</h1>
<p hidden="{{hideParagraph}}">You can hide this paragraph.</p>
<!-- поддерживаются условия -->
{{#if user.loggedIn}}
<a href='/logout'>log out</a>
{{else}}
<a href='/login'>log in</a>
{{/if}}
<!-- а также циклы -->
<ul>
{{#each list as item}}
<li>{{item.title}}</li>
{{/each}}
</ul>
В Svelte нет кастомных директив (во всяком случае пока), однако имеется несколько видов директив «из коробки»:
<!-- Event handlers -->
<button on:click="set({ count: count + 1 })">+1</button>
<!-- Two-way binding -->
<input bind:value="count" />
<!-- Refs (like Vue) -->
<canvas ref:canvas width="200" height="200"></canvas>
<!-- Transitions -->
<div transition:fly="{y:20}">hello!</div>
Не уверен, что кастомные директивы вообще появятся. Все же похоже что этот «AngularJS-style» подход потихоньку себя изживает.
Custom methods & helpers
Методы инстанса компонента и хелпер-функции для шаблонов можно определить так:
<script>
export default {
helpers: {
toUppeCase: (str) => {}
},
methods: {
toast: (msg) => {}
}
};
</script>
Plugins
Судя по всему в Svelte есть какие-то наработки по системе плагинов. Правда документация пока умалчивает об этом. Те же Transitions реализованы в виде отдельных модулей и собраны в отдельный пакет svelte-transitions. Очевидно можно писать свои собственные переходы.
Тоже есть отдельный пакет с дополнительными методами для инстанса компонента (svelte-extras), которые можно подгружать отдельно, приближая API Svelte к тому же Ractive:
ractive.set('list.3.name', 'Rich');
// базоый set() не умеет устанавливать вложенные значения напрямую
svelte.setDeep('list.3.name', 'Rich');
Вообще система плагинов пока не слишком развита или во всяком случае описана в документации. Ждемс.
SSR
Серверный рендеринг также имеется, правда работает он довольно странно, по сравнению с тем же Ractive:
// просто и не принужденно в Ractive
const html = ractive.toHTML();
// немного более замороченно в Svelte
require( 'svelte/ssr/register' );
const html = svelte.render( data );
// css также можно отрендерить на сервере
const styles = svelte.renderCss();
State management
У Svelte есть своя интерпретация механизма хранилища, которая с одной стороны имеет некоторые сходства с Redux/Vuex, однако отличий все же больше. Подробнее можно почитать в документации, однако основное отличие заключается в том, что Svelte Store делает акцент не на механизме изменения стейта (иммутабильность, one-way data flow и все такое), а на механизме обмена (шеринга) стейта между компонентами. К примеру, использование «store» вместо «data» в компоненте верхнего уровня приведет к тому, что данные хранилища будут автоматически доступны всем child-компонентам. Честно говоря, мне не довелось еще попользовать этот механизм, но насколько я понимаю, это что-то вроде глобального стейта в пределах одной иерархии компонентов, а не всего приложения.
Также в Svelte Store нет никаких экшенов и коммитов. Однако, если такой механизм управления доступом необходим, разрешается расширять класс Store, для имплементации подобной логики.
Бонус
Выше я кратко описал основные возможности Svelte. Учитывая его минималистичность, а также фактически встроенный «tree-shaking» (компилируется всегда только то, что реально используется), возможности Svelte выглядят весьма не плохо.
Однако, есть еще несколько прикольный штук, которым стоит уделить отдельное внимание.
Async support
Начну с моего любимого — нативная поддержка асинхронных значений на основе промиссов. Выглядит это так:
{{#await promise}}
<p>loading...</p>
{{then data}}
<p>got data: {{data.value}}</p>
{{catch err}}
<p>whoops! {{err.message}}</p>
{{/await}}
Это офигенно упрощает жизнь. В Ractive мы тоже можем делать подобные штуки, правда для этого приходится пользоваться специальным Promise-adaptor. Svelte дает это из коробки.
Namespaces
Штука которую пока нигде больше не видел — с помощью неймспейсов можно определять область применения компонента. Например, если наш компонент применим только для использования внутри svg-тега, мы можем так и указать:
<g transform='translate({{x}},{{y}}) scale({{size / 366.5}})'>
<circle r="336.5" fill="{{fill}}"/>
<path d="m-41.5 298.5c-121-21-194-115-212-233v-8l-25-1-1-18h481c6 13 10 27 13 41 13 94-38 146-114 193-45 23-93 29-142 26z"/>
</g>
<script>
export default {
namespace: 'svg'
};
</script>
Интересная возможность. Пока не до конца ясно, насколько это реально полезно, однако определенно есть перспективы.
<:Self> tags
Также необычная фича — рекурсивное включение компонента:
{{#if countdown > 0}}
<p>{{countdown}}</p>
<:Self countdown='{{countdown - 1}}'/>
{{else}}
<p>liftoff!</p>
{{/if}}
Довольно неожиданная реализация countdown счетчика, не правда ли? )))
<:Window> tags
Весьма сомнительная фича, но тоже нигде больше не встречал. Данный вид тега позволяет декларативно навешивать события на window:
<:Window on:keydown='set({ key: event.key, keyCode: event.keyCode })'/>
Sapper
Об этом проекте узнал буквально вчера. Проект пока не более чем наброски идей, однако похоже что Рич собирается делать что-то вроде аналога Next.js/Nuxt.js. Уверен, что если все получится, аналог будет сделан с присущей Ричу долей индивидуальности.
Эпилог
Спасибо всем тем, кто дочитал до конца. Как говориться, ставьте лайки и подписывайтесь на мой канал и да прибудет с вами Сила!
Если серьезно, надеюсь мне удалось в общих чертах рассказать о таком интересном явлении как Svelte. От себя могу добавить, что использование Svelte в уже двух проектах полностью себя оправдало. Лично мне немного не хватает богатства возможностей Ractive, однако минималистичность, а также сам принцип работы Svelte сильно подкупает использовать его и дальше. Сейчас серьезно подумываю заюзать его в новом, довольно крупном проекте и со временем возможно полностью перебраться с Ractive на Svelte.
Надеюсь вы тоже откроете для себя Svelte. Удачи!
Автор: PaulMaly