这是您需要了解的有关Angular中的动态组件的知识

像专业人士一样动态创建Angular组件

如果您一直在使用AngularJS进行编程,则可能已经习惯于即时生成HTML字符串,通过$ compile服务运行它们,并链接到数据模型(作用域)以获得双向数据绑定。

const template = '<span>generated on the fly: {{name}}</span>' const linkFn = $compile(template); const dataModel = $scope.$new(); dataModel.name = ' dynamic ' // link data model to a template linkFn(dataModel);

在AngularJS中,指令可以以任何可能的方式修改DOM,而框架不知道修改的内容。 但是,这种方法的问题与任何动态环境都一样-很难针对速度进行优化。 动态模板评估当然不是AngularJS被视为缓慢框架的主要原因,但它无疑有助于声誉。

在研究了Angular内部很长时间之后,似乎新的框架设计很大程度上是由对速度的需求所驱动。 您会在源代码中找到许多类似这样的评论:

Attention: Adding fields to this is performance sensitive! Note: We use one type for all nodes so that loops that loop over all nodes of a ViewDefinition stay monomorphic! For performance reasons, we want to check and update the list every five seconds.

因此,较新的框架中的Angular成员决定提供较少的灵活性以换取更大的速度。 并介绍了JIT和AOT编译器以及静态模板。 和工厂。 和工厂解析器。 以及许多其他对AngularJS社区不熟悉的事物。 但是不用担心。 如果您以前曾经遇到过这些概念,并且想知道继续阅读这些概念并获得启发。

组件工厂和编译器

在Angular中,每个组件都是从工厂创建的。 并且工厂由编译器使用您在@Component装饰器中提供的数据生成。 如果在阅读了许多网络文章之后,您仍然不确定该装饰器的作用是阅读《 实现自定义组件装饰器》

引擎盖下的Angular使用视图的概念。 运行中的框架本质上是一棵视图树。 每个视图由不同类型的节点组成:元素节点,文本节点等。 每个节点在其用途上都有专门的用途,因此处理此类节点所需的时间尽可能短。 每个节点都有各种提供程序,例如ViewContainerRef和TemplateRef。 每个节点都知道如何响应诸如ViewChildren和ContentChildren之类的查询。

每个节点都有很多信息。 现在,为了优化速度,在构建节点时必须提供所有这些信息,并且以后无法更改。 这就是编译过程的工作-收集所有必需的信息,并将其封装为组件工厂的形式。

假设您定义一个组件及其模板,如下所示:

@Component({ selector: 'a-comp', template: '<span>A Component</span>' }) class AComponent {}

编译器使用这些数据生成以下组件工厂:

