上例子:https://github.com/wuzhouyang/angular-dynamic-component-example
接触 angular2 也有好几月了,由于在做的项目dom的操作貌似比较频繁,而且并不能针对dom来编程,这样的话与模板的耦合度便十分大,所以只能针对组件编程,动态加载组件来实现项目中相关的功能,那就不得不用到angular2中一些比较低等级的api。此文要记录的便是目前用到的两种动态创建组件的方案。
(一)动态加载已经声明的组件
针对我的项目场景:用户拖动相关的块到特定区域,区域中便会生成相应的UI控件,此UI控件有自己的模板、行为等等。生成UI 后便会在区域中显示出来。所以我觉得将UI控件封为一个小组件,再动态加载,是个不错的方案。angular2中是如何动态加载组件的?
要实现这个功能,得先简单了解angular2 中相关的api
- ViewChild:一个属性装饰器,用来从模板视图中获取对应的元素,可以通过模板变量获取,获取时可以通过 read 属性设置查询的条件,就是说可以把此视图转为不同的实例
- ViewContainerRef :一个视图容器,可以在此上面创建、插入、删除组件等等
- ComponentFactoryResolve:一个服务,动态加载组件的核心,这个服务可以将一个组件实例呈现到另一个组件视图上
有了这三个,一个简单的思路便连贯了:特定区域就是一个视图容器,可以通过 ViewChild 来实现获取和查询,然后使用ComponentFactoryResolve将已声明未实例化的组件解析成为可以动态加载的 component,再将此component 呈现到此前的视图容器中
为了实现此功能,我写了一个简单的例子
我单独写了一个特性模块来测试,下面是结构图
特性模块涉及到 lazyload 的知识,我也不多讲了。直接看结构说明
- dy1.component.ts
- dy2.component.ts
这两个就是将要动态加载的组件,内容不多,就是为了测试,如图
- dynamic.component.ts
这个是此特性模块对应的组件实例,用于声明一些逻辑动作。简单看看此组件代码
可以看到在顶部导入处必需的三个都在。在组件类的顶部我通过模板变量的方式获取了此组件模板视图上了一个元素来作为视图容器,可以看看模板的代码
红框处便是我们要在上面动态加载组件的容器。
看看获取容器的代码
通过模板变量名获取,然后 可以通过 read 选项设置为一个 ViewContainerRef ,最终在生命钩期 ngAfterViewInit 过后便会获取此区域的一个 ViewContainerRef 实例。
看看主要的加载组件函数
我们已经通过组件类的构造函数注入了ComponentFactoryResolve服务,现在便可以调用其方法来解析得到一个 componentFactory 了,resolveComponentFactory 解析一个已经声明的组件得到一个可动态加载的 componentFactory,这里的DY1Component我们已经在顶部导入了。然后我们可以直接调用容器的createComponent函数将解析出来的componentFactory动态呈现到容器视图上。
然后我们就可以开心的运行、点击 ”动态加载组件“ 的按钮了。。
不开心的是,报错了,相信我们可以得到这样的报错
原来动态加载的组件必须声明在特性模块的 entryComponents 中,下面是angular官网对于entryComponents的说明
Specifies a list of components that should be compiled when this module is defined. For each component listed here, Angular will create a ComponentFactory
and store it in the ComponentFactoryResolver
.
就是说此处声明的组件 Angular 都会创建一个ComponentFactory并将其存储在ComponentFactoryResolver中,也就是动态加载必需的步骤。
所以我们将其加到特性模块 entryComponents中
然后我们又可以开心地运行,点击了。。。
然而这次我们又得到另一个错误了
意思就是DY1Component 还没有声明 — — ||| 。 所以还需要将要动态加载的组件声明为是此特性模块的组件,如图
然后这次可以开心的看例子了
对于这个方案,上面展示的是最简单的一面,没有考虑到项目的优化还有代码的简洁。项目的优化问题体现在后期项目的压缩和预编译中,如果我们是想要通过组件的名字来动态加载组件,可能在优化有组件的名字都被统一压缩成一个字母,所以会导致找不到组件的问题;代码的简洁呢因人而异,我是喜欢保持根 module 的简洁,所以有必要另起文件来作为媒介。
说一说改进的方式
新建一个管理类
导入动态加载的组件,声明一个供名字获取组件的变量,还有一个供根模块声明的变量
接下来就可以修改之前的引用代码了:
可以看到当动态组件多的时候,根 module还是能保持简洁。并且在根组件中可以通过名字获取对应的组件,不怕后期项目优化的影响。
(二)动态创建模块的方式来加载动态创建的组件
不同的需求有不同的方案,对于上面的需求与方案无疑是很适合的,但是需要我们先创建好组件,再声明到根 module 中,至少缺失了一些灵活性。
现在我又有了另外一个项目场景:我拖动生成了UI 控件,只是为了展示对应的样式,UI控件是需要第三方环境支持的,所以我需要加工拖动的数据,抛给后台处理,后台返回包含表示模板、组件的字符串的 JSON 数据回来,然后我通知另外一个网站,此网站包含了第三方环境,让他们去动态创建这些组件。一句话概括,就是我要动态创建不存在的组件而不是已经声明的组件。
要完成动态创建组件的,我们得先看看相关的api
- Compiler:用于在运行时运行angular编译器来创建 ComponentFactory 的服务,然后可以使用它来创建和呈现组件实例
其实最简单的了解这一个就能实现了,我们知道 容器创建和呈现组件的函数需要一个 ComponentFactory,而Compiler能够在运行时动态创建一个ComponentFactory,那就十分符合需求了。
我新增了一些代码
这些都是在根组件中的代码,引入了 Compiler 服务。新增一个 createModule函数,通过 Component 和 NgModule 修饰器动态创建新的组件和模块,然后调用 Compiler 的 compileModuleAndAllComponentsSync 方法获取一个新的ComponentFactory。然后容器的呈现还是一样,直接 createComponent。在模板的按钮中设置对应的动作后就可以开心的运行和点击了。。
这次十分顺利,没有任何报错。
可能我们会有疑惑,这个不也还是事先声明了动态的组件。
其实只是我这个例子展示的问题,这里的 模板、组件类都是可以动态当成参数设置的,下面此图是我在项目中用到的
aot build的问题
使用了这种方案,在项目正常开发预览的时候是没有任何问题的,但是当我构建项目(aot 等)运行的时候,就会出现下面报错
看错误可以知道在项目构建后是缺失angular编译器的,原因就是使用了AOT后已经是预编译了,编译器就会从中移除。为了解决这一问题,我们可以在这个把 compiler 编译器在构建的时候打到当前的特性包中
通过此处代码就可以将一个angular 编译器打到当前包中。再次运行build构建,可以看到已经没有之前的错误了,但是缺因此得到另一个错误
此错误暂时没能解决,待更新!