Микросервисный подход в веб-разработке: micro frontends
В этой статье поговорим о микросервисном подходе в веб-разработке пользовательских интерфейсов.
Типичное веб-приложение состоит из HTML-верстки, CSS-стилей и JavaScript-кода, который позволяет достичь максимального уровня интерактивности и отзывчивости. Чем выше сложность приложения, тем сложнее пользовательский интерфейс, а вследствие этого — и инструменты, которые нужны для его разработки. Именно поэтому фронтенд-разработка превратилась из простого набора дополнений для пользовательского интерфейса в сложную экосистему с большим количеством инструментов и высоким порогом входа.
Проблема и решение
Пользовательские интерфейсы в вебе продолжают развиваться. Подтверждение этому можно увидеть и на сайте:
Согласно статистике, каждый месяц объем JavaScript-кода в веб-приложениях возрастает, что на большинстве проектов ведет к увеличению следующих параметров:
- время разработки в связи с высоким уровнем сложности кода;
- время тестирования;
- временной интервал между релизами.
Большие приложения, у которых такие проблемы, зачастую называют монолитами из-за используемого архитектурного подхода.
Монолитная архитектура — это архитектурный подход, в котором вся основная логика приложения собрана в одном месте. Монолитное приложение состоит из однослойного объединения разных компонент в одно целое.
Разработка новой функциональности в таком приложении, где большой объем устаревшего кода, с каждым годом становится все дороже для бизнеса. Со схожей проблемой уже сталкивались бэкенд-разработчики. Одно из решений большинства проблем монолитной архитектуры получило название «микросервисы» (Microservices).
Микросервисная архитектура — это полная противоположность монолитной архитектуры. Используя такой подход вместо одного большого приложения, мы создаем набор небольших слабосвязанных и легко заменяемых модулей, которые взаимодействуют друг с другом. Одно из главных достоинств микросервисной архитектуры — возможность использовать наилучший технический стек для каждой отдельной задачи.
Помимо этого, можно выделить и другие достоинства микросервисной архитектуры в сравнении с монолитной:
- модульность;
- уменьшение времени на тестирование;
- сокращение времени на деплой и возможность делать это параллельно;
- возможность горизонтального масштабирования команды.
А можно ли использовать микросервисный подход в веб-разработке пользовательских интерфейсов? Ответ: да! Можно и нужно! Именно такой подход и называют микрофронтенд (micro frontends).
Микрофронтенд (Micro frontends)
Микрофронтенд (micro frontend) — архитектурный подход, в котором независимые приложения собраны в одно большое приложение. Он дает возможность объединить в одном приложении разные виджеты или страницы, написанные разными командами с использованием разных фреймворков (см. рис. ниже).
Главные преимущества микрофронтенд-подхода — в разработке больших энтерпрайз-приложений:
- модульная архитектура. Отдельные виджеты или страницы — это полностью независимые приложения;
- скорость тестирования. Изменения в одном виджете или странице можно протестировать изолированно и только в этом приложении, не тратя времени на тестирование всего остального функционала;
- параллельные деплойменты. Отдельные виджеты или страницы могут и должны деплоиться независимо.
Помимо очевидных достоинств такого подхода, у него есть и существенные недостатки:
- увеличение общей сложности приложения;
- дублирование кода. Каждое приложение разрабатывается отдельной командой, которая принимает свои технические решения. Это ведет к повторной загрузке одинаковых фреймворков, библиотек и общему дублированию кода, который мог быть использованным повторно;
- JS-бандл монолитного приложения всегда будет меньше, чем совокупность бандлов в микрофронтенд-архитектуре;
- возможные проблемы с кешированием и версионностью приложений;
- глобальные переменные или CSS-стили — это вещи, о которых стоит забыть в микрофронтенд-архитектуре, если приложения полностью не изолированы.
Использование такого архитектурного подхода на маленьких проектах и в маленьких командах несет больше проблем и дополнительной сложности в разработке, чем преимуществ. Но большие проекты вместе с распределенными командами, наоборот, получают больший выигрыш от создания микрофронтенд-приложений. Именно поэтому сегодня микрофронтенд-архитектура уже широко используется многими крупными компаниями в своих веб-приложениях:
К сожалению, пока не существует конкретной спецификации для построения микрофронтенд-архитектуры. Вот, пожалуй, самые доступные и простые способы и техники для построения микрофронтенд-приложений:
- IFrames;
- библиотека Tailor.js;
- фреймворк single-spa.
Пройдемся по каждому из перечисленных подходов.
IFrames
IFrame — это давняя технология, которая, несмотря на всю свою неактуальность, дает возможность построить микрофронтенд-архитектуру. Используя IFrame, каждый отдельный виджет можно поместить в IFrame, который загружает нужное приложение. При использовании такого подхода вы, скорее всего, столкнетесь со следующими проблемами:
- производительность;
- сложность поддержки.
Крайне не рекомендую строить микрофронтенд-архитектуру, используя IFrame. Сегодня существуют другие способы сделать это проще и эффективней.
Библиотека Tailor.js
Здесь вы можете прочитать больше о самой библиотеке. Компания Zalando создала целую экосистему для построения микрофронтенд-архитектуры, и Tailor.js — это часть экосистемы. Особенность Tailor.js — то, что это пакет для Node.js, и ориентирован он на построение микрофронтенд-архитектуры с серверным рендерингом.
Для меня дополнительная особенность этого пакета — недостаток документации. Живые примеры проектов я смог найти только в отдельных топиках GitHub, где обычные пользователи спрашивают совет и прикрепляют ссылки на свои репозитории с кодом. Если вам нужен серверный рендеринг, то эта библиотека точно пригодится.
Single-spa
Основной и, по моему мнению, лучший подход в построении микрофронтенд-архитектуры — это фреймворк single-spa. Вот основные причины, по которым я советую выбрать single-spa:
- рабочие примеры с использованием современных фреймворков и библиотек;
- хорошая документация;
- возможность шарить зависимости между отдельными виджетами;
- поддержка независимых деплойментов;
- сборка приложения на стороне клиента;
- готовая экосистема врапперов для быстрой интеграции существующих приложений в микрофронтенд-архитектуру.
Single-spa — это фреймворк, который дает возможность объединить разные приложения, независимо от используемой библиотеки или фреймворка, в одно целое. Под капотом single-spa набор существующих инструментов вместе с собственными решениями:
- SystemJS — загрузчик модулей, который нужен для асинхронной загрузки отдельных приложений;
- врапперы — single-spa предоставляет отдельные врапперы под каждый фреймворк, который создает обертку над приложением, нужную для интеграции и подключения отдельного приложения в общее single-spa;
- API — single-spa предоставляет набор инструментов, которые нужны для коммуникации между отдельными приложениями, подписку на события и т. д.
Типичное приложение с использованием single-spa выглядит так:
А вот так выглядит коммуникация между отдельными элементами микрофронтенд-архитектуры, построенной с использованием single-spa:
Root Application — это корень приложения. Именно здесь происходит подключение sigle-spa как основного фреймворка, а также конфигурация SystemJS для корректной загрузки внешних приложений.
Каждое дочернее приложение для корректной интеграции должно предоставлять публичные методы bootstrap, mount, unmount, которые используются фреймворком single-spa для мануального бутстрэппинга приложения. Почти под каждый современный фреймворк существует готовый враппер, который упрощает эту задачу и автоматизирует некую часть процесса. На сайте single-spa можно найти список всех фреймворков, под которые существуют готовые врапперы.
Построить набор приложений с использованием микрофронтенд-подхода и single-spa можно как путем создания полностью всей инфраструктуры и приложений с нуля, так и на основе существующего приложения. Рассмотрим примеры того, как это выглядит, создавая набор полностью новых приложений с использованием React.js и Angular 8.
Конфигурация билда для React.js под single-spa представлена здесь.
import React from 'react' import ReactDOM from 'react-dom' import singleSpaReact from 'single-spa-react' import { property } from 'lodash' import setPublicPath from './set-public-path.js' const reactLifecycles = singleSpaReact({ React, ReactDOM, loadRootComponent: () => import(/* webpackChunkName: "react-app" */'./App.js').then(property('default')), domElementGetter, }) export const bootstrap = [ () => { return setPublicPath() }, reactLifecycles.bootstrap, ] export const mount = [ reactLifecycles.mount, ] export const unmount = [ reactLifecycles.unmount, ] export const unload = [ reactLifecycles.unload, ] function domElementGetter() { let el = document.getElementById("react-app"); if (!el) { el = document.createElement('div'); el.id = 'react-app'; document.body.appendChild(el); } return el; }
Используя существующий враппер single-spa для React.js, мы создаем интерфейс с методами bootstrap, mount, unmount, где соответственно описываем, как должно бутстрэппиться наше приложение. Враппер помогает инкапсулировать внутреннюю имплементацию и создать API для правильного подключения в single-spa фреймворка.
Похожим образом это выглядит и для Angular 8.
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { Router } from '@angular/router'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import singleSpaAngular from 'single-spa-angular'; import { singleSpaPropsSubject } from './single-spa/single-spa-props'; if (environment.production) { enableProdMode(); } const lifecycles = singleSpaAngular({ bootstrapFunction: singleSpaProps => { singleSpaPropsSubject.next(singleSpaProps); return platformBrowserDynamic().bootstrapModule(AppModule); }, template: '<app-root />', Router, NgZone: NgZone, }); export const bootstrap = lifecycles.bootstrap; export const mount = lifecycles.mount; export const unmount = lifecycles.unmount;
Так же, как и в случае с React.js, мы предоставляем интерфейс для ручного бутстрэппинга приложения на Angular 8.
Кроме использования специфичных врапперов, приложение должно быть собрано как amd-модуль. Каждый такой модуль асинхронно подключается в корень всей микрофронтенд-архитектуры — Root Application. Ниже пример элементарной имплементации.
<!DOCTYPE html> <html> <head> <title>Root application</title> <meta name="importmap-type" content="systemjs-importmap"> <script type="systemjs-importmap"> { "imports": { "angular-app": "http://localhost:4201/main.js", "react-app": "http://localhost:4202/main.js", "single-spa": "https://unpkg.com/[email protected]/lib/system/single-spa.min.js" } } </script> <!-- Loading of required libraries shouyld be added --> </head> <body> <header class="header-fixed"> <nav> <a href="/angular-app">Angular app</a> <a href="/react-app">React app</a> </nav> </header> <script> System.import('single-spa').then(function (singleSpa) { singleSpa.registerApplication( 'angular-app', () => System.import('angular-app'), location => location.pathname.startsWith('/angular-app') singleSpa.registerApplication( 'react-app', () => System.import('react-app'), location => location.pathname.startsWith('/react-app') ) singleSpa.start(); }) </script> </body> </html>
Две главные части, на которые стоит обратить внимание:
- script-тег, в котором нужно описать маппинг названия приложения, на адрес, с которого single-spa будет подгружать amd-модуль для этого приложения;
- script-тег, где нужно непосредственно зарегистрировать приложение с помощью метода registerApplication. Здесь нужно указать адрес, при переходе на который system.js будет загружать соответствующий модуль.
Как можно увидеть, Root Application — простой HTML-файл с основными конфигурациями для загрузки других приложений. В одном микрофронтенд-приложении можно зарегистрировать множество микроприложений. Если при регистрации приложения в 3-м параметре метода registerApplication указать просто /, такое приложение будет загружаться для каждого доступного адреса. Именно такой подход желательно использовать для создания навигационной панели или частей приложения, которые являются общими и не должны загружаться повторно при переходе между приложениями.
Полностью рабочий пример того, как это работает, можно найти в моем GitHub-репозитории.
В этом репозитории представлена пошаговая имплементация микрофронтенд-архитектуры, финальная рабочая версия — в ветке с названием step-7.
Нам, разработчикам, далеко не всегда приходится создавать приложения с нуля. Single-spa дает возможность полностью использовать существующие приложения как элементы микрофронтенд-архитектуры, будь то Root Application или асинхронно загружаемые микроприложения.
Если из существующего приложения нужно создать микроприложение, совместимое с single-spa и имеющее возможность загружаться асинхронно, в первую очередь нужно искать соответствующий враппер. Для большинства современных фреймворков и библиотек single-spa предоставляет врапперы, готовые к загрузке через npm, а также документацию, описывающую, как их правильно настроить. Если же для используемого фреймворка не существует готового враппера, то всегда можно написать его самостоятельно. Настроив враппер и собрав amd-модуль, вы получите готовое к подключению микроприложение; все, что останется, это добавить соответствующую конфигурацию в Root Application. С собственного опыта скажу, что для современных фреймворков и библиотек, таких как Angular 2+, React, Vue, которые собираются с помощью Webpack, конвертация в микроприложение проходит быстро и без дополнительных «танцев с бубнами».
Если из существующего приложения нужно создать Root Application, тогда в первую очередь нужно в index.html подключить библиотеки для single-spa и добавить все необходимые для загрузки других микроприложений конфигурации. После того как ваш index.html стал Root Application в single-spa, можно приступить к настройке остальной части приложения для корректной работы в single-spa-архитектуре так, как это описано в предыдущем абзаце.
Проблемы и недостатки
Большая проблема для обеих задач — это старые (legacy) приложения, написанные на старых фреймворках и собираемые с использованием старых инструментов. Например, приложение на AngularJS или Backbone, которое собирается с помощью gulp или grunt, может «сожрать» очень много вашего времени, прежде чем будет корректно сконфигурировано. С собственного опыта могу выделить следующие проблемные места:
- зависимости — очень много зависит от того, каким образом в вашем приложении загружаются зависимости. Все, что не import, потенциально может вызвать проблемы на этапе сборки или выполнения;
- стили, картинки, шрифты — скорее всего, все это будет поломано или с багами после первой успешной сборки вашего микроприложения.
На этапе конфигурации проблемы, с которыми вы столкнетесь, будут самые разнообразные, поэтому запаситесь терпением 😉
Кроме сложной конфигурации, выделим проблемы, которые могут возникнуть при построении микрофронтенд-архитектуры с использованием single-spa:
- кеширование микроприложений. Без правильной стратегии по кешированию микроприложения, как и Root Application, будут кешироваться в браузере и игнорировать любые новые изменения, которые будут релизнуты;
- дебаггинг. Если что-то не работает, дебаггинг без дополнительных конфигураций и надстроек может быть довольно тяжелым процессом, так как вам придется дебажить не приложения, а отдельные amd-модули без сорс мап;
- общий размер приложения вместе со всеми асинхронными микроприложениями увеличится, в сравнении с монолитным подходом;
- общая производительность микрофронтенд-приложения будет ниже, чем у монолита;
- повторение кода. Повторная загрузка кода — как библиотек, так и целых фреймворков — ведет к ухудшению быстродействия всего приложения, а не только отдельной страницы. Single-spa дает возможность шарить зависимости между приложениями, но помните: чем больше у вас зависимостей, библиотек, компонент, которые шарятся между приложениями, тем больше ваша микрофронтенд-архитектура похожа на монолитную;
- SEO-оптимизация. Загрузка отдельных приложений вместе с бутстрэппингом полностью проходит на клиенте, что значительно усложняет SEO-оптимизацию. Single-spa не поддерживает серверный рендеринг, для добавления поддержки можно использовать связку single-spa + Tailor.js.
Выводы
Микрофронтенд-архитектура, как и любая другая архитектура, решает одни проблемы, создавая другие. Микрофронтенд-архитектура усложнит вашу жизнь и жизнь вашей команды, если:
- над проектом работает меньше 10 человек и нет планов к расширению;
- ваше приложение не состоит из абстрактных изолированных модулей, которые в перспективе могут быть отдельными приложениями;
- багфиксинг вместе с разработкой новых фич проходит быстро и без трудностей, связанных с размером вашего приложения.
Один из частых вопросов: можно ли использовать single-spa для миграции с одного фреймворка на другой (например, с AngularJS на Angular 2+)? Можно, но не нужно: практически всегда есть более легкий и нативный способ миграции, который требует меньше конфигурационной работы и который будет работать быстрее.