История о том, как мы на Module Federation съезжали. Часть 2

от автора

Привет! С вами снова Максим. Во второй части будет о том, что мы придумали.

Когда мы решили пилиться после сбоя, про который я рассказал в первой части, начали с личного кабинета. Главную страницу, операции, магазины и многое другое решили вынести отдельно

Главную страницу, операции, магазины и многое другое решили вынести отдельно

В терминах Module Federation есть такие приложения:

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

Remote — микрофронт, который выполняет определенную задачу в нашем ЛК, например позволяет работать с транзакциями. Такой тип приложений может работать в двух режимах с host-приложением: встраиваться в него и standalone — когда мы хотим запустить эту часть изолированно от всего, мы сами должны получать пользователей и данные по ним.

Есть хедер — часть host-приложения, есть футер и блок — то место, куда встраивается remote. То есть личный кабинет очень-очень хорошо ложится на эту историю

Есть хедер — часть host-приложения, есть футер и блок — то место, куда встраивается remote. То есть личный кабинет очень-очень хорошо ложится на эту историю

Такой исход монолита в микросервисы задевает не только фронтов, бэк-разработчики чаще нас используют микросервисный подход при построении архитектуры своих решений. А как известно, чем больше микросервисов, тем сложнее этим управлять и взаимодействовать с ними с фронта. И тут мы понимаем, что уже 2023 год, JS работает не только в браузерах, и начинаем внедрять

(Микро)фронтенды и микросервисы с помощью Webpack
Привет! Меня зовут Максим, я фронтенд-разработчик компании Тинькофф, лид команды фронтендов, которые…

habr.com

Мы выбрали Module Federation, потому что сейчас это одно из лучших коробочных решений на рынке. Плюс мы используем Angular, который с 12-й версии полностью поддерживает всю эту историю. Как и React с 17 и Vue 3.

Настройка Module Federation в самом начале выглядит вот так:

plugins: [      new ModuleFederationPlugin({            remotes: {                 main: 'main@http://localhost:4201/remoteEntry.js',                 operations: 'operations@http://localhost:4202/remoteEntry.js',                 stores: 'stores@http://localhost:4203/remoteEntry.js',             },             shared: share({                 '@angular/core': {singleton: true, strictVersion: true, requiredVersion: 'auto'},                 '@angular/common': {singleton: true, strictVersion: true, requiredVersion: 'auto'},                 '@angular/common/http': {singleton: true, strictVersion: true, requiredVersion: 'auto'},                 '@angular/router': {singleton: true, strictVersion: true, requiredVersion: 'auto'},                 '@angular/forms': {singleton: true, strictVersion: true, requiredVersion: 'auto'},                 ...sharedMappings.getDescriptors(),             }),        }),        sharedMappings.getPlugin(), ],

Есть блок, отвечающий за ремоуты:

remotes: {     main: 'main@http://localhost:4201/remoteEntry.js',     operations: 'operations@http://localhost:4202/remoteEntry.js',     stores: 'stores@http://localhost:4203/remoteEntry.js', },

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

Думаю, те, кто хоть раз видел документацию Webpack Module Federation, встречали похожий конфиг:

