在从外部 JSON 文件构造的模板中使用延迟加载的 Angular 2 个组件

Consuming lazy-loaded Angular 2 components in a template constructed from an external JSON file

我正在构建一个 angular 组件 doc/demo 站点,该站点列举了 Angular 组件列表和一组包含使用细节和示例的 JSON 文档。

这是一个基本组件的示例:

import { Component } from '@angular/core';

@Component({
    selector: 'my-button',
    template: `<button class="btn btn-default">I am a banana</button>`
})
export class Button {}

首先,我使用预构建脚本自动生成一个 componentLoader 文件,如下所示:

import { Route } from '@angular/router';
import { Button } from '../app/components/elements/button/button';

export const COMPONENT_ROUTES: Route[] = [{ path: 'button', component: Button }];
export const COMPONENT_DECLARATIONS: Array<any|any[]> = [Button];

然后我将其加载到 app.module.ts(为简洁起见删除了其他导入):

import { COMPONENT_ROUTES, COMPONENT_DECLARATIONS } from './componentLoader';
// ...
@NgModule({
    ...
    declarations: [ ... ].concat(COMPONENT_DECLARATIONS)

这很好用;我可以在视图组件中使用按钮:

import { Component } from '@angular/core';

@Component({
    selector: 'button-test',
    template: `
    <my-button></my-button>
`
})
export class ButtonTest {}

哇!

现在,我希望能够枚举包含 JSON 文档的文件夹,以查找与该组件及其使用方式相关的详细信息。 JSON 看起来像这样:

{
    "name": "button",
    "description": "An example of an 'I am a banana' button",
    "exampleHTML": "<div><my-button></my-button></div>"
}

请注意,在 exampleHTML 中,我如何尝试 使用 我们在 componentLoader.

中加载的组件

为了生成文档视图,我构建了一个文档服务,它接受一个 id 参数并在 /docs 目录中搜索匹配的 JSON 文档:

import {Injectable} from '@angular/core';

declare const require: any;

@Injectable()

export class DocsService {
    constructor() { }
    getDoc(id: string) {
        const json: string = require('../docs/' + id + '.json');
        return json;
    }   
}

然后detail组件导入服务并消费模板中的JSONdoc组件:

import { Component } from '@angular/core';
import { ActivatedRoute, Params }    from '@angular/router';
import { DocsService } from '../../services/docs.service';

declare const require: any;
const template: string = require('./componentDetail.html');

@Component({
    selector: 'componentDetail',
    template: `<div>Name: {{componentDoc.name}}</div>
               Examples:<br><br><div [innerHTML]="componentExample"></div>`
})

export class ComponentDetail {

    public componentDoc: any
    private _componentExample: string
    public get componentExample(): SafeHtml {
       return this._sanitizer.bypassSecurityTrustHtml(this._componentExample);
    }

    constructor(
        private route: ActivatedRoute,
        private docsService: DocsService,
        private _sanitizer: DomSanitizer
    ) {         
        this.route.params.forEach((params: Params) => {
          if (params['id'] !== undefined) {
            let id = params['id'];
            this.componentDoc = this.docsService.getDoc(id)
            this._componentExample = this.componentDoc.exampleHTML
          } else { throw Error('Not found') }
        });
    }
}

问题:

可能是因为这些组件的加载方式,或者因为我没有正确地将 HTML 加载到 Angular 的模板引擎中,或者因为有一个运行的黑盒 Webpack 服务在构建链的中间, 应该 显示实际 button 组件的模板是完全空白的。

在组件详细信息视图中,我可以手动将模板字符串设置为 <my-button></my-button> 并且它可以工作,但是当我尝试从 JSON 文档加载它时却没有。

如何获取细节组件以实际呈现 JSON 文档中使用的按钮 组件

See working Plnkr Here

您可以通过创建动态组件来解决问题,而不是使用 [innerHTML] 指令。

为此,您需要使用 Compiler.compileModuleAndAllComponentsAsync 方法动态加载模块及其组件。

首先,您需要创建一个模块,其中包含您希望能够在动态 HTML.

中使用的应用程序组件

将您的componentLoader更改为这样的模块

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Button } from '../../app/components/elements/button/button';

const COMPONENT_DECLARATIONS: Array<any|any[]> = [
    Button
];

@NgModule({ 
    declarations: COMPONENT_DECLARATIONS, 
    exports: COMPONENT_DECLARATIONS, 
    imports: [CommonModule] 
})
export class ComponentsModule {}

创建 dynamicComponent.service.ts 将用于生成动态模块和组件。在动态加载的 HTML 中使用组件的关键是我们如何在 DynamicComponentModule.

中导入 ComponentsModule
import { 
  Component,
  Compiler,
  ModuleWithComponentFactories,
  ComponentFactory,  
  NgModule,
  Type
} from '@angular/core';
import { Injectable }                from '@angular/core';
import { CommonModule }              from '@angular/common';

import { ComponentsModule }          from '../components/components.module';

@Injectable()
export class DynamicComponentService {

  private componentFactoriesCache: ComponentFactory<any>[] = [];  

  constructor(private compiler: Compiler) { }

  public getComponentFactory(templateHtml: string): Promise<ComponentFactory<any>> {
    return new Promise<ComponentFactory<any>>((resolve) => {
        let factory = this.componentFactoriesCache[templateHtml];
        if ( factory ) { 
            resolve(factory);
        } else {
            let dynamicComponentType = this.createDynamicComponent(templateHtml);
            let dynamicModule = this.createDynamicModule(dynamicComponentType);

            this.compiler.compileModuleAndAllComponentsAsync(dynamicModule)
                .then((mwcf: ModuleWithComponentFactories<any>) => {
                    factory = mwcf.componentFactories.find(cf => cf.componentType === dynamicComponentType);
                    this.componentFactoriesCache[templateHtml] = factory;
                    resolve(factory);
            });
        }
     });
  }

  private createDynamicComponent(templateHtml: string): Type<NgModule> {
    @Component({
      template: templateHtml,
    })
    class DynamicDocComponent {}

    return DynamicDocComponent;
  }

  private createDynamicModule(dynamicComponentType: Type<Component>) {
    @NgModule({
      declarations: [dynamicComponentType],
      imports: [CommonModule, ComponentsModule]
    })
    class DynamicDocComponentModule {}

    return DynamicDocComponentModule;
  }

}

ComponentsModuleDynamicComponentService 导入 app.module.ts

@NgModule({
    ...
    imports: [        
        ...
        ComponentsModule
    ],
    providers: [
        ...
        DynamicComponentService
    ]
    ...
})
export class AppModule {}

最后 ComponentDetail 被更改为使用新的 DynamicComponentService 来获取动态组件的 ComponentFactory

有了新的 ComponentFactory 之后,您就可以使用视图容器创建组件并使用动态 html.

填充组件
import { 
  Component,  
  ComponentRef,
  ViewChild,   
  ViewContainerRef, 
  AfterContentInit,
  OnDestroy
} from '@angular/core';
import { ActivatedRoute, Params }    from '@angular/router';

import { DocsService }               from './services/docs.service';
import { DynamicComponentService }   from './services/dynamicComponent.service';

@Component({
  selector: 'componentDetail',
  template:  `<div>Name: {{componentDoc.name}}</div>
               Examples:<br><br><template #container></template>
  `
})

export class ComponentDetail implements AfterContentInit, OnDestroy {

  private componentRouteID: string;
  private componentDoc: any;
  private componentExampleHtml: string;
  private componentRef: ComponentRef<any>;

  @ViewChild('container', { read: ViewContainerRef }) 
  private container: ViewContainerRef;

  constructor(private route: ActivatedRoute,
              private docsService: DocsService,
              private dynamicComponentService: DynamicComponentService) {
      this.route.params.forEach((params: Params) => {
          this.componentRouteID = params['id'];
          if (this.componentRouteID ) {
              this.componentDoc = this.docsService.getDoc(this.componentRouteID);            
              this.componentExampleHtml = this.componentDoc.exampleHTML;
          } else {
             throw Error('Not found'); 
          }
        });
  }

  public ngAfterContentInit() {
    this.dynamicComponentService.getComponentFactory(this.componentExampleHtml).then((factory) => {
        this.componentRef = this.container.createComponent(factory);        
    });
  }

  public ngOnDestroy() {    
    this.componentRef.destroy();
  }
}