Read in:
Русский

Фронтенд: $mol

Фронтенд trip2g построен на $mol — реактивном UI-фреймворке Дмитрия Карловского. Он покрывает два отдельных интерфейса: панель администратора для владельцев сайта и виджеты на default-шаблоне (вход, поиск, пейволл и др.).

Что такое $mol

$mol — фреймворк компонентов с реактивной системой на основе файберов. Ключевые отличия от React/Vue:

  • Нет виртуального DOM — компоненты привязаны к реальным DOM-узлам напрямую
  • Весь прикладной код пишется синхронно; асинхронные операции прозрачно интегрируются через $mol_wire_sync
  • Компоненты описываются декларативно в .view.tree-файлах; поведение — в .view.ts
  • Встроенная i18n: строки с маркером @ вытягиваются в файлы *.view.tree.locale=ru.json
  • Часть монорепозитория MAM — модули резолвятся по имени директории

Фреймворк небольшой (~10k строк), модульный — билд-тул написан на нём же.

Структура директорий

assets/ui/          ← симлинк → ../mam/trip2g/
  admin/            ← дерево компонентов панели администратора
  auth/             ← вход / выход
  user/             ← виджеты для читателей
    search/         ← поиск по сайту
    space/          ← пространство подписки
  graphql/          ← сгенерированные типы + mol-адаптер
  monkeypatch/      ← патчи рантайма (локаль, URL)

assets/ui/ — симлинк в рабочее пространство MAM. Все компоненты имеют префикс $trip2g_.

Панель администратора

Панель ($trip2g_admin) оборачивает флоу аутентификации ($trip2g_auth) и после входа рендерит каталог из нескольких разделов. Разделы: дашборд, пользователи, контент, telegram, монетизация, интеграции, SEO, система. Навигация управляется URL через $mol_state_arg.

Каждый раздел следует одному шаблону CRUD:

Директория Назначение
catalog/ Таблица всех записей
show/ Детальный просмотр одной записи
update/ Форма редактирования
create/ Форма создания (где применимо)
button/ Кнопки действий (удалить, запустить и т.п.)

В подвале — переключатель языка ($mol_locale_select, ru/en) и переключатель светлой/тёмной темы ($mol_lights_toggle).

Виджеты на страницах сайта

Встраиваются дефолтным шаблоном в обычные страницы сайта:

Компонент Назначение
$trip2g_auth Вход через email → код; кнопки OAuth
$trip2g_user_search Полнотекстовый и векторный поиск по сайту
$trip2g_user_paywall Пейволл / управление доступом
$trip2g_user_signinwall Запрос входа перед пейволлом
$trip2g_user_favoritenote Переключатель избранного

Флоу аутентификации: пользователь вводит email → сервер отправляет 6-значный код → пользователь вводит код → JWT сохраняется в куки. OAuth (Google, GitHub) поддерживается как альтернатива.

Виджет поиска отправляет запрос к GraphQL-операции siteSearch, рендерит результаты с подсвеченными фрагментами и показывает предупреждение при векторном поиске, если точных совпадений нет.

Интеграция с GraphQL

На бэкенде используется gqlgen, на фронтенде — кастомный кодогенератор (graphqlmol.js). Генератор читает schema.graphqls и queries.ts и создаёт:

  1. Полные TypeScript-типы для каждого query/mutation/subscription
  2. Адаптер $trip2g_graphql_request, интегрирующийся с реактивной системой файберов $mol

Паттерн использования — всегда вызывать фабрику вне класса, чтобы объект запроса был общим для всех экземпляров компонента:

// На уровне модуля: создаём переиспользуемый реактивный запрос
const data_request = $trip2g_graphql_request(/* GraphQL */ `
    query AdminBackgroundQueues {
        admin {
            allBackgroundQueues {
                nodes { id pendingCount stopped }
            }
        }
    }
`)

export class $trip2g_admin_backgroundqueue_catalog extends ... {
    @$mol_mem
    data(reset?: null) {
        // Вызов data_request() участвует в реактивном графе mol.
        // При первом вызове запускает HTTP-запрос; при повторных — возвращает
        // кешированный результат. reset=null инвалидирует кеш.
        const res = data_request()
        return $trip2g_graphql_make_map(res.admin.allBackgroundQueues.nodes)
    }
}

$trip2g_graphql_make_map конвертирует массив узлов в Map<id, node>, по которой могут реактивно итерироваться каталог-компоненты.

После изменения schema.graphqls или internal/db/queries.sql нужно перегенерировать типы:

npm run graphqlgen

Локальная разработка

MAM-билдер резолвит модули $trip2g_* по совпадению имён директорий в рабочем пространстве. Настройка:

git clone https://github.com/hyoo-ru/mam.git
ln -s /path/to/trip2g/assets/ui mam/trip2g

cd mam
npm start trip2g/admin   # или trip2g/user, trip2g/forms и т.д.

Конфликт имён GraphQL-переменных: MAM воспринимает каждый токен вида $name в GraphQL-запросе как потенциальную ссылку на модуль и пытается найти директорию с таким именем. Для переменных $filter, $id, $input нужно заранее создать пустые заглушки:

cd mam
mkdir filter id input limit format fragment note

Это известная особенность — директории не используются при сборке, но их отсутствие вызывает ошибки резолвинга.

Docker-сборка

Сборка $mol-фронтенда занимает ~65с в Docker. mam хранит кэш компиляции только в памяти процесса — каждый новый запуск начинается с нуля. Дискового кэша нет, инкрементальной пересборки между запусками npm start нет.

Проверенные подходы, которые не помогают:

  • Разбивка COPY на отдельный externaldeps-слой — mam пересобирает всё независимо от Docker layer cache
  • BuildKit cache mounts на - директории с артефактами — экономят ~9с в лучшем случае, mam игнорирует собственный вывод при перезапуске

Единственное архитектурное решение — держать mam в watch mode (где in-memory кэш сохраняется) и собирать фронтенд вне Docker, затем копировать готовые артефакты через COPY. До реализации этого ~65с компиляции — базовая стоимость сборки.

Монкипатчи

assets/ui/monkeypatch/monkeypatch.ts применяет два патча в рантайме:

1. Сброс кеша локализации

JSON-файлы локалей (web.locale=ru.json) отдаются с долгоживущими заголовками кеша. Патч добавляет в URL локали хеш JS-бандла:

web.locale=ru.json?h=<bundle-hash>

Хеш считывается из параметра ?h= URL текущего тега <script src="...?h=...">. При изменении бандла меняется хеш — браузеры запрашивают свежие файлы локализации.

2. Удаление хвостового #! из ссылок

$mol использует hashbang-URL (#!) для клиентской маршрутизации. Когда $mol покрывает лишь часть страницы (виджеты, не полноценное SPA), каждая внутренняя ссылка получает хвостовой #! — видимый в адресной строке. Патч переопределяет $mol_state_arg.make_link, убирая хвостовой #! и формируя чистые URL вида https://trip2g.com/ru/user/protocol вместо https://trip2g.com/ru/user/protocol#!.

$trip2g_monkeypatch_apply() нужно вызвать один раз при инициализации приложения, до рендера любого компонента.