Головна / Статті

Одне запитання до Todo, дві архітектури: що 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-додатку, та коли краще використовувати більш потужну клієнтську фреймворк. Для більш повного порівняння hypermedia з стандартом 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 КБ, а час створення інтерактивного інтерфейсу на телефоні середнього класу зменшився з майже трьох секунд до менше ніж однієї секунди. Кількість запитів типу pull request зменшилась через меншу кількість коду для перевірки, а нові розробники могли ознайомитися з проектом протягом одного дня, оскільки кодова база складалася переважно з HTML із кількома додатковими атрибутами.

Розглядайте ці цифри як ілюстрацію, а не як стандарт оцінки. Вони походять з одного проекту, виміряного неформально на одному пристрої та в мережі. Напрямок цього ефекту є правдоподібним для будь-якого додатку, де більша частина клієнтського коду слугує лише для передачі даних між JSON та DOM, але вимірюйте свій власний додаток до та після змін, замість того щоб припускати однакові результати.

Коли htmx — не правильний інструмент

Це не є аргументом на користь видалення React з усіх проектів. Додатки з справді складною взаємодією з клієнтом, такі як інструменти дизайну, електронні таблиці чи редактори для співпраці в реальному часі, потребують значного обсягу даних на клієнті та фреймворку, створеного для їх керування. Примус до використання моделі запитів та обміну даними призведе до постійних проблем.

Швидкий спосіб вирішити це питання — поставити кілька запитань щодо функціоналу, який у вас є:

  • Чи потрібна швидка відповідь від системи, швидша за час передачі даних через мережу, наприклад під час перетягування елементів, малювання чи введення тексту з миттєвою зворотною відповіддю?
  • Чи має цей функціонал працювати офлайн чи синхронізувати зміни кількох користувачів у реальному часі?
  • Чи потрібно клієнту зберігати та обробляти велику кількість даних локально?

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

Підсумок

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

Вивчення React залишається розумним кроком у кар’єрі, і багато проектів потребують цього інструменту. Це виправдовує необхідність знання цього інструменту; проте це не виправдовує його використання у кожному додатку, чиїми справжніми потребами є форма та таблиця.