Webpack Module Federation — микрофронтенд на современных технологиях (часть 2) – deep dive
По мотивам вопросов из предыдущей статьи мы решили написать вторую часть и рассказать, что удалось сделать еще.
На повестке дня:
Роутинг хостового приложения (React/Vue Routing внутри Angular)
Полноценные адаптеры для работы удаленных плагинов
Использование сервисных модулей в рантайме
Роутинг
В первой части статьи мы создали pet project, в котором использовали хост на Angular и удаленные плагины на Angular. В такой связке роутинг хостового приложения отрабатывает все варианты его нативного использования. Например, в хостовом приложении есть роут /plugin, а в плагине есть два роута /first и /second. Получается, в готовом приложении мы получим /plugin/first и /plugin/second. Такая система будет идеально работать на любом уровне вложенности.
Разберем пример чилдового роутинга для Angular плагина.
Рассматриваем задачу: в Angular приложении использовать плагин, написанный на другом фреймворке – React.
Здесь обычно возникает вопрос: а зачем вы это делаете? И почему не написать все на одном фреймворке? Отвлечемся на минутку от техники и поясним.
Действительно, в простых и средних по сложности проектах, если есть возможность посадить все команды на одни технологии, это стоит сделать. Задача совмещать несколько фреймфорков актуальна для сложных проектов, где независимые команды из разных доменов (микросервисов), разрабатывают свои компоненты, в своих релизных циклах, на разных технологиях. Далее эти компоненты должны использоваться в одном интерфейсе. Наш опыт говорит, что мир усложняется, поэтому усложняются и IT-системы, и таких сложных проектов становится все больше и больше.
Например, возьмем рабочее место оператора контактного центра. Вот мы видим на экране финансовую информацию, которая разрабатывалась командой подразделения Billing, она надежна как танк и редко меняется. А вот рядом виджет с новомодным Next Best Action, написанной динамичной командой хипстеров, которые постоянно экспериментируют. А еще чат-боты, скриптинги, маркетинговые акции, визарды для решения технических проблем, базы знаний и многое другое. Под каждым компонентом большое количество бизнес-логики и большие команды, их разрабатывающие.
Согласитесь, навязывать той или иной команде технологии неправильно, это будет противоречить идее микросервисной архитектуры.
Вернемся к технике.
Если просто создать Angular роут /React в нашем хост приложении и положить под него React плагин с роутингом, то все будет работать, но не совсем так, как ожидалось.
Создаем роутинг в React плагине.
В такой реализации при переходе на /React, мы увидим, что URL в браузере поменяется на <host_url>/first. Это не то, что мы ожидаем. Решаем проблему с помощью basename. Идея: нужно пробросить из хост приложения роут, под которым лежит наш React плагин в сам плагин, чтобы инициализировать BrowserRouter с правильным basename. В двух словах, basename - это URL, относительно которого будет строиться react роутинг.
Итак, пробрасываем на стороне хоста. Понятное дело, нужно значение routePath непосредственно из конфигурации самого плагина.
Принимаем на стороне плагина.
Получаем готовое полноценное решение Angular Router + React Browser Router.
Супер нативные адаптеры для микрофронтендов
Рассмотрим:
Angular адаптер
React адаптер
Vue адаптер
Инициализировать какой-либо плагин с набором пропсов – просто, но это решает только половину задачи. А если мы хотим использовать удаленный плагин как часть своего приложения, как глупую компоненту, просто как компоненту, которой иногда нужны пропсы от хоста или другого плагина?
Создаем Change Detector для наших плагинов. Детектор уже есть, давайте научимся пропсы в плагины прокидывать :)
Схема с пропсами должна быть всем понятна, но, на всякий случай, нарисуем картинку.
Допущение. На стороне плагина получить обновленные пропсы можно только в сеттере, так как мы их пробрасываем руками, а не стандартным механизмом обнаружения изменений.
Angular Adapter
Components
Мы немного дописали/переписали наш Angular адаптер, и получился вот такой код.
Нововведения заключаются в возможности использовать любой компонент из любого модуля из любого удаленного репозитория с необходимыми провайдерами. Это значит, что если у нас в репозитории плагина есть модуль с компонентом, который использует сервис, который провайдит этот же модуль, мы можем экспортировать целиком этот модуль с его провайдерами и не переживать, что какой-то сервис потеряется.
Как это реализуется:
На стороне плагина нам нужно предоставить модуль и компоненту, которую хотим использовать. Одного модуля недостаточно, потому что все component factory этого модуля не попадут в бандл (tree-shakable components) при build –prod, и останется один модуль с инжектором.
Выглядит это вот так:
На стороне, принимающей плагин, должна быть расширенная конфигурация, вида:
Эти 4 проперти, обведенные красным, обеспечивают загрузку модуля и компоненты плагина.
Вот план, как будем грузить:
Вот код:
Этот код развязывает нам руки! Теперь можно достать любую компоненту из любого модуля :)
А сервисы можно? Можно!
Для чего использовать shared сервисы из remote модулей? У нас есть набор бекендсервисов, которые предоставляют нам REST API. Каждый сервис отвечает за свой бизнес. Когда двум плагинам нужно обратиться к одному бекенду, каждый плагин должен создать GraphQL query, добавить реализацию GraphQL модуля к себе в репозиторий, не забыть про модели данных. (╯°□°)╯︵ ┻━┻.
Идея заключается в создании master UI сервисов для бекендов. Тогда для каждого бекенда будет один Singleton сервис на всё приложение (для хоста и всех плагинов). И тянуть мы их будем в рантайме.
Запровайдим сервис в remote модуле вот так:
Это нужно, чтобы вытащить его потом из инжектора по стринге:
Скажем прямо – то, как мы сможем использовать этот сервис – не идеально, но теперь мы хотя бы можем это делать. Все так сложно, потому что мы из инжектора вытащим не просто сервис, а сервис со всеми его зависимостями, если таковые имеются. И дополнительно на стороне консьюмера не нужно ничего провайдить, все само заработает!
React adapter
Адаптер для связки Angular-React будет чуть проще. Поскольку expose’ить из React намного проще – можно не делить на умные и глупые компоненты, то и в реализации адаптера предусматривать это не будем.
Мы должны уметь:
Нарисовать компоненту под роутом
Нарисовать компоненту в любом месте Angular компоненты через адаптер
Под роутом мы уже умеем:
А рисовать компоненты в произвольном месте – давайте учиться. Концептуально будет так же, как с Angular плагином. Нужно просто вставить React адаптер для плагина в шаблон, положить в него конфигурацию плагина и пропсы.
Адаптер нарисует нам наш плагин:
Установит новые пропсы, когда они придут:
И не забудем сделать unmount, когда компонента задестроится:
Профит :)
Vue adapter
Код адаптера для vue структурно похож на адаптеры для React и Angular. Основное отличие только в самом коде рендера, компоненты или приложения.
private async renderComponent(configuration: FederationPlugin, props?: Record<string, unknown>): Promise<void> {
this.configuration = configuration;
const component = await loadRemoteModule({
remoteEntry: configuration.remoteEntry,
remoteName: configuration.remoteName,
exposedModule: configuration.exposedModule
});
const vueComponent = component[configuration.moduleClassName || 'default'];
this.vueComponentRef = createApp(vueComponent, {
...this.props
});
this.vueComponentRef.mount(this.hostRef.nativeElement);
}
Получаем компоненту и, соответственно, «маунтим» ее на наш хост элемент адаптера.
Также можно написать самовызывающийся адаптер, который подойдет для случая, когда мы хотим отдать логику по байндингу настроек , настройке роутинга и подключения каких-то библиотек на откуп ремоут плагину.
const selfRunApp = component[configuration.moduleClassName || 'default'];
selfRunApp(this.hostRef.nativeElement, this.props, configuration.routePath);
И уже во vue проекте, экспортируем bootstrap функцию, как пример:
// bootstrapFunction.js
export default (refElement, props, base) => {
const router = createRouter({
history: createWebHistory(base),
routes
})
const app = createApp(App, {
data: () => {
return (this.props || {});
}
});
app.use(router);
app.mount(refElement);
}
в которой просто используем переданный элемент и прикрепляем к нему наш компонент, приложение.
Так как Module Federation позволяет expose’ить любой javascript, конфигурация вебпака не сильно изменится.
new ModuleFederationPlugin({
name: "vue_app",
filename: "remoteEntry.js",
exposes: {
"./Content": "./src/components/Content",
"./Button": "./src/components/Button",
"./App": "./src/bootstrapFunction",
},
}),
P.S.
Весь код, о котором мы поговорили можно найти на github.