![a29735d61d7de51353b9c0212a5bf966.png](https://img-blog.csdnimg.cn/img_convert/a29735d61d7de51353b9c0212a5bf966.png)
翻译文章,原文 动态组件
如果原来用过angularjs(angular的第一版),那么你一定比较习惯动态生成html字符,然后用$compile服务运行,并把它和数据模型绑定(scope)从而获得双向数据绑定功能。
如
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编译器,静态模板,工厂函数(factory),factory resolver,这些对angularjs社区来说比较陌生。但是不用担心,如果以前碰到过这类概念并且想知道这些概念到底指的什么,请继续阅读也许能从中得到一些启迪。
组件工厂和编译器
在angular中所有的组件都是由工厂(factory)创建的。所有的工厂都是编译器使用@Component装饰器(decorator)提供的元数据生成 的。如果不熟悉装饰器这里有篇文章可以帮助你理解 Implementing custom component decorator 装饰器的概念。
Angular框架,引入了视图的概念。运行的框架实际就是一颗视图树,每一个视图由各种不同的节点组成,如元素节点,文字节点等。这些节点各司其职,功能单一,这样处理起来就会花费尽可能少的时间,也就是更快了,对于这些节点也有各种处理工具,比如ViewContainerRef和TemplateRef。每个节点都知道如何响应ViewChild和ContentChildren等查询
每一个节点都有很多的信息,为了优化速度,在构建阶段这些信息就必须可用,并且在以后不可以再修改,这个过程就是编译器所做的事情,收集必要的信息并把这些信息封装在组件工厂中。
例如下面这个组件;
/@Component({
selector: 'a-comp',
template: '<span>A Component</span>'
})
class AComponent {}
用上面的信息,编译器会生成如下的组件工厂:
function View_AComponent_0(l) {
return jit_viewDef1(0,[
elementDef2(0,null,null,1,'span',...),
jit_textDef3(null,['A Component ',...])
]
这个函数描述了一个视图组件的是如何构成的以便在组件实例化时使用。函数第一个和第二个节点分别时element和text,实例化时每一个节点都可以通过参数列表获得必要的信息,编译器的任务就是解析所有的必要的依赖并在运行时提供给节点。
通过工厂函数可以很容易的创建一个组件的实例,然后使用ViewContainerRef插入DOM,我在另一篇文章Exploring Angular DOM manipulations 提到了这个方法。下面是大概的样子
export class SampleComponent implements AfterViewInit {
@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
ngAfterViewInit() {
this.vc.createComponent(componentFactory);
}
}
所以现在的问题是如何才能使用一个组件的组件工厂
angular modules 和 ComponentFactoryResolver
angularjs 就存在模块,但是这些模块对于指令没有真正的命名空间,经常导致命名冲突,也没有办法把一个指令真正的封装在一个模块内,angular吸取了angularjs的经验提供了合适的声明类型如:directives,component,pipes。
和angularjs一样组件必须属于一个模块,组件自己无法独立存在,如果使用另一个模块的组件,在当前模块必须先import另一个模块
/@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
属性里的和 组件模板里的,然后为这些组件生成工厂。这些工厂函数可以在调试工具中看到
![fbf08c23135e5eada5c2b95544a6d38e.png](https://img-blog.csdnimg.cn/img_convert/fbf08c23135e5eada5c2b95544a6d38e.png)
前面提到如果可以使用组件的工厂那么就可以通过它创建组件,并把组件插入到视图中。其实所有的模块都提供了service给它的组件来访问组件工厂,这个service就是ComponentFactoryResolver,所以如果在模块内定义了BComponent
并且想使用这个组件的工厂,这时候可以使用ComponentFactoryResolver这个service。
export class AppComponent {
constructor(private resolver: ComponentFactoryResolver) {
// now the `f` contains a reference to the cmp factory
const f = this.resolver.resolveComponentFactory(BComponent);
}
上面的例子要求组件都在一个模块里或者组件已经被导入了。
动态模块加载和编译
如果组件是在其它模块定义的,这时我想在组件真正在使用的时候再加载,即按需加载如何做到?这种情况和angular router 使用loadChildren配置项类似。
运行时加载模块有两种方法,第一种是用angular的SystemJsNgModuleLoader ,如果使用SystemJS做为loader的话,经常被用来加载子路由,使用公开方法load把模块加载到浏览器,编译模块及其所有的组件。这个函数需要一个模块文件路径和文件导出的模块名称,最终返回ModuleFactory
loader.load('path/to/file#exportName')
如果模块文件没有导出名称,那么loader将用模块的默认名称 即export default的那个,另外还要设置一下依赖注入
providers: [
{
provide: NgModuleFactoryLoader,
useClass: SystemJsNgModuleLoader
}
]
这里可以为provide提供任何的令牌,但是angular的路由模块用的是NgModuleFactoryLoader
,所以最好还是和route使用一样的token。下面的代码加载了一个模块并获取组件工厂的样例:
/@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
的方法有一个问题,由于SystemJsNgModuleLoader
调用的是编译器的compileModuleAsync方法, 这个方法只会为在entryComponents
里声明的或者组件模板里的组件创建工厂,如果不是在 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);
})
})
}
但是这种加载模块的方式,使用的是angular并不推荐的非公开api。
动态创建组件
在前面的段落中演示了在angular中是如何创建动态组件的,过程中需要调用模块的组件工厂方法,并且组件或模块都是预定义好的,在运行时可以选择立即加载或按需加载,其实也可以不预先定义模块和组件,依然可以动态创建模块和组件就像angularjs一样,看看文章一开始的例子
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);
动态创建并把内容添加到视图的过程大概如下:
- 创建组件类,定义组件需要的属性,并用装饰器装饰类
- 创建模块类,把上面的组件加到模块声明里,用装饰器装饰模块类
- 编译器编译模块和所有组件,获得组件工厂
模块只是一个带有装饰器的类,组件也一样。装饰器其实就是一个简单的函数,在运行时可以调用装饰器在增强类的功能,下面的代码演示了动态创建一个模块和组件并把它添加到视图
/@ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;
constructor(private _compiler: Compiler,
private _injector: Injector,
private _m: NgModuleRef<any>) {
}
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 = this.vc.createComponent(f);
cmpRef.instance.name = 'dynamic';
})
}
这段代码类是匿名的,为了调试方便可以给类一个名字。
Ahead-of-Time 编译
上面例子中使用的编译器,叫做运行时编译(JIT),相对还有另一种叫运行前编译(AOT)。其实在angular里只有一种编译器,具体是JIT还是AOT取决于你在什么时候使用它。如果在浏览器中运行代码时使用那就是JIT,如果在运行程序前把所有的组件都编译好,那么就是AOT,但是后者有很多好处比如更快的渲染速度和更小的代码体积。具体可以看文档
使用AOT意味着代码在运行时不会再有编译器(提前编译好,运行时不要编译器了),上面的例子中如果只是用ComponentFactoryResolver
那么代码还可以运行,但是动态编译组件就无法正常运行了,那么使用AOT又想要JIT如何做到呢,很简单,
import { JitCompilerFactory } from '@angular/compiler';
export function createJitCompiler() {
return new JitCompilerFactory([{
useDebug: false,
useJit: true
}]).createCompiler();
}
import { AppComponent } from './app.component';
@NgModule({
providers: [{provide: Compiler, useFactory: createJitCompiler}],
...
})
export class AppModule {
}
上面从//@angular/compiler 导入 JitCompilerFactory
, 使用createJitCompiler工厂函数,设置编译器用JIT方式,在aopp moduel里通过Compiler令牌注入,这样整个应用都会用这个编译器,应用其他部分不用修改。
组件销毁
最后的一件事就是组件的销毁,如果组件是手动创建的,那么父组件销毁的时候不要忘了销毁所有手动创建的组件
ngOnDestroy() {
if(this.cmpRef) {
this.cmpRef.destroy();
}
}
上面,会把组件视图从视图容器移除并销毁
ngOnChanges
所有动态加载的组件,和静态的组件一样angular会提供变更检测,也就是说ngDoCheck钩子函数会被调用,但是ngOnChanges钩子不会触发,即使动态组件提供了/ @Input
并且父组件的相应属性发生了变化,这是因为这是因为编译期间编译器会生成执行输入检查的函数,这个函数是组件工厂的一部分,并且它只能根据模板的信息生成,又因为我们在静态模板中没有使用动态组件,因此这个函数在编译期间没有生成