
Este é o primeiro artigo sobre este tópico, um total de 3 estão planejados:
- * Crie um aplicativo raiz a partir do seu projeto existente, adicione 3 micro-aplicativos a ele (vue, react, angular)
- Comunicação entre microaplicativos
- Trabalhar com git (implantar, atualizações)
Índice
- uma parte comum
- Porque é necessário
- Crie um contêiner raiz (veja a definição abaixo) a partir do seu monólito
- Crie um micro-aplicativo VUE (vue-app)
- Criar um micro-aplicativo REACT (react-app)
- Crie um microaplicativo ANGULAR (angular-app)
1. Parte geral
O objetivo deste artigo é adicionar a capacidade de usar um projeto monolítico existente como um contêiner raiz para uma arquitetura de microsserviço.
O projeto existente é feito no angular 9.
Para arquitetura de microsserviço, usamos a biblioteca single-spa .
Você precisa adicionar 3 projetos ao projeto raiz, usamos tecnologias diferentes: vue-app, angular-app, react-app (ver p. 4, 5, 6).
Em paralelo com a criação deste artigo, estou tentando implementar essa arquitetura em um projeto de produção no qual estou trabalhando atualmente. Portanto, tentarei descrever todos os erros que tenho no processo de desenvolvimento e suas soluções.
Aplicação raiz (doravante raiz) - a raiz (contêiner) de nosso aplicativo. Colocaremos (registraremos) todos os nossos microsserviços nele. Se você já tem algum projeto e deseja implementar essa arquitetura nele, seu projeto existente será o aplicativo raiz, de onde, ao longo do tempo, você tentará retirar pedaços de seu aplicativo, criar microsserviços separados e registrá-lo neste contêiner.
Essa abordagem de criar um contêiner raiz fornecerá uma excelente oportunidade para migrar para outra tecnologia sem muita dor.
Por exemplo, decidimos mudar completamente de angular para vue, mas o projeto é ousado e, no momento, traz muito dinheiro para o negócio.
Sem a arquitetura de microsserviço, isso não teria aparecido na mente, apenas para pessoas desesperadas que acreditam em unicórnios e que todos nós somos um holograma.
Para mudar para uma nova tecnologia, na realidade é necessário reescrever todo o projeto, e só então poderíamos ficar altos com seu aparecimento na batalha.
Outra opção é a arquitetura de microsserviço. Você pode criar um projeto root a partir do seu monolith, adicionar um novo projeto lá no mesmo vue, configurar roaming no root, está feito. Você pode entrar na batalha, gradualmente cortar pequenos pedaços da raiz do projeto e transferi-los para o seu micro-projeto vue. Isso deixa apenas os arquivos em seu contêiner raiz que são necessários para importar seu novo projeto.
Isso pode ser feito aqui e agora, sem perda, sem sangue e, o mais importante, é real.
Vou usar angular como root, já que o projeto existente foi escrito nele.
A interface geral na qual o aplicativo de página única será empacotado:
bootstrap (mounter, bus) - chamado depois que o serviço é carregado, ele dirá qual elemento da casa você precisa montar, fornecerá um barramento de mensagem para o qual o microsserviço se inscreverá e será capaz de ouvir e enviar solicitações e o comando de
montagem () - montar a aplicação em casa
unmount () - desmontar a aplicação
unload () - descarregar a aplicação
No código, mais uma vez descreverei a operação de cada método localmente no local de uso.
2. Por que é necessário
Vamos começar neste ponto estritamente em ordem.
Existem 2 tipos de arquitetura:
- Monolith
- Arquitetura de microsserviço

