0

0

关于Angular动态组件你知道多少?这里有在angularjs中动态创建组件详解

寻∝梦

寻∝梦

发布时间:2018-09-07 16:04:14

|

2944人浏览过

|

来源于php中文网

原创

本文主要解释如何在 Angular 中动态创建组件(注:在模板中使用的组件可称为静态地创建组件)。下面就让我们一起来看这篇文章吧

如果你之前使用 angularjs(第一代 angular 框架)来编程,可能会使用 $compile 服务生成 html,并连接到数据模型从而获得双向绑定功能:

const template = 'generated on the fly: {{name}}'
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 阶段被使用而已。

至于 factory,是 Angular Compiler 把你写的组件如 a.component.ts 编译为 a.component.ngfactory.js,即 Compiler 使用 @Component decorator 作为原材料,把你写的组件/指令类编译为另一个视图工厂类。

回到刚刚的 JIT/AOT Compiler,如果 a.component.ngfactory.js 是在 build 阶段生成的那就是 AOT Compiler,这个 Compiler 不会被打包到依赖包里;如果是在 run 阶段生成,那 Compiler 就需要被打包到依赖包里,被用户下载到本地,在运行时 Compiler 会编译组件/指令类生成对应的视图工厂类,仅此而已。下文将会看下这些 *.ngfactory.js 文件代码是什么样的。

至于 factory resolver,那就更简单了,就是一个对象,通过它拿到那些编译后的 factory 对象。

组件工厂和编译器

Angular 中每一个组件是由组件工厂创建的,组件工厂又是由编译器根据你写的 @Component 装饰器里的元数据编译生成的。如果你在网上读了大量的 decorator 文章还有点迷惑,可以参考我写的这篇 Medium 文章 Implementing custom component decorator

Angular 内部使用了 视图 概念,或者说整个框架是一颗视图树。每一个视图是由大量不同类型节点(node)组成的:元素节点,文本节点等等(注:可查看 译 Angular DOM 更新机制)。每一个节点都有其专门作用,这样每一个节点的处理只需要花很少的时间,并且每一个节点都有 ViewContainerRefTemplateRef 等服务供使用,还可以使用 ViewChild/ViewChildrenContentChild/ContentChildren 做 DOM 查询这些节点。

注:简单点说就是 Angular 程序是一颗视图树,每一个视图(view)又是有多种节点(node)组成的,每一个节点又提供了模板操作 API 给开发者使用,这些节点可以通过 DOM Query API 拿到。

每一个节点包含大量信息,并且为了性能考虑,一旦节点被创建就生效,后面不容许更改(注:被创建的节点会被缓存起来)。节点生成过程是编译器搜集你写的组件信息(注:主要是你写的组件里的模板信息),并以组件工厂形式封装起来。

假设你写了如下的一个组件:

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

编译器根据你写的信息生成类似如下的组件工厂代码,代码只包含重要部分(注:下面整个代码可理解为视图,其中 elementDef2jit_textDef3 可理解为节点):

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

上面代码基本描述了组件视图的结构,并被用来实例化一个组件。其中,第一个节点 elementDef2 就是元素节点定义,第二个节点 jit_textDef3 就是文本节点定义。你可以看到每一个节点都有足够的参数信息来实例化,而这些参数信息是编译器解析所有依赖生成的,并且在运行时由框架提供这些依赖的具体值。

从上文知道,如果你能够访问到组件工厂,就可以使用它实例化出对应的组件对象,并使用 ViewContainerRef API 把该组件/视图插入 DOM 中。如果你对 ViewContainerRef 感兴趣,可以查看 译 探索 Angular 使用 ViewContainerRef 操作 DOM。应该如何使用这个 API 呢(注:下面代码展示如何使用 ViewContainerRef API 往视图树上插入一个视图):

蝉妈妈AI
蝉妈妈AI

电商人专属的AI营销助手

下载
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit() {
        this.vc.createComponent(componentFactory);
    }
}

好的,从上面代码可知道只要拿到组件工厂,一切问题就解决了。现在,问题是如何拿到 ComponentFactory 组件工厂对象,继续看。

模块(Modules)和组件工厂解析器(ComponentFactoryResolver)

尽管 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 属性注册的组件,或模板里使用的组件编译为组件工厂(注:在所有静态模板中使用的组件如 来做,但是动态编译需要编译器,就没法运行了。但是,如果非得要使用动态编译,那就得把编译器作为开发依赖一起打包,然后代码被下载到浏览器里,这样做需要点安装步骤,不过也没啥特别的,看看代码:

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

上面代码中,我们使用 entryComponentsentryComponents 类来实例化出一个编译器工厂,然后通过标识 DialogContentComp 来注册编译器工厂实例。以上就是所需要修改的全部代码,就这么点东西需要修改添加,很简单不是么。

组件销毁

如果你使用动态加载组件方式,最后需要注意的是,当父组件销毁时,该动态加载组件需要被销毁:

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

上面代码将会从视图容器里移除该动态加载组件视图并销毁它。

ngOnChanges

对于所有动态加载的组件,Angular 会像对静态加载组件一样也执行变更检测,这意味着 Sources 也同样会被调用(注:可查看 Medium 这篇文章 If you think ngDoCheck means your component is being checked — read this article)。然而,就算动态加载组件申明了 BComponent 输入绑定,但是如果父组件输入绑定属性发生改变,该动态加载组件的 AppComponent 不会被触发。这是因为这个检查输入变化的 BComponent 函数,只是在编译阶段由编译器编译后重新生成,该函数是组件工厂的一部分,编译时是根据模板信息编译生成的。因为动态加载组件没有在模板中被使用,所以该函数不会由编译器编译生成。

Github

本文的所有示例代码存放在 Github

注:本文主要讲了组件 BComponent 如何动态加载组件 loadChildren,如果两个在同一个 SystemJsNgModuleLoader,直接调用 ComponentFactoryResolver 等 API 就行;如果不在同一个 SystemJsNgModuleLoader,就使用 SystemJsNgModuleLoader 模块加载器就行。

好了,本篇文章到这就结束了(想看更多就到PHP中文网AngularJS使用手册中学习),有问题的可以在下方留言提问

1.6.png
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 {
    componentFactories: ComponentFactory[];
    ngModuleFactory: NgModuleFactory;
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);
        })
    })
}
const template = 'generated on the fly: {{name}}'
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) {
}

ngAfterViewInit() {
  const template = 'generated on the fly: {{name}}';

  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';
    })
}
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();
  }
}

相关专题

更多
php文件怎么打开
php文件怎么打开

打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

2020

2023.09.01

php怎么取出数组的前几个元素
php怎么取出数组的前几个元素

取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

1339

2023.10.11

php反序列化失败怎么办
php反序列化失败怎么办

php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

1244

2023.10.11

php怎么连接mssql数据库
php怎么连接mssql数据库

连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

948

2023.10.23

php连接mssql数据库的方法
php连接mssql数据库的方法

php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

1402

2023.10.23

html怎么上传
html怎么上传

html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

1231

2023.11.03

PHP出现乱码怎么解决
PHP出现乱码怎么解决

PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1440

2023.11.09

php文件怎么在手机上打开
php文件怎么在手机上打开

php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1303

2023.11.13

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.2万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 1.9万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号