>웹 프론트엔드 >JS 튜토리얼 >Angular 동적 구성요소에 대해 얼마나 알고 있나요? 다음은 Anglejs에서 구성 요소를 동적으로 생성하는 방법에 대한 자세한 설명입니다.

Angular 동적 구성요소에 대해 얼마나 알고 있나요? 다음은 Anglejs에서 구성 요소를 동적으로 생성하는 방법에 대한 자세한 설명입니다.

寻∝梦
寻∝梦원래의
2018-09-07 16:04:142723검색
이 글에서는 Angular에서 동적으로 컴포넌트를 생성하는 방법을 주로 설명합니다. (참고: 템플릿에 사용되는 컴포넌트는 정적으로 생성된 컴포넌트라고 할 수 있습니다.) 이 글을 함께 살펴볼까요

이전에도 AngularJS(1세대 Angular 프레임워크)를 사용하여 프로그래밍을 했다면 $compile 서비스를 사용하여 HTML을 생성하고 데이터에 연결했을 수도 있습니다. model 따라서 양방향 바인딩 기능이 얻어집니다. $compile 服务生成 HTML,并连接到数据模型从而获得双向绑定功能:

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 阶段被使用而已。

至于 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: '<span>A Component</span>'
})
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

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

    ngAfterViewInit() {
        this.vc.createComponent(componentFactory);
    }
}
AngularJS의 명령은 DOM을 수정할 수 있지만 무엇이 수정되었는지 알 수 있는 방법이 없습니다. 이 접근 방식의 문제점은 동적 환경에서와 동일하므로 성능 최적화가 어렵습니다. 동적 템플릿은 확실히 AngularJS 성능 저하의 주요 원인은 아니지만 중요한 이유 중 하나이기도 합니다. 🎜🎜Angular의 내부 코드를 잠시 살펴본 후, 새로 설계된 프레임워크가 성능에 큰 중요성을 부여한다는 것을 알았습니다. Angular 소스 코드에서 다음 문장을 자주 찾을 수 있습니다(참고: 명확한 이해를 위해 번역되지 않음). 🎜그래서 Angular 디자이너는 JIT 및 AOT 컴파일러 도입, 정적 템플릿(정적 템플릿), 지시문/모듈 팩토리(ComponentFactory), 팩토리 파서( ComponentFactoryResolver). 이러한 개념은 AngularJS 커뮤니티에 낯설고 심지어 적대적일 수도 있지만 걱정하지 마십시오. 이전에 이러한 개념에 대해 들어봤지만 지금 그것이 무엇인지 알고 싶다면 이 기사를 계속 읽으면 도움이 될 것입니다. 🎜🎜참고: 실제로 JIT/AOT 컴파일러는 동일한 컴파일러를 의미하지만 이 컴파일러는 빌드 단계 또는 실행 단계에서 사용됩니다. 🎜Factory의 경우 Angular Compiler는 a.comComponent.ts와 같이 작성한 구성 요소를 a.comComponent.ngfactory.js로 컴파일합니다. 즉, 컴파일러는 @Component 데코레이터를 원시 자료로 사용하여 작성한 구성 요소/명령어 클래스를 다른 클래스로 컴파일합니다. 보기. 🎜🎜JIT/AOT 컴파일러로 돌아가서, 빌드 단계에서 a.comComponent.ngfactory.js가 생성되면 이 컴파일러는 종속성 패키지에 패키지되지 않습니다. 실행 단계 다음에는 컴파일러가 종속성 패키지로 패키징되고 사용자가 로컬로 다운로드해야 합니다. 런타임 시 컴파일러는 해당 뷰 팩토리 클래스를 생성하기 위해 구성 요소/명령어 클래스를 컴파일합니다. 아래에서는 *.ngfactory.js 파일의 코드가 어떤 모습인지 살펴보겠습니다. 🎜🎜팩토리 리졸버의 경우, 컴파일된 팩토리 객체를 얻을 수 있는 객체입니다. 🎜🎜

컴포넌트 팩토리 및 컴파일러