Com o monólito, tudo é bastante simples e tão familiar quanto possível para todos nós. Coesão forte, grandes blocos de código, um repositório compartilhado, vários métodos.
No início, a arquitetura monolítica é a mais conveniente e rápida possível. Não há problemas e dificuldades na criação de arquivos de integração, camadas intermediárias, modelos de eventos, barramentos de dados, etc.
O problema aparece quando o seu projeto cresce, aparece um monte de funções complexas e separadas para diferentes propósitos. Toda essa funcionalidade começa a ser vinculada dentro do projeto a alguns modelos gerais, estados, utilitários, interfaces, métodos, etc.
Além disso, o número de diretórios e arquivos no projeto torna-se enorme com o tempo, há problemas de localização e compreensão do projeto como um todo, a "visão superior" se perde, o que dá clareza ao que estamos fazendo, onde o que está e quem precisa.
Além de tudo isso, a Lei de Eagleson está em ação , que diz que seu código, que você não olhou por 6 meses ou mais, parece que outra pessoa o escreveu.
O mais doloroso é que tudo vai crescer exponencialmente, com isso vão começar as muletas, que devem ser acrescentadas devido à complexidade de manter o código em conexão com o exposto e, com o tempo, as ondas de termos irresponsáveis ocorrendo.
Como resultado, se você tiver um projeto ativo em constante evolução, isso se tornará um grande problema, o eterno descontentamento de sua equipe, um grande número de pessoas - horas para fazer pequenas alterações no projeto, um limite mínimo de entrada para novos funcionários e muito tempo para levar o projeto para a batalha. Isso tudo leva à desordem, bem, nós amamos a ordem?
Isso sempre acontece com um monólito?
Claro que não! Tudo depende do tipo do seu projeto, dos problemas que surgem durante o desenvolvimento da equipe. Seu projeto pode não ser tão grande, para realizar alguma tarefa de negócio complexa, isso é normal e acredito que esteja correto.
Em primeiro lugar, devemos estar atentos aos parâmetros do nosso projeto.
Vou tentar tirar os pontos pelos quais você pode entender se realmente precisamos de uma arquitetura de microsserviço:
- 2 ou mais equipes estão trabalhando no projeto, o número de desenvolvedores front-end é 10+;
- Seu projeto consiste em 2 ou mais modelos de negócios, por exemplo, você tem uma loja online com um grande número de mercadorias, filtros, notificações e a funcionalidade de distribuição de entrega de correio (2 modelos separados, não pequenos negócios que irão interferir um no outro). Tudo isso pode viver separadamente e não depender um do outro.
- O conjunto de recursos de IU aumenta diariamente ou semanalmente sem afetar o resto do sistema.
Microfronts são usados para:
- Partes separadas do front-end podem ser desenvolvidas, testadas e implantadas de forma independente;
- Partes do frontend podem ser adicionadas, removidas ou substituídas sem remontagem;
- .
- , - «», - ( ) -.
- ,
- .
single-spa ?
- (, React, Vue Angular) , .
- Single-spa , , .
- .
Microservice, no meu entendimento, é um aplicativo independente de página única que resolverá apenas uma tarefa do usuário. Este aplicativo também não precisa resolver toda a tarefa da equipe.
SystemJS é uma biblioteca JS de código aberto comumente usada como polyfill para navegadores.
O polyfill é um pedaço de código JS usado para fornecer funcionalidade moderna para navegadores mais antigos que não o suportam.
Um dos recursos do SystemJS é um mapa de importação, que permite importar um módulo pela rede e mapeá-lo para um nome de variável.
Por exemplo, você pode usar um mapa de importação para uma biblioteca React que é carregada por meio de um CDN:
MAS!
Se você está criando um projeto do zero, mesmo considerando que determinou todos os parâmetros do seu projeto, você decidiu que terá um grande superprojeto Mega com uma equipe de 30+ pessoas, espere!
Gosto muito da ideia do notório fundador da ideia de microsserviços - Martin Fowler .
Ele propôs combinar a abordagem monolítica e microsserviços em um (MonolithFirst). Sua ideia principal é a seguinte:
você não deve iniciar um novo projeto com microsserviços, mesmo se estiver totalmente confiante de que o futuro aplicativo será grande o suficiente para justificar essa abordagem
Também descreverei as desvantagens de usar essa arquitetura aqui:
- A interação entre fragmentos não pode ser alcançada com métodos de tubo padrão (DI, por exemplo).
- E quanto às dependências comuns? Afinal, o tamanho do aplicativo aumentará aos trancos e barrancos se eles não forem retirados dos fragmentos.
- Alguém ainda deve ser responsável pelo roteamento na aplicação final.
- Não está claro o que fazer com o fato de que diferentes microsserviços podem estar localizados em domínios diferentes
- O que fazer se um dos fragmentos não estiver disponível / não puder ser renderizado.
3. Criação de um contêiner raiz
E então, chega de teoria, é hora de começar.
Vá para o console
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
Em ts.config.app.json, importe globalmente declarações (tipos)
// ts.config.app.json
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
(+) "systemjs"
]
},
Adicione a app-routing.module.ts todos os micro-aplicativos que adicionamos à raiz
// 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' }
}
]
},
Você também precisa adicionar configuração
// extra-webpack.config.json
module.exports = (angularWebpackConfig, options) => {
return {
...angularWebpackConfig,
module: {
...angularWebpackConfig.module,
rules: [
...angularWebpackConfig.module.rules,
{
parser: {
system: false
}
}
]
}
};
}
Vamos mudar o arquivo package.json, adicionar a ele tudo que é necessário para o trabalho ou
// 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",
}
Adicione as bibliotecas necessárias ao 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"
]
}
}
},
Crie uma pasta de spa único na raiz do projeto . Vamos adicionar 2 arquivos a ele.
1. route-reuse-strategy.ts - nosso arquivo de roteamento de microsserviços.
Se um aplicativo filho estiver roteando internamente, esse aplicativo interpretará isso como uma alteração de rota.
Por padrão, isso irá destruir o componente atual e substituí-lo por uma nova instância do mesmo componente spa-host.
Essa estratégia de reutilização de rota examina routeData.app para determinar se a nova rota deve ser tratada como a mesma rota da anterior, garantindo que não remontemos o aplicativo filho quando o aplicativo filho especificado for roteado internamente.
// 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. Serviço single-spa.service.ts
O serviço armazenará o método para montar (montar) e desmontar (desmontar) aplicativos de micro-front-end.
mount é uma função de ciclo de vida que será chamada sempre que um aplicativo registrado não for montado, mas sua função de atividade retorna true. Quando chamada, esta função deve olhar para o URL para determinar a rota ativa e, em seguida, criar elementos DOM, eventos DOM, etc.
unmount é uma função de ciclo de vida que será chamada sempre que um aplicativo registrado for montado, mas sua função de atividade retorna false. Quando chamada, esta função deve limpar todos os elementos 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])
);
}
}
Em seguida, criamos um diretório container / app / spa-host .
Este módulo implementará o registro e o mapeamento de nossos aplicativos de micro frontend para fazer o root.
Vamos adicionar 3 arquivos ao módulo.
1. O próprio módulo 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. Componente spa-host.component.ts - coordena a instalação e desmontagem de aplicativos de micro-front-end
// 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 - verifica se o nome do aplicativo na rota é diferente, analisa o serviço anterior, se for também, basta ir até ele.
// 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);
}
}
Registramos tudo o que adicionamos ao app.module
// app.module.ts
providers: [
...,
{
(+) provide: RouteReuseStrategy,
(+) useClass: MicroFrontendRouteReuseStrategy
}
]
Vamos mudar 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 => {});
Em seguida, criamos um arquivo import-map.json na pasta de compartilhamento. O arquivo é necessário para adicionar mapas de importação.
No momento, ele será esvaziado e preenchido conforme os aplicativos forem adicionados ao 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. Crie um microaplicativo VUE (vue-app)
Agora que adicionamos a capacidade de se tornar um aplicativo raiz ao nosso projeto monolítico, é hora de criar nosso primeiro microaplicativo externo com spa único.
Primeiro, precisamos instalar globalmente o create-single-spa, uma interface de linha de comando que nos ajudará a criar novos projetos de spa único com comandos simples.
Vá para o console
npm install --global create-single-spa
Crie um aplicativo vue simples usando um comando no console
create-single-spa
A interface da linha de comando solicitará que você selecione um diretório, nome do projeto, organização e tipo de aplicativo a ser criado

