Хабрахабр

[Перевод] Микрофронтенды: о чем это мы?

Все эти годы вы, frontend-разработчик, писали монолиты, хотя и понимали, что это дурная привычка. Вы делили свой код на компоненты, использовали require или import и определяли npm-пакеты в package.json или плодили гит-репозитории в вашем проекте, но все равно писали монолит.
Пришло время изменить положение.

Почему ваш код можно считать монолитом?

По своей природе все frontend-приложения монолитны – кроме приложений, реализующих микро-фронтенды. Причина в том, что вы разрабатываете с использованием библиотеки React, и работу ведут две команды. Обе должны использовать одну версию React и держать друг друга в курсе обновлений, а значит, неизменно решать конфликты при мерже кода. Они не полностью независимы друг от друга в кодовой базе. Вероятно, они вообще пользуются одним репозиторием и одной системой сборки. Микросервисы могут спасти от монолитности приложения! Но как же так? Ведь они для бэкенда! *неимоверное удивление*

Что же такое микросервисы?

Говоря простым языком, микросервисы — это техника разработки, которая позволяет разработчикам делать независимые поставки функционала (релизы) для разных частей платформы, и при этом релизы не ломают друг друга. Независимые поставки позволяют им собирать изолированные или слабосвязанные сервисы. Есть несколько правил, делающих такую архитектуру устойчивее. Вкратце их можно определить так: каждый сервис должен быть маленьким и выполнять лишь одну задачу. Следовательно, работающая над ним команда тоже должна быть маленькой. Насколько крупными могут быть проект и команда, объясняют Джеймс Льюис и Мартин Фаулер:

Самые крупные из них отвечают стратегии Amazon о «команде на две пиццы» — не более 10-12 человек. Разработчики, взаимодействующие с микросервисами, называют разные размеры. Обратный полюс – команды из 5-6 человек, где каждый поддерживает один сервис.

Вот схема, объясняющая отличие монолита от микросервисов:

Когда все сервисы поддерживаются одной командой, велик риск, что по мере роста компании frontend-команда перестанет за UI успевать. Из схемы видно, что каждый сервис в системе микросервисов является отдельным приложением, кроме UI — он остался единым целым! В этом состоит уязвимость данной архитектуры.

Предположим, что компания выросла и взяла на вооружение гибкие методологии разработки (это я про Agile). Архитектура может принести и организационные проблемы. Конечно, в нашем абстрактном примере руководители начнут разделять задачи frontend’а и backend’а, и кросс-функциональные команды не будут по-настоящему кросс-функциональны. Они требуют небольших кросс-функциональных команд. Управление подобной командой не для слабонервных. И все усилия будут тщетными: команда может выглядеть гибкой, но на деле будет сильно разделена. Для решения этих и многих других проблем пару лет назад возникла идея микрофронтедов, быстро завоевавшая популярность. На каждой планерке будет вставать вопрос: достаточно ли frontend-задач, достаточно ли backend-задач в спринте?

Решение проблемы: микрофронтенды

Решение выглядит довольно очевидно, ведь аналогичные принципы давно и успешно применялись в работе над backend-сервисами: разделить монолитный фронтенд на небольшие UI-фрагменты. Однако UI не совсем похож на сервисы – это интерфейс между конечным пользователем и продуктом, он должен быть продуманным и системным. Более того, в эпоху одностраничных приложений, целые приложения запускаются через браузер на клиентской стороне. Это уже не простые HTML-файлы, это сложные компоненты, которые могут заключать в себе различную UI и бизнес-логику. Теперь, пожалуй, необходимо дать определение микрофронтендам.

У каждой из команд есть своя миссия, свое поле работы, на котором она специализируется. Принцип микрофронтендов: представление вебсайта или веб-приложения как набор функций, за которые отвечают независимые команды. Команда кросс-функциональна и разрабатывает
весь цикл – от базы данных до пользовательского интерфейса (micro-fontend.org).

Мой текущий опыт показывает, что многим компаниям может быть сложно принять предложенную выше архитектуру. Другим — груз старого кода не дает перейти на новую архитектуру. Поэтому необходим более плавный, легкий и надежный способ миграции. Подробнее рассмотрев архитектуру, я попробую предложить свое видение решения проблемы. Прежде чем углубиться в детали, познакомимся с терминологией.

Общая структура и еще немного терминологии

Мы получим несколько более мелких приложений с той же структурой, что и у монолитного приложения. Представим, что мы разделяем структуру монолитного приложения по вертикали, по бизнес-функциям. Оно, в свою очередь, объединит UI тех маленьких приложений. Но если мы добавим специальное приложение поверх этих небольших монолитных приложений, то пользователи будут взаимодействовать с ним. *искреннее восхищение* Назовем этот уровень связующим, ведь он берет UI-элементы каждого микросервиса и соединяет их в единый интерфейс – вот самая прямая реализация микрофронтенда.

Как известно, сегодняшняя фронтенд-экосистема очень разнообразна и может быть достаточно сложной. Чтобы было понятнее, далее я буду называть каждое маленькое монолитное приложение микроприложением, поскольку это не просто микросервисы, а автономные приложения – у каждого из них есть UI-элементы и каждое из них представляет полноценную бизнес-функцию. И такие простые, очевидные решения могут оказаться неподходящими в процессе реализации продукта.

