在从外部 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 文档中使用的按钮 组件?
您可以通过创建动态组件来解决问题,而不是使用 [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;
}
}
将 ComponentsModule
和 DynamicComponentService
导入 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();
}
}
我正在构建一个 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 文档中使用的按钮 组件?
您可以通过创建动态组件来解决问题,而不是使用 [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;
}
}
将 ComponentsModule
和 DynamicComponentService
导入 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();
}
}