По мере того, как приложения со временем усложняются, требуя масштабируемости «на лету» и высокой скорости реагирования, архитектура микро-фронтенд, основанная на компонентах Angular, становится все более эффективным решением для сложных веб-приложений.
Микро-фронтенд — это архитектура, которая рассматривает веб-приложение как набор приложений, разрабатываемых отдельными командами. Каждая команда специализируется на определенной области бизнеса или цели. Такая кросс-функциональная команда создает функциональность сверху донизу, от сервера до пользовательского интерфейса.
Плюсы микро-фронтенд архитектуры
- Автоматизация CI /CD. Поскольку каждое приложение интегрируется и развертывается независимо, это упрощает CI/CD. Так как все модули разделены, то не нужно беспокоиться обо всем приложении при внедрении нового модуля. Если в коде модуля есть ошибка, CI/CD прервет весь процесс сборки.
Гибкость команд разработчиков. Многочисленные команды могут разрабатывать и развивать информационные системы, работая по отдельности. - Единая ответственность. Каждая команда микро-фронтеда на 100% фокусируется на функциональности своего микро-фронтенд приложения.
- Возможность повторного использования. Микро-фронтенд приложение может быть повторно использовано несколькими командами в разных системах.
- Технологический агностицизм. Архитектура микро-фронтенд не зависит от технологии. Возможно использовать компоненты, разработанные на разных фреймворков веб-разработки (React, Vue, Angular и т.д.).
- Простой порог входа в систему. Небольшие модули легче изучать и понимать новым разработчикам, входящим в команды, чем монолитную архитектуру с огромной структурой кода.
Демонстрационное приложение
Мы разработаем приложение с микро-фронтенд архитектурой, показанное на рисунке ниже:

