Микросервисная архитектура уже давно де-факто стала стандартом при разработке больших и сложных систем. Она имеет целый ряд преимуществ: это и строгое деление на модули, и слабая связность, и устойчивость к сбоям, и постепенность выхода в продакшн, и независимое версионирование компонентов.
Правда, зачастую, говоря о микросервисной архитектуре, упоминают только бэкенд-архитектуру, а фронтенд как был, так и остается монолитным. Получается, что мы сделали великолепный бэк, а фронт тянет нас назад.
Сегодня я расскажу вам, как мы делали микросервисный фронт в нашем SaaS-решении и с какими проблемами столкнулись.
Проблематика
Изначально разработка в нашей компании выглядела так: есть много команд, занимающихся разработкой микросервисов, каждый из которых публикует свой API. И есть отдельная команда, которая занимается разработкой SPA для конечного пользователя, используя API разных микросервисов. При таком подходе все работает: разработчики микросервисов знают все об их реализации, а разработчики SPA знают все тонкости пользовательских взаимодействий. Но появилась проблема: теперь каждый фронтендер должен знать все тонкости всех микросервисов. Микросервисов становится все больше, фронтендеров становится все больше — и Agile начинает разваливаться, так как появляется специализация внутри команды, то есть исчезают взаимозаменяемость и универсальность.
Так мы пришли к следующему этапу — модульной разработке. Команда фронтенда поделилась на подкоманды. Каждая отвечала за свою часть приложения. Стало намного лучше, но со временем и этот подход исчерпал себя в силу ряда причин.
- Все модули разнородные, со своей спецификой. Для каждого модуля лучше подходят свои технологии. При этом выбор технологий — трудновыполнимая задача в условиях SPA.
- Так как приложение SPA (а в современном мире это означает компиляцию в единый бандл или как минимум сборку), то одновременно могут делаться только выдачи всего приложения. Риск каждой выдачи растет.
- Все сложнее заниматься управлением зависимостями. Разным модулям нужны разные (возможно, специфичные) версии зависимостей. Кто-то не готов перейти на обновленный API зависимости, а кто-то не может сделать фичу из-за баги в старой ветке зависимости.
- Из-за второго пункта релизный цикл у всех модулей должен быть синхронизирован. Все ждут отстающих.
Режем фронтенд
Наступил момент накопления критической массы, и фронтенд решили разделить на… фронтендные микросервисы. Давайте определим, что такое фронтендный микросервис:
- полностью изолированная часть UI, никоим образом не зависящая от других; радикальная изолированность; буквально разрабатывается как отдельное приложение;
- каждый фронтендный микросервис отвечает за некий набор бизнес-функций от начала до конца, то есть является полнофункциональным сам по себе;
- может быть написан на любых технологиях.
Но мы пошли дальше и ввели еще один уровень деления.
Понятие фрагмента
Фрагментом мы называем некий бандл, состоящий из js + css + дескриптора развертывания
. По сути, это независимая часть UI, которая должна выполнять набор правил разработки, для того чтобы его можно было использовать в общем SPA. Например, все стили должны быть максимально специфичны для фрагмента. Никаких попыток прямого взаимодействия с другими фрагментами быть не должно. Необходимо иметь специальный метод, которому можно передать DOM-элемент, где фрагмент должен отрисоваться.
Благодаря дескриптору мы можем сохранить информацию обо всех зарегистрированных фрагментах окружения, а затем иметь к ним доступ по ID.
Подобный подход позволяет разместить два приложения, написанных на разных фреймворках, на одной странице. Также это дает возможность написать универсальный код, который позволит динамически подгружать нужные фрагменты на страницу, инициализировать их и управлять жизненным циклом. Для большинства современных фреймворков достаточно соблюдать «правила гигиены», чтобы это стало возможным.
В тех случаях, когда фрагмент не имеет возможности «сожительствовать» с другими на одной странице, есть fallback-сценарий, при котором мы отрисовываем фрагмент в iframe (решение сопутствующих проблем остается за рамками данной статьи).
Все, что нужно сделать разработчику, желающему использовать существующий фрагмент на странице, — это:
- Подключить скрипт микросервисной платформы на страницу.
<script src="//{URL to static cache service}/api/v1/mui-platform/muiPlatform.js"></script>
- Вызвать метод добавления фрагмента на страницу.
window.MUI.createFragment( // fragment name "hello-label", // fragment model { text: "HelloLabelFragment text from run time" }, // fragment position { selector: ".hello-label-placeholder", position: "afterend" }) .then(callback);
Также для общения фрагментов между собой есть шина, построенная на Observable
и rxjs
. Написана она на NativeJS. Кроме того, в SDK поставляются обертки для разных фреймворков, которые помогают использовать эту шину нативно. Пример для Angular 6 — утилитный метод, возвращающий rxjs/Observable
:
import {fromEvent} from "@netcracker/mui-platform/angular2-factory/modules/shared/utils/event-utils"
fromEvent("<event-name>");
fromEvent(EventClassType);
Кроме того, платформа предоставляет набор сервисов, которые часто используются разными фрагментами и являются базовыми в нашей инфраструктуре. Это такие сервисы, как локализация/интернационализация, авторизационный сервис, работа с кросс-доменными куками, local storage и многое другое. Для их использования в SDK также поставляются обертки для разных фреймворков.
Объединяем фронтенд
Для примера можем рассмотреть такой подход в SPA админки (она объединяет разные возможные настройки с разных микросервисов). Содержимое каждой закладки мы можем сделать отдельным фрагментом, поставлять и разрабатывать который будет каждый микросервис по отдельности. Благодаря этому мы можем сделать простую «шапку», которая будет показывать соответствующий микросервис при клике на закладку.
Развиваем идею фрагмента
Разработка одной закладки одним фрагментом далеко не всегда позволяет решить все возможные задачи. Часто бывает необходимо в одном микросервисе разработать некую часть UI, которая потом будет переиспользоваться в другом микросервисе.
И тут нам тоже помогают фрагменты! Так как все, что нужно фрагменту, — это DOM-элемент для отрисовки, мы выдаем любому микросервису глобальный API, через который он может разместить любой фрагмент внутри своего DOM-дерева. Для этого достаточно передать ID фрагмента и контейнер, в котором ему надо отрисоваться. Остальное сделается само!
Теперь мы можем строить «матрешку» любого уровня вложенности и переиспользовать целые куски UI без необходимости поддержки в нескольких местах.
Часто бывает так, что на одной странице находятся несколько фрагментов, которые должны менять свое состояние при изменении неких общих данных на странице. Для этого у них есть глобальная (NativeJS) шина событий, через которую они могут общаться и реагировать на изменения.
Общие сервисы
В микросервисной архитектуре неизбежно появляются центральные сервисы, данные из которых нужны всем остальным. Например, сервис локализации, который хранит переводы. Если каждый микросервис в отдельности начнет лазить за этими данными на сервер, мы получим просто вал запросов при инициализации.
Для решения этой проблемы мы разработали реализации NativeJS сервисов, которые предоставляют доступ к таким данным. Это дало возможность не делать лишних запросов и кешировать данные. В некоторых случаях — даже заранее выводить такие данные на страницу в HTML, чтобы совсем избавиться от запросов.
Кроме того, были разработаны обертки над нашими сервисами для разных фреймворков с целью сделать их использование очень естественным (DI, фиксированный интерфейс).
Плюсы фронтендных микросервисов
Самое важное, что мы получаем от разделения монолита на фрагменты, — возможность выбора технологий каждой командой в отдельности и прозрачное управление зависимостями. Но кроме того, это дает следующее:
- очень четко разделенные зоны ответственности;
- независимые выдачи: каждый фрагмент может иметь свой релизный цикл;
- повышение стабильности решения в целом, так как выдача отдельных фрагментов не влияет на другие;
- возможность легко откатывать фичи, выкатывать их на аудиторию частично;
- фрагмент легко помещается в голове каждого разработчика, что приводит к реальной
взаимозаменяемости членов команды; кроме того, каждый фронтендер может глубже понять все тонкости взаимодействия с соответствующим бэкендом.
Решение с микросерисным фронтендом выглядит неплохо. Ведь теперь каждый фрагмент (микросервис) может сам решать, как деплоиться: нужен ли просто nginx для раздачи статики, полноценный middleware для агрегации запросов к бэкам или поддержки websockets либо еще какая-нибудь специфика в виде бинарного протокола передачи данных внутри http. Кроме того, фрагменты могут сами выбирать способы сборки, методы оптимизации и прочее.
Минусы фронтендных микросервисов
Никогда нельзя обойтись без ложки дегтя.
- Взаимодействие между фрагментами невозможно обеспечить стандартными ламповыми методами (DI, например).
- Как быть с общими зависимостями? Ведь размер приложения будет расти как на дрожжах, если их не выносить из фрагментов.
- За роутинг в конечном приложении все равно должен отвечать кто-то один.
- Что делать, если один из фрагментов недоступен / не может отрисоваться.
- Неясно, что делать с тем, что разные микросервисы могут находиться на разных доменах.
Заключение
Наш опыт использования такого подхода доказал его жизнеспособность. Скорость вывода фич в продакшн увеличилась в разы. Количество неявных зависимостей между частями интерфейса свелось практически к нулю. Мы получили консистентный UI. Можно безболезненно проводить тесты фич, не привлекая к этому большое количество людей.
К сожалению, в одной статье очень сложно осветить весь спектр проблем и решений, которые можно встретить на пути повторения такой архитектуры. Но для нас плюсы явно перевешивают минусы. Если Хабр проявит интерес к раскрытию подробностей реализации этого подхода, мы обязательно напишем продолжение!
Автор: ShimON