Pesquisa de site

typescript - Carregar módulo de forma preguiçosa no angular 8


Eu tenho um aplicativo de painel onde eu estava carregando widgets preguiçosamente (não vinculados a uma rota).

Eu estava fazendo isso definindo um objeto {name: string, loadChildren: string}. Então, no meu app.module, eu faria provideRoutes(...).

Isso faria com que o CLI criasse um chunk para cada módulo de widget.

Então, em tempo de execução, eu usaria o SystemJsModuleLoader para carregar essa string e obter um NgModuleRef.

Usando isso, eu poderia criar o componente a partir do módulo e chamar createComponent no ViewContainerRef.

Aqui está essa função:

 loadWidget(
    name: string,
    container: ViewContainerRef,
    widget: Widget
  ): Promise<{ instance: WidgetComponent; personlize?: { comp: any; factory: ComponentFactoryResolver } }> {
    if (this.lazyWidgets.hasOwnProperty(name)) {
      return this.loader.load(this.lazyWidgets[name]).then((moduleFactory: NgModuleFactory<any>) => {
        const entryComponent = (<any>moduleFactory.moduleType).entry;
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

        const comp = container.createComponent(compFactory);
        (<WidgetComponent>comp.instance).widget = widget;
        const personalize = (<any>moduleFactory.moduleType).personalize;
        if (personalize) {
          return {
            instance: <WidgetComponent>comp.instance,
            personlize: {
              comp: personalize,
              factory: moduleRef.componentFactoryResolver,
              injector: moduleRef.injector
            }
          };
        } else {
          return {
            instance: <WidgetComponent>comp.instance
          };
        }
      });
    } else {
      return new Promise(resolve => {
        resolve();
      });
    }

No Angular 8, o loadChildren muda para a função import.

Em vez de um NgModuleRef, você obtém a instância real do módulo.

Eu pensei que poderia consertar meu código pegando aquele módulo, compilando-o para obter o NgModuleRef e depois mantendo o resto do código igual.

Parece que no modo AOT, porém, o compilador não é incluído no pacote.

Então, basicamente estou preso agora com uma instância do componente que eu preciso, mas sem maneira de adicioná-lo ao contêiner View.

Isso requer um resolvedor de fábrica de componentes que eu não consigo obter.

Eu acho que minha pergunta é como pegar uma instância de um componente e adicioná-la ao container de visualização no Angular 8. Por enquanto, voltei a usar a versão em string do loadChildren, mas isso só funcionará até a versão 9 ser lançada.

Aqui está a versão com o compilador que não funciona em AOT

 if (this.lazyWidgets.hasOwnProperty(name)) {
      return this.lazyWidgets[name]().then((mod: any) => {
        const moduleFactory = this.compiler.compileModuleSync(mod);
        const entryComponent = (<any>moduleFactory.moduleType).entry;
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

        const comp = container.createComponent(compFactory);
        (<WidgetComponent>comp.instance).widget = widget;
        const personalize = (<any>moduleFactory.moduleType).personalize;
        if (personalize) {
          return {
            instance: <WidgetComponent>comp.instance,
            personlize: {
              comp: personalize,
              factory: moduleRef.componentFactoryResolver,
              injector: moduleRef.injector
            }
          };
        }

E aqui está um exemplo de como eu estava pensando em fazer isso, mas então não havia uma maneira de adicioná-lo ao ViewContainerRef.

A instância do módulo implementa uma interface que requer uma propriedade 'entry'.

Isto define o componente real a ser carregado:

  if (this.lazyWidgets.hasOwnProperty(name)) {
      return this.lazyWidgets[name]().then((mod: any) => {

        const comp = mod.entry;
        (<WidgetComponent>comp.instance).widget = widget;
        const personalize = mod.personalize;
        if (personalize) {
          return {
            instance: <WidgetComponent>comp.instance,
            personlize: {
              comp: personalize,
               factory: null //this no longer works:  moduleRef.componentFactoryResolver,
              // injector: moduleRef.injector
            }
          };
        } else {
          return {
            instance: <WidgetComponent>comp.instance
          };
        }
      });
    } 

EDITAR:

Tentei adicionar um exemplo no stackblitz, mas o compilador está transformando minha string em funções. Pelo menos o código está mais legível. Isso é o que eu estava fazendo no Angular 8. Eu basicamente preciso de uma maneira de fazer isso com import() em vez de string mágica.

https://stackblitz.com/edit/angular-9yaj4l

Resposta #1

No Angular 8, o resultado da função loadChildren é ou um Promise do tipo NgModule no modo JIT ou um Promise de NgModuleFactory no modo AOT.

Com isso em mente, você pode reescrever seu serviço da seguinte forma:

import { 
    Injectable, Compiler, Injector, Type, 
    ViewContainerRef, ComponentFactoryResolver,
    NgModuleFactory, Inject 
} from '@angular/core';

@Injectable()
export class LazyLoaderService {

  constructor(private injector: Injector,
    private compiler: Compiler,
    @Inject(LAZY_WIDGETS) private lazyWidgets: 
       { [key: string]: () => Promise<NgModuleFactory<any> | Type<any>> }) { }


  async load(name: string, container: ViewContainerRef) {
    const ngModuleOrNgModuleFactory = await this.lazyWidgets[name]();
 
    let moduleFactory;

    if (ngModuleOrNgModuleFactory instanceof NgModuleFactory) {
      // aot mode
      moduleFactory = ngModuleOrNgModuleFactory;
    } else {
      // jit mode
      moduleFactory = await this.compiler.compileModuleAsync(ngModuleOrNgModuleFactory);
    }

    const entryComponent = (<any>moduleFactory.moduleType).entry;
    const moduleRef = moduleFactory.create(this.injector);

    const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

    const comp = container.createComponent(compFactory);
  }   
}

Exemplo Stackblitz

Dica: Sempre olhe o código-fonte quando estiver em dúvida

  • https://github.com/angular/angular/blob/72ecc453639eae017f75653c9004adc406ed2ee6/packages/router/src/router_config_loader.ts#L46-L59

  • https://github.com/angular/angular/blob/32886cf9ace539e14e2b387cd8afb10715c8d3de/aio/src/app/custom-elements/elements-loader.ts#L56-L68

Resposta #2

Parece que ao usar o Router, a constante lazyWidgets não deveria ter a propriedade name, mas sim a propriedade path:

export const lazyWidgets: { path: string, loadChildren: () => .....

Caso contrário, você receberá erro:

Configuração inválida da rota '': as rotas devem ter um caminho ou um combinador especificado

Artigos relacionados