Зачем нужны веб-фреймворки и как обойтись без них

Источник: «What Web Frameworks Solve And How To Do Without Them (Part 1)»
В этой статье подробно рассматривается несколько технических возможностей, которые являются общими для всех фреймворков. Объясняется как они реализуются в различных фреймворках и какова стоимость их применения.

Недавно меня сильно заинтересовало сравнение фреймворков с ванильным JavaScript. Это началось после некоторого разочарования, которое я испытал, используя React в некоторых из моих внештатных проектов. И с моим недавним, более близким знакомством с веб-стандартами в качестве редактора спецификаций.

Мне было интересно узнать, в чём сходства и различия между фреймворками. Что может предложить веб-платформа в качестве более компактной альтернативы и достаточно ли этого. Моя цель не в том, чтобы критиковать фреймворки, а в том, чтобы понять затраты и выгоды. Определить, существует ли альтернатива и увидеть, можем ли мы извлечь из неё уроки, даже если мы решили использовать фреймворк.

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

Фреймворки

Я выбрал четыре фреймворка: React — доминирующий, на текущий момент, и три новых претендента утверждающих, что делают разные вещи иначе, чем React.

Подводя итог тому, что фреймворки говорят о своих дифференциаторах:

Что фреймворки делают

В самих фреймворках упоминаются слова декларативный, реактивный и виртуальный DOM. Давайте разберёмся, что они означают.

Декларативное программирование

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

На заре декларативных фреймворков, примерно в 2010 году, API-интерфейсы DOM были намного более простыми и многословными, а для написания веб-приложений с императивным JavaScript требовалось много шаблонного кода. Именно тогда концепция "Model-View-ViewModel" (MVVM) стала распространённой с новаторскими фреймворками Knockout и AngularJS, представляющими декларативный уровень JavaScript, который справлялся с этой сложностью внутри библиотек.

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

Привязка данных

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

Все популярные UI-фреймворки представляют ту или иную форму привязки данных, и их руководства начинаются с примера привязки данных.

Вот привязка данных в JSX (SolidJS и React):

function HelloWorld() {
const name = "Solid or React";

return (
<div>Hello {name}!</div>
)
}

Привязка данных в Lit:

class HelloWorld extends LitElement {
@property()
name = 'lit';

render() {
return html`<p>Hello ${this.name}!</p>`;
}
}

Привязка данных в Svelte:

<script>
let name = 'world';
</script>

<h1>Hello {name}!</h1>

Реактивность

Реактивность — декларативный способ выражения распространения изменений.

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

Движок React сравнивает результаты рендеринга с предыдущим результатом и при меняет разницу к DOM. Этот способ распространения изменений называется виртуальный DOM.

В SolidJS это делается более явно, с его хранилищем и встроенными элементами. Например, элемент Show будет отслеживать внутренние изменения, а не виртуальны DOM.

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

В Lit реактивность достигается с помощью свойств элемента, в основном полагаясь на встроенную реактивность пользовательских элементов HTML.

Логика

Когда фреймворк предоставляет декларативный интерфейс для привязки данных с реализацией реактивности, он также должен предоставить какой-то способ выражения некоторой логики, которая традиционно пишется в императивно. Базовыми строительными блоками логики являются if и for", и все основные фреймворки в той или иной степени предоставляют эти строительные блоки.

Условные выражения

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

const [hasError, setHasError] = useState(false);
return hasError ? <label>Message</label> : null;

setHasError(true);

SolidJS предоставляет встроенный условный компонент Show:

<Show when={state.error}>
<label>Message</label>
</Show>

Svelte предоставляет директиву #if:

{#if state.error}
<label>Message</label>
{/if}

В Lit вы можете использовать тернарную операцию в функции render:

render(){
return this.error ? html`<label>Message</label>`: null;
}

Списки

Другим распространённым примитивом фреймворка является обработка списка. Списки являются ключевой часть пользовательского интерфейса — список контактов, уведомлений и т.д. И для эффективной работы они должны быть реактивными, а не обновлять весь список при изменении одного элемента.

В React обработка списка выглядит так:

contacts.map((contact, index) =>
<li key={index}>
{contact.name}
</li>)

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

В SolidJS используются встроенные элементы for и index:

<For each={state.contacts}>
{contact => <DIV>{contact.name}</DIV> }
</For>

Внутри SolidJS используется собственной хранилище в сочетании с for и index, чтобы решить, какие элементы обновлять при изменении элементов списка. Он более понятен, чем React, что позволяет избежать сложности виртуального DOM.

Svelte используете директиву each, которая переносится на основе своих обновлений:

{#each contacts as contact}
<div>{contact.name}</div>
{/each}

Lit предоставляет функцию repeat, которая работает аналогично отображению списка на основе key в React:

repeat(contacts, contact => contact.id,
(contact, index) => html`<div>${contact.name }</div>`

Компонентная модель

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

Примечание: Это большая тема, и я надеюсь охватить её в будущей статье, потому что эта статья станет слишком длинной. :)

Цена

Фреймворки обеспечивают декларативную привязку данных, примитивы потока управления (условные выражения и списки) и реактивный механизм распространения изменений.

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

Фреймворки полезны? Да. Они дают нам эти удобные функции. Но правильно ли задавать этот вопрос? Использование фреймворка сопряжено с определёнными затратами. Давайте посмотрим какова цена их использования.

Размер пакета

При взгляде на размеры пакета, мне нравится смотреть на минимизированные не сжатый Gzip размер. Этот размер, является наиболее актуальным для вычисления стоимости затрат CPU на выполнение JavaScript.

Похоже, что сегодняшние фреймворки справляются с задачей сохранения небольшого размера лучше, чем React. Виртуальный DOM требует большое количество JavaScript кода.

Сборка

Каким-то образом мы привыкли собирать наши веб-приложения. Невозможно начать фронтенд проект без настройки Node.js и бандлера, такого как Webpack, имеющего дело с некоторыми недавними изменениями конфигурации в стартовом пакете Babel-TypeScript и всем этим джазом.

Чем выразительнее и меньше разер пакета, тем больше нагрузка на инструменты сборки и время транспиляции.

Svelte утверждает, что виртуальный DOM — это чистые накладные расходы. Я согласен, но, возможно сборка (как в случае с Svelte и SolidJS) и пользовательские механизмы шаблонов на стороне клиента (как в случае с Lit) также являются чистыми накладными расходами другого рода?

Отладка

Со сборкой и транспиляцией появляется новый тип расходов.

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

В React вызов стека никогда не бывает вашим — React обрабатывает события за вас. Это отлично работает когда нет ошибок. Но попробуйте определить причину бесконечного цикла повторной отрисовки и вас ждёт мир боли.

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

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

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

Обновления

В этой статье я рассмотрел четыре фреймворка, но их гораздо больше (AngularJS, Ember.js, и Vue.js, ещё несколько из них). Можете ли вы рассчитывать на то, что фреймворк, его разработчики, и его экосистема будут работать на вас по мере его развития?

Одна вещь которая вызывает большее разочарование, чем исправление ваших ошибок — поиск обходных путей ошибок фреймворка. И ещё одна вещь, которая расстраивает сильнее ошибок фреймворка — ошибки возникающие после обновления фреймворка без изменений вашего кода.

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

Итог

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

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

Дополнительные материалы

Предыдущая Статья

JavaScript: Проверка формы с Constraint Validation API

Следующая Статья

Зачем нужны веб-фреймворки. Ванильная Альтернатива.