Devido ao fato dos aplicativos serem carregados em um iframe, há problemas com o layout, os plug-ins não funcionam corretamente, os clientes ainda baixam dois bundles com Angular, mesmo que as versões do Angular no aplicativo e do Frame Manager sejam iguais. E usar iframe em 2020 parece falta de educação. Mas e se abandonarmos os frames e carregarmos todos os aplicativos em uma janela?
Descobriu-se que isso é possível, e agora vou lhe dizer como implementá-lo.
Soluções possíveis
Spa único : "Um roteador javascript para microsserviços front-end" - conforme indicado no site da biblioteca. Permite que você execute simultaneamente aplicativos escritos em diferentes estruturas na mesma página. A solução não funcionou para nós: a maior parte da funcionalidade não era necessária e o carregador System.js usado nele em alguns casos cria problemas ao construir com webpack. E usar um carregador de módulo com webpack não parece ser a melhor solução.
Elementos angulares: este pacote permite envolver componentes angulares em componentes da web. Você pode agrupar todo o aplicativo. Em seguida, você terá que adicionar um polyfill para navegadores antigos, e criar um componente da web a partir de um aplicativo inteiro com seu próprio roteamento parece uma decisão ideologicamente errada.
Implementação do gerenciador de quadros
Vamos ver como o carregamento de aplicativos sem quadros no gerenciador de quadros é implementado usando um exemplo.
A configuração inicial é assim: temos um aplicativo principal - principal. Ele sempre carrega primeiro e deve carregar outros aplicativos dentro dele mesmo - app-1 e app-2. Vamos criar três aplicativos usando o comando ng new <app-name> . A seguir, configuraremos o proxy para que os arquivos html e js do aplicativo requerido sejam enviados para solicitações como /<app-name>/*.js , /<app-name>/*.html , e as estatísticas do aplicativo principal sejam enviadas para todas as outras solicitações.
proxy.conf.js
const cfg = [
{
context: [
'/app1/*.js',
'/app1/*.html'
],
target: 'http://localhost:3001/'
},
{
context: [
'/app2/*.js',
'/app2/*.html'
],
target: 'http://localhost:3002/'
}
];
module.exports = cfg;
Para os aplicativos app-1 e app-2, especificaremos o baseHref no angular.json app1 e app2, respectivamente. Também alteraremos os seletores do componente raiz para app-1 e app-2.
Esta é a aparência do aplicativo principal
Primeiro, vamos carregar pelo menos um subaplicativo. Para fazer isso, você precisa carregar todos os arquivos js especificados em index.html.
Descubra urls de arquivos js: faça uma solicitação http para index.html, analise a string usando DOMParser e selecione todas as tags de script. Vamos converter tudo em um array e mapeá-lo para um array de endereços. Os endereços obtidos dessa forma conterão location.origin, portanto, o substituímos por uma string vazia:
private getAppHTML(): Observable<string> {
return this.http.get(`/${this.currentApp}/index.html`, {responseType: 'text'});
}
private getScriptUrls(html: string): string[] {
const appDocument: Document = new DOMParser().parseFromString(html, 'text/html');
const scriptElements = appDocument.querySelectorAll('script');
return Array.from(scriptElements)
.map(({src}) => src.replace(this.document.location.origin, ''));
}
Existem endereços, agora você precisa carregar os scripts:
private importJs(url: string): Observable<void> {
return new Observable(sub => {
const script = this.document.createElement('script');
script.src = url;
script.onload = () => {
this.document.head.removeChild(script);
sub.next();
sub.complete();
};
script.onerror = e => {
sub.error(e);
};
this.document.head.appendChild(script);
});
}
O código adiciona elementos de script com o src necessário ao DOM e, depois de baixar os scripts, ele exclui esses elementos - uma solução bastante padrão, o carregamento em webpack e system.js é implementado de forma semelhante.
Depois de carregar os scripts - em teoria - temos tudo para iniciar o aplicativo embutido. Mas, na verdade, teremos uma reinicialização do aplicativo principal. Parece que o aplicativo que está sendo carregado está de alguma forma em conflito com o principal, o que não aconteceu quando carregado no iframe.
Carregando pacotes do webpack
Angular usa webpack para carregar módulos. Em uma configuração padrão, o webpack divide o código nos seguintes pacotes:
- main.js - todo o código do cliente;
- polyfills.js - polyfills;
- styles.js - estilos;
- vendor.js - todas as bibliotecas usadas no aplicativo, incluindo Angular;
- runtime.js - tempo de execução do webpack;
- <module-name> .module.js - módulos lazy.
Se você abrir qualquer um desses arquivos, logo no início poderá ver o código:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([/.../])
E em runtime.js:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
Funciona assim: quando o pacote é carregado, ele cria o array webpackJsonp, se ainda não existir, e empurra seu conteúdo para ele. O tempo de execução do webpack substitui a função push dessa matriz para que você possa carregar novos pacotes posteriormente e processa tudo o que já está na matriz.
Tudo isso é necessário para que a ordem de carregamento dos maços não importe.
Portanto, se você carregar um segundo aplicativo Angular, ele tentará adicionar seus módulos ao tempo de execução do webpack já existente, o que, na melhor das hipóteses, levará à reinicialização do aplicativo principal.
Altere o nome de webpackJsonp
Para evitar conflitos, você precisa alterar o nome do array webpackJsonp. O Angular CLI usa sua própria configuração do webpack, mas pode ser estendido, se desejado. Para fazer isso, você precisa instalar o pacote angular-builders / custom-webpack:
npm i -D @ angular-builders / custom-webpack.
Em seguida, no arquivo angular.json na configuração do projeto, substitua architect.build.builder por @ angular-builders / custom-webpack: browser e adicione a architect.build.options :
"customWebpackConfig": {
"path": "./custom-webpack.config.js"
}
Você também precisa substituir architect.serve.builder por @ angular-builders / custom-webpack: dev-server para que isso funcione localmente com o servidor dev.
Agora você precisa criar um arquivo de configuração do webpack, que é especificado acima em customWebpackConfig: custom-webpack.config.js
Ele define as configurações personalizadas, você pode ler mais na documentação oficial .
Estamos interessados em jsonpFunction .
Você pode definir essa configuração em todos os aplicativos carregados para evitar conflitos (se depois disso os conflitos ainda persistirem, provavelmente você foi amaldiçoado):
module.exports = {
output: {
jsonpFunction: Math.random().toString()
},
};
Agora, se tentarmos carregar todos os scripts da maneira descrita acima, veremos um erro:
Antes de carregar o aplicativo, você precisa adicionar seu elemento raiz ao DOM:
private addAppRootElement(appName: string) {
const rootElementSelector = APP_CFG[appName].rootElement;
this.appRootElement = this.document.createElement(rootElementSelector);
this.appContainer.nativeElement.appendChild(this.appRootElement);
}
Vamos tentar novamente - uau, o aplicativo foi carregado!
Alternar entre aplicativos
Removemos o aplicativo anterior do DOM e podemos alternar entre os aplicativos:
destroyApp () {
if (!this.currentApp) return;
this.appContainer.nativeElement.removeChild(this.appRootElement);
}
Mas há falhas aqui: quando vamos app-1 → app-2 → app-1, recarregamos os pacotes js para o aplicativo app-1 e executamos seu código. Além disso, não destruímos os aplicativos carregados anteriormente, o que leva a vazamentos de memória e consumo desnecessário de recursos.
Se você não baixar novamente os pacotes do aplicativo, o processo de bootstrap não será executado sozinho e o aplicativo não será carregado. Você precisa delegar o processo de inicialização do bootstrap ao aplicativo principal.
Para fazer isso, vamos reescrever o arquivo main.ts dos aplicativos carregados:
const BOOTSTRAP_FN_NAME = 'ngBootstrap';
const bootstrapFn = (opts?) => platformBrowserDynamic().bootstrapModule(AppModule, opts);
window[BOOTSTRAP_FN_NAME] = bootstrapFn;
O método bootstrapModule não é executado imediatamente, mas é armazenado em uma função de wrapper que reside em uma variável global. No aplicativo principal, você pode acessá-lo e executá-lo quando necessário.
Para destruir o aplicativo e corrigir vazamentos de memória, você precisa chamar o método destroy do módulo do aplicativo raiz (AppModule). O método platformBrowserDynamic (). BootstrapModule retorna um link para ele, o que significa que nossa função de wrapper:
this.getBootstrapFn$().subscribe((bootstrapFn: BootstrapFn) => {
this.zone.runOutsideAngular(() => {
bootstrapFn().then(m => {
this.ngModule = m; //
});
});
});
this.ngModule.destroy(); //
Depois de chamar destroy () no módulo raiz, os métodos ngOnDestroy () de todos os serviços e componentes do aplicativo (se forem implementados) serão chamados.
Tudo funciona. Mas se o aplicativo carregado contiver módulos lazy, eles não serão capazes de carregar:
pode ser visto que o caminho do aplicativo está faltando no endereço (deve haver /app2/lazy-lazy-module.js ). Para resolver este problema, você precisa sincronizar o href de base do aplicativo principal e o carregado:
private syncBaseHref(appBaseHref: string) {
const base = this.document.querySelector('base');
base.href = appBaseHref;
}
Agora tudo funciona como deveria.
Resultado
Vamos ver quanto tempo leva para carregar um subaplicativo, colocando console.time () antes de carregar scripts no aplicativo principal e console.timeEnd () no construtor do componente raiz do aplicativo principal.
Quando os aplicativos app-1 e app-2 são carregados pela primeira vez, vemos algo assim:
Muito rápido. Mas se você retornar ao aplicativo baixado anteriormente, poderá ver os seguintes números: O
aplicativo é carregado instantaneamente, pois todos os pedaços necessários já estão na memória. Mas agora você precisa ter mais cuidado com as referências e assinaturas de objetos não utilizados, porque mesmo quando o aplicativo é destruído, eles podem causar vazamentos de memória.
Gerenciador de quadros sem quadros
A solução descrita acima é implementada no gerenciador de quadros, que suporta o carregamento de aplicativos com ou sem iframes. Cerca de um quarto de todos os aplicativos do Tinkoff Business agora são carregados sem quadros, e seu número está crescendo constantemente.
E, graças à solução descrita, aprendemos como mexer no Angular e nas bibliotecas comuns usadas no gerenciador de quadros e nos aplicativos, o que aumentou ainda mais a velocidade de carregamento e trabalho. Falaremos sobre isso no próximo artigo.
Repositório com código de amostra