Как мы распилили монолит. Часть 2, Frame Manager
Привет, меня зовут Стас, я работаю в команде Тинькофф Бизнеса. В прошлой статье мой коллега Ваня рассказа, как у нас устроена архитектура приложений. Несколько раз Ваня упомянул некий Frame Manager, который служит оркестратором приложений, и сейчас я расскажу про него более подробно.
Что такое Тинькофф Бизнес
Тинькофф Бизнес — банк, предлагающий решения для малого и среднего бизнеса: зарплатный проект, расчетно-кассовое обслуживание, конструктор документов и еще порядка 20 других.
Все это реализовано в приложениях. Все приложения разрабатываются отдельными командами и имеют свои релизные циклы. Но также все приложения работают с единой авторизацией, содержат общую часть бизнес-логики, вынесенную в отдельную библиотеку, и используют общие UI-компоненты.
Вернемся на 2 года назад
Типичное приложение Тинькофф Бизнеса выглядело примерно так:
Вверху — шапка с навигацией по приложению, а справа — сайдбар с навигацией по продуктам.
Тогда идея микрофронтенда еще не была такой популярной, но мы уже двигались в этом направлении: сайдбар представлял собой отдельное Angular-приложение. Основное приложение загружало сайдбар в iframe, что позволяло релизить приложения независимо.
Были у этого подхода и минусы: при переходе между продуктами приходилось ждать полной загрузки страницы с двумя Angular-приложениями. А из-за того, что большинство приложений используют одни и те же запросы к API бэкенда, пользователям приходилось ждать, пока они повторно отработают.
Идея Frame Manager
Мы жили с такой архитектурой, пока не появилась глобальная задача для всех приложений — редизайн. Тогда пришла идея:, а почему бы не сделать своего рода инверсию контроля и вместо того, чтобы приложения загружали внутри себя сайдбар, сайдбар сам бы загружал приложения?
Это позволяло сохранить плюсы текущей архитектуры и избавляло от вышеназванных проблем (и приносило новые, ха-ха).
Начальный прототип
На начальном этапе мы создали прототип с минимальной функциональностью, необходимой для загрузки других приложений. На тестовом стенде был заведен отдельный домен. Поменялись роуты в nginx для статики приложений: раньше по путям /sme, /account, /salary и т. д. загружалась статика соответствующих приложений. Теперь же по всем путям отдавалась статика Frame Manager, а к роутам статики самих приложений добавился постфикс /static.
Чтобы стало понятнее, разберем пример: нужно загрузить приложение, находящееся на пути /some-app с каким-то роутом some-route. Отбросив детали, посмотрим, какой процесс происходит при загрузке /some-app/some-route/:
- Nginx по данному пути отдает статику Frame Manager.
- Frame Manager загружается. Основываясь на роуте, он понимает, что нужно загрузить приложение some-app.
- Создается iframe с src=»/some-app/static/», в котором загружается основное приложение.
В самих приложениях при этом понадобились существенные доработки. Поэтому мы форкнули master ветки приложений и добавили туда необходимые изменения, после чего подняли отдельные экземпляры самих приложений с внесенными изменениями.
Первые проблемы
Так мы перевели 4 приложения во Frame Manager и убедились, что решение рабочее. Предстоял перевод всех остальных приложений. И тут мы столкнулись с проблемой: одновременно поддерживать обычную версию и версию для работы с Frame Manager оказалось слишком дорого.
Нам приходилось постоянно обновлять новые версии приложений на каждое изменение master«a старых версий, разрешать появляющиеся конфликты, часто ломалась уже существующая функциональность, затраты на регресс-тестирование увеличились почти вдвое — все это отнимало слишком много времени. Было понятно, что нужно новое решение.
Улучшения
Если бы одна версия приложения могла работать и с сайдбаром, и с Frame Manager, это избавило бы нас от многих проблем. Давайте посмотрим, что можно сделать.
Прежде всего нужно как-то определять, во Frame Manager ли запущено приложение. Сделать это довольно просто: нужно сравнить ссылки window.top и window.self. Если они окажутся не равны — значит, мы во фрейме, то есть во Frame Manager! Но, если есть приложения, которые по умолчанию открываются в iframe, нужно добавить дополнительную логику. Так, у нас было приложение-виджет, которое изначально открывалось во фрейме и стало считать, что оно всегда находится во Frame Manager«e, из-за чего ломалось в старом режиме.
Теперь рассмотрим подробнее, какие изменения необходимы в приложениях и как можно поддерживать работу в двух режимах:
- Синхронизация url. Так как приложение загружается внутри iframe, оно работает в отдельном изолированном контексте и переходы по роутам никак не отображаются в адресной строке браузера. Кроме того, при переходе по прямой ссылке на какой-то роут приложение будет считать, что текущий роут — это корень. Чтобы синхронизировать состояние url«а приложения и Frame Manager, нужен отдельный сервис. В случае с загрузкой в старом режиме мы просто игнорируем данную логику.
- Сервис для обмена данными. Сервис нужен для обмена данными между Frame Manager«ом и приложениями. Один из кейсов использования: шаринг данных между приложениями. Этот сервис у нас уже был и использовался для совместной работы сайдбара и приложений. В любом случае, реализуется это не сложно, есть много вариантов, у нас логика основана на post messages и custom events. Понадобились лишь небольшие доработки на стороне Frame Manager.
- Замена стилей. Так как изначально стояла задача редизайна, нужно заменить стили при загрузке во Frame Manager. Решается несложно, в Angular есть много способов для этого.
- Замена конфигов. Адреса эндпоинтов и другие переменные, необходимые для работы приложений, хранятся в глобальном объекте TCS, который определяется в отдельном скрипте config.js до инициализации приложения. В случае загрузки с Frame Manager некоторые переменные нужно переопределять.
- Замена base href. Так как изменились роуты в nginx, нужно было поменять base href для корректной загрузки скриптов и статики (добавить постфикс /static/). Кейс оказался не из легких: дело в том, что base href учитывается только первый раз при загрузке страницы, то есть в рантайме Ангуляра его не подменить. Как вариант, можно написать скрипт, который смотрит, в каком окружении запущено приложение, и подставляет соответствующий base href, и добавить его в каждое приложение в самое начало страницы.
- Авторизация. Для авторизации во всех приложениях используется отдельный скрипт, встраиваемый в index.html. В новом варианте этот скрипт встраивается во Frame Manager, и его повторное использование в приложениях будет приводить к ошибкам. Можно внести изменение в логику скрипта, чтобы она игнорировалась, если приложение загружено внутри Frame Manager.
Все это рабочие решения, но в них нет достаточной гибкости. Добавились новые ветки с логикой, которую также нужно поддерживать в разных местах. Да и в целом все выглядит переусложненной и достаточно неустойчивой конструкцией.
Переизобретаем работу с iframe
Тогда появилась идея немного хакнуть процесс загрузки index.html приложения. Вместо того чтобы загружать приложение в iframe, указывая атрибут src, можно сделать xhr-запрос за index.html, получить страницу в текстовом виде, обработать ее и загрузить в iframe. Это даст полный контроль над загружаемым приложением: позволит определять base href, удалять ненужные скрипты, патчить стили, переопределять переменные и многое другое!
Да, mokey patching не приветствуется среди разработчиков и считается плохой практикой, но если уж команда Angular его использует в библиотеке zone.js, чем мы хуже? Могут возникнуть сомнения насчет производительности: парсинг html выглядит дорогой операцией. Но, как правило, начальная страница Angular-приложения не превышает 50 строк, а во всех браузерах (даже в IE 10!) есть удобный api DOMParser, позволяющий из строки получить DOM.
Давайте посмотрим, что делает Frame Manager в процессе загрузки приложения (сам Frame Manager уже загружен):
- Основываясь на пути, загружает index.html приложения.
- Парсит страницу, преобразуя ее в DOM, в памяти удаляет ненужные скрипты, подменяет base href, глобальные переменные с конфигами и стили.
- Создает iframe-элемент, куда записывает полученный документ (преобразованный обратно в строку) с помощью document.write ().
- Прокидывает приложению роут, на который оно должно снавигироваться. Также прокидывает модели, необходимые для работы бизнес-логики, через сервис для обмена данными.
Таким образом, из вышеперечисленных шести необходимых изменений в логике только первое (синхронизация url) нужно реализовывать внутри приложения, все остальное берет на себя Frame Manager!
Что получили
Полностью изменили внешний вид приложения, практически не внося изменений в код самого приложения.
Получили возможность переопределять или добавлять глобальные переменные и стили.
export const business = {
'sidebar.b-main__sidebar': {
display: 'none'
},
'.b-main': {
'margin-left': '260',
position: 'relative',
display: 'block',
width: '1104px',
'min-height': '100vh',
margin: '0 auto'
}
};
{
id: 'products',
name: 'Все продукты',
icon: 'products',
frameSupported: true,
applications: [
{
id: 'products',
path: '/products',
apiPrefix: '/products',
hasMenuConfig: true,
dynamicCompanyChange: true,
}
]
}
При этом конфиги лежат в репозитории, отдельном от Frame Manager, что позволяет менять некоторые параметры работы приложений безрелизно.
Также создали бесшовные переходы между приложениями, вынесли авторизацию во Frame Manager. Добились того, что благодаря шарингу данных между Frame Manager и приложениями не делаются лишние запросы.
Не обошлось и без проблем: некоторые chrome-плагины (КриптоПро, redux devtools) перестали работать в загружаемом приложении, так как при взаимодействии терялась ссылка на window. Понадобились дополнительные доработки.
В итоге в конце 2019 года мы успешно перевели все приложения на Frame Manager, а сайдбар канул в Лету. Но работа над Frame Manager продолжалась, и возник новый вопрос: можно ли как-то еще улучшить и оптимизировать работу фронтенда в Тинькофф Бизнесе? Оказалось, что можно! Но об этом — в следующей статье.