shared: share({     '@angular/core': {singleton: true, strictVersion: true, requiredVersion: 'auto'},     '@angular/common': {singleton: true, strictVersion: true, requiredVersion: 'auto'},     '@angular/common/http': {singleton: true, strictVersion: true, requiredVersion: 'auto'},     '@angular/router': {singleton: true, strictVersion: true, requiredVersion: 'auto'},     '@angular/forms': {singleton: true, strictVersion: true, requiredVersion: 'auto’},     ...sharedMappings.getDescriptors(), }),

Такие конфигурации порождают множество вопросов. Но сначала скажем о плюсах

Плюсы и минусы Module Federation

Сначала перечислю плюсы MFE.

Работает из коробки. Не нужно дополнительных настроек. Проследовали по документации, подняли два приложения — все работает. Считаем, что мы микрофронтендер и молодец.

Удобное управление зависимостями. Известно, какие зависимости должны шариться между сервисами, какие не должны, какие будут иметь свой инстанс, а какие не будут. Все видим сразу же.

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

Но есть и минусы MFE. 

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

Фолбэки. Module Federation работает с помощью http-загрузки, то есть он идет и загружает чанк зависимого приложения. Что будет, если оно не загрузится? На выходе получаем схему: хост приложения идет за конфигом, передает конфигурацию angular- роутеру или react-роутеру и все работает хорошо

На выходе получаем схему: хост приложения идет за конфигом, передает конфигурацию angular- роутеру или react-роутеру и все работает хорошо

Конфигурация — это простой массив объектов:

[   {   "remoteEntry": "http://localhost:4201/remoteEntry.js",   "remoteName": "main",   "exposedModule": "./Module",   "displayName": "navigation.main",   "routePath": "main",   "ngModuleName": "RemoteEntryModule"   },   {   "remoteEntry": "http://localhost:4202/remoteEntry.js",   "remoteName": "stores",   "exposedModule": "./Module",   "displayName": "navigation.stores",   "routePath": "stores",   "ngModuleName": "RemoteEntryModule"   } ]

Почти все поля перекликаются с Webpack, в котором мы все описываем. У Module Federation есть несколько enum и интерфейсов, которые позволяют типизировать объекты.

Мы берем параметр, делаем свой тип, добавляем туда переменные, которые нам нужны, расширяем как нам нужно и описываем в этом конфиге.

import {LoadRemoteModuleOptions} from '@angular-architects/module-federation';  export type Microfrontend = LoadRemoteModuleOptions & {   remoteName: string;   displayName: string;   routePath: string;   ngModuleName: string; }; 

Код загрузчика:

export class MicrofrontendLoaderService {   constructor(     private readonly router: Router,     private readonly httpClient: HttpClient,     private readonly destroy$: TuiDestroyService   ) {}    buildDynamicRoutes(): Observable<boolean> {     return this.resolveConfig().pipe(       takeUntil(this.destroy$),       tap(cfg =>       this.router.resetConfig(         buildApplicationRoutes(cfg),       ),       ),       mapTo(true),     )   }      private resolveConfig(): Observable<Microfrontend[]> {      return this.httpClient.get<Microfrontend[]>('/assets/config/mf/config.json')   } } 

Первое, что делает загрузчик, — идет за конфигом. Конфиг лежит локально рядом с приложением на этапе деплоя, и приложение его считывает. Дальнейший план — отдельно управлять этой конфигурацией, то есть унести его на S3 и кэшировать.

Приложение на старте вычитывает конфиг и передает в специальную функцию. На выходе получается массив с роутами, которые нужно преобразовать и построить дерево. Чтобы этот конфиг загрузить, мы в angular вешаем его загрузку на хук APP Initializer:

providers: [   {     provide: APP_INITIALIZER,     useFactory: (microfrontendService: MicrofrontendService) => () => microfrontendService.buildDynamicRoutes(),     deps: [MicrofrontendService],     multi: true,   }, ],

Функцию, которая все перетряхивает, я назвал router shaker. Она может выглядеть немного громоздкой, потому что в итоге возвращает массив объектов, который выглядит точно так же, как дефолтный конфиг для роутинга: 

import {loadRemoteModule} from '@angular-architects/module-federation';  export function buildApplicationRoutes(options: Microfrontend[]): Routes {   const mfRoutes: Routes = Array.from(options).map(o => ({     path: o.routePath,     loadChildren: () => loadRemoteModule(o).then(m => m[o.ngModuleName]),     canActivate: [AuthGuard],   }));      return [     ...mfRoutes,     {       path: '',       redirectTo: 'main',       pathMatch: 'full',     },     {       path: '**',       redirectTo: '404',       pathMatch: 'full',     },   ]; }

Самое главное здесь — запись, которая берет наш массив объектов из конфига и превращает его в массив объектов, нужный для роутера:

const mfRoutes: Routes = Array.from(options).map(o => ({   path: o.routePath,   loadChildren: () => loadRemoteModule(o).then(m => m[o.ngModuleName]),   canActivate: [AuthGuard], })); 

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

Теперь у нас есть конфигурационный файлик, который лежит рядом с микрофронтом, и мы динамически все загружаем.  Можно сказать, что мы — динамические микрофронтендеры и можем этим управлять.

Плюсы динамических загрузок — вместо заключения

У динамических загрузок три основных плюса:

  1. Кэширование конфига в коде при обращении.

  2. Конфиг как отдельная репа с CI/CD и безрелизное добавление новых сервисов.

  3. CDN для раздачи конфига.

Кэширование — чтобы не гонять постоянно файлик. Он будет меняться редко, поэтому нам нужно кэширование. И кэширование внутри кода, потому что рано или поздно этот список объектов внутри кода понадобится, чтобы хотя бы хедер отрисовать динамически.

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

Приложение большое, и работает над ним не только моя команда, поэтому конфиг улетел в отдельный репозиторий со своим CI/CD, деплоем в CDN. В любой момент я могу прийти, потушить одно из приложений закомментировав его или поставив флаг disable, задеплоить на прод host-приложение, которое использует этот конфиг, и сразу же его подтянут и будут использовать. CDN нужен, чтобы все было быстро, хорошо кэшировалось и мы не думали о каких-либо проблемах.

Вот такие динамические пирожочки второй части, а в третьей расскажу про фолбэки. Не переключайтесь! 


ссылка на оригинал статьи https://habr.com/ru/articles/726880/


Комментарии

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

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