Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Некоторое время назад появилась задача обновить монолит фронтенда большой высоконагруженной системы, работающей 24/7 – перевести с устаревшего фреймворка Knockout на современный React. Задача возникла, когда старая архитектура перестала соответствовать требованиям бизнеса. Команде поставили задачу реализовать новые функции, но в существующей архитектуре сделать это оказалось практически невозможно. Хотим поделиться своим опытом, как сделать такой проект проще.
Основная часть нашего продукта представляет собой единый бизнес-сценарий, отдельные модули которого тесно взаимосвязаны. Его нельзя просто взять и переписать начисто – это отдельный проект с собственным бюджетом. А во время разработки придётся поддерживать две параллельные версии, одна из которых к тому же не будет работоспособной – ни одна из частей приложения не несёт никакой ценности сама по себе, без связи с другими частями. Поэтому мы стали переписывать его модуль за модулем, окно за окном.
Одним из ключевых требований проекта было, чтобы для пользователей переход прошёл незаметно. Поэтому все изменения скрывались внутри – наши разработчики выстроили обмен данными между старым и новым фреймворком, вычистили множество ошибок, которые всплывают на тестах из-за такой нестандартной архитектуры.
Дальше расскажем подробнее про главные задачи, которые нам нужно было решить по ходу миграции, и как мы с ними справились.
Как подружить между собой два поколения фреймворка
React – один из самых популярных инструментов фронтенд-разработки в мире. Он отлично взаимодействует с другими инструментами, которые могут понадобиться в каждом конкретном проекте, позволяет создавать масштабируемые веб-приложения любого уровня сложности.
Knockout и React принципиально по-разному обрабатывают данные. Задача в том, чтобы все модули, на чём бы они ни были написаны, вовремя понимали, что происходит с данными, как они меняются.
Чтобы старые Knockout-модули могли работать с React-компонентами, команда создала интеграционные связки. В результате данные могут быть изменены как на стороне Knockout, так и на стороне React. Цикличный сценарий отслеживает изменения в обеих частях приложения, чтобы правильно обрабатывать такие ситуации:
1. При изменении данных со стороны Knockout изменения сразу пробрасываются в React.
2. Если запрос на изменение приходит со стороны React, то изменения сначала применяются в Knockout, а затем по уже отработанной схеме пробрасываются обратно в React.
Это избавляет нас от угрозы бесконечного цикла, когда изменение данных в Knockout вызывает изменения в React, а произошедшие в ответ изменения в React вызывают их снова в Knockout.
Как учитывать изменения в разных модулях вёрстки
Второй сложный момент заключался в разных способах создания HTML-разметки на экране. При встраивании React-компонента в вёрстку Knockout, внутренняя часть компонента работает как надо, но если view-модуль убирается со страницы средствами Knockout, то React-компонент об этом не узнает и со стороны JavaScript этот сценарий никак не обработается.
React будет «думать», что компонент всё ещё находится на странице, будет пытаться что-то с ним делать, менять данные, отправлять запросы и т.д. В итоге в продукте возникают утечки памяти, интерфейс зависает.
Чтобы этого не происходило, команда реализовала механизм отслеживания изменений в вёрстке с помощью браузерного API MutationObserver. При исчезновении контейнера с React-компонентом он вызывает нужные методы, чтобы этот компонент был размонтирован также и средствами React.
Благодаря этим двум механизмам интеграции мы получили возможность сочетать в пользовательском сценарии React и Knockout-компоненты. В результате пользователь последовательно проходит по сценарию и никакие данные не теряются. Можно вернуться с какого-то шага назад, и вся введённая информация будет на экране в том виде, в каком её ожидают увидеть. И итоговое окно со всеми опциями отражается корректно. Мы же можем поэтапно переписывать большое и сложное приложение вместо того, чтобы в течение года или двух писать код «в стол».
Как выстраивать целевую архитектуру
Без временных костылей в таком проекте не обойтись, но все они должны находиться на старой стороне (то есть в нашем случае – на стороне Knockout). React-модули сразу работают так, как мы хотим видеть, а другие компоненты продукта под них подстраиваются. Отсюда же правило – все данные идут от React, он задаёт формат и содержание. Задача интеграционных обвязок – приспособить эти данные для обработки на Knockout. Когда процесс миграции будет закончен, все костыли вместе со старыми модулями удаляются из системы переключением фича-флага, и сервис сразу работает, как надо.
Как упростить тестирование и согласование UI-элементов
Команде жизненно необходима возможность быстро разрабатывать и тестировать UI-элементы. Мы для этого используем библиотеку Storybook. Это отдельная веб-страница со своим интерфейсом, который похож на своеобразный магазин всех разрабатываемых UI-компонентов. Они существуют в Storybook отдельно от основного приложения. Такой подход позволяет, во-первых, убрать зависимость от бэкенда и окружений, во-вторых, легко эмулировать любой сценарий с помощью моков данных.
В блоке слева собраны все элементы, которые есть на разрабатываемой странице. Дизайнер может их перетаскивать, комбинировать, смотреть, как разные компоненты работают вместе и по отдельности. Ползунками внизу можно регулировать внешний вид, на соседней вкладке собраны все возможные действия, которые можно проделывать с макетом.
При работе в Storybook наш модуль взаимодействия с бэкендом автоматически перестаёт отправлять настоящие запросы, а вместо них возвращает мокированные данные, хотя со стороны компонента всё выглядит точно так же. Мы разработали механизмы генерации тестовых данных на основе библиотеки Faker. Если появится новый компонент, которому нужно получать данные пользователей или их документов, не нужно создавать новые заглушки и в случае возникновения изменений в контрактах поддерживать их в многочисленных файлах по всему проекту. Достаточно вызвать соответствующий метод генерации моков и тестовые данные будут готовы, а при изменениях нужно поддержать их только в одном месте.
Кроме того, использование Storybook позволяет проводить промежуточную приемку визуальных компонент системы. Для этого мы развернули его на UAT-окружении, где проходят приёмку у заказчика все задачи. Заглядывая в Storybook, наши заказчики в любой момент могут увидеть, как идёт разработка компонентов, какие появились новые компоненты и что изменилось в старых.
Подводя итог
Каждый подобный проект уникален, множество деталей зависит от того, с какого фреймворка на какой вы мигрируетесь. Два главных совета от наших фронтендеров:
Сразу двигайтесь к идеальной картине мира, все компромиссы выстраивайте на стороне фреймворка, от которого отказываетесь.
Создайте удобную среду для тестирования, используйте инструменты вроде Storybook.