docs: фреймворком приложения выбран Vue 3 с роутером 5 и сборкой Vite
- заведены записка разведки docs/research/spa-framework.md со сравнением Svelte, Vue и React на одном экране и решение ADR-2026-08-11-spa-on-vue - docs/conventions/web-ui.md переписан под Vue: компоненты, маршруты, состояние, обращение к API и показ ошибок - закрыт вопрос «Приложение» в docs/architecture.md, уточнена задача spa-skeleton
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
# Приложение пишем на Vue, а Node входит в гейт и в образ
|
||||||
|
|
||||||
|
- **Дата:** 2026-08-11
|
||||||
|
- **Источник:** [../research/spa-framework.md](../research/spa-framework.md) —
|
||||||
|
записка разведки `spa-framework-choice`
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Приложение пишем на **Vue 3** с роутером пятой версии и собираем **Vite** в
|
||||||
|
статику, которую бинарник вшивает через `go:embed` и раздаёт сам. Маршруты
|
||||||
|
задаём своей таблицей через `createRouter`; сборочную надстройку роутера под
|
||||||
|
маршруты по файлам не включаем.
|
||||||
|
|
||||||
|
Вместе с этим в проект входит **шаг сборки статики**: Node и `npm` становятся
|
||||||
|
нужны на машине разработчика, отдельным шагом в `task gate` и слоем сборки в
|
||||||
|
`Dockerfile`.
|
||||||
|
|
||||||
|
Это два решения, а не одно, но принимаются они вместе: шаг сборки появляется при
|
||||||
|
любом из трёх кандидатов, и отдельно от выбора фреймворка его обсуждать не о чем.
|
||||||
|
|
||||||
|
## Почему Vue
|
||||||
|
|
||||||
|
Разведка мерила три кандидата на одном и том же экране и нашла единственное
|
||||||
|
различие, которое расходится в разы:
|
||||||
|
|
||||||
|
> Различает единственное — **размер того, что скачивает телефон**, и он
|
||||||
|
> расходится вчетверо.
|
||||||
|
|
||||||
|
Вшивание в бинарник, цена шага сборки в гейте и установка на телефон у всех трёх
|
||||||
|
оказались одинаковыми и потому ничего не решают.
|
||||||
|
|
||||||
|
По размеру Vue стоит посередине — 33 326 Б на четыре экрана против 17 314 у
|
||||||
|
Svelte и 72 402 у React. Выбран он не по этому числу, а по устойчивости
|
||||||
|
экосистемы, и оба отвергнутых кандидата отвергнуты с названной ценой:
|
||||||
|
|
||||||
|
> **Svelte** — легче Vue вдвое, но своего роутера не имеет, а тот, что есть,
|
||||||
|
> держит один человек. Владелец выбрал экосистему, которая переживёт проект, а не
|
||||||
|
> минимальный размер: 33 КБ на телефоне не отличаются от 17 КБ на глаз, а
|
||||||
|
> брошенная зависимость отличается.
|
||||||
|
>
|
||||||
|
> **React** — вчетверо тяжелее Svelte и вдвое тяжелее Vue, а взамен даёт
|
||||||
|
> экосистему, которой приложению на четыре экрана не на что потратиться.
|
||||||
|
|
||||||
|
Роутер берём пятой версии, а не четвёртой, по тому же доводу: она стабильна с
|
||||||
|
29 января 2026 и несёт метку `latest`, то есть чинить будут её, а не
|
||||||
|
предшественницу. Её сборочная надстройка добавляет 34 пакета в установку, и это
|
||||||
|
принятая цена; на собранный файл она не влияет и необязательна.
|
||||||
|
|
||||||
|
## Почему это ADR
|
||||||
|
|
||||||
|
Запись проходит триггер **дорогим откатом**: переход на другой фреймворк
|
||||||
|
переписывает все экраны разом, а не один файл. Шаг сборки сюда же — он меняет
|
||||||
|
требования к машине разработчика и к образу, и снять его потом можно только
|
||||||
|
вместе с приложением.
|
||||||
|
|
||||||
|
Прежнего решения запись не пересматривает: htmx был снят решением о SPA от
|
||||||
|
2026-08-10, до заведения этого журнала, и парного статуса «заменено на» ставить
|
||||||
|
нечему.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` разметка отделена от кода однофайловым компонентом, а роутер и хранилище
|
||||||
|
состояния идут из тех же рук, что и сам фреймворк: третьей библиотеки под них
|
||||||
|
заводить не нужно.
|
||||||
|
- `+` собранная статика — три файла и значок, поэтому `go:embed` берёт каталог
|
||||||
|
обычной строкой, а бинарник остаётся самодостаточным.
|
||||||
|
- `−` **гейт перестаёт зависеть только от Go.** Красный шаг сборки статики
|
||||||
|
становится таким же поводом остановиться, как красный `go build`, а машина
|
||||||
|
разработчика получает второе требуемое окружение сверх `ffmpeg`.
|
||||||
|
- `−` **в образ добавляется слой Node** ради шага, результат которого — три
|
||||||
|
файла; насколько дольше собирается образ и насколько тяжелеет, не замерялось.
|
||||||
|
- `−` в проект приходит `node_modules` на 92 МБ и 84 пакета, за которыми надо
|
||||||
|
следить отдельно от зависимостей Go: `gitleaks` и `golangci-lint` про них
|
||||||
|
ничего не знают.
|
||||||
|
- `−` приложение весит 33 КБ там, где на Svelte весило бы 17. Разница куплена
|
||||||
|
сознательно и обратно не отыгрывается.
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
| Дата | Запись | Статус |
|
| Дата | Запись | Статус |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2026-08-11 | [Приложение пишем на Vue, а Node входит в гейт и в образ](ADR-2026-08-11-spa-on-vue.md) | |
|
||||||
| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | |
|
| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | |
|
||||||
| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | |
|
| 2026-08-11 | [Хранилище, файлы и вход переезжают в PocketBase](ADR-2026-08-11-pocketbase-storage-with-admin-panel.md) | |
|
||||||
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
|
| 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | |
|
||||||
|
|||||||
@@ -134,11 +134,13 @@
|
|||||||
администратора при этом Authelia не закрывает: у неё свой пароль
|
администратора при этом Authelia не закрывает: у неё свой пароль
|
||||||
суперпользователя.
|
суперпользователя.
|
||||||
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
||||||
устанавливаемое на телефон; фреймворк выбирает разведка
|
устанавливаемое на телефон, а фреймворком взят Vue 3 с роутером пятой версии и
|
||||||
`spa-framework-choice`, и до её итога
|
сборкой Vite — 2026-08-11,
|
||||||
[conventions/web-ui.md](conventions/web-ui.md) стоит почти пустой. Шаг сборки
|
[ADR](adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в
|
||||||
фронтенда меняет требования к машине разработчика и к образу — решение уровня
|
[research/spa-framework.md](research/spa-framework.md). Тем же решением Node
|
||||||
ADR.
|
входит в гейт и слоем в сборку образа. Пишет это `spa-skeleton`; во что
|
||||||
|
обходится слой Node в образе, не замерялось. Не решено, брать ли готовый набор
|
||||||
|
компонентов.
|
||||||
- **Уведомления.** Пользователь веба узнаёт о готовности только опросом.
|
- **Уведомления.** Пользователь веба узнаёт о готовности только опросом.
|
||||||
Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web
|
Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web
|
||||||
Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и
|
Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и
|
||||||
|
|||||||
@@ -39,10 +39,9 @@ htmx, а здесь решено делать SPA — и перенесённы
|
|||||||
`0600`, самодокументируемый `config.dist.toml`, проверка на старте.
|
`0600`, самодокументируемый `config.dist.toml`, проверка на старте.
|
||||||
- [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT
|
- [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT
|
||||||
ULID, разбор на входной границе, естественные ключи у деталей.
|
ULID, разбор на входной границе, естественные ключи у деталей.
|
||||||
- [web-ui.md](web-ui.md) — веб-UI: что решено про приложение (SPA, установка на
|
- [web-ui.md](web-ui.md) — веб-UI: Vue 3 с Vite и статикой в бинарнике,
|
||||||
телефон, статика в бинарнике) и что ждёт выбора фреймворка. **Почти пуста:**
|
однофайловые компоненты, таблица маршрутов, состояние в экране, одна обёртка
|
||||||
редакция на htmx снята 2026-08-10 вместе со сменой решения, а новую писать не
|
над `fetch`, показ ошибок и состояний списка.
|
||||||
под что до разведки `spa-framework-choice`.
|
|
||||||
|
|
||||||
## Механизировано
|
## Механизировано
|
||||||
|
|
||||||
|
|||||||
+75
-12
@@ -4,19 +4,25 @@
|
|||||||
спецификация поведения — что именно приложение показывает и какие действия
|
спецификация поведения — что именно приложение показывает и какие действия
|
||||||
обязано поддерживать, живёт в спеке OpenSpec.
|
обязано поддерживать, живёт в спеке OpenSpec.
|
||||||
|
|
||||||
**Фреймворк не выбран, и до выбора эта конвенция пуста.** Здесь стоит только то,
|
Фреймворк выбран 2026-08-11 разведкой `spa-framework-choice`:
|
||||||
что решено и от фреймворка не зависит; всё остальное появится по итогу разведки
|
[ADR](../adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в
|
||||||
`spa-framework-choice`, которая же заведёт запись в `research/` и ADR.
|
[research/spa-framework.md](../research/spa-framework.md). Прежняя редакция
|
||||||
|
описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята
|
||||||
|
целиком вместе со сменой решения на SPA 2026-08-10.
|
||||||
|
|
||||||
Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные
|
**Кода приложения ещё нет.** Правила ниже выведены из выбора и из замера на
|
||||||
фреймворки. Решение сменилось на SPA 2026-08-10, и та редакция снята целиком:
|
пробном экране, а не из написанного кода: первым их применяет и проверяет
|
||||||
частичная подмена фрагментов, ветвление по `HX-Request` и деградация без JS к
|
`spa-skeleton`. Место, где правило разойдётся с тем, что окажется удобным, —
|
||||||
SPA не относятся ни одним пунктом.
|
повод править эту запись, а не обходить её молча.
|
||||||
|
|
||||||
Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок
|
Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок
|
||||||
наружу — [errors.md](errors.md).
|
наружу — [errors.md](errors.md).
|
||||||
|
|
||||||
## Что решено
|
**Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в
|
||||||
|
команду сборки, а не стоит отдельным шагом. Правил линтера для кода приложения
|
||||||
|
пока нет.
|
||||||
|
|
||||||
|
## Что решено про само приложение
|
||||||
|
|
||||||
- **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт
|
- **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт
|
||||||
контракт данных, разметку собирает клиент.
|
контракт данных, разметку собирает клиент.
|
||||||
@@ -33,9 +39,66 @@ SPA не относятся ни одним пунктом.
|
|||||||
[ready-notification](../../tasks/items/ready-notification.md).
|
[ready-notification](../../tasks/items/ready-notification.md).
|
||||||
- **Записи звука в приложении не делаем** — файл выбирают системным диалогом.
|
- **Записи звука в приложении не делаем** — файл выбирают системным диалогом.
|
||||||
|
|
||||||
|
## Фреймворк и сборка
|
||||||
|
|
||||||
|
- **Vue 3, TypeScript, сборка Vite.** Серверной отрисовки нет, надстройки над
|
||||||
|
фреймворком (Nuxt) нет: она ждёт рядом процесс Node, а у нас статика в
|
||||||
|
бинарнике.
|
||||||
|
- **Компонент — однофайловый, `<script setup lang="ts">`.** Options API не
|
||||||
|
пишем: два способа объявить компонент в одном приложении — второй способ
|
||||||
|
делать то же самое.
|
||||||
|
- **Собранная статика неизменяема и адресуется хешем в имени.** Имена придумывает
|
||||||
|
Vite, руками их не задаём: от этого зависит обновление установленного
|
||||||
|
приложения.
|
||||||
|
- **Шаг сборки входит в `task gate` и в сборку образа.** Красная сборка статики
|
||||||
|
роняет гейт наравне с `go build`.
|
||||||
|
|
||||||
|
## Маршруты
|
||||||
|
|
||||||
|
- **Четыре экрана, одна таблица маршрутов** через `createRouter`. Маршруты по
|
||||||
|
файлам не включаем: сборочная надстройка роутера пятой версии стоит 34 пакета
|
||||||
|
в установке и на четырёх маршрутах не окупается.
|
||||||
|
- **Адреса обычные, а не после решётки** (`createWebHistory`). Отсюда требование
|
||||||
|
к серверу: неизвестный путь **вне** `/api/` отдаёт `index.html`, а не `404`;
|
||||||
|
пути внутри `/api/` в приложение не проваливаются никогда.
|
||||||
|
- **Экран не знает, как он открыт.** Данные экран берёт по своему адресу, а не
|
||||||
|
получает от предыдущего: приложение открывают по ссылке и обновляют страницу
|
||||||
|
посередине.
|
||||||
|
|
||||||
|
## Состояние и обращение к API
|
||||||
|
|
||||||
|
- **Состояние экрана живёт в экране** — `ref` и `computed` по месту. Общее между
|
||||||
|
экранами выносим в composable-функцию `use…`.
|
||||||
|
- **Хранилища состояния (Pinia) не заводим**, пока два экрана не потребуют одних
|
||||||
|
и тех же данных одновременно. Заведём — это правка этой записи с названной
|
||||||
|
причиной.
|
||||||
|
- **Обращение к API — через `fetch` и через одну свою обёртку.** Сторонних
|
||||||
|
клиентов (axios и подобных) не берём: внешних ресурсов у нас нет, а разбор
|
||||||
|
ответа и отображение ошибки всё равно свои.
|
||||||
|
- **Обёртка — единственное место, где читается код ответа.** Она же превращает
|
||||||
|
ошибку контракта в доменную ошибку приложения; экран получает готовый текст, а
|
||||||
|
не `Response`.
|
||||||
|
|
||||||
|
## Показ ошибок и состояний
|
||||||
|
|
||||||
|
- **Текст ошибки приходит с сервера и показывается как есть.** Своих текстов под
|
||||||
|
коды ответа приложение не сочиняет: единая форма ошибки — обязанность API
|
||||||
|
([json-api-for-spa](../../tasks/items/json-api-for-spa.md)), и второй словарь
|
||||||
|
на клиенте разошёлся бы с первым.
|
||||||
|
- **Отсутствие связи — состояние, а не ошибка.** Сорванный запрос показывается
|
||||||
|
строкой «связи нет», а не пустым экраном и не сообщением браузера.
|
||||||
|
- **У каждого списка три состояния и все три нарисованы:** загружается, пусто,
|
||||||
|
есть данные. Пустой список без надписи неотличим от незагруженного.
|
||||||
|
- **Текст, который видит пользователь, — русский** ([CLAUDE.md](../../CLAUDE.md),
|
||||||
|
«Язык»). Код и идентификаторы английские, включая имена компонентов и файлов.
|
||||||
|
|
||||||
## Что не решено
|
## Что не решено
|
||||||
|
|
||||||
Фреймворк, форма сборки, способ хранения состояния на клиенте, правила
|
- **Набор компонентов и стили.** Своя разметка или готовый набор — не решено, а
|
||||||
разбиения на компоненты, обращение к API и показ ошибок. Всё это — предмет
|
готовый способен удвоить собранный файл
|
||||||
`spa-framework-choice`; писать их наперёд, не зная фреймворка, значит написать
|
([research/spa-framework.md](../research/spa-framework.md), «Чего разведка не
|
||||||
правила, которые придётся выбросить второй раз.
|
узнала»).
|
||||||
|
- **Устройство service worker и версионирование статики** — задача
|
||||||
|
[installable-pwa](../../tasks/items/installable-pwa.md).
|
||||||
|
- **Где живёт сессия и как приложение узнаёт вошедшего** — открытый вопрос
|
||||||
|
«Учётные записи» в [../architecture.md](../architecture.md).
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
## Как снималось
|
## Как снималось
|
||||||
|
|
||||||
На живом потоке не снималось ничего: поведение внешних сервисов на границах не
|
На живом потоке не снималось ничего: поведение внешних сервисов на границах не
|
||||||
проверяли. Обе записи сделаны в песочнице на пустой базе.
|
проверяли. Все записи сделаны в песочнице — на пустой базе либо в каталоге вне
|
||||||
|
репозитория.
|
||||||
|
|
||||||
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
|
Внешних источников, о которых разведка нужна, четыре — Telegram Bot API, Yandex
|
||||||
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
|
SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, что стоит
|
||||||
@@ -21,5 +22,6 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
|||||||
|
|
||||||
| Дата | Запись | О чём |
|
| Дата | Запись | О чём |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| 2026-08-11 | [Фреймворк приложения: Svelte, Vue и React на одном экране](spa-framework.md) | Размер собранной статики, цена шага сборки, что у трёх кандидатов одинаково |
|
||||||
| 2026-08-11 | [Очередь задач: своя таблица против готовой библиотеки](job-queue.md) | Цена River и goqite в пакетах, захват одним запросом, чего нет для PocketBase |
|
| 2026-08-11 | [Очередь задач: своя таблица против готовой библиотеки](job-queue.md) | Цена River и goqite в пакетах, захват одним запросом, чего нет для PocketBase |
|
||||||
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
|
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Фреймворк приложения: Svelte, Vue и React на одном экране
|
||||||
|
|
||||||
|
Отвечает на вопрос разведки `spa-framework-choice`: какой фреймворк берём под
|
||||||
|
приложение на четыре экрана, которое собирается в статику, вшивается в бинарник
|
||||||
|
через `go:embed` и ставится на телефон.
|
||||||
|
|
||||||
|
Кандидатов назвал владелец: Svelte, Vue и React, все с Vite. Мера тоже названа им
|
||||||
|
— размер собранной статики, простота вшивания и цена шага сборки в гейте, а не
|
||||||
|
популярность. Серверную отрисовку и надстройки над фреймворками — SvelteKit,
|
||||||
|
Nuxt, Next — не рассматривали: конвенция
|
||||||
|
[../conventions/web-ui.md](../conventions/web-ui.md) уже требует статику в
|
||||||
|
бинарнике, а все три надстройки по умолчанию ждут процесс Node рядом.
|
||||||
|
|
||||||
|
## Как снималось
|
||||||
|
|
||||||
|
Дата замеров — 2026-08-11. Всё считал в каталоге вне репозитория, который удалён
|
||||||
|
вместе с песочницей. Node 24.18.0, npm 11.16.0, Vite 8.2.1, TypeScript 6.0.3.
|
||||||
|
|
||||||
|
- **Каркасы** — `npm create vite@latest <имя> -- --template svelte-ts|vue-ts|react-ts`.
|
||||||
|
Из каждого удалил демонстрационные картинки и компонент-счётчик, чтобы в сборку
|
||||||
|
попал только пробный экран.
|
||||||
|
- **Пробный экран** один и тот же по смыслу: список записей, опрос состояния
|
||||||
|
незавершённых раз в две секунды, полоса ошибки, пустое состояние, разбор даты.
|
||||||
|
60 строк на Vue, 64 на Svelte, 69 на React; стили — один и тот же файл на 15
|
||||||
|
правил, и в сборке он у всех троих совпал до байта (883 Б), что и подтверждает
|
||||||
|
одинаковость экрана.
|
||||||
|
- **Четыре маршрута** — тот же экран плюс три заглушки и переходы между ними:
|
||||||
|
столько экранов у цели [web-access](../../tasks/items/web-access.md). Роутеры
|
||||||
|
`svelte-spa-router` 5.1.1, `vue-router` 5.2.0 и 4.6.4, `react-router` 8.3.0.
|
||||||
|
- **Размеры** — `stat -c%s` и `gzip -9c | wc -c` по файлам `dist/`. Числа Vite в
|
||||||
|
своём выводе печатает по другому уровню сжатия, поэтому в таблицах ниже стоят
|
||||||
|
мои.
|
||||||
|
- **Установка** — `npm ci --cache <свой пустой каталог>`; у каждого каркаса кэш
|
||||||
|
свой, иначе первый прогон скачивает общие пакеты за остальных.
|
||||||
|
- **Сборка** — `npm run build` трижды подряд с удалением `dist` и кэша Vite,
|
||||||
|
в таблице лучшее из трёх. Числа сняты на машине разработчика, не в гейте.
|
||||||
|
- **Совместимость `svelte-spa-router` со Svelte 5** взята из его описания в
|
||||||
|
реестре, а не проверена: `peerDependencies` объявляет `svelte: ^5.0.0`.
|
||||||
|
|
||||||
|
## Числа
|
||||||
|
|
||||||
|
| Мера | Svelte 5.56.8 | Vue 3.5.41 | React 19.2.8 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Пробный экран, скрипт | 35 598 Б / 13 931 Б gzip | 62 493 / 24 372 | 191 797 / 59 679 |
|
||||||
|
| Четыре маршрута с роутером | 45 053 / **17 314** | 86 890 / **33 326** | 228 750 / **72 402** |
|
||||||
|
| Стили, у всех один файл | 883 / 474 | 883 / 474 | 883 / 474 |
|
||||||
|
| Файлов в `dist/` | 3 плюс значок | то же | то же |
|
||||||
|
| Пакетов в установке, каркас | 49 | 48 | 27 |
|
||||||
|
| Пакетов с роутером | 51 | 84 (роутер 5) / 50 (роутер 4) | 29 |
|
||||||
|
| `node_modules` с роутером | 74 МБ | 92 МБ | 91 МБ |
|
||||||
|
| Установка с пустым кэшем | 7,4 с | 9,1 с | 9,4 с |
|
||||||
|
| `npm ci` с тёплым кэшем | 0,44 с | 0,41 с | 0,39 с |
|
||||||
|
| Сборка и проверка типов | 0,32 с плюс 1,16 с | 1,08 с | 0,81 с |
|
||||||
|
|
||||||
|
Пакеты считал так: имена первого уровня в `node_modules` плюс имена второго
|
||||||
|
уровня внутри областей `@…`. В `package-lock.json` записей больше — 74, 72 и 69
|
||||||
|
у каркасов, — потому что он перечисляет двоичные сборки Rollup и oxlint под все
|
||||||
|
платформы, а ставится одна.
|
||||||
|
|
||||||
|
Проверка типов у Vue и React входит в `npm run build` (`vue-tsc -b && vite build`
|
||||||
|
и `tsc -b && vite build`), у Svelte вынесена в отдельную команду `npm run check`
|
||||||
|
и в сборке не участвует — отсюда две цифры в последней строке.
|
||||||
|
|
||||||
|
## Что оказалось одинаковым и потому ничего не выбирает
|
||||||
|
|
||||||
|
- **Вшивание в бинарник.** У всех троих `dist/` — это `index.html`, один файл
|
||||||
|
скрипта и один файл стилей с хешем в имени плюс значок. Ни один не кладёт
|
||||||
|
файлов, начинающихся с точки или подчёркивания, поэтому `go:embed` берёт
|
||||||
|
каталог обычной строкой, без `all:`.
|
||||||
|
- **Node в гейте и в образе.** Шаг сборки статики нужен всем троим одинаково: на
|
||||||
|
машине разработчика, в `task gate` и слоем сборки в `Dockerfile`.
|
||||||
|
- **Установка на телефон.** `vite-plugin-pwa` 1.3.0 от фреймворка не зависит: в
|
||||||
|
его `peerDependencies` стоит Vite, и ни одного фреймворка там нет.
|
||||||
|
- **Цена шага сборки.** Секунда с небольшим у всех троих, и на фоне сборки Go и
|
||||||
|
`golangci-lint` в гейте это не различие.
|
||||||
|
|
||||||
|
Различает единственное — **размер того, что скачивает телефон**, и он расходится
|
||||||
|
вчетверо.
|
||||||
|
|
||||||
|
## Кандидаты
|
||||||
|
|
||||||
|
### Svelte
|
||||||
|
|
||||||
|
Компилятор, а не библиотека времени выполнения: в собранный файл попадает почти
|
||||||
|
только свой код, отсюда 17 314 Б на четыре экрана — вчетверо меньше React.
|
||||||
|
Реактивность и хранилище состояния встроены, третьей библиотеки под них не нужно.
|
||||||
|
|
||||||
|
Против: своего роутера у Svelte нет, а `svelte-spa-router` держит один человек.
|
||||||
|
Проверка типов идёт отдельной командой, то есть в гейте это второй шаг.
|
||||||
|
|
||||||
|
### Vue
|
||||||
|
|
||||||
|
Библиотека с официальным роутером и официальным хранилищем состояния. 33 326 Б на
|
||||||
|
четыре экрана — вдвое легче React и вдвое тяжелее Svelte. Разметка отделена от
|
||||||
|
кода однофайловым компонентом, документация переведена на русский.
|
||||||
|
|
||||||
|
Пятая версия роутера тянет в установку 34 пакета сверх четвёртой (84 против 50):
|
||||||
|
в неё встроена сборочная надстройка под маршруты по файлам. На собранный файл это
|
||||||
|
не влияет — 33 326 Б против 33 848 Б у четвёртой версии, то есть пятая даже чуть
|
||||||
|
легче, — и **надстройка не обязательна**: замер шёл на своей таблице маршрутов
|
||||||
|
через `createRouter`, ни один плагин Vite для этого не регистрировался.
|
||||||
|
|
||||||
|
Пятая версия — стабильная, а не предварительная: 5.0.0 вышла 29 января 2026,
|
||||||
|
текущая 5.2.0 — 15 июля, метка `latest` стоит на ней.
|
||||||
|
|
||||||
|
### React
|
||||||
|
|
||||||
|
Экосистема больше, чем у двух других, — числом я её не мерил, — а пакетов в
|
||||||
|
установке меньше всех: 27. Всё остальное против: 72 402 Б на четыре экрана, и ниже этого пола он не опускается, потому что
|
||||||
|
пол задаёт сама библиотека. Роутер, хранилище состояния и работа с запросами —
|
||||||
|
третьими библиотеками, каждая со своим сроком жизни.
|
||||||
|
|
||||||
|
## Что решено и от чего отказались
|
||||||
|
|
||||||
|
Решение — **Vue с роутером пятой версии**, записано в
|
||||||
|
[ADR-2026-08-11-spa-on-vue](../adr/ADR-2026-08-11-spa-on-vue.md).
|
||||||
|
|
||||||
|
Отвергнуты:
|
||||||
|
|
||||||
|
- **Svelte** — легче Vue вдвое, но своего роутера не имеет, а тот, что есть,
|
||||||
|
держит один человек. Владелец выбрал экосистему, которая переживёт проект, а не
|
||||||
|
минимальный размер: 33 КБ на телефоне не отличаются от 17 КБ на глаз, а
|
||||||
|
брошенная зависимость отличается;
|
||||||
|
- **React** — вчетверо тяжелее Svelte и вдвое тяжелее Vue, а взамен даёт
|
||||||
|
экосистему, которой приложению на четыре экрана не на что потратиться: чужих
|
||||||
|
компонентов оно не берёт, весь показ данных — список, форма загрузки и текст.
|
||||||
|
|
||||||
|
## Чего разведка не узнала
|
||||||
|
|
||||||
|
- **Как числа изменятся на настоящих экранах.** Мерил один экран и три заглушки;
|
||||||
|
загрузка файла с полосой хода, форма настроек и таблица расхода вырастут у всех
|
||||||
|
трёх. Переносится отношение, а не абсолютные значения.
|
||||||
|
- **Цену готовых наборов компонентов.** Не мерил вовсе, а именно она способна
|
||||||
|
удвоить собранный файл.
|
||||||
|
- **Во что обходится шаг сборки в образе.** Слой Node в `Dockerfile` не
|
||||||
|
собирался: время сборки образа и его вес после добавления слоя неизвестны.
|
||||||
|
Замер сделает `spa-skeleton`, которая этот слой и пишет.
|
||||||
|
- **Сколько живёт сборочная надстройка роутера пятой версии.** 34 пакета в
|
||||||
|
установке — число, а не суждение о том, как часто они ломаются.
|
||||||
@@ -5,16 +5,20 @@
|
|||||||
- **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
|
- **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
|
||||||
- **Теги:** goal:web-access
|
- **Теги:** goal:web-access
|
||||||
|
|
||||||
Двигает «Завершение» цели опосредованно: сам по себе каркас пользователю ничего
|
Ни одной строки «Завершения» цели каркас не закрывает: он готовит четыре
|
||||||
не даёт, но без него ни один экран не соберётся. Видимая польза — приложение
|
экранные — 1, 2, 3 и 4, — но сам по себе пользователю ничего не даёт. Видимое от
|
||||||
открывается и показывает, что оно живо и кто вошёл.
|
него одно: приложение открывается и достаёт данные с живого сервера, а не отдаёт
|
||||||
|
пустую страницу.
|
||||||
|
|
||||||
Берётся после `spa-framework-choice`: собирать не на чем, пока фреймворк не
|
Фреймворк выбран 2026-08-11 — Vue 3 с роутером пятой версии и сборкой Vite
|
||||||
выбран.
|
([ADR](../../docs/adr/ADR-2026-08-11-spa-on-vue.md)); правила кода приложения
|
||||||
|
лежат в [conventions/web-ui.md](../../docs/conventions/web-ui.md).
|
||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
- новый каталог фронтенда: исходники, зависимости, конфигурация сборки;
|
- новый каталог фронтенда: исходники, зависимости, конфигурация сборки;
|
||||||
|
- роут, отдающий `index.html` на неизвестный путь вне `/api/`: адреса маршрутов
|
||||||
|
обычные, а не после решётки;
|
||||||
- `Taskfile.yml` — шаг сборки статики и его место в `task gate` и `task image`;
|
- `Taskfile.yml` — шаг сборки статики и его место в `task gate` и `task image`;
|
||||||
- `Dockerfile` — сборка статики внутри образа, чтобы выкладка не зависела от
|
- `Dockerfile` — сборка статики внутри образа, чтобы выкладка не зависела от
|
||||||
машины разработчика;
|
машины разработчика;
|
||||||
@@ -34,10 +38,19 @@
|
|||||||
тест либо ручная проверка на запущенном сервере с сессией.
|
тест либо ручная проверка на запущенном сервере с сессией.
|
||||||
- Шаг сборки статики входит в `task gate` и краснеет при ошибке сборки. Оракул —
|
- Шаг сборки статики входит в `task gate` и краснеет при ошибке сборки. Оракул —
|
||||||
намеренно сломанный исходник роняет `task gate`.
|
намеренно сломанный исходник роняет `task gate`.
|
||||||
|
- Обновление страницы на любом маршруте приложения открывает тот же экран, а
|
||||||
|
адрес внутри `/api/` в приложение не проваливается. Оракул — тест на два
|
||||||
|
запроса: неизвестный путь вне `/api/` отдаёт разметку, неизвестный путь внутри
|
||||||
|
`/api/` отдаёт ошибку контракта.
|
||||||
|
|
||||||
## Рамки
|
## Рамки
|
||||||
|
|
||||||
Экранов, кроме заглушки с именем вошедшего, не делаем — это `upload-and-status-screen`
|
Экранов, кроме заглушки с именем вошедшего, не делаем — это `upload-and-status-screen`
|
||||||
и `records-list-screen`. Установку на телефон не делаем — это `installable-pwa`.
|
и `records-list-screen`. Установку на телефон не делаем — это `installable-pwa`.
|
||||||
Появление шага сборки в гейте меняет требования к машине разработчика: это
|
Готовый набор компонентов не берём: он не выбран, а способен удвоить собранный
|
||||||
решение уровня ADR, и оно записывается.
|
файл.
|
||||||
|
|
||||||
|
Шаг сборки входит в гейт и в образ решением
|
||||||
|
[ADR](../../docs/adr/ADR-2026-08-11-spa-on-vue.md) 2026-08-11 — заново это
|
||||||
|
решение не принимается. Во что слой Node обходится образу по времени и по весу,
|
||||||
|
не замерялось: замер делает эта задача.
|
||||||
|
|||||||
Reference in New Issue
Block a user