Главная / Статьи

Одна задача, две архитектуры: что удаляет htmx из стека CRUD

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

Издатель: REACTAPP.TOP6 мин чтения1479 слов

Источники: Stop Over-Engineering CRUD Apps: Why HTMX Might Be All You Need

Большинство внутренних инструментов и панелей администрирования выполняют те же четыре действия: создание, чтение, обновление и удаление записей. Однако стандартный способ их создания превратился в полноценное одностраничное приложение с фреймворком компонентов, менеджером состояния, библиотекой для загрузки данных, JSON-эндпоинтами и клиентским кодом, основной задачей которого является преобразование этих данных из JSON обратно в HTML. В процессе разработки форма для добавления одной строки в базу данных оказывается зависимой от полудюжины пакетов и двух инструментов сборки.

В этой статье рассматривается одна конкретная функция — добавление элемента в список задач — как с использованием React, так и htmx. Сравнение процесса обработки запросов позволяет легко определить, какие компоненты действительно необходимы в вашем собственном приложении типа CRUD, и когда более мощная клиентская фреймворк-среда остается наиболее подходящим решением. Для более полного сравнения хайпермедиа и стандартных решений SPA смотрите htmx против стандартных решений SPA.

Требования, которые на самом деле имеют большинство приложений типа CRUD

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

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

Как htmx меняет взаимодействие с сервером

htmx позволяет обычным элементам HTML отправлять HTTP-запросы с помощью атрибутов. Сервер отвечает фрагментом HTML вместо JSON, и htmx вставляет этот фрагмент на странице в указанное место.

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

Та же функция в обоих стилях

Версия на React

Вот традиционный компонент React для добавления задачи. Он хранит в состоянии текст ввода и список, отправляет JSON-данные на API, парсит ответ, добавляет новый элемент и очищает поле ввода.

function AddTodo() {
  const [text, setText] = useState("")
  const [todos, setTodos] = useState([])

const addTodo = async () => {
    const res = await fetch("/api/todos", {
      method: "POST",
      body: JSON.stringify({ text }),
    })
    const todo = await res.json()
    setTodos([...todos, todo])
    setText("")
  }
  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(t => <li key={t.id}>{t.text}</li>)}
      </ul>
    </div>
  )
}

Даже в этом небольшом примере есть детали, которые необходимо соблюсти правильно. Запрос отправляет тело в формате JSON без заголовка Content-Type, из-за чего многие серверы не будут интерпретировать его как JSON. Функция обновления setTodos([...todos, todo]) зачитывает значение todos из того состояния, в котором произошла кликовка, поэтому два быстрых запроса могут привести к потере элемента; функциональная версия setTodos(prev => [...prev, todo]) избегает этой проблемы. Кроме того, список содержит только элементы, добавленные в текущей сессии, поскольку загрузка уже существующих элементов потребовала бы дополнительного эффекта и конечной точки.

Версия htmx

Эквивалентом в htmx является форма и пустой список.

<form hx-post="/todos" hx-target="#list" hx-swap="beforeend">
  <input name="text" />
  <button>Add</button>
</form>

<ul id="list"></ul>

Три атрибута определяют поведение. hx-post="/todos" отправляет поля формы на указанный URL при её отправке. hx-target="#list" указывает место, куда будет отправлен ответ, а hx-swap="beforeend" вставляет его внутрь этого элемента после существующих дочерних элементов, вместо замены его содержимого. Кнопке не нужен обработчик, поскольку кнопка внутри формы автоматически отправляет её при нажатии.

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

@app.route("/todos", methods=["POST"])
def add_todo():
    text = request.form["text"]
    todo = save_todo(text)
    return render_template("todo_row.html", todo=todo)

Это весь фронтенд для этой функции: нет хуков, нет состояния клиента и нет поведения перерисовки для отладки поздно ночью. Шаблон todo_row.html просто отображает один элемент <li> для новой записи. Поскольку тот же шаблон может использоваться при первой отрисовке страницы, исходный список и новые строки делят один и тот же маркап, вместо того чтобы использовать отдельный компонент JSX и серверскую визуализацию, которые необходимо синхронизировать.

Для реальных развертываний существует несколько практических замечаний. Запросы, изменяющие состояние, по-прежнему требуют защиты от CSRF, обычно путем включения токена в качестве скрытого поля или заголовка запроса. Кроме того, если вы также укажете форме обычные атрибуты action и method, она будет продолжать работать как обычная отправка формы, даже если JavaScript не удастся загрузить.

Отслеживание двух жизненных циклов запроса

Разница становится наиболее очевидной, когда вы пошагово отрисовываете каждый этап работы. Сначала — маршрут SPA.

Browser                    Server
--------                   --------
Click button
   |
   v
JS event handler
   |
   v
fetch() call  ------->  API endpoint
                              |
                              v
                         Query database
                              |
                              v
                         Return JSON
   ^                          |
   |__________________________|
   |
   v
Parse JSON
   |
   v
Update state
   |
   v
Re-render component
   |
   v
Diff virtual DOM
   |
   v
Patch real DOM

После того как база данных возвращает JSON, клиенту всё равно приходится его парсить, обновлять состояние, перерисовывать интерфейс, сравнивать изменения и вносить правки в DOM.

Теперь — маршрут htmx для того же клика.

Browser                    Server
--------                   --------
Click button
   |
   v
htmx request  ------->  Route handler
                              |
                              v
                         Query database
                              |
                              v
                         Render HTML piece
   ^                          |
   |__________________________|
   |
   v
Swap HTML into page

Несколько этапов просто исчезают: парсинг JSON на стороне клиента, обновление состояния, сравнение изменений, а также любые усилия по поддержанию согласованности копии данных у клиента. Всё это время эти данные принадлежали базе данных на сервере. Подход htmx отказывается от поддержания второй, клиентской версии этих данных.

Какие изменения происходят на практике

В одном из описанных случаев миграции административного проекта клиента замена фронтенда на React на шаблоны, генерируемые на сервере, в сочетании с htmx сократила размер пакета JavaScript примерно с 340 КБ до 12 КБ, в то время как время старта интерактивной работы на смартфоне среднего класса сократилось с почти трех секунд до менее одной секунды. Количество пул-запросов уменьшилось из-за меньшего объема кода для ревью, а новые разработчики могли освоить проект уже к концу дня, поскольку кодовая база состояла в основном из HTML с несколькими дополнительными атрибутами.

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

Когда htmx — не подходящий инструмент

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

Быстрый способ принять решение — задать несколько вопросов о функционале, с которым вы сталкиваетесь:

  • Требуется ли такое взаимодействие, чтобы реагировать быстрее, чем за один обход сети, например при перетаскивании элементов, рисовании или вводе текста с мгновенной обратной связью?
  • Должно ли приложение работать офлайн или синхронизировать изменения нескольких пользователей в реальном времени?
  • Нужно ли клиенту хранить и обрабатывать большое количество данных локально?

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

Заключение

  • Выбирайте инструменты в соответствии с требованиями; простые решения типа CRUD редко нуждаются в состоянии на стороне клиента.
  • С использованием htmx сервер генерирует фрагменты, а атрибуты определяют их размещение, что устраняет необходимость в преобразовании данных в JSON и дублировании источника информации.
  • Сохраняйте защиту от CSRF, настоящие атрибуты action и общие шаблоны строк.
  • Резервируйте полноценные клиентские фреймворки для взаимодействий, требующих значительных ресурсов с клиентской стороны.

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