🎜Angular의 모든 컴포넌트는 컴포넌트 팩토리에 의해 생성되며, 컴포넌트 팩토리는 사용자가 작성한 @Component 데코레이터에 따라 컴파일러에 의해 생성됩니다. 메타데이터가 컴파일되고 생성됩니다. 인터넷에서 많은 데코레이터 기사를 읽은 후에도 여전히 약간 혼란스럽다면 제가 쓴 사용자 정의 구성 요소 데코레이터 구현을 쓴 Medium 기사를 참조하세요. 🎜🎜Angular는 내부적으로 view 개념을 사용하거나 전체 프레임워크가 뷰 트리입니다. 각 뷰는 요소 노드, 텍스트 노드 등 다양한 유형의 노드로 구성됩니다. (참고: Translation Angular DOM 업데이트 메커니즘을 참조하세요.) 각 노드에는 고유한 전문 역할이 있으므로 각 노드의 처리에는 약간의 시간만 소요되며 각 노드에는 ViewContainerRefTemplateRef와 같은 서비스가 사용됩니다. , ViewChild/ViewChildrenContentChild/ContentChildren을 사용하여 이러한 노드에 대한 DOM 쿼리를 수행할 수도 있습니다. 🎜🎜참고: 간단히 말하면 Angular 프로그램은 뷰 트리입니다. 각 뷰는 개발자가 사용할 수 있는 템플릿 작업 API를 제공합니다. DOM 쿼리 API를 통해 얻을 수 있습니다. . 🎜🎜각 노드에는 많은 양의 정보가 포함되어 있으며 성능상의 이유로 노드가 생성되면 적용되며 이후 변경은 허용되지 않습니다(참고: 생성된 노드는 캐시됩니다). 노드 생성 프로세스는 컴파일러가 사용자가 작성한 구성 요소 정보(참고: 주로 작성한 구성 요소의 템플릿 정보)를 수집하고 이를 구성 요소 팩토리 형식으로 캡슐화하는 것입니다. 🎜🎜다음과 같이 컴포넌트를 작성한다고 가정해 보겠습니다. 🎜
@NgModule({
    // imports CommonModule with declared directives like
    // ngIf, ngFor, ngClass etc.
    imports: [CommonModule],
    ...
})
export class SomeModule {}
🎜 컴파일러는 여러분이 작성한 정보를 기반으로 다음과 유사한 컴포넌트 팩토리 코드를 생성합니다. 코드에는 중요한 부분만 포함되어 있습니다. (참고: 아래 전체 코드는 View, 여기서 elementDef2jit_textDef3노드로 이해될 수 있음): 🎜
const COMMON_DIRECTIVES: Provider[] = [
    NgClass,
    NgComponentOutlet,
    NgForOf,
    NgIf,
    ...
];

@NgModule({
    declarations: [COMMON_DIRECTIVES, ...],
    exports: [COMMON_DIRECTIVES, ...],
    ...
})
export class CommonModule {
}
🎜위 코드는 기본적으로 구조를 설명합니다. 구성 요소 보기의 구성 요소를 인스턴스화하는 데 사용됩니다. 그 중 첫 번째 노드 elementDef2는 요소 노드 정의이고 두 번째 노드 jit_textDef3는 텍스트 노드 정의입니다. 각 노드에는 인스턴스화하기에 충분한 매개변수 정보가 있고 이 매개변수 정보는 모든 종속성을 구문 분석하는 컴파일러에 의해 생성되며 이러한 종속성의 특정 값은 런타임 시 프레임워크에서 제공되는 것을 볼 수 있습니다. 🎜

从上文知道,如果你能够访问到组件工厂,就可以使用它实例化出对应的组件对象,并使用 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 组件工厂对象,继续看。

模块(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 属性注册的组件,或模板里使用的组件编译为组件工厂(注:在所有静态模板中使用的组件如 <a-comp></a-comp>,即静态组件;在 entryComponents 定义的组件,即动态组件,动态组件的一个最佳示例如 Angular Material Dialog 组件,可以在 entryComponents 中注册 DialogContentComp 组件动态加载对话框内容)。你可以在 Sources 标签里看到编译后的组件工厂文件:

Angular 동적 구성요소에 대해 얼마나 알고 있나요? 다음은 Anglejs에서 구성 요소를 동적으로 생성하는 방법에 대한 자세한 설명입니다.

从上文中我们知道,如果我们能拿到组件工厂,就可以使用组件工厂创建对应的组件对象,并插入到视图里。实际上,每一个模块都为所有组件提供了一个获取组件工厂的服务 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);
  }

这是在两个组件 AppComponentBComponent 都定义在一个模块里才行,或者导入其他模块时该模块已经有组件 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);

从上面代码可以总结动态创建视图的一般流程如下:

  1. 定义组件类及其属性,并使用装饰器装饰组件类

  2. 定义模块类,在模块类中申明组件类,并使用装饰器装饰模块类

  3. 编译模块和模块中所有组件,拿到所有组件工厂

模块类也仅仅是带有模块装饰器的普通类,组件类也同样如此,而由于装饰器也仅仅是简单地函数而已,在运行时可用,所以只要我们需要,就可以使用这些装饰器如 @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>

为了更好的调试信息,你可以使用任何类来替换上面代码中的匿名类。

Ahead-of-Time Compilation

上文中说到的编译器说的是 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/compilerJitCompilerFactory 类来实例化出一个编译器工厂,然后通过标识 Compiler 来注册编译器工厂实例。以上就是所需要修改的全部代码,就这么点东西需要修改添加,很简单不是么。

组件销毁

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

ngOnDestroy() {
  if(this.cmpRef) {
    this.cmpRef.destroy();
  }
}

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

ngOnChanges

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

Github

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

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

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

위 내용은 Angular 동적 구성요소에 대해 얼마나 알고 있나요? 다음은 Anglejs에서 구성 요소를 동적으로 생성하는 방법에 대한 자세한 설명입니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.