? 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
Lançamos nosso micro-aplicativo
npm i
npm run serve --port 8000
Ao inserirmos o caminho no navegador localhost : 8080 / , no caso de vue, veremos uma tela em branco. O que aconteceu?
Como não há arquivo index.js no micro-aplicativo gerado.
O Single-spa oferece um playground para baixar o aplicativo da Internet, então vamos usá-lo primeiro.
Adicionar ao index.js
single-spa-playground.org/playground/instant-test?name=@some-name/vue-app&url=8000Ao criar o aplicativo raiz, adicionamos um mapa com antecedência para carregar nosso projeto 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"
}
}
Pronto! Agora, de nosso projeto de raiz angular, podemos carregar microaplicativos escritos em vue.
5. Crie um microaplicativo REACT (react-app)
Criamos um aplicativo de reação simples semelhante usando o comando no console
create-single-spa
Nome da organização: somename
Nome do projeto : 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
Vamos verificar se adicionamos um mapa de importação em nosso aplicativo raiz
{
"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",
}
}
Feito! Agora, em nossa rota react-app, carregamos o microprojeto react.
6. Crie um microaplicativo ANGULAR (angular-app)
Criamos um microaplicativo Angular exatamente da mesma maneira que os 2 anteriores
create-single-spa
Nome da organização: somename
Nome do projeto: 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
Vamos verificar se adicionamos um mapa de importação em nosso aplicativo raiz
{
"imports": {
... ,
"@somename/angular-app": "//localhost:8082/main.js",
}
}
Vamos lançar, conferir, tudo deve funcionar.
Este é meu primeiro post no Habré, ficarei muito grato pelos seus comentários.