Проблемы, которые нужно решить

Когда родилась идея данной статьи, я завел на Reddit тему для ее обсуждения. Благодаря участникам сообщества и их откликам, я могу привести список проблем, требующих решения.

Проблема №1: добиться цельного и согласованного поведения от UI, когда у нас несколько абсолютно автономных микроприложений

В таком случае, все остальные микроприложения должны будут зависить от этой UI-библиотеки. Панацеи не существует, но есть идея создать общую UI-библиотеку, которая тоже бы являлась независимым микроприложением. И это убивает их независимость.

Преимущество такого решения в том, что мы получим глобальную настраиваемую тему для всех приложений. Другой вариант – создать общие CSS-переменные на корневом уровне.

Среди минусов данного подхода будут повторяющаяся реализация UI-элементов и необходимость постоянной проверки дизайна сходных элементов во всех микроприложениях. Либо же мы можем сделать SASS-переменные и примеси общими для всех команд.

Проблема №2: убедиться, что одна команда не переписывает CSS другой команды

Поместив это ограничение на связующий уровень, вы можете сократить общее время разработки, но в то же время возрастет ответственность связующего уровня. Во-первых, можно ограничить область видимости CSS с помощью селекторов, сформированных по названию микроприложения.

Преимущество такого подхода в том, что ограничением занимается браузер. Во-вторых, можно заставить каждое микроприложение стать кастомным веб-компонентом. К тому же кастомные элементы не поддерживаются браузерами на 100% — тем более, если вам нужна поддержка IE. Однако у всего есть цена: с Shadow DOM почти невозможно проводить рендеринг на стороне сервера.

Проблема №3: сделать глобальную информацию общей для разных микроприложений

HTML5 обладает достаточно мощным функционалом, почти неизученным большинством фронтенд-разработчиков.
Одна из таких функций – кастомные события, которые позволят вам делать информацию общей для микроприложений. Эта проблема одна из самых распространенных, но решается довольно легко.

Если вам нужен более тонкий обработчик глобальных состояний, можно реализовать небольшой общий Redux – таким образом получается более реактивная архитектура. Также вам может помочь реализация pub-sub или T39.

Проблема №4: если все микроприложения автономны, как проводить маршрутизацию на стороне клиента?

Во всех основных современных фреймворках имеются мощные механизмы маршрутизации на стороне клиента с помощью состояния истории браузера. Решение проблемы зависит от реализации. Проблема в том, какое приложение отвечает за текущий route и когда именно.

Допустим, у нас есть определение маршрута /content/:id. Общий маршрутизатор решит часть c /content, и решенный маршрут будет передан ContentMicroApp. Мой прагматический подход заключается в создании общего клиентского маршрутизатора, ответственного только за маршруты верхнего уровня, а остальное отдано на откуп соответствующим микроприложениям. ContentMicroApp – автономный сервер, который будет вызываться только с /:id.

Проблема №5: а точно ли нам нужна SSR (server-side rendering), возможна ли она при использовании микрофронтендов?

Если вы хотите связать микроприложения с помощью iframes, забудьте о рендеринге на стороне сервера. Рендер на стороне сервера – дело непростое. Однако если каждое из микроприложений способно рендерить контент на стороне сервера, то связующий слой будет отвечать только за объединение HTML-фрагментов на стороне сервера. Аналогично, веб-компоненты для связывания не сильнее iframes.

Как ее произвести?» Проблема №6: «Интеграция с имеющимся окружением нужна как воздух!

Для интеграции с имеющимся системами, я хочу описать свое видение, которое я называю “постепенным внедрением”.

После этого мы можем определить существующую систему как микроприложение (LegacyMicroApp), объявив к нему специальный роут. Прежде всего мы должны реализовать связующий слой так, чтобы он обладал функционалом прозрачного прокси-сервера. Весь попадающий на связующий уровень траффик будет прозрачно проксироваться имеющейся системе, поскольку других микроприложений у нас пока нет.

Мы возьмем небольшой кусочек LegacyMicroApp, удалив главную навигацию, заменив ее зависимостью. Следующая ступень – постепенное внедрение. Эта зависимость – микроприложение, реализованное с помощью новенькой блестящей технологии, NavigationMicroApp.

Теперь LegacyMicroApp будет перехватывать все роуты через зависимость NavigationMicroApp и обрабатывать уже внутри себя.

Затем аналогичным способом мы переделаем футер.

Так мы будем продолжать откусывать от LegacyMicroApp по кусочку, пока от него ничего не останется.

Проблема №7: оркестровать сторону клиента, чтобы не приходилось каждый раз перезагружать страницу

На стороне клиента мы, загрузив единый HTML, не можем загружать при смене URL отдельные части. Связующий слой решает проблемы на стороне клиента, но не на стороне сервера. Проблема в том, что у этих фрагментов могут быть зависимости, и эти зависимости нужно уметь разрешать на стороне клиента. Следовательно, нам нужен механизм, который загружает фрагменты асинхронно. Это означает, что микрофронтенд-решение должно предлагать механизм загрузки микроприложений и внедрения зависимостей (dependency injection).

