From 54ca4c0e50e2b86f4401da957b1a8196bf3c04c5 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Tue, 11 Aug 2026 14:51:55 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=84=D1=80=D0=B5=D0=B9=D0=BC=D0=B2?= =?UTF-8?q?=D0=BE=D1=80=D0=BA=D0=BE=D0=BC=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20Vue=203=20=D1=81=20=D1=80=D0=BE=D1=83=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=205=20=D0=B8=20=D1=81=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20Vite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - заведены записка разведки 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 --- docs/adr/ADR-2026-08-11-spa-on-vue.md | 76 ++++++++++++++ docs/adr/README.md | 1 + docs/architecture.md | 12 ++- docs/conventions/README.md | 7 +- docs/conventions/web-ui.md | 87 +++++++++++++--- docs/research/README.md | 4 +- docs/research/spa-framework.md | 139 ++++++++++++++++++++++++++ tasks/items/spa-skeleton.md | 27 +++-- 8 files changed, 324 insertions(+), 29 deletions(-) create mode 100644 docs/adr/ADR-2026-08-11-spa-on-vue.md create mode 100644 docs/research/spa-framework.md diff --git a/docs/adr/ADR-2026-08-11-spa-on-vue.md b/docs/adr/ADR-2026-08-11-spa-on-vue.md new file mode 100644 index 0000000..609bb02 --- /dev/null +++ b/docs/adr/ADR-2026-08-11-spa-on-vue.md @@ -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. Разница куплена + сознательно и обратно не отыгрывается. diff --git a/docs/adr/README.md b/docs/adr/README.md index 71cf8f4..f617444 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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-pocketbase-storage-with-admin-panel.md) | | | 2026-08-11 | [Проверки не зовут внешних программ](ADR-2026-08-11-stub-adapters-in-tests.md) | | diff --git a/docs/architecture.md b/docs/architecture.md index c283c64..93da052 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -134,11 +134,13 @@ администратора при этом Authelia не закрывает: у неё свой пароль суперпользователя. - **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA, - устанавливаемое на телефон; фреймворк выбирает разведка - `spa-framework-choice`, и до её итога - [conventions/web-ui.md](conventions/web-ui.md) стоит почти пустой. Шаг сборки - фронтенда меняет требования к машине разработчика и к образу — решение уровня - ADR. + устанавливаемое на телефон, а фреймворком взят Vue 3 с роутером пятой версии и + сборкой Vite — 2026-08-11, + [ADR](adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в + [research/spa-framework.md](research/spa-framework.md). Тем же решением Node + входит в гейт и слоем в сборку образа. Пишет это `spa-skeleton`; во что + обходится слой Node в образе, не замерялось. Не решено, брать ли готовый набор + компонентов. - **Уведомления.** Пользователь веба узнаёт о готовности только опросом. Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и diff --git a/docs/conventions/README.md b/docs/conventions/README.md index 2804e98..d067aaf 100644 --- a/docs/conventions/README.md +++ b/docs/conventions/README.md @@ -39,10 +39,9 @@ htmx, а здесь решено делать SPA — и перенесённы `0600`, самодокументируемый `config.dist.toml`, проверка на старте. - [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT ULID, разбор на входной границе, естественные ключи у деталей. -- [web-ui.md](web-ui.md) — веб-UI: что решено про приложение (SPA, установка на - телефон, статика в бинарнике) и что ждёт выбора фреймворка. **Почти пуста:** - редакция на htmx снята 2026-08-10 вместе со сменой решения, а новую писать не - под что до разведки `spa-framework-choice`. +- [web-ui.md](web-ui.md) — веб-UI: Vue 3 с Vite и статикой в бинарнике, + однофайловые компоненты, таблица маршрутов, состояние в экране, одна обёртка + над `fetch`, показ ошибок и состояний списка. ## Механизировано diff --git a/docs/conventions/web-ui.md b/docs/conventions/web-ui.md index 5505fee..ce654eb 100644 --- a/docs/conventions/web-ui.md +++ b/docs/conventions/web-ui.md @@ -4,19 +4,25 @@ спецификация поведения — что именно приложение показывает и какие действия обязано поддерживать, живёт в спеке OpenSpec. -**Фреймворк не выбран, и до выбора эта конвенция пуста.** Здесь стоит только то, -что решено и от фреймворка не зависит; всё остальное появится по итогу разведки -`spa-framework-choice`, которая же заведёт запись в `research/` и ADR. +Фреймворк выбран 2026-08-11 разведкой `spa-framework-choice`: +[ADR](../adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в +[research/spa-framework.md](../research/spa-framework.md). Прежняя редакция +описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята +целиком вместе со сменой решения на SPA 2026-08-10. -Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные -фреймворки. Решение сменилось на SPA 2026-08-10, и та редакция снята целиком: -частичная подмена фрагментов, ветвление по `HX-Request` и деградация без JS к -SPA не относятся ни одним пунктом. +**Кода приложения ещё нет.** Правила ниже выведены из выбора и из замера на +пробном экране, а не из написанного кода: первым их применяет и проверяет +`spa-skeleton`. Место, где правило разойдётся с тем, что окажется удобным, — +повод править эту запись, а не обходить её молча. Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок наружу — [errors.md](errors.md). -## Что решено +**Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в +команду сборки, а не стоит отдельным шагом. Правил линтера для кода приложения +пока нет. + +## Что решено про само приложение - **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт контракт данных, разметку собирает клиент. @@ -33,9 +39,66 @@ SPA не относятся ни одним пунктом. [ready-notification](../../tasks/items/ready-notification.md). - **Записи звука в приложении не делаем** — файл выбирают системным диалогом. +## Фреймворк и сборка + +- **Vue 3, TypeScript, сборка Vite.** Серверной отрисовки нет, надстройки над + фреймворком (Nuxt) нет: она ждёт рядом процесс Node, а у нас статика в + бинарнике. +- **Компонент — однофайловый, `