odnaco.me: магазин лётной экипировки — от RiotJS к веб-компонентам

Клиент
Odnaco
Год
2026
Что делали
Сайты на ProcessWire
Стек
ProcessWire, PHP, Статический кэш, Нативные веб-компоненты, Vanilla JS
Сайт
odnaco.me

Комбинезон собирается из размера, цвета, варианта и опций прямо на странице, цену считает сервер, а весь JavaScript сайта — 18,7 КБ после сжатия.

6
веб-компонентов вместо Riot-тегов
Конфигуратор, кнопка «в корзину», корзина, мини-корзина в шапке, индикатор загрузки и иконки: 16 КБ исходного кода на всё
B
18,7КБ
JavaScript на весь сайт
Один файл после сжатия, вместе с общей библиотекой форм и картинок. Фреймворка в нём нет
B
3,8КБ
HTML страницы товара
После сжатия. Страницу отдаёт Apache готовым файлом, PHP при обычном заходе не запускается
B
2017
первый заказ через сайт
Каталог ведётся с декабря 2015 года, магазин принимает заказы с июля 2017-го
B
odnaco.me

Магазин без платформы

Odnaco шьёт снаряжение для парапланеристов: лётные комбинезоны, концертины для укладки крыла, «пончики» — чехлы для быстрой упаковки — и мелкие аксессуары. Каталог на сайте появился в 2015 году, заказы через сайт идут с 2017-го.

Готовая платформа магазина здесь была бы лишней. Товаров два десятка, заметная часть шьётся под заказ, и заказ — это не «артикул x количество», а комбинезон конкретного размера, цвета и комплектации. Поэтому всё написано на ProcessWire с нуля: товары и их опции — обычные страницы и поля в админке, корзина хранится в сессии на сервере, оформленный заказ становится страницей в разделе заказов, а письма уходят менеджеру и покупателю.

Оплата — наличными, переводом на карту или по счёту, без онлайн-эквайринга: для магазина такого масштаба этого достаточно.

Каталог

Четыре раздела и 21 товар. Карточка показывает цену «от»: окончательная зависит от выбранной комплектации. На телефоне меню разделов прячется за кнопкой в шапке.Четыре раздела и 21 товар. Карточка показывает цену «от»: окончательная зависит от выбранной комплектации. На телефоне меню разделов прячется за кнопкой в шапке.
Четыре раздела и 21 товар. Карточка показывает цену «от»: окончательная зависит от выбранной комплектации. На телефоне меню разделов прячется за кнопкой в шапке.

Товар как конструктор

В ролике выше — комбинезон «Полусамосброс». У него шесть размеров, включая пошив по меркам, девять цветов, три исполнения — летнее, демисезонное и зимнее — и восемь дополнительных опций, от шлёвок до съёмного капюшона. Каждый выбор сразу пересчитывает цену на кнопке: за пять нажатий 19 000 ₽ превращаются в 21 700 ₽.

В админке всё это — повторяющиеся поля самой страницы товара: у цвета есть название и оттенок, у исполнения и опции — название и надбавка. Менеджер добавляет опцию или снимает её с публикации, и конфигуратор узнаёт об этом при следующей загрузке страницы, без правки кода.

Цену считает сервер. Браузер присылает в корзину только выбор — идентификаторы товара, цвета, исполнения и опций, — а стоимость складывается из цен, записанных на страницах. Сумма на кнопке — подсказка покупателю, а не цифра, которой верит магазин.

Один тег в шаблоне

<!-- product.php: the whole configurator is one tag -->
<product-configurator uid="<?=$page->uid?>"></product-configurator>

// components never call each other: they talk over one small bus
AppBus.trigger('cart_add', uid, config)   // the button, on click

// AppStore is the only code that talks to the server,
// and what it sends is a choice of ids, never a price
post('/api/cart/add', { uid, config })
// config: {"size":"M","color":"…","variant":"…","options":["…","…"]}

Компоненты не знают друг о друге: кнопка объявляет событие, запрос к серверу отправляет одно общее хранилище.

От RiotJS к веб-компонентам

Первая версия фронтенда была написана на RiotJS 3: компоненты-теги в файлах .tag, собранные riot-compiler, связь между ними через RiotControl и jQuery для всего остального. Для 2017 года — разумный выбор. К 2026-му третья версия Riot фактически умерла и плохо уживалась с современными инструментами сборки.

Мы переписали фронтенд на нативные веб-компоненты — то, что браузер умеет сам, без библиотеки. Теги сменили шесть элементов: <product-configurator>, <cart-add>, <cart-view>, <mini-cart>, <ajax-indicator> и <svg-sprite>. Shadow DOM не понадобился: компоненты рисуют обычную разметку и пользуются общими стилями сайта, как и раньше. Вместо RiotControl — маленькая шина событий, вместо jQuery — урезанная до 27 методов копия библиотеки cash: ровно то, что сайт действительно вызывает.

Последним шагом, в августе 2026 года, старая сборка на gulp уступила место нашей: esbuild, стили, спрайт иконок и имена файлов с хэшем, чтобы браузер хранил их год и никогда не получал устаревших. Весь JavaScript сайта теперь — один файл в 18,7 КБ после сжатия, и фреймворка в нём нет.

Корзина и оформление

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

Каталог отдаётся готовыми файлами

Страницы каталога меняются редко, а открываются часто, поэтому их отдаёт не PHP, а Apache — готовыми HTML-файлами из статического кэша StaticPages. Страница товара весит 3,8 КБ после сжатия и приходит без cookie сессии.

При этом магазину нужна защита от подделки запросов, а общий для всех файл не может хранить личный токен посетителя. Поэтому StaticPages вырезает токен перед сохранением страницы, а скрипт получает свежий первым же запросом к API, вместе с данными товара и корзины. Любой запрос, меняющий корзину, идёт с этим токеном; без него сервер отвечает отказом.

Картинки приходят в формате, который понимает браузер: фото шириной 800 пикселей весит 17 КБ в AVIF, 19 КБ в WebP и 37 КБ в JPEG — вместо 286 КБ оригинала.