Перечисленные выше проблемы можно объединить в следующие темы:

Сторона клиента

  • Оркестрация
  • Маршрутизация
  • Изоляция микроприложений
  • Взаимодействие приложений
  • Единство UI микроприложений

Сторона сервера

  • Серверный рендеринг
  • Маршрутизация
  • Управление зависимостями

Гибкая и мощная, но простая архитектура

Ради этого стоило перетерпеть начало статьи! Основные элементы и требования микрофронтенд-архитектуры наконец-то начали вырисовываться 😉

*предвосхищение фидбека*
Здесь я в общих чертах опишу архитектуру проекта, описав вкратце его основные компоненты. Руководствуясь обозначенными требованиями и вызывающими беспокойство вопросами, я начал разрабатывать решение под названием microfe.

Легче всего начать со стороны клиента, которая обладает тремя отдельными основными структурами: AppsManager, Loader, Router, а также одной дополнительной, MicroAppStore.

Основная задача AppsManager – создание дерева зависимостей. AppsManager
AppsManager – ядро оркестрации микро-приложений на стороне клиента. Как только все зависимости разрешены, AppsManager запускает микроприложение.

Он отвечает за загрузки приложений для клиентской стороны. Loader
Еще одна важнейшая часть оркестровки клиентской стороны – Loader.

В отличие от обычных маршрутизаторов стороны клиента, маршрутизатор microfe обладает ограниченным функционалом. Router
Для выполнения маршрутизации на стороне клиента я внедрил Router в microfe. Допустим, у нас есть URL /content/detail/13 и ContentMicroApp. Он обрабатывает не страницы, а микроприложения. В таком случае маршрутизатор microfe обработает URL до /content/* и вызовет часть ContentMicroApp /detail/13.

Он обладает сходным функционалом, что и библиотека Redux, но с одним нюансом: он более гибкий в отношении асинхронного изменения данных и объявления reducer’a. MicroAppStore
Для решения клиентского взаимодействия между микроприложениями я внедрил в microfe MicroAppStore.

***

Сторона сервера, возможно, немного более сложна в реализации, но имеет более простую структуру. Она состоит из двух основных частей – StitchingServer и MicroAppServer.

MicroAppServer

Оно определяет зависмости, типы и URL схемы MicroAppServer Думаю, что о serve рассказывать излишне – здесь ничего интересного. Минимально возможный функционал MicroAppServer можно выразить так: init и serve.
Когда MicroAppServer загружается, первое что он должен делать — это вызвать SticthingServer и зарегистрировать эндпоинт с объявленным микро-приложением.

StitchingServer

Когда MicroAppServer регистрируется в StichingServer, StichingServer записывает объявление MicroAppServer. StitchingServer позволяет зарегистрировать endpoint в MicroAppServers.

Позже StitchingServer использует объявление для разрешения MicroAppServices от требуемого URL.

Дополнительный шаг – добавление к CSS-селекторам уникального префикса MicroAppServer для предотвращения конфликта между микроприложениями на стороне клиента. Разрешив MicroAppServer и все его зависимости, в названиях всех соответствующих путей в CSS, JS и HTML появится соответствующий публичный URL.

Затем на сцену выходит главная задача StitchingServer: компоновка всех полученных частей и возврат цельной HTML-страницы.

Пара слов о других реализациях

Еще до того, как в 2016 году появился термин микрофронтенд, многие крупные компании пытались решать схожие проблемы – например, Facebook с его BigPipe.
Сейчас идея набирает обороты. Компании самого разного масштаба интересуются этой темой, инвестируя в нее время и деньги. Например, Zalando предоставила открытый код своего решения Project Mosaic. Могу сказать, что microfe и Project Mosaic следуют аналогичным подходам, но с некоторыми кардинальными отличиями. Если microfe прибегает к полностью децентрализованной маршрутизации для большей независимости каждого микроприложения, Project Mosaic предпочитает централизованную маршрутизацию и определение шаблона для каждого маршрута. Кстати говоря, Project Mosaic позволяет легко проводить АB-тестирование и динамическую генерацию шаблона прямо на лету.

Это очень простое решение, которое не требует особой серверной структуры и привлечения DevOps. Есть и другие подходы, в частности, использование ifram’ов в качестве связующего слоя – очевидно, не на стороне сервера, а на стороне клиента. Оно может быть реализовано фронтенд-командой самостоятельно, а значит, создает меньше организационных проблем для компании и стоит дешевле.

Проект полагается на соглашения о наименованиях каждого приложения для разрешения и загрузки микроприложений. Еще существует фреймворк single-spa. Так что фреймворк может быть полезным для знакомства и экспериментов над системой в вашей локальной среде. Легко уловить идею и следовать шаблонам. Минус проекта в том, что вам придется строить каждое микроприложения строго определенным путем – иначе, фреймворк может его не принять.

Заключение (и ссылки)

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

micro fe app registry server
micro front end infrastructure

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть