Это первая статья по в данной теме, всего их планируется 3:
- * Создание root application из вашего существующего проекта, добавление в него 3 микро-приложения (vue, react, angular)
- Общение между микро-приложениями
- Работа с git (deploy, обновления)
Оглавление
- Общая часть
- Зачем это нужно
- Создание root контейнера (определение см. ниже) из вашего монолита
- Создаем микро-приложение VUE (vue-app)
- Создаем микро-приложение REACT (react-app)
- Создаем микро-приложение ANGULAR (angular-app)
1. Общая часть
Задача этой статьи: добавить возможность использовать существующий монолитный проект как root контейнер для микросервисной архитектуры.
Существующий проект выполнен на angular 9.
Для микросервисной архитектуры используем библиотеку single-spa.
В root проект необходимо добавить 3 проекта, используем разные технологии: vue-app, angular-app, react-app (см. п. 4, 5, 6).
Параллельно с созданием этой статьи, я стараюсь внедрить эту архитектуру в боевой проект, над которым в данный момент работаю. Следовательно буду стараться описывать все ошибки, которые у меня возникнут в процессе разработки и их решения.
Root application (далее root) — корень (контейнер) нашего приложения. В него мы будем класть (регистрировать) все наши микросервисы. Если вы уже имеете какой либо проект и хотите реализовать в нем эту архитектуру, то root application будет именно ваш существующий проект, откуда со временем вы будете стараться выгрызать куски вашего приложения, создавать отдельные микросервисы и регистрировать его в этом контейнере.
Такой подход создания root контейнера даст отличную возможность перехода на другой технологию без особой боли.
К примеру мы решили переехать с angular на vue полностью, но проект жирный, и в данный момент приносит много денег бизнесу.
Без микросервисной архитектуры, в мыслях бы этого не могло появиться, только у отчаянных людей, которые верят в единорогов и что мы все — голограмма.
Для того, чтобы перейти на новую технологию в реальности необходимо переписать весь проект, и только тогда мы смогли бы кайфануть от его появления на бою.
Другой вариант, это микросервисная архитектура. Вы можете создать root проект из своего монолита, добавить туда новый проект на том же vue, настроить роуминг в root, готово. Можно лить в бой, постепенно выпиливать с root проекта небольшие кусочки и переносить их в ваш vue микро-проект. В результате в вашем root контейнере останется только те файлы, которые необходимы для импорта вашего нового проекта.
Это можно сделать прям здесь и сейчас, без потерь, крови и главное реально.
В качестве root я буду использовать angular, так как существующий проект был написан именно на нем.
Общий интерфейс, в который будет заворачиваться single page application:
bootstrap(mounter, bus) — вызывается после загрузки сервиса, скажет в какой элемент дома нужно монтироваться, даст ему шину сообщений на которую микросервис у себя подпишется и сможет слушай и посылать запросы и команду
mount() — монтировать приложение из дома
unmount() — демонтаж приложения
unload() — выгрузка приложения
В коде я локально по месту использования буду еще раз в описывать работу каждого метода.
2. Зачем это нужно
Начнем в этом пункте строго по порядку.
Существует 2 типа архитектуры:
- Монолит
- Микросервисная архитектура
С монолитом все довольно просто и максимально всем нам знакомо. Сильная связанность, огромные блоки кода, общий репозиторий, куча методов.
На старте монолитная архитектура максимально удобна и быстра. Нет никаких проблем и сложностей в создании каких либо интеграционных файлов, прослоек, событийных моделей, шин данных и тд.
Проблема появляется когда разрастается ваш проект, появляется много отдельного, сложного функционала разного назначения. Весь этот функционал начинает завязываться внутри проекта на какие то общие модели, состояния, утилиты, интерфейсы, методы и тп.
Так же количество директорий и файлов в проекте со временем становится огромное, появляются проблемы поиска и понимания проекта в целом, теряется «взгляд сверху», который придает ясность того, чем мы занимаемся, где что лежит и кому это нужно.
В придачу ко всему этому срабатывает закон Иглсона, который говорит, что Ваш код, который вы не просматривали 6 или более месяцев, выглядит так, будто его написал кто-то другой.
Самое больное, все будет обрастать в геометрической прогрессии, в результате начнется костыли, которые необходимо добавить из за опять же сложности поддержания кода в связи с вышеописанным и со временем встречающихся волн невменяемых сроков.
В результате, если у вас живой проект, который постоянно развивается, это станет большой проблемой, вечным недовольством вашей команды, огромное количество человека — часов на внесения несущественных изменений в проект, низкий порог входа для новых сотрудников и много времени на выкатывание проекта на бой. Это все приводит к беспорядку, ну мы же любим порядок?
Всегда ли так происходит с монолитом?
Конечно же нет! Все зависит от типа вашего проекта, от тех проблем, которые возникают при командной разработке. Ваш проект может быть не такой большой, выполнять какую то одну сложную бизнес задачу, это нормально и полагаю — правильно.
В первую очередь нам необходимо обратить внимание на параметры нашего проекта.
Попробую вынести пункты, по которым можно понять, так ли нам необходима микросервисная архитектура:
- Над проектом работает 2 и более команды, количество фронтенд разработчиков 10+;
- Ваш проект состоит из 2 и более бизнес модели, например у вас интернет магазин с огромным количеством товаров, фильтров, нотификации, и функционал курьерского распределения доставок (2 отдельные не маленькие бизнес модели, которые будут друг другу мешать). Это все может жить отдельно и не зависеть друг от друга.
- Набор возможностей UI растёт ежедневно или еженедельно, не оказывая влияния на остальную часть системы.
Микрофронтенды применяются для того, чтобы:
- Отдельные части фронтенда могли разрабатываться, тестироваться и развёртываться независимо;
- Отдельные части фронтенда могли быть добавлены, удалены или заменены без повторной сборки;
- Разные части фронтенда могли быть созданы с помощью разных технологий.
- В идеале, связи между этими микро-приложениями не должно быть от слова «совсем», поэтому этот пункт должен тоже влиять на принятии решения о выпиливании бизнес-кейсов (или кейсы для расширенного пользовательского опыта) в отдельный микро-сервис.
- Скорость разработки должна быть постоянной, несмотря на рост приложения
- Разные команды должны иметь возможность использовать собственные инструменты.
Какие бонусы мы еще можем получить от single-spa библиотеки?
- Вы можете управлять большими общими зависимостями (например, библиотеками React, Vue или Angular) проще с помощью карты импорта, как вы увидите позже в этой должности.
- Single-spa имеет ленивую загрузку включен для модулей в браузере, так что ваше приложение будет загружать модули только тогда, когда это необходимо.
- Разделение приложения на несколько модулей в браузере позволяет разрабатывать и развертывать приложение независимо друг от друга.
Микросервис в моем понимании — самостоятельный single page application, который будет решать только одну задачу пользователя. Это приложение так же не должно решать задачу команды целиком.
SystemJS — это библиотека JS с открытым исходным кодом, которая обычно используется в качестве полифилла для браузеров.
Полифилл является частью кода JS, используемого для обеспечения современной функциональности для старых браузеров, которые не поддерживают его.
Одной из особенностей SystemJS является карта импорта, которая позволяет импортировать модуль по сети и сопоставить его с переменным именем.
Например, можно использовать карту импорта для библиотеки React, которая загружается через CDN:
НО!
Если вы создаете проект с нуля, даже с учетом того, что вы определили все параметры вашего проекта, решили что у вас будет огромный Мега супер проект с командой 30+ человек, постойте!
Мне очень нравится мысль небезызвестного основоположника идеи микросервисов — Мартин Фаулер.
Он предложил объединить монолитный подход и микросервисы в один (MonolithFirst). Основная его идея звучит так:
не следует начинать новый проект с микросервисов даже при полной уверенности, что будущее приложение будет достаточно большим, чтобы оправдать такой подход
Так же здесь опишу минусы использования такой архитектуры:
- Взаимодействие между фрагментами невозможно обеспечить стандартными ламповыми методами (DI, например).
- Как быть с общими зависимостями? Ведь размер приложения будет расти как на дрожжах, если их не выносить из фрагментов.
- За роутинг в конечном приложении все равно должен отвечать кто-то один.
- Неясно, что делать с тем, что разные микросервисы могут находиться на разных доменах
- Что делать, если один из фрагментов недоступен / не может отрисоваться.
3. Создание root контейнера
И так, хватит теории, пора начинать.
Заходим в консоль
ng add single-spa-angular
npm i systemjs@6.1.4,
npm i -d @types/systemjs@6.1.0,
npm import-map-overrides@1.8.0
В ts.config.app.json глобально импортируем декларации (типы)
// ts.config.app.json
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
(+) "systemjs"
]
},
Добавляем в app-routing.module.ts все микроприложения, которые мы добавим в root
// app-routing.module.ts
{
path: 'vue-app',
children: [
{
path: '**',
loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),
data: { app: '@somename/vue-app' }
}
]
},
{
path: 'angular-app',
children: [
{
path: '**',
loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),
data: { app: '@somename/angular-app' }
}
]
},
{
path: 'react-app',
children: [
{
path: '**',
loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),
data: { app: '@somename/react-app' }
}
]
},
Так же нужно добавить config
// extra-webpack.config.json
module.exports = (angularWebpackConfig, options) => {
return {
...angularWebpackConfig,
module: {
...angularWebpackConfig.module,
rules: [
...angularWebpackConfig.module.rules,
{
parser: {
system: false
}
}
]
}
};
}
Изменим файл package.json, добавим в него все необходимые для работы либы
// package.json
"dependencies": {
...,
(+) "single-spa": "^5.4.2",
(+) "single-spa-angular": "^4.2.0",
(+) "import-map-overrides": "^1.8.0",
(+) "systemjs": "^6.1.4",
}
"devDependencies": {
...,
(+) "@angular-builders/custom-webpack": "^9",
(+) "@types/systemjs": "^6.1.0",
}
Добавляем необходимые библиотеки в angular.json
// angular.json
{
...,
"architect": {
"build": {
...,
"scripts": [
...,
(+) "node_modules/systemjs/dist/system.min.js",
(+) "node_modules/systemjs/dist/extras/amd.min.js",
(+) "node_modules/systemjs/dist/extras/named-exports.min.js",
(+) "node_modules/systemjs/dist/extras/named-register.min.js",
(+) "node_modules/import-map-overrides/dist/import-map-overrides.js"
]
}
}
},
В корне проекта создаем папку single-spa. В него добавим 2 файла.
1. route-reuse-strategy.ts — файл маршрутизации наших микросервисов.
Если дочернее приложение выполняет маршрутизацию внутри себя, это приложение интерпретирует это как изменение маршрута.
По умолчанию это приведет к уничтожению текущего компонента и замене его новым экземпляром того же компонента spa-host.
Эта стратегия повторного использования маршрута смотрит на routeData.app, чтобы определить, должен ли новый маршрут быть обрабатывается как тот же маршрут, что и предыдущий, гарантируя, что мы не перемонтируем дочернее приложение, когда указанное дочернее приложение маршруты внутри себя.
// route-reuse-strategy.ts
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';
import { Injectable } from '@angular/core';
@Injectable()
export class MicroFrontendRouteReuseStrategy extends RouteReuseStrategy {
shouldDetach(): boolean {
// маршрут не сохраняется
return false;
}
store(): void { }
shouldAttach(): boolean {
return false;
}
// время присоединения маршрута
retrieve(): DetachedRouteHandle {
return null;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig || (future.data.app && (future.data.app === curr.data.app));
}
}
2. Сервис single-spa.service.ts
В сервисе будет храниться метод монтирования (mount) и демонтирования (unmount) микро-фронтенд приложений.
mount — функция жизненного цикла, которая будет вызываться всякий раз, когда зарегистрированное приложение не смонтировано, но его функция активности возвращает true. При вызове эта функция должна просмотреть URL-адрес, чтобы определить активный маршрут, а затем создать элементы DOM, события DOM и т.п.
unmount — функция жизненного цикла, которая будет вызываться всякий раз, когда монтируется зарегистрированное приложение, но ее функция активности возвращает false. При вызове эта функция должна очищать все элементы DOM.
//single-spa.service.ts
import { Injectable } from '@angular/core';
import { mountRootParcel, Parcel, ParcelConfig } from 'single-spa';
import { Observable, from, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class SingleSpaService {
private loadedParcels: {
[appName: string]: Parcel;
} = {};
mount(appName: string, domElement: HTMLElement): Observable<unknown> {
return from(System.import<ParcelConfig>(appName)).pipe(
tap((app: ParcelConfig) => {
this.loadedParcels[appName] = mountRootParcel(app, {
domElement
});
})
);
}
unmount(appName: string): Observable<unknown> {
return from(this.loadedParcels[appName].unmount()).pipe(
tap(( ) => delete this.loadedParcels[appName])
);
}
}
Далее создаем директорию container/app/spa-host.
Этот модуль будет реализовывать регистрации и отображение наших микро-фронтенд приложений в root.
Добавим в модуль 3 файла.
1. Сам модуль spa-host.module.ts
//spa-host.module.ts
import { RouterModule, Routes } from '@angular/router';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SpaUnmountGuard } from './spa-unmount.guard';
import { SpaHostComponent } from './spa-host.component';
const routes: Routes = [
{
path: '',
canDeactivate: [SpaUnmountGuard],
component: SpaHostComponent,
},
];
@NgModule({
declarations: [SpaHostComponent],
imports: [CommonModule, RouterModule.forChild(routes)]
})
export class SpaHostModule {}
2. Компонент spa-host.component.ts — координирует монтаж и демонтаж микро-фронтенд приложений
// spa-host.component.ts
import { Component, OnInit, ViewChild, ElementRef, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import {SingleSpaService} from '../../single-spa/single-spa.service';
@Component({
selector: 'app-spa-host',
template: '<div #appContainer></div>',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpaHostComponent implements OnInit {
@ViewChild('appContainer', { static: true })
appContainerRef: ElementRef;
appName: string;
constructor(private singleSpaService: SingleSpaService, private route: ActivatedRoute) { }
ngOnInit() {
// тащим название подгружаемой карты
this.appName = this.route.snapshot.data.app;
this.mount().subscribe();
}
// собираем наш подгруженный проект по выбранному роуту
mount(): Observable<unknown> {
return this.singleSpaService.mount(this.appName, this.appContainerRef.nativeElement);
}
// разбираем
unmount(): Observable<unknown> {
return this.singleSpaService.unmount(this.appName);
}
}
3. spa-unmount.guard.ts — проверяет, если имя приложения в роуте другое, разбираем предыдущий сервис, если тоже, просто переходим на него.
// spa-unmount.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SpaHostComponent } from './spa-host.component';
@Injectable({ providedIn: 'root' })
export class SpaUnmountGuard implements CanDeactivate<SpaHostComponent> {
canDeactivate(
component: SpaHostComponent,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot
): boolean | Observable<boolean> {
const currentApp = component.appName;
const nextApp = this.extractAppDataFromRouteTree(nextState.root);
if (currentApp === nextApp) {
return true;
}
return component.unmount().pipe(map(_ => true));
}
private extractAppDataFromRouteTree(routeFragment: ActivatedRouteSnapshot): string {
if (routeFragment.data && routeFragment.data.app) {
return routeFragment.data.app;
}
if (!routeFragment.children.length) {
return null;
}
return routeFragment.children.map(r => this.extractAppDataFromRouteTree(r)).find(r => r !== null);
}
}
Регистрируем все что добавили в в app.module
// app.module.ts
providers: [
...,
{
(+) provide: RouteReuseStrategy,
(+) useClass: MicroFrontendRouteReuseStrategy
}
]
Изменим main.js.
// main.ts
import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { start as singleSpaStart } from 'single-spa';
import { getSingleSpaExtraProviders } from 'single-spa-angular';
import { AppModule } from './app/app.module';
import { PlatformLocation } from '@angular/common';
if (environment.production) {
enableProdMode();
}
singleSpaStart();
// название проекта
const appId = 'container-app';
// Так как наше приложение использует маршрутизацию, мне необходимо импортировать функцию getSingleSpaExtraProviders.
platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule).then(module => {
NgZone.isInAngularZone = () => {
// @ts-ignore
return window.Zone.current._properties[appId] === true;
};
const rootPlatformLocation = module.injector.get(PlatformLocation) as any;
const rootZone = module.injector.get(NgZone);
// tslint:disable-next-line:no-string-literal
rootZone['_inner']._properties[appId] = true;
rootPlatformLocation.setNgZone(rootZone);
})
.catch(err => {});
Далее создаем файл import-map.json в папке share. Файл нужен для добавления карт импорта.
В данный момент он будет у нас пустой и наполняться по мере добавления в root приложений.
<head>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My first microfrontend root project</title>
<base href="/">
...
(+) <meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap" src="/assets/import-map.json"></script>
</head>
<body>
<app-root></app-root>
<import-map-overrides-full></import-map-overrides-full>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
</html>
4. Создаем микро-приложение VUE (vue-app)
Теперь, когда мы добавили в свой монолитный проект возможность стать root приложением, пора создать свое первое внешнее микро-приложение с single-spa.
Во-первых, нам нужно установить глобально create-single-spa, интерфейс командной строки, который поможет нам создавать новые проекты single-spa с помощью простых команд.
Заходим в консоль
npm install --global create-single-spa
Создаем простое приложение vue с помощью команды в консоле
create-single-spa
Интерфей командной строки предложит выбрать директорию, название проекта, организации и тип приложения для создания
? Directory for new project vue-app
? Select type to generate single-spa application / parcel
? Which framework do you want to use? vue
? Which package manager do you want to use? npm
? Organization name (use lowercase and dashes) somename
Запускаем наше микро-приложение
npm i
npm run serve --port 8000
Когда мы введем путь в браузере localhost:8080/, в случае с vue мы увидим пустой экран. Что же произошло?
Так как в созданном микро-приложении нет файла index.js.
Single-spa предоставляет игровую площадку, с которой можно загружать приложение через интернет, поэтому давайте сначала воспользуемся ей.
Добавим в index.js
single-spa-playground.org/playground/instant-test?name=@some-name/vue-app&url=8000При создании root приложения, мы заранее добавили карту для загрузки нашего vue проекта.
{
"imports": {
... ,
"vue": "https://unpkg.com/vue",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
"@somename/vue-app": "//localhost:8080/js/app.js"
}
}
Готова! Теперь с нашего angular root проекта мы можем загружать микро-приложения, написанное на vue.
5. Создаем микро-приложение REACT (react-app)
Создаем так же простое приложение react с помощью команды в консоле
create-single-spa
Название организации: somename
Название проекта: react-app
? Directory for new project react-app
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Organization name (use lowercase and dashes) somename
Проверим, добавили ли мы карту импорта в нашем root приложении
{
"imports": {
... ,
"react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.development.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.development.js",
"@somename/react-app": "//localhost:8081/somename-projname.js",
}
}
Готово! Теперь по нашем роуту react-app у нас загружается react микро-проект.
6. Создаем микро-приложение ANGULAR (angular-app)
Angular микро-приложение создаем абсолютно так же, как и 2 предыдущих
create-single-spa
Название организации: somename
Название проекта: angular-app
? Directory for new project angular-app
? Select type to generate single-spa application / parcel
? Which framework do you want to use? angular
? Which package manager do you want to use? npm
? Organization name (use lowercase and dashes) somename
Проверим, добавили ли мы карту импорта в нашем root приложении
{
"imports": {
... ,
"@somename/angular-app": "//localhost:8082/main.js",
}
}
Запускаем, проверяем, все должно работать.
Это мой первый пост на Хабре, буду очень благодарен за ваши комментарии.