Из новостей
31 мая 2025 года на mos.ru, официальном сайте Мэра Москвы, появилась краткая заметка «Новый сайт ВДНХ объединил все проекты выставки на одной платформе». В заметке Наталья Сергунина, заммэра Москвы, сообщила о том, что у сайта изменился визуальный стиль и интерфейс, появилась карта с 3D-моделями павильонов и возможность приобрести билеты в разные места комплекса на одной платформе.
В статье ниже я расскажу о технических деталях масштабной работы, которая предшествовала этой заметке.
Предисловие
Меня зовут Алексей Постригайло, больше 20 лет занимаюсь системной интеграцией и управлением проектами, сейчас — старший партнер ИТ-интегратора Энсайн.
Когда перед моими ребятами встала задача модернизации портала ВДНХ, главной сложностью было не выбрать «модные» технологии, а создать стабильную, согласованную систему, способную выдерживать высокие нагрузки. Изначально портал работал на CMS «1С-Битрикс: Управление сайтом», мощном, но не всегда оптимальном для многосайтовых сценариев решении. Техподдержка регулярно сталкивалась с проблемами: падением производительности при пиковых нагрузках, сложностями управления контентом для 14 отдельных сайтов и отсутствием единой мультиязычной поддержки.
Перед нами открылась живописная картина, знакомая многим крупным организациям с богатой историей: за каждым направлением — музеями, лекторием, выставочными площадками — стоял отдельный сайт (cosmos.vdnh.ru, lektorij.vdnh.ru, expo.vdnh.ru и другие). Эти ресурсы не просто существовали автономно — они кардинально отличались дизайном, навигацией и даже технической реализацией. Настоящий архипелаг сайтов.
Например, пользователь переходил с сайта Музея космонавтики на страницу Лектория и оказывался в совершенно другом визуальном мире — как будто переходил от одного павильона ВДНХ и в другой, а попадал — внезапно — в театр. Или в кино. Ну, вы поняли. Контент-менеджерам приходилось дублировать новости в нескольких системах, а посетители терялись в, скажем так, хореографически независимых интерфейсах.
Таким образом, главной целью проекта было объединение разрозненных ресурсов ВДНХ в единую систему с удобной афишей мероприятий и общей базой данных. Заказчик требовал даже при высоких нагрузках отзывчивый интерфейс , минимальное время отклика и встроенные механизмы SEO-оптимизации. Дополнительно нужно было обеспечить миграцию контента — новостей, событий и локаций — со старого vdnh.ru без потери структуры, а также разработать отдельную структуру данных для сохранения работоспособности внешних приложений (стелы и мобильного приложения), чтобы при переезде на новый сервер они продолжали функционировать без доработки кода.
Окей. Куда мигрировать? Дмитрий Бухвалов, начальник управления развития ИС ВДНХ, предложил перейти на связку Laravel (бэкенд) и Nuxt.js (фронтенд) — это сужало технологический стек заказчика (ВДНХ), и позволяло добиться модульности, высокой производительности и простоты поддержки. Мне идея тоже показалась привлекательной — и не зря: новая архитектура сократила срок разработки до 6 месяцев, включая дизайн, согласования, разработку документации. Кроме того, админ-панель получилась настолько интуитивной, что обучение контент-менеджеров у моих коллег заняло всего 2 часа, а разработчики заказчика уже через неделю стали самостоятельно дорабатывать её, добавляя новые функции без дополнительных инструкций. И вишенкой на этом торте стало время отклика: благодаря переходу на REST API, где данные теперь обрабатываются без полной перезагрузки страницы, оно сократилось на бэкэнде почти в 15 раз — с 900 до 62 мс.
Теперь по порядку - как мы этому шли.
Почему Laravel + Nuxt?
Во-первых, как я уже отметил выше, заказчик уже имел экспертизу в Laravel и Vue.js (следовательно и Nuxt) — это сокращало время адаптации его команды. Во-вторых, Nuxt с серверным рендерингом (SSR) давал двойной выигрыш: поисковые системы видели готовый HTML-контент, а пользователи получали мгновенный отклик при переходе между страницами — сайт как бы «просчитывал» их действия заранее (подробнее мы поговорим об этих механизмах ниже).
Как мы выстроили архитектуру