function View_AComponent_0(l) { return jit_viewDef1(0,[ (l()(),jit_elementDef2(0,null,null,1, 'span' ,...)), (l()(),jit_textDef3(null,[ 'My name is ' ,...])) ]

它描述了组件视图的结构,并在实例化组件时使用。 第一个节点是元素定义,第二个节点是文本定义。 您可以看到,通过参数列表实例化每个节点时,都会获得所需的信息。 解析所有必需的依赖项并在运行时提供它们是编译器的工作。

如果您有权访问工厂,则可以轻松地从中创建组件实例,并使用viewContainerRef将其插入DOM。 我已经在探索Angular DOM操作中写过它。 它是这样的:

export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit() { this.vc.createComponent( componentFactory ); } }

因此,现在的问题是如何访问组件工厂,我们很快就会看到。

Angular模块和ComponentFactoryResolver

尽管AngularJS也有模块,但缺少指令的真实名称空间。 总是存在冲突的可能性,并且没有办法将实用程序指令封装在特定模块中。 幸运的是,Angular吸取了教训,现在为声明式类型(指令,组件和管道)提供了适当的命名空间。

就像在AngularJS中一样,新框架中的每个组件都是某个模块的一部分。 组件本身并不存在,如果要使用其他模块中的组件,则必须导入该模块:

@NgModule({ // imports CommonModule with declared directives like // ngIf, ngFor, ngClass etc. imports: [CommonModule], ... }) export class SomeModule {}

反过来,如果一个模块要提供某些组件供其他模块组件使用,则必须导出这些组件。 这是CommonModule的工作方式:

const COMMON_DIRECTIVES: Provider[] = [ NgClass, NgComponentOutlet, NgForOf, NgIf, ... ]; @NgModule({ declarations: [COMMON_DIRECTIVES, ...], exports: [COMMON_DIRECTIVES, ...], ... }) export class CommonModule { }

因此,每个组件都绑定到一个特定的模块,并且您不能在不同的模块中声明相同的组件。 如果这样做,将会出现错误:

Type X is part of the declarations of 2 modules: ...

Angular编译应用程序时,它将获取在模块的entryComponents中定义或在组件模板中找到的组件,并为其生成组件工厂。 您可以在“源”选项卡中看到这些工厂:

在上一节中,我们确定了如果可以访问组件工厂,则可以使用它来创建组件并插入视图中。 每个模块都为其所有组件提供了便利的服务,以使组件出厂。 该服务是ComponentFactoryResolver 。 因此,如果您在模块上定义了BComponent并希望保留其工厂,则可以从属于该模块的组件中使用此服务:

export class AppComponent { constructor(private resolver: ComponentFactoryResolver) { // now the `f` contains a reference to the cmp factory const f = this.resolver.resolveComponentFactory(BComponent); }

仅当两个组件都在同一模块中定义或导入具有已解决组件工厂的模块时,此方法才有效。

动态模块加载和编译

但是,如果您的组件是在另一个模块上定义的,而您实际上并不需要该组件,那么该组件又该如何加载呢? 我们能做到这一点。 这将类似于路由器使用loadChildren配置选项所做的事情。

有两种方法可以在运行时加载模块。 第一个是使用Angular提供的SystemJsNgModuleLoader 。 如果您将SystemJS用作加载程序,则路由器将其用于加载子路由。 它具有一个公共方法加载,该加载将模块加载到浏览器并编译该模块及其中声明的所有组件。 此方法采用具有模块和导出名称的文件路径,并返回ModuleFactory

loader.load('path/to/file#exportName')

如果未指定导出名称,则加载的文件将使用默认的导出名称。 要注意的另一件事是SystemJsNgModuleLoader需要进行一些注入的DI设置,因此您应将其定义为这样的提供程序:

providers: [ { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader } ]

您当然可以指定要提供的任何令牌,但是路由器模块使用NgModuleFactoryLoader,因此使用相同的方法可能是一件好事。

因此,这是加载模块并获取组件工厂的完整代码:

@Component({ providers: [ { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader } ] }) export class ModuleLoaderComponent { constructor(private _injector: Injector, private loader: NgModuleFactoryLoader) { } ngAfterViewInit() { this. loader.load('app/t.module#TModule') .then((factory) => { const module = factory.create(this._injector); const r = module.componentFactoryResolver; const cmpFactory = r.resolveComponentFactory(AComponent); // create a component and attach it to the view const componentRef = cmpFactory.create(this._injector); this.container.insert(componentRef.hostView); }) } }

但是使用SystemJsNgModuleLoader存在一个问题。 在后台 ,它使用编译器的compileModuleAsync方法。 此方法仅为在模块的entryComponents中声明或在组件模板中找到的组件创建工厂。 但是,如果您不想将组件声明为入口组件怎么办? 有一个解决方案-自己加载模块并使用compileModuleAndAllComponentsAsync方法。 它为模块上的所有组件生成工厂,并将它们作为ModuleWithComponentFactories的实例返回:

class ModuleWithComponentFactories<T> { componentFactories: ComponentFactory<any>[]; ngModuleFactory: NgModuleFactory<T>;

这是完整的代码,显示了如何自己加载模块并访问所有组件工厂的方法:

ngAfterViewInit() { System.import('app/t.module').then((module) => { _compiler.compileModuleAndAllComponentsAsync(module.TModule) .then((compiled) => { const m = compiled.ngModuleFactory.create(this._injector); const factory = compiled.componentFactories[0]; const cmp = factory.create(this._injector, [], null, m); }) }) }

请记住,这种方法使用了不支持作为公共API的编译器。 这是文档所说的

此列表中的一个有意遗漏的是@ angular / compiler,它目前被认为是低级api,并且可能会进行内部更改。 这些更改不会影响使用更高级别的api(命令行界面或通过@ angular / platform-b​​rowser-dynamic进行的JIT编译)的任何应用程序或库。 只有非常特定的用例才需要直接访问编译器API(主要是IDE,lint等的工具集成)。 如果您正在从事这种集成,请首先与我们联系。

动态创建组件

从前面的部分中,您了解了如何在Angular中创建动态组件。 您知道此过程需要访问放置在模块上的组件工厂。 到目前为止,我使用的模块是在运行时之前定义的,可以急切或延迟地加载。 但是,好处是您不必事先定义模块然后再加载它们。 您可以像在AngularJS中一样动态地创建模块和组件。

让我们以我最初展示的示例为例,看看如何在Angular中实现相同的效果。 因此,这再次是示例:

const template = '<span>generated on the fly: {{name}}</span>' const linkFn = $compile(template); const dataModel = $scope.$new(); dataModel.name = ' dynamic ' // link data model to a template linkFn(dataModel);

创建动态内容并将其附加到视图的一般流程如下:

  1. 定义一个组件类及其属性并装饰该类
  2. 定义模块类,将组件添加到模块声明中并装饰模块类
  3. 编译模块和所有组件以掌握组件工厂

该模块只是一个带有装饰器的类。 组件也是如此。 由于装饰器是简单的函数,并且在运行时可用,因此我们可以随时使用它们来装饰类。 以下是动态创建和附加组件的方法:

ngAfterViewInit() { const template = '<span>generated on the fly: {{name}}</span>'; const tmpCmp = Component({template: template})(class {}); const tmpModule = NgModule({declarations: [tmpCmp]})(class {}); this._compiler.compileModuleAndAllComponentsAsync(tmpModule) .then((factories) => { const f = factories.componentFactories[0]; const cmpRef = f.create(this._injector, [], null, this._m); cmpRef.instance.name = 'dynamic'; this.vc.insert(cmpRef.hostView); }) }

您可能希望在装饰器中将匿名类替换为命名类,以获得更好的调试信息。

销毁组件

最后一件事是,如果您手动添加了组件,请不要忘记在销毁父组件时销毁它们:

ngOnDestroy() { if(this.cmpRef) { this.cmpRef.destroy(); } }

您发现文章中的信息有帮助吗?

From: https://hackernoon.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值