[译] 关于 Angular 动态组件你需要知道的
原文链接: Here is what you need to know about dynamic components inAngular
本文主要解释如何在 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); AngularJS 中指令可以修改 DOM,但是没法知道修改了什么。这种方式的问题和动态环境一样,很难优化性能。动态模板当然不是 AngularJS 性能慢的主要元凶,但也是重要原因之一。 我在看了 Angular 内部代码一段时间后,发现这个新设计的框架非常重视性能,在 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 Compiler,静态模板(static templates),指令/模块工厂(ComponentFactory),工厂解析器(ComponentFactoryResolver)。对 AngularJS 社区来说,这些概念很陌生,甚至充满敌意,不过不用担心,如果你之前仅仅是听说过这些概念,但现在想知道这些是什么,继续阅读本文,将让你茅塞顿开。 注:实际上,JIT/AOT Compiler 说的是同一个 Compiler,只是这个 Compiler 在 building time 阶段还是在 running time 阶段被使用而已。 组件工厂和编译器Angular 中每一个组件是由组件工厂创建的,组件工厂又是由编译器根据你写的 Angular 内部使用了 视图 概念,或者说整个框架是一颗视图树。每一个视图是由大量不同类型节点(node)组成的:元素节点,文本节点等等(注:可查看 译 Angular DOM 更新机制)。每一个节点都有其专门作用,这样每一个节点的处理只需要花很少的时间,并且每一个节点都有 注:简单点说就是 Angular 程序是一颗视图树,每一个视图(view)又是有多种节点(node)组成的,每一个节点又提供了模板操作 API 给开发者使用,这些节点可以通过 DOM Query API 拿到。 每一个节点包含大量信息,并且为了性能考虑,一旦节点被创建就生效,后面不容许更改(注:被创建的节点会被缓存起来)。节点生成过程是编译器搜集你写的组件信息(注:主要是你写的组件里的模板信息),并以组件工厂形式封装起来。 假设你写了如下的一个组件: @Component({ selector: 'a-comp',template: '<span>A Component</span>' }) class AComponent {} 编译器根据你写的信息生成类似如下的组件工厂代码,代码只包含重要部分(注:下面整个代码可理解为视图,其中 function View_AComponent_0(l) { return jit_viewDef1(0,[ elementDef2(0,null,1,'span',...),jit_textDef3(null,['My name is ',...]) ] 上面代码基本描述了组件视图的结构,并被用来实例化一个组件。其中,第一个节点 从上文知道,如果你能够访问到组件工厂,就可以使用它实例化出对应的组件对象,并使用 ViewContainerRef API 把该组件/视图插入 DOM 中。如果你对 export class SampleComponent implements AfterViewInit { @ViewChild("vc",{read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit() { this.vc.createComponent(componentFactory); } } 好的,从上面代码可知道只要拿到组件工厂,一切问题就解决了。现在,问题是如何拿到 ComponentFactory 组件工厂对象,继续看。 模块(Modules)和组件工厂解析器(ComponentFactoryResolver)尽管 AngularJS 也有模块,但它缺少指令所需要的真正的命名空间,并且会有潜在的命名冲突,还没法在单独的模块里封装指令。然而,很幸运,Angular 吸取了教训,为各种声明式类型,如指令、组件和管道,提供了合适的命名空间(注:即 Angular 提供的 就像 AngularJS 那样,Angular 中的组件是被封装在模块中。组件自己并不能独立存在,如果你想要使用另一个模块的一个组件,你必须导入这个模块: @NgModule({ // imports CommonModule with declared directives like // ngIf,ngFor,ngClass etc. imports: [CommonModule],... }) export class SomeModule {} 同样道理,如果一个模块想要提供一些组件给别的模块使用,就必须导出这些组件,可以查看 const COMMON_DIRECTIVES: Provider[] = [ NgClass,NgComponentOutlet,NgForOf,NgIf,... ]; @NgModule({ declarations: [COMMON_DIRECTIVES,...],exports: [COMMON_DIRECTIVES,... }) export class CommonModule { } 所以每一个组件都是绑定在一个模块里,并且不能在不同模块里申明同一个组件,如果你这么做了,Angular 会抛出错误: Type X is part of the declarations of 2 modules: ... 当 Angular 编译程序时,编译器会把在模块中
从上文中我们知道,如果我们能拿到组件工厂,就可以使用组件工厂创建对应的组件对象,并插入到视图里。实际上,每一个模块都为所有组件提供了一个获取组件工厂的服务 ComponentFactoryResolver。所以,如果你在模块中定义了一个 export class AppComponent { constructor(private resolver: ComponentFactoryResolver) { // now the `factory` contains a reference to the BComponent factory const factory = this.resolver.resolveComponentFactory(BComponent); } 这是在两个组件 动态加载和编译模块但是如果组件在其他模块定义,并且这个模块是按需加载,这样的话是不是完蛋了呢?实际上我们照样可以拿到某个组件的组件工厂,方法同路由使用 有两种方式可以在运行时加载模块。第一种方式 是使用 SystemJsNgModuleLoader 模块加载器,如果你使用 SystemJS 加载器的话,路由在加载子路由模块时也是用的 loader.load('path/to/file#exportName') 注: NgModuleFactory 源码是在 如果没有指定具体的导出模块名称,加载器会使用默认关键字 providers: [ { provide: NgModuleFactoryLoader,useClass: SystemJsNgModuleLoader } ] 你当然可以在 模块加载并获取组件工厂的完整代码如下: @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); }) } } 但是在使用 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,[],m); }) }) } 然而,记住,这个方法使用了编译器的私有 API,下面是源码中的 文档说明: One intentional omission from this list is 运行时动态创建组件从上文中我们知道如何通过模块中的组件工厂来动态创建组件,其中模块是在运行时之前定义的,并且模块是可以提前或延迟加载的。但是,也可以不需要提前定义模块,可以像 AngularJS 的方式在运行时创建模块和组件。 首先看看上文中的 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(tmpCmp); cmpRef.instance.name = 'dynamic'; }) } 为了更好的调试信息,你可以使用任何类来替换上面代码中的匿名类。 Ahead-of-Time Compilation上文中说到的编译器说的是 Just-In-Time(JIT) 编译器,你可能听说过 Ahead-of-Time(AOT) 编译器,实际上 Angular 只有一个编译器,它们仅仅是根据编译器使用在不同阶段,而采用的不同叫法。如果编译器是被下载到浏览器里,在运行时使用就叫 JIT 编译器;如果是在编译阶段去使用,而不需要下载到浏览器里,在编译时使用就叫 AOT 编译器。使用 AOT 方法是被 Angular 官方推荐的,并且官方文档上有详细的 原因解释 —— 渲染速度更快并且代码包更小。 如果你使用 AOT 的话,意味着运行时不存在编译器,那上面的不需要编译的示例仍然有效,仍然可以使用 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 { } 上面代码中,我们使用 组件销毁如果你使用动态加载组件方式,最后需要注意的是,当父组件销毁时,该动态加载组件需要被销毁: ngOnDestroy() { if(this.cmpRef) { this.cmpRef.destroy(); } } 上面代码将会从视图容器里移除该动态加载组件视图并销毁它。 ngOnChanges对于所有动态加载的组件,Angular 会像对静态加载组件一样也执行变更检测,这意味着 Github本文的所有示例代码存放在 Github。 注:本文主要讲了组件 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- itop AD 自动导入脚本 webservices/AD_import_accounts.php
- 三万字无坑搭建基于Docker+K8S+GitLab/SVN+Jenkins+Harbor持
- angularjs – 如何在量角器中的输入字段上测试占位符文本?
- angularjs – ng-app是否等待document.ready?
- scala – 什么是Coneel中的克隆
- Nuget程序包还原失败找不到1.4.4版本的程序包Angularjs
- angularjs – 量角器Js运行到linux机器
- angularjs – 如何在Angular应用程序中存储验证令牌
- bootstrap小例子(1)
- AngularJS For循环与数字和范围