Мы с архитектором решили разделить систему на три ключевых микросервиса (см. рис. 1), чтобы каждый компонент мог масштабироваться независимо.
На бэкенде мы использовали Laravel, создав специализированный API-сервис для обмена данными с фронтендом, включая поддержку legacy-интеграций.
Для фронтенда выбрали Nuxt.js — это дало нам «два в одном»: быструю загрузку страниц благодаря рендерингу на сервере (о нем ниже) и плавную работу в браузере как у одностраничного приложения (SPA).
Отдельный микросервис — админ-панель на Laravel Orchid — стал цифровым пультом управления для сотрудников. Мы намеренно сделали её минималистичной: менеджеры тратят меньше времени на обучение, а бизнес — на поддержку.
Я исходил из соображений, что такой подход позволит заказчику гибко наращивать мощности под пиковые нагрузки — например, для массовых мероприятий типа открытия катка или выставки «Россия» — и поддерживать стабильную работу системы даже при многократном росте аудитории.
Архитектура под высокую нагрузку
Гибридный рендеринг с SSR и клиентской подгрузкой
Мы внедрили гибридную модель рендеринга, чтобы совместить преимущества серверного рендеринга (SSR) для критически важного контента и клиентской подгрузки для динамических элементов.
Как работает гибридная модель? Ребята разделили процесс рендеринга — то есть формирования страницы — на два этапа. 1. Когда пользователь открывает сайт, сервер подготавливает HTML-код для критически важных элементов — заголовков, описаний, основного контента — и отдаёт его пользователю. Это происходит благодаря механизму серверного рендеринга (SSR), где страница заранее «собирается» на стороне сервера перед отправкой в браузер. Как следствие, мы добиваемся мгновенного отображения первого экрана и улучшаем SEO-позиции в поисковых системах, потому что роботы видят готовый контент сразу, без ожидания. 2. После загрузки базовой структуры эстафету принимает клиентская часть. Динамические элементы — например, пагинация событий, фильтры или обновляемые блоки — подгружаются асинхронно, как бы вдогонку, через REST-запросы. Вы это видели, когда приложение на вашем смартфоне обновляет ленту новостей: основной интерфейс вы видите сразу, а дополнительные данные «подтягиваются» по мере необходимости. На ВДНХ мы использовали Vue/Nuxt с технологией «ленивой загрузки» (lazy-loading), которая экономит ресурсы, поскольку подгружает компоненты только при их попадании в зону видимости. Таким образом, во-первых, посетители не ждут полной загрузки страницы — они сразу взаимодействуют с контентом, что снижает процент отказов. Во-вторых, сервер не перегружается: статичные разделы кэшируются (например, через NGINX - об его механизмах расскажем ниже), а динамические элементы обрабатываются «по требованию». |
Благодаря этой модели интерфейс получился и быстрым, и интерактивным: контент подгружается фоново, сохраняя плавность SPA-приложений.
Следующим этапом мы реализовали многоуровневое кэширование:
Nginx кэширует SSR-рендеры,
Memcached кэширует запросы к базе данных и уже сериализованные ранее JSON-файлы, чтобы избежать повторной сериализации при каждом запросе;
PHP-бэкенд отслеживает любые изменения в сущностях (например, в расписании) и автоматически перекэширует данные.
Redis работает с динамическими данными: сортированные множества (Sorted Sets) хранят ID событий афиши, позволяя быстро генерировать персонализированные подборки без прямых запросов к БД.
На уровне PHP-бэкенда мы внедрили тегированное кэширование — при изменении расписания мероприятий автоматически обновляются только связанные данные.
Как работает многоуровневое кэширование? Сайт ВДНХ — это крупный выставочный комплекс, где одновременно проходят сотни мероприятий. Если каждый раз показывать расписание «с нуля», серверы не выдержат наплыва посетителей. Предложил решение: разложить процесс на этапы, чтобы мгновенно выдавать клиентским приложениям уже готовые компоненты, не тратя ресурсы зря. Начинается всё с Nginx — это наш «координатор входов». Когда гость заходит на сайт, Nginx проверяет, нет ли уже подготовленной SSR-версии страницы (той самой, о которой мы говорили выше). Если такая версия есть, Nginx сразу её показывает, не отправляя запрос к серверам Nuxt. Далее в дело вступает Redis — in-memory база данных и наш «оперативный менеджер». Он обрабатывает динамические запросы, например, обновления расписания. Сортированные множества (Sorted Sets) здесь работают как умные фильтры — я к ним еще вернусь. Например, когда пользователь ищет концерты на завтра, Redis мгновенно выдает подборку. Дело в том, что в Redis ID событий хранятся в оперативной памяти (хотя можно настроить и для хранения на диске) и упорядочены по дате — не нужно каждый раз «бегать» в основную базу данных. Отдельно отметим тегированное кэширование — нашу «магию». Допустим, в разделе «Искусство» добавили новую экспозицию — система автоматически инвалидирует кэш этого раздела. Афиши (множества) также пересобираются, но подмена данных происходит бесшовно: новые данные мгновенно заменяют старые, как только полностью просчитаются. Это экономит ресурсы и предотвращает «рассинхронизацию» — посетители всегда видят актуальное расписание. Клиентские состояния мы храним прямо в браузере пользователя — это как личная цифровая карта гостя выставки. |
Таким образом, мы оставили проект на той же инфраструктуре и он способен выдерживать в разы больше посетителей без апгрейда серверов — тоже экономия!
По метрикам:
за счёт распределения нагрузки сократился TTFB (Time To First Byte) — сервер больше не «задумывается» при массовых запросах;
LCP (Largest Contentful Paint) улучшился на 40% (метрика позволяет измерить, как быстро пользователь сможет увидеть основное содержимое страницы);
Lighthouse-тесты подтвердили скорость загрузки первого экрана <2 секунд даже при пиковых нагрузках.
Масштабируемая микросервисная архитектура через Docker Swarm
Как я писал выше, мы разделили архитектуру на три независимых микросервиса: админ-панель, API и фронтенд на Nuxt, упаковав их в Docker-контейнеры. Docker Swarm стал нашим «дирижёром» — в нем можно удобно масштабировать каждый микросервис, добавляя новые контейнеры при пиковых нагрузках и равномерно распределяя запросы по алгоритму Round Robin. Это позволило избежать перегрузки отдельных серверов — например, во время новогодних выставок заказчик может динамически увеличивать количество инстансов API-сервиса, не затрагивая фронтенд.
Оптимизация фильтров через Redis Sorted Sets
Возвращаюсь, как обещал, к сортированным множествам Redis.
Мы очень быстро пришли к выводу, что классический подход к фильтрам мероприятий с прямыми запросами к БД оказался тупиковым: при 100+ параметрах фильтрации количество комбинаций росло экспоненциально, а база данных не справлялась даже с 10 запросами в секунду. Появилось решение — материализация данных, то есть сохранение набора в упорядоченной структуре через Redis Sorted Sets.
В оперативной памяти Redis хранятся упорядоченные ID событий для каждой категории, тега и пр, что позволяет мгновенно формировать пересечение множеств для подборки по фильтрам без обращения к PostgreSQL (см. Листинг 1). Например, запрос «концерты для детей бесплатно в ближайшие выходные» обрабатывается за 3-5 мс — Redis просто извлекает пресортированный список из памяти.
/**
* Получает список событий с фильтрацией по параметрам
*
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function index(): AnonymousResourceCollection
{
// Парсим даты начала и окончания периода
$start = request()->filled('start')
? Carbon::parse(request()->input('start'))
: now();
$end = request()->filled('end')
? Carbon::parse(request()->input('end'))
: now();
// Подготавливаем фильтры для выборки событий
$filter = [
EventSets::THEMES => $this->getInputAsArray('themes'),
EventSets::TAGS => $this->getInputAsArray('tags'),
EventSets::EVENT_THEMES => $this->getInputAsArray('eventThemes'),
EventSets::EVENT_TYPES => $this->getInputAsArray('eventTypes'),
EventSets::BADGES => $this->getInputAsArray('badges'),
EventSets::PRICE => $this->getInputAsArray('priceLevels'),
EventSets::SHOW => request()->filled('show')
? [request()->input('show')]
: [],
];
// Получаем ID событий для исключения
$exclude = $this->getInputAsArray('exclude');
// Получаем данные из пересечения множеств по фильтрам
$events = EventBasicResource::getSets(
start: $start,
end: $end,
filter: $filter,
exclude: $exclude,
method: 'paginate',
params: [
request()->input('per_page', 10), // Количество на страницу
['*'], // Выбираем все колонки
'page', // Имя параметра пагинации
request()->input('page', 1), // Текущая страница
]
);
// Возвращаем данные в формате JSON-ресурса
return EventBasicResource::collection($events);
}
Листинг 1. Фильтрация и пагинация событий с возможностью поиска по временному диапазону, тегам, типам и другим параметрам
Для снижения нагрузки на БД мы разделили кэш на два уровня: Redis обрабатывает фильтры, а Memcached кэширует отдельные записи мероприятий (см. Листинг 2). В результате нагрузка на базу данных сократилась на 90%, а детальную динамику производительности можно увидеть на рисунках 2, 3 и 4 ниже.



Замеры проводились на железе:
Architecture: |
x86_64 |
CPU(s): |
12 |
Model name: |
Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz |
RAM: |
66 GiB |
/**
* Получает данные из кэша или сохраняет результат запроса в кэш, если данных нет
*
* @param \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relation $query Построитель запроса или отношение
* @param string $method Метод выполнения запроса ('first', 'get', 'value' и т.д.)
* @param mixed $params Параметры для передачи в метод выполнения
* @param mixed $additionalCacheTags Дополнительные теги кэша
*
* @return mixed Результат выполнения запроса
*
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public static function getCache(Builder|Relation $query, string $method = 'first', mixed $params = [], mixed $additionalCacheTags = []): mixed
{
// Генерируем уникальный ключ кэша на основе:
// 1. SQL-запроса
// 2. Метода выполнения
// 3. Параметров запроса
$key = static::getCacheKey([
$query->toRawSql(),
$method,
json_encode($params),
]);
// Формируем массив тегов кэша, объединяя:
// 1. Базовые теги из getCacheTags()
// 2. Дополнительные переданные теги
$tags = array_merge(
static::getCacheTags(),
Arr::wrap($additionalCacheTags)
);
// Получаем данные из кэша или выполняем запрос и сохраняем результат:
// 1. Используем теги для группировки кэша
// 2. Устанавливаем время жизни кэша - 24 часа
// 3. Если данных нет в кэше, выполняем запрос и сохраняем результат
return Cache::tags($tags)->remember(
$key,
Carbon::now()->addHours(24),
static function () use ($query, $method, $params): mixed {
return $query->$method(...Arr::wrap($params));
}
);
}
Листинг 2. Основной метод работы с MemCached в ресурсах на бэкэнде
Автоматизация CI/CD через Gitea
Я поставил задачу настроить конвейер так, чтобы обновления попадали в продакшен без ручного вмешательства — то есть классический CI/CD, на базе Gitea. Платформа автоматически запускает сборку, тесты (включая проверку уязвимостей через Composer Audit и NPM Audit) и деплой. Если что-то ломается, Docker Swarm откатывает версию за считанные минуты. Это дало нам скорость: от коммита до рабочего кода проходит меньше часа, плюс команда занимается разработкой, а не рутиной.
Единый портал с визуальной дифференциацией
Вернемся к исходной задаче: не только объединить 14 разрозненных сайтов ВДНХ в единую цифровую платформу, но и обеспечить одновременно единство и уникальность визуального стиля каждого направления — будь то космическая экспозиция или литературный лекторий. Нам удалось решить и эту задачу (см. рис. 5).
Здесь снова ключевым решением стали SSR-страницы. При заходе посетителя на страницу сервер мгновенно генерирует HTML-каркас с уникальным цветовым оформлением и структурой меню. Параметры этого каркаса и оформления контент-менеджеры настраивают через админку Laravel. В итоге, раздел «Космос» получает тёмно-синюю палитру с анимацией звёздного неба, а «Лекторий» — классическое бежевое оформление с акцентами в виде книжных иконок.

Для реализации динамических цветовых схем мы использовали CSS-переменные, которые устанавливаются вместе с HTML-каркасом на этапе SSR. Это позволяет избежать задержек в применении стилей — страница сразу отображается с правильным оформлением. Этот процесс можно сравнить с театральными декорациями, которые меняются между актами — сервер заранее готовит нужное оформление, чтобы страница отображалась целостно с первого мгновения. При этом все стили кэшируются через старый добрый Redis — как мы видели выше, он с этим справляется прекрасно.
Приятную гибкость системе придаёт модульная структура контента. Редакторы собирают страницы как кубики Lego — перетаскивают блоки с афишами, баннерами или текстовыми материалами, выбирая из нескольких визуальных шаблонов для каждого компонента. В целом это классика CMS, но есть нюанс! Когда в систему добавляется событие, оно автоматически появляется во всех связанных разделах — наконец-то, разрешилась проблема, которая долго портила контент-менеджерам жизнь.
Динамические цветовые схемы с алгоритмической генерацией
Итак, вернемся к подгрузке.
Сервер заранее знает, какие цвета и стили нужны для конкретного раздела сайта. Когда пользователь открывает страницу, перед тем как отдать готовую страницу браузеру, сервер за доли секунды проверяет, по какому «адресу» находится пользователь, и вшивает нужные CSS-переменные прямо в HTML-каркас.
У этого механизма раньше была известная проблема — «мерцание стилей». Дело в том, что стили могли подгружаться с задержкой, и страница сначала отображалась без оформления, а потом резко менялась. Теперь мы устранили эту проблему, сделав так, чтобы браузер получал все необходимые инструкции по внешнему виду сразу, как только начинает загружать страницу. Визуальное восприятие улучшилось, клиентский опыт — тоже.
Конструктор страниц на базе Editor.js
Я предложил еще одну штуку, чтобы контент-менеджеры могли обновлять контент удобно и оперативно, не дергая разработчиков. Эта штука — drag-and-drop система сборки страниц. Каждый блок — текст, слайдер, карта или подборка событий — имеет несколько визуальных вариантов, сохраняющих общую стилистику портала.
Тоже казалось бы классический редактор в CMS, но есть нюанс — гибридный подход. В нашем редакторе базовые элементы реализованы через готовые плагины Editor.js (например, для изображений и каруселей, см. рис. 6), а для сложных сценариев мы разработали собственный контент-блок для мест и событий. Такой подход обеспечил гибкость: готовые плагины Editor.js ускорили внедрение базовых функций, а наш кастомный блок адаптировали под уникальные требования заказчика, позволяя масштабировать систему по мере роста задач. Таким образом, нам удалось предложить клиенту простоту редактирования и возможность добавления сложных виджетов. Результат — публикация контента сократилась до минут (хотя с импортом данных нам пришлось изрядно «попотеть» — потребовалось преобразовывать исходные HTML-структуры в блоки Editor.js).
Вообще, в этом проекте, а особенно в этой задаче я прямо настоял (хотя ребята не сильно-то и возражали), чтобы «все делалось, как для себя» (я чуть ниже расскажу поподробнее, что я имею в виду под этим подходом). Отмечу, что заказчики такой подход обычно приветствуют. Пардон, отвлекся — возвращаюсь к «фичам».

Многоязычная поддержка через Excel-интеграцию
Для быстрого запуска англоязычной и китайской версий мы отказались от стандартных систем локализации и разработали собственное решение. Этот модуль позволяет экспортировать элементы интерфейса типа "Купить билет" или "Расписание" в Excel, внести переводы в файл, и импортировать обратно (рис. 7) — локализованные элементы встанут на место.

Тут тоже важный нюанс — наличием перевода можно кастомизировать публикацию контента . Например, если раздел не переведен, например, на китайский, он считается нерелевантным для этой аудитории и потому не отображается. Контент-менеджеры работают с тремя вкладками при создании новостей, событий и других сущностей (рис. 8) — они могут сразу перевести весь контент, вернуться позже или вообще не заполнять перевод. На стороне пользователя система всегда корректно отображает доступные языковые версии.

Адаптивное меню с возможностью динамического обновления
Чтобы меню стало по-настоящему гибким, мы построили его как отдельный «живой модуль», который работает независимо от основного сайта. Здесь навигация — это умный конструктор (рис. 9): редакторы могут перетаскивать пункты, добавлять визуальные акценты или временные разделы для акций, словно собирая пазл. Всё это происходит в реальном времени, без необходимости останавливать весь портал для обновлений.
Архитектурно мы выделили меню в отдельный модуль с доступом через общее API. Реализация получилась типа большого офиса, где каждый отдел имеет свой вход, но при этом все связаны общими коридорами. Когда редактор меняет структуру меню через админку, изменения сохраняются в базу данных, а фронтенд (на Vue/Nuxt с SSR) подхватывает их через общее API. Для ускорения работы используются механизмы кеширования. За счёт серверного рендеринга (SSR) обновлённая навигация моментально отображается пользователям, даже если они уже открыли страницу — не нужно ждать перезагрузки сайта или повторного запуска системы.
Временные разделы, например для мероприятий, работают как «всплывающие витрины»: мы задаём срок их жизни, и система автоматически убирает их по окончании события. Это снижает «человеческий» фактор и экономит время сотрудников.
Такая модель позволяет реагировать на изменения буквально за минуты. Например, если внезапно закрывается павильон, контент-менеджерам не нужно срочно дергать разработчиков — просто скрываем раздел в админке, и сайт адаптируется.

Мобильная оптимизация
Особенно пристально я держал руку на пульсе мобильной оптимизации. Причина очевидна — мобильный трафик составляет 85%, поэтому пришлось пересмотреть буквально всё — от дизайна до фронтенда.
Чтобы страницы грузились мгновенно даже при слабом интернете, мы реализовали несколько оптимизаций. Первую мы уже обсуждали выше — «ленивая загрузка», при которой картинки и компоненты подгружаются по мере того, когда пользователь до них доскролливает.
Вторая оптимизация связана с тем, что мы научили сервер определять тип устройства заранее — для смартфонов рендерится облегчённая версия страницы без «тяжёлых» элементов, которые есть в десктопной версии.
Особую роль играет технология прогрессивных веб-приложений — Nuxt PWA (Progressive Web App). Благодаря сервис-воркерам (фоновым скриптам) сайт кэширует ключевые данные — это дает возможность пользователи просматривать расписание мероприятий или карту территории даже без интернета (например, в метро или зоне слабого сигнала). Это не только удобство, но и инструмент удержать аудиторию.
Третья оптимизация. Мы проанализировали код через PageSpeed Insights и убрали всё лишнее: неиспользуемые стили, скрипты, автоматическую загрузку компонентов (рис. 10). При сборке мы подготавливаем оптимальный код для новых и старых браузеров. Соответственно, каждый пользователь получает весь код, а «движок» браузера уже сам решает, что ему под силу «переварить».

Оптимизация интеграции с 2GIS для снижения затрат на API
Когда мы проектировали интерактивную карту, мы выяснили, что базовое использование API 2GIS — а в нем каждый запрос тарифицируется — может быть весьма дорогим удовольствием. Тогда мы решили пересчитывать данные о маршрутах не в реальном времени, а только после того, как контент-менеджер завершит редактирование. Это значит, что система отправляет запрос к API только после того, как редактор нажмет кнопку «Пересчитать длительность».
Кроме того, мы научили систему кэшировать базовую информацию о локациях — ID, иконки, названия. Теперь при загрузке карты сначала загружаются данные из API бэкэнда на PHP, а дальше по мере необходимости подгружаются детали. При таком подходе, помимо сокращения нагрузки на API 2GIS, сократилось и время отклика интерфейса для пользователей.
И последняя картографическая оптимизация связана тем, что 2GIS ограничивает построение маршрутов для удаленных локаций. Поэтому, если пользователь находится далеко от ВДНХ, система автоматически переключается на публичные Яндекс.Карты — это еще немного снизило затраты.
Использование Swagger для документации API
Чтобы ускорить онбординг новых разработчиков и упростить сопровождение систем, мы внедрили Swagger, который автоматически создаёт интерактивную документацию на основе аннотаций в Laravel (это часть нашей политики, о ней расскажем отдельно). Интеграторы получают доступ к примерам запросов, схемам данных и возможности тестирования API прямо в браузере — это особенно удобно при работе с Legacy API, где данные передаются неструктурированно через POST-запросы. Мы преобразовали неудобные форматы (представления) данных в понятные структуры, сделав API удобнее для интеграций и отладки компонентов.
В частности, ранее у ВДНХ уже были свои приложения, которые работали на своей структуре данных и пользовались старым API. Мы написали практически новый проект, который помог бесшовно интегрировать новые решения и уже функционирующие системы — например, мобильное приложение ВДНХ и информационные стелы. Зачем ломать старое, если его можно подключить к новому, сохранив единую логику данных?
Делай, как для себя
Этот блок последний. Он не технический, а скорее принципиальный, я про него предупреждал выше, говоря о подходах и принципах. Если принципы наших технических решений вас не интересуют — это нормально, можно здесь остановиться, дальше статья заканчивается. Если интересуют — продолжайте.
Итак, скажу банальность — если не закладывать принципы удобной поддержки в техническое решение, оно неизбежно станет одноразовым и непригодным. Это как вентиляция в доме — забудешь про нее, и стены плесенью покроются. Поэтому я за долгосрочность, за то, чтобы всё было как для себя — см. ниже.
Код как часы: самодокументируемость
За 20 лет работы с различными системами мы вывели для себя главный, незыблемый принцип «Пиши как один». . Это значит, что даже при командной разработке код должен выглядеть так, будто его писал один человек. Для этого используем:
Жесткую типизацию и аннотации, чтобы код объяснял сам себя без лишних комментариев.
Для фронтенда — eslint, инструмент, который автоматически приводит код к единому стилю и исключает споры о форматировании; в бэкенде — cs-fixer. Оба инструмента избавляют команду от рутинных правок и упрощают онбординг новых разработчиков.
Гибкость без vendor lock-in
В данном проекте ребята всё построили на нативных возможностях Laravel, без сторонних фреймворков. Мы сознательно избегали привязки к кастомным решениям, чтобы клиент мог выбирать другого подрядчика без боли.
В целом, я прошу использовать классические решения везде, где возможно. Обычно, изобретать велосипед и не требуется — нужно просто правильно подбирать обкатанные решения и грамотно их сочетать.
Что в итоге?
В итоге получилась система, которая:
стала на бэкэнде работать в ~15 раз быстрее — время отклика, напомним, сократилось с 900 мс до 62;
сделана также удобно, как для себя - редакторы и контент-менеджеры заказчика не дадут соврать;
готова к передаче другому подрядчику в любой момент — благодаря стандартным технологиям, общепринятым подходам и нашим принципам разработки.
P.S.
В конце мая созванивался с клиентом. Он поведал, что они окончательно перенесли сайт из бета-домена на основной домен; что его контент-менеджеры и штатные разработчики в полном объеме приняли эстафету по ведению сайта и получают в основном позитивный фидбэк.
Безусловно, бэклог уже наполняется мелкими задачами (например, добавить на сайт кнопочки, фишечки и пр.), но это уже задел на новый договор. На данный же момент заказчик считает проект удавшимся.
Он доволен.
Мы тоже.
P.P.S.
Огромное спасибо нашим заказчикам — Айку Гасояну (проектный менеджер ВДНХ) и Михаилу Воронину (руководитель отдела программирования)! Именно они не давали мне с командой увлекаться перфекционизмом, своевременно возвращая на землю — «ребята, если что, вы делаете продукт для нас, а не для себя. Помните, да?». И именно они ставили реалистичные задачи (вместо красивых слайдов) и давали обратную связь. Это ценно. И это работало лучше магии.
Vorchun
Наконец то статья как на старом добром Хабре ) Хотя я не про пхп и ларавел, но получил удовольствие от прочтения. И кое-то новое отметил для себя. Спасибо за хорошо оформленный пост