Cet article explique principalement comment créer dynamiquement des composants dans Angular (Remarque : les composants utilisés dans les modèles peuvent être appelés composants créés statiquement). Jetons un coup d'œil ensemble à cet article
Si vous avez déjà utilisé AngularJS (le framework Angular de première génération) pour programmer, vous avez peut-être utilisé le service $compile
pour générer du HTML et vous connecter aux données model Ainsi, la fonction de liaison bidirectionnelle est obtenue :
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);
Les instructions dans AngularJS peuvent modifier le DOM, mais il n'y a aucun moyen de savoir ce qui a été modifié. Le problème de cette approche est le même que dans un environnement dynamique, ce qui rend difficile l'optimisation des performances. Les modèles dynamiques ne sont certainement pas le principal responsable de la lenteur des performances d'AngularJS, mais ils en sont également l'une des raisons importantes.
Après avoir regardé le code interne d'Angular pendant un moment, j'ai découvert que ce framework nouvellement conçu attache une grande importance à la performance. Vous trouverez souvent ces phrases dans le code source d'Angular (remarque : non traduites pour une compréhension claire). ):
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.
Ainsi, les concepteurs angulaires ont décidé de sacrifier la flexibilité pour obtenir d'énormes améliorations de performances, telles que l'introduction du compilateur JIT et AOT, des modèles statiques, de l'usine de directives/modules (ComponentFactory ), résolveur d'usine (ComponentFactoryResolver). Ces concepts peuvent être inconnus et même hostiles à la communauté AngularJS, mais ne vous inquiétez pas, si vous n'avez entendu parler de ces concepts qu'avant et que vous souhaitez maintenant savoir de quoi il s'agit, continuez à lire cet article et il vous éclairera.
Remarque : En fait, le compilateur JIT/AOT fait référence au même compilateur, mais ce compilateur est utilisé dans l'étape de construction ou dans l'étape d'exécution.En ce qui concerne l'usine, Angular Compiler compile le composant que vous avez écrit, tel que a.component.ts, dans a.component.ngfactory.js. Autrement dit, Compiler utilise le décorateur @Component comme matière première pour compiler la classe de composant/instruction. vous avez écrit dans un autre composant une classe de fabrique de vues.
Retour au compilateur JIT/AOT tout à l'heure, si a.component.ngfactory.js est généré dans la phase de construction, c'est le compilateur AOT qui ne sera pas empaqueté dans un package de dépendances ; est généré lors de la phase d'exécution, le compilateur doit être empaqueté dans un package de dépendances et téléchargé localement par l'utilisateur au moment de l'exécution, le compilateur compilera la classe de composant/instruction pour générer la classe de fabrique de vues correspondante, et c'est tout. Ci-dessous, nous verrons à quoi ressemble le code de ces fichiers *.ngfactory.js.
Quant au résolveur d'usine, c'est encore plus simple. C'est un objet grâce auquel vous pouvez obtenir les objets d'usine compilés.
Chaque composant dans Angular est créé par une fabrique de composants, et la fabrique de composants est écrite par le compilateur en fonction de votre @Component
décorateur Métadonnées compilées et générées . Si vous êtes encore un peu confus après avoir lu de nombreux articles sur les décorateurs sur Internet, vous pouvez vous référer à cet article Medium que j'ai écrit Implémentation d'un décorateur de composants personnalisés .
Angular utilise en interne le concept view, ou l'ensemble du framework est un arbre de vue. Chaque vue est composée d'un grand nombre de types de nœuds différents : nœuds d'éléments, nœuds de texte, etc. (Remarque : vous pouvez visualiser la traduction du mécanisme de mise à jour Angular DOM ). Chaque nœud a son propre rôle spécialisé, de sorte que le traitement de chaque nœud ne prend que peu de temps, et chaque nœud dispose de services tels que ViewContainerRef
et TemplateRef
à utiliser. Vous pouvez également utiliser ViewChild/ViewChildren
et ContentChild/ContentChildren
Do. une requête DOM pour ces nœuds.
Remarque : pour faire simple, le programme Angular est un arbre de vues. Chaque vue est composée de plusieurs nœuds. Chaque nœud fournit une API d'opération de modèle aux développeurs. Ces nœuds peuvent être obtenus via la requête DOM. API.
Chaque nœud contient une grande quantité d'informations, et pour des raisons de performances, cela prend effet une fois le nœud créé, et aucune modification ultérieure n'est autorisée (remarque : le nœud créé sera mis en cache). Le processus de génération de nœuds consiste en ce que le compilateur collecte les informations sur les composants que vous écrivez (remarque : principalement les informations de modèle dans le composant que vous écrivez) et les encapsule sous la forme d'une fabrique de composants.
Supposons que vous écriviez un composant comme suit :
@Component({ selector: 'a-comp', template: '<span>A Component</span>' }) class AComponent {}
Le compilateur génère un code de fabrique de composants similaire au suivant en fonction des informations que vous avez écrites. Le code ne contient que des parties importantes (Remarque : L'intégralité du code ci-dessous peut être compris comme View, où elementDef2
et jit_textDef3
peuvent être compris comme Node) :
function View_AComponent_0(l) { return jit_viewDef1(0,[ elementDef2(0,null,null,1,'span',...), jit_textDef3(null,['My name is ',...]) ]
Le code ci-dessus décrit essentiellement la structure de la vue du composant et est utilisé pour instancier un composant. Parmi eux, le premier nœud elementDef2
est la définition du nœud d'élément et le deuxième nœud jit_textDef3
est la définition du nœud de texte. Vous pouvez voir que chaque nœud dispose de suffisamment d'informations sur les paramètres pour être instancié, et ces informations sur les paramètres sont générées par le compilateur analysant toutes les dépendances, et les valeurs spécifiques de ces dépendances sont fournies par le framework au moment de l'exécution.
从上文知道,如果你能够访问到组件工厂,就可以使用它实例化出对应的组件对象,并使用 ViewContainerRef API 把该组件/视图插入 DOM 中。如果你对 ViewContainerRef
感兴趣,可以查看 译 探索 Angular 使用 ViewContainerRef 操作 DOM。应该如何使用这个 API 呢(注:下面代码展示如何使用 ViewContainerRef
API 往视图树上插入一个视图):
export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit() { this.vc.createComponent(componentFactory); } }
好的,从上面代码可知道只要拿到组件工厂,一切问题就解决了。现在,问题是如何拿到 ComponentFactory 组件工厂对象,继续看。
尽管 AngularJS 也有模块,但它缺少指令所需要的真正的命名空间,并且会有潜在的命名冲突,还没法在单独的模块里封装指令。然而,很幸运,Angular 吸取了教训,为各种声明式类型,如指令、组件和管道,提供了合适的命名空间(注:即 Angular 提供的 Module
,使用装饰器函数 @NgModule
装饰一个类就能得到一个 Module
)。
就像 AngularJS 那样,Angular 中的组件是被封装在模块中。组件自己并不能独立存在,如果你想要使用另一个模块的一个组件,你必须导入这个模块:
@NgModule({ // imports CommonModule with declared directives like // ngIf, ngFor, ngClass etc. imports: [CommonModule], ... }) export class SomeModule {}
同样道理,如果一个模块想要提供一些组件给别的模块使用,就必须导出这些组件,可以查看 exports
属性。比如,可以查看 CommonModule
源码的做法(注:查看 L24-L25):
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 编译程序时,编译器会把在模块中 entryComponents
属性注册的组件,或模板里使用的组件编译为组件工厂(注:在所有静态模板中使用的组件如 <a-comp></a-comp>
,即静态组件;在 entryComponents
定义的组件,即动态组件,动态组件的一个最佳示例如 Angular Material Dialog 组件,可以在 entryComponents
中注册 DialogContentComp
组件动态加载对话框内容)。你可以在 Sources
标签里看到编译后的组件工厂文件:
从上文中我们知道,如果我们能拿到组件工厂,就可以使用组件工厂创建对应的组件对象,并插入到视图里。实际上,每一个模块都为所有组件提供了一个获取组件工厂的服务 ComponentFactoryResolver。所以,如果你在模块中定义了一个 BComponent
组件并想要拿到它的组件工厂,你可以在这个组件内注入这个服务并使用它:
export class AppComponent { constructor(private resolver: ComponentFactoryResolver) { // now the `factory` contains a reference to the BComponent factory const factory = this.resolver.resolveComponentFactory(BComponent); }
这是在两个组件 AppComponent
和 BComponent
都定义在一个模块里才行,或者导入其他模块时该模块已经有组件 BComponent
对应的组件工厂。
但是如果组件在其他模块定义,并且这个模块是按需加载,这样的话是不是完蛋了呢?实际上我们照样可以拿到某个组件的组件工厂,方法同路由使用 loadChildren
配置项按需加载模块很类似。
有两种方式可以在运行时加载模块。第一种方式 是使用 SystemJsNgModuleLoader 模块加载器,如果你使用 SystemJS 加载器的话,路由在加载子路由模块时也是用的 SystemJsNgModuleLoader
作为模块加载器。SystemJsNgModuleLoader
模块加载器有一个 load
方法来把模块加载到浏览器里,同时编译该模块和在该模块中申明的所有组件。load
方法需要传入文件路径参数,并加上导出模块的名称,返回值是 NgModuleFactory:
loader.load('path/to/file#exportName')
注:NgModuleFactory 源码是在packages/core/linker
文件夹内,该文件夹里的代码主要是粘合剂
代码,主要都是一些接口类供Core
模块使用,具体实现在其他文件夹内。
如果没有指定具体的导出模块名称,加载器会使用默认关键字 default
导出的模块名。还需注意的是,想要使用 SystemJsNgModuleLoader
还需像这样去注册它:
providers: [ { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader } ]
你当然可以在 provide
里使用任何标识(token),不过路由模块使用 NgModuleFactoryLoader
标识,所以最好也使用相同 token
。(注:NgModuleFactoryLoader
注册可查看源码 L68,使用可查看 L78)
模块加载并获取组件工厂的完整代码如下:
@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
时还有个问题,上面代码的 load()
函数内部(注:参见 L70)其实是使用了编译器的 compileModuleAsync 方法,该方法只会为在 entryComponents
中注册的或者在组件模板中使用的组件,去创建组件工厂。但是如果你就是不想要把组件注册在 entryComponents
属性里,是不是就完蛋了呢?仍然有解决方案 —— 使用 compileModuleAndAllComponentsAsync 方法自己去加载模块。该方法会为模块里所有组件生成组件工厂,并返回 ModuleWithComponentFactories
对象:
class ModuleWithComponentFactories<t> { componentFactories: ComponentFactory<any>[]; ngModuleFactory: NgModuleFactory<t>;</t></any></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,下面是源码中的 文档说明:
One intentional omission from this list is@angular/compiler
, which is currently considered a low level api and is subject to internal changes. These changes will not affect any applications or libraries using the higher-level apis (the command line interface or JIT compilation via@angular/platform-browser-dynamic
). Only very specific use-cases require direct access to the compiler API (mostly tooling integration for IDEs, linters, etc). If you are working on this kind of integration, please reach out to us first.
从上文中我们知道如何通过模块中的组件工厂来动态创建组件,其中模块是在运行时之前定义的,并且模块是可以提前或延迟加载的。但是,也可以不需要提前定义模块,可以像 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);
从上面代码可以总结动态创建视图的一般流程如下:
定义组件类及其属性,并使用装饰器装饰组件类
定义模块类,在模块类中申明组件类,并使用装饰器装饰模块类
编译模块和模块中所有组件,拿到所有组件工厂
模块类也仅仅是带有模块装饰器的普通类,组件类也同样如此,而由于装饰器也仅仅是简单地函数而已,在运行时可用,所以只要我们需要,就可以使用这些装饰器如 @NgModule()/@Component()
去装饰任何类。下面代码完整展示如何动态创建组件:
@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'; }) }</any>
为了更好的调试信息,你可以使用任何类来替换上面代码中的匿名类。
上文中说到的编译器说的是 Just-In-Time(JIT) 编译器,你可能听说过 Ahead-of-Time(AOT) 编译器,实际上 Angular 只有一个编译器,它们仅仅是根据编译器使用在不同阶段,而采用的不同叫法。如果编译器是被下载到浏览器里,在运行时使用就叫 JIT 编译器;如果是在编译阶段去使用,而不需要下载到浏览器里,在编译时使用就叫 AOT 编译器。使用 AOT 方法是被 Angular 官方推荐的,并且官方文档上有详细的 原因解释 —— 渲染速度更快并且代码包更小。(想看更多就到PHP中文网AngularJS开发手册中学习)
如果你使用 AOT 的话,意味着运行时不存在编译器,那上面的不需要编译的示例仍然有效,仍然可以使用 ComponentFactoryResolver
来做,但是动态编译需要编译器,就没法运行了。但是,如果非得要使用动态编译,那就得把编译器作为开发依赖一起打包,然后代码被下载到浏览器里,这样做需要点安装步骤,不过也没啥特别的,看看代码:
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
类来实例化出一个编译器工厂,然后通过标识 Compiler
来注册编译器工厂实例。以上就是所需要修改的全部代码,就这么点东西需要修改添加,很简单不是么。
如果你使用动态加载组件方式,最后需要注意的是,当父组件销毁时,该动态加载组件需要被销毁:
ngOnDestroy() { if(this.cmpRef) { this.cmpRef.destroy(); } }
上面代码将会从视图容器里移除该动态加载组件视图并销毁它。
对于所有动态加载的组件,Angular 会像对静态加载组件一样也执行变更检测,这意味着 ngDoCheck
也同样会被调用(注:可查看 Medium 这篇文章 If you think ngDoCheck means your component is being checked — read this article)。然而,就算动态加载组件申明了 @Input
输入绑定,但是如果父组件输入绑定属性发生改变,该动态加载组件的 ngOnChanges
不会被触发。这是因为这个检查输入变化的 ngOnChanges
函数,只是在编译阶段由编译器编译后重新生成,该函数是组件工厂的一部分,编译时是根据模板信息编译生成的。因为动态加载组件没有在模板中被使用,所以该函数不会由编译器编译生成。
本文的所有示例代码存放在 Github。
注:本文主要讲了组件b-comp
如何动态加载组件a-comp
,如果两个在同一个module
,直接调用 ComponentFactoryResolver 等 API 就行;如果不在同一个module
,就使用 SystemJsNgModuleLoader 模块加载器就行。
好了,本篇文章到这就结束了(想看更多就到PHP中文网AngularJS使用手册中学习),有问题的可以在下方留言提问
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!