Что решают веб-фреймворки и как обойтись без них (часть 1)
Опубликовано: 2022-03-10Недавно меня очень заинтересовало сравнение фреймворков с ванильным JavaScript. Это началось после некоторого разочарования, которое я испытал, используя React в некоторых из моих внештатных проектов, и с моим недавним, более близким знакомством с веб-стандартами в качестве редактора спецификаций.
Мне было интересно посмотреть, каковы общие черты и различия между фреймворками , что может предложить веб-платформа в качестве более компактной альтернативы и достаточно ли этого. Моя цель не в том, чтобы критиковать фреймворки, а в том, чтобы понять затраты и выгоды, определить, существует ли альтернатива, и увидеть, можем ли мы извлечь из нее уроки, даже если мы решим использовать фреймворк.
В этой первой части я подробно расскажу о нескольких технических функциях, общих для разных фреймворков, и о том, как они реализованы в разных фреймворках. Я также рассмотрю стоимость использования этих фреймворков.
Фреймворки
Я выбрал для рассмотрения четыре фреймворка: React, доминирующий на сегодняшний день, и три новых претендента, которые утверждают, что делают что-то иначе, чем React.
- Реагировать
«React позволяет безболезненно создавать интерактивные пользовательские интерфейсы. Декларативные представления делают ваш код более предсказуемым и простым в отладке». - SolidJS
«Solid следует той же философии, что и React… Однако у него совершенно другая реализация, которая отказывается от использования виртуального DOM». - Стройный
«Svelte — это радикально новый подход к созданию пользовательских интерфейсов… этап компиляции, который происходит при создании приложения. Вместо использования таких методов, как сравнение виртуальных DOM, Svelte пишет код, который хирургическим путем обновляет DOM при изменении состояния вашего приложения». - горит
«Основываясь на стандартах веб-компонентов, Lit добавляет только… реактивность, декларативные шаблоны и несколько продуманных функций».
Подводя итог тому, что фреймворки говорят об их дифференциаторах:
- React упрощает создание пользовательских интерфейсов с помощью декларативных представлений.
- SolidJS следует философии React, но использует другую технику.
- Svelte использует подход к пользовательскому интерфейсу на этапе компиляции.
- Lit использует существующие стандарты с некоторыми дополнительными упрощенными функциями.
Какие фреймворки решают
В самих фреймворках упоминаются слова декларативный, реактивный и виртуальный DOM. Давайте углубимся в то, что они означают.
Декларативное программирование
Декларативное программирование — это парадигма, в которой логика определяется без указания потока управления. Мы описываем, каким должен быть результат, а не какие шаги приведут нас к нему.
На заре декларативных фреймворков, примерно в 2010 году, API-интерфейсы DOM были намного более простыми и многословными, а для написания веб-приложений с императивным JavaScript требовалось много шаблонного кода. Именно тогда концепция «модель-представление-представление» (MVVM) стала преобладающей с революционными на тот момент средами Knockout и AngularJS, обеспечивающими декларативный уровень JavaScript, который обрабатывал эту сложность внутри библиотеки.
MVVM сегодня не является широко используемым термином, и это своего рода разновидность более старого термина «привязка данных».
Привязка данных
Привязка данных — это декларативный способ выражения того, как данные синхронизируются между моделью и пользовательским интерфейсом.
Все популярные UI-фреймворки предоставляют ту или иную форму привязки данных, и их руководства начинаются с примера привязки данных.
Вот привязка данных в JSX (SolidJS и React):
function HelloWorld() { const name = "Solid or React"; return ( <div>Hello {name}!</div> ) }
Привязка данных в Лит:
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.
Логика
Когда фреймворк предоставляет декларативный интерфейс для привязки данных с реализацией реактивности, он также должен предоставить какой-то способ выражения некоторой логики, которая традиционно пишется в императивном режиме. Базовыми строительными блоками логики являются «если» и «для», и все основные фреймворки в той или иной степени выражают эти строительные блоки.
Условные
Помимо привязки основных данных, таких как числа и строки, каждый фреймворк предоставляет «условный» примитив. В 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. Это размер, который больше всего влияет на затраты ЦП на выполнение JavaScript.
- ReactDOM весит около 120 КБ.
- SolidJS весит около 18 КБ.
- Лит около 16 Кб.
- Svelte весит около 2 КБ, но размер сгенерированного кода варьируется.
Кажется, что сегодняшние фреймворки лучше, чем 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-интерфейсах веб-платформы; не всегда нужно идти с передовой.
Резюме
Мы углубились в понимание основных проблем, которые пытаются решить фреймворки, и того, как они решают их, сосредоточившись на привязке данных, реактивности, условных выражениях и списках. Мы также смотрели на стоимость.
Во второй части мы увидим, как можно решить эти проблемы вообще без использования фреймворка, и чему мы можем научиться из этого. Следите за обновлениями!
Особая благодарность за технические обзоры следующим лицам: Йехонатану Даниву, Тому Бигелайзену, Бенджамину Гринбауму, Нику Рибалу и Луи Лазарису.