Модуль Header & Footer
Эта часть содержит по крайней мере 2 компонента, готовых к экспорту. Прежде всего, нам нужно создать новое приложение и настроить angular builder, который позволит нам использовать пользовательские конфигурации webpack.
ng new layout
npm i --save-dev ngx-build-plus
Теперь нам нужно создать webpack.config.js и webpack.prod.config.js файлы в корне нашего приложения.
// webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin =require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4205/", uniqueName: "layout", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ name: "layout", library: { type: "var", name: "layout" }, filename: "remoteEntry.js", exposes: { Header: './src/app/modules/layout/header/header.component.ts', Footer: './src/app/modules/layout/footer/footer.component.ts' }, shared: { "@angular/core": { singleton: true, requiredVersion:'auto' }, "@angular/common": { singleton: true, requiredVersion:'auto' }, "@angular/router": { singleton: true, requiredVersion:'auto' }, }, }), ], }; // webpack.prod.config.js module.exports = require("./webpack.config");
Модуль Federation позволяет нам совместно использовать общие пакеты npm между различными микро-фронтендами. Это уменьшает полезную нагрузку для модулей с отложенной загрузкой.
Мы можем настроить минимально необходимую версию, допускается две или более версий для одного пакета. Более подробная информация о возможных вариантах плагина находится здесь: ссылка на плагин.
У нас есть exposes раздел, здесь мы можем определить, какие элементы нам нужно разрешить экспортировать из нашего приложения. В нашем случае мы экспортируем только 2 компонента.
Теперь нужно добавить пользовательский конфигурационный файл в angular.json и изменить сборщик по умолчанию на ngx-build-plus:
{ ... "projects": { "layout": { "projectType": "application", "schematics": { "@schematics/angular:component": { "style": "scss" }, "@schematics/angular:application": { "strict": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "ngx-build-plus:browser", "options": { "outputPath": "dist/layout", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [], "extraWebpackConfig": "webpack.config.js" }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "extraWebpackConfig": "webpack.prod.config.js", "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "ngx-build-plus:dev-server", "configurations": { "production": { "browserTarget": "layout:build:production" }, "development": { "browserTarget": "layout:build:development", "extraWebpackConfig": "webpack.config.js", "port": 4205 } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "layout:build" } }, "test": { "builder": "ngx-build-plus:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.scss" ], "scripts": [], "extraWebpackConfig": "webpack.config.js" } } } } }, "defaultProject": "layout" }
Модуль Register Page
Этот модуль будет содержать всю логику для страницы входа / регистрации.
Также создаем приложение и устанавливаем пользовательский сборщик для использования конфигураций webpack.
ng new registerPage
npm i --save-dev ngx-build-plus
После этого создаем webpack.config.js и webpack.prod.config.js
// webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4201/", uniqueName: "register", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ name: "register", library: { type: "var", name: "register" }, filename: "remoteEntry.js", exposes: { RegisterPageModule: "./src/app/modules/register/register-page.module.ts", }, shared: { "@angular/core": { singleton: true, requiredVersion: 'auto' }, "@angular/common": { singleton: true, requiredVersion: 'auto' }, "@angular/router": { singleton: true, requiredVersion: 'auto' }, }, }), ], }; // webpack.prod.config.js module.exports = require("./webpack.config");
Как можно заметить, здесь мы экспортируем только модуль страницы регистрации. Мы можем использовать его как модуль с отложенной загрузкой.
Кроме того, нам нужно изменить builder по умолчанию на ngx-build-plus и добавить конфигурации webpack в файл angular.json (так же, как мы делали для предыдущего модуля).
Модуль Dashboard
Этот модуль предоставляет данные для авторизованного пользователя. Так же создаем приложение со своим конфигурационным файлом webpack:
// webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4204/", uniqueName: "dashboard", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ name: "dashboard", library: { type: "var", name: "dashboard" }, filename: "remoteEntry.js", exposes: { DashboardModule: "./src/app/modules/dashboard/dashboard.module.ts", }, shared: { "@angular/core": { singleton: true, requiredVersion:'auto' }, "@angular/common": { singleton: true, requiredVersion:'auto' }, "@angular/router": { singleton: true, requiredVersion:'auto' }, }, }), ], };
Главное приложение Shell
Основное приложение, которое загружает все микро-фронтенды в одно, называется Shell.
ng new shell
npm i --save-dev ngx-build-plus
Добавляем пользовательский конфигурационный webpack:
// webpack.config.js const webpack = require("webpack"); const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin"); module.exports = { output: { publicPath: "http://localhost:4200/", uniqueName: "shell", }, optimization: { runtimeChunk: false, }, plugins: [ new ModuleFederationPlugin({ shared: { "@angular/core": { eager: true, singleton: true }, "@angular/common": { eager: true, singleton: true }, "@angular/router": { eager: true, singleton: true }, }, }), ], };
Настроим конфигурационный webpack в файле angular.json.
В environment/environment.ts мы объявляем все микро-фронтенды (для версии prod нам нужно заменить адрес локального хоста на развернутый общедоступный адрес):
export const environment = { production: false, microfrontends: { dashboard: { remoteEntry: 'http://localhost:4204/remoteEntry.js', remoteName: 'dashboard', exposedModule: ['DashboardModule'], }, layout: { remoteEntry: 'http://localhost:4205/remoteEntry.js', remoteName: 'layout', exposedModule: ['Header', 'Footer'], } } };
Создадим утилиты для объединения модулей.
// src/app/utils/federation-utils.ts type Scope = unknown; type Factory = () => any; interface Container { init(shareScope: Scope): void; get(module: string): Factory; } declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>; declare const __webpack_share_scopes__: { default: Scope }; const moduleMap: Record<string, boolean> = {}; function loadRemoteEntry(remoteEntry: string): Promise<void> { return new Promise<void>((resolve, reject) => { if (moduleMap[remoteEntry]) { return resolve(); } const script = document.createElement('script'); script.src = remoteEntry; script.onerror = reject; script.onload = () => { moduleMap[remoteEntry] = true; resolve(); // window is the global namespace }; document.body.append(script); }); } async function lookupExposedModule<T>( remoteName: string, exposedModule: string ): Promise<T> { // Initializes the share scope. This fills it with known provided modules from this build and all remotes await __webpack_init_sharing__('default'); const container = window[remoteName] as Container; // Initialize the container, it may provide shared modules await container.init(__webpack_share_scopes__.default); const factory = await container.get(exposedModule); const Module = factory(); return Module as T; } export interface LoadRemoteModuleOptions { remoteEntry: string; remoteName: string; exposedModule: string; } export async function loadRemoteModule<T = any>( options: LoadRemoteModuleOptions ): Promise<T> { await loadRemoteEntry(options.remoteEntry); return lookupExposedModule<T>( options.remoteName, options.exposedModule ); }
и утилиты для сборки lazy loaded маршрутов:
// src/app/utils/route-utils.ts import { loadRemoteModule } from './federation-utils'; import { Routes } from '@angular/router'; import { APP_ROUTES } from '../app.routes'; import { Microfrontend } from '../core/services/microfrontends/microfrontend.types'; export function buildRoutes(options: Microfrontend[]): Routes { const lazyRoutes: Routes = options.map((o) => ({ path: o.routePath, loadChildren: () => loadRemoteModule(o).then((m) => m[o.ngModuleName]), canActivate: o.canActivate, pathMatch: 'full' })); return [ ...APP_ROUTES, ...lazyRoutes ]; }
Нам нужно определить микро-фронтеннд сервис:
// src/app/core/services/microfrontends/microfrontend.service.ts import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { MICROFRONTEND_ROUTES } from 'src/app/app.routes'; import { buildRoutes } from 'src/app/utils/route-utils'; @Injectable({ providedIn: 'root' }) export class MicrofrontendService { constructor(private router: Router) {} /* * Initialize is called on app startup to load the initial list of * remote microfrontends and configure them within the router */ initialise(): Promise<void> { return new Promise<void>((resolve) => { this.router.resetConfig(buildRoutes(MICROFRONTEND_ROUTES)); return resolve(); }); } }
Файл для типа:
// src/app/core/services/microfrontends/microfrontend.types.ts import { LoadRemoteModuleOptions } from "src/app/utils/federation-utils"; export type Microfrontend = LoadRemoteModuleOptions & { displayName: string; routePath: string; ngModuleName: string; canActivate?: any[] };
Нам нужно определить микро-фронтеды согласно маршрутам:
// src/app/app.routes.ts import { Routes } from '@angular/router'; import { LoggedOnlyGuard } from './core/guards/logged-only.guard'; import { UnloggedOnlyGuard } from './core/guards/unlogged-only.guard'; import { Microfrontend } from './core/services/microfrontends/microfrontend.types'; import { environment } from 'src/environments/environment'; export const APP_ROUTES: Routes = []; export const MICROFRONTEND_ROUTES: Microfrontend[] = [ { ...environment.microfrontends.dashboard, exposedModule: environment.microfrontends.dashboard.exposedModule[0], // For Routing, enabling us to ngFor over the microfrontends and dynamically create links for the routes displayName: 'Dashboard', routePath: '', ngModuleName: 'DashboardModule', canActivate: [LoggedOnlyGuard] }, { ...environment.microfrontends.registerPage, exposedModule: environment.microfrontends.registerPage.exposedModule[0], displayName: 'Register', routePath: 'signup', ngModuleName: 'RegisterPageModule', canActivate: [UnloggedOnlyGuard] } ]
Сервис в нашем основном приложении:
// src/app/app.module.ts import { APP_INITIALIZER, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { APP_ROUTES } from './app.routes'; import { LoaderComponent } from './core/components/loader/loader.component'; import { NavbarComponent } from './core/components/navbar/navbar.component'; import { MicrofrontendService } from './core/services/microfrontends/microfrontend.service'; export function initializeApp( mfService: MicrofrontendService ): () => Promise<void> { return () => mfService.initialise(); } @NgModule({ declarations: [ AppComponent, NavbarComponent, LoaderComponent ], imports: [ BrowserModule, AppRoutingModule, RouterModule.forRoot(APP_ROUTES, { relativeLinkResolution: 'legacy' }), ], providers: [ MicrofrontendService, { provide: APP_INITIALIZER, useFactory: initializeApp, multi: true, deps: [MicrofrontendService], }, ], bootstrap: [AppComponent] }) export class AppModule { }
Необходимо загрузить Footer и Header компоненты. Для этого нам надо обновить app компонент:
// src/app/app.component.html <main> <header #header></header> <div class="content"> <app-navbar [isLogged]="auth.isLogged"></app-navbar> <div class="page-content"> <router-outlet *ngIf="!loadingRouteConfig else loading"></router-outlet> <ng-template #loading> <app-loader></app-loader> </ng-template> </div> </div> <footer #footer></footer> </main>
а файл src/app/app.component.ts будет выглядеть так:
import { ViewContainerRef, Component, ComponentFactoryResolver, OnInit, AfterViewInit, Injector, ViewChild } from '@angular/core'; import { RouteConfigLoadEnd, RouteConfigLoadStart, Router } from '@angular/router'; import { loadRemoteModule } from './utils/federation-utils'; import { environment } from 'src/environments/environment'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements AfterViewInit, OnInit{ @ViewChild('header', { read: ViewContainerRef, static: true }) headerContainer!: ViewContainerRef; @ViewChild('footer', { read: ViewContainerRef, static: true }) footerContainer!: ViewContainerRef; loadingRouteConfig = false; constructor(private injector: Injector, private resolver: ComponentFactoryResolver, private router: Router ) {} ngOnInit() { this.router.events.subscribe(event => { if (event instanceof RouteConfigLoadStart) { this.loadingRouteConfig = true; } else if (event instanceof RouteConfigLoadEnd) { this.loadingRouteConfig = false; } }); } ngAfterViewInit(): void { // load header loadRemoteModule({ ...environment.microfrontends.layout, exposedModule: environment.microfrontends.layout.exposedModule[0], }) .then(module => { const factory = this.resolver.resolveComponentFactory(module.HeaderComponent); this.headerContainer?.createComponent(factory, undefined, this.injector); }); // load footer loadRemoteModule({ ...environment.microfrontends.layout, exposedModule: environment.microfrontends.layout.exposedModule[1], }) .then(module => { const factory = this.resolver.resolveComponentFactory(module.FooterComponent); this.footerContainer?.createComponent(factory, undefined, this.injector); }); } }
Взаимодействие между микро-фронтендами
У нас есть несколько способов обмена данными между различными микро-фронтендами. В нашем случае мы решили использовать пользовательское событие для связи. Пользовательское событие позволяет нам отправлять пользовательские данные с помощью полезной нагрузки события.
Один модуль должен отправлять пользовательские события следующим образом:
const busEvent = new CustomEvent('app-event-bus', { bubbles: true, detail: { eventType: 'auth-register', customData: 'some data here' } }); dispatchEvent(busEvent);
Другие микро-фронтенды могут подписаться на это событие:
onEventHandler(e: CustomEvent) { if (e.detail.eventType === 'auth-register') { const isLogged = Boolean(localStorage.getItem('token')); this.auth.isLogged = isLogged; if (isLogged) { this.router.navigate(['/']); } else { this.router.navigate(['/signup']); } } } ngOnInit() { this.$eventBus = fromEvent<CustomEvent>(window, 'app-event-bus').subscribe((e) => this.onEventHandler(e)); // ... }
Заключение
Архитектура микро-фронтендов приобретает все большую и большую популярность, поскольку с течением времени кодовые базы веб-приложений становятся все более сложными. Крайне важно уметь проводить четкие границы между микро-фронтед приложениями и командами их разрабатывающих. Очень важно установить правильное взаимодействия и согласованность между техническими командами, что позволит успешно разрабатывать, развивать, поддерживать и внедрять сложные веб-приложения.
ссылка на оригинал статьи https://habr.com/ru/company/auriga/blog/659199/
Добавить комментарий