angular2 学习笔记 ( Dynamic Component 动态组件)

时间:2022-04-14 02:42:16

更新 : 2020-02-21 

lazy load 的时候可以用 webpack 的 prefetch 哦

import(/* webpackPrefetch: true */`./bar/bar.component`)

refer :

https://netbasal.com/welcome-to-the-ivy-league-lazy-loading-components-in-angular-v9-e76f0ee2854a

https://webpack.js.org/api/module-methods/#magic-comments

更新: 2019-12-14

之前好像有讲过, create component 或者 template 都好, 创建好后是没有 detech change 的

然后 container.insert 也是没有帮我们 detech change 的. 但很多时候我们不需要自己 detech change

那是因为通常我们是通过某个事件触发,然后动态出模板. 这个时候如果模板 append 的地方本身就有 detech change 那么就会一起了。

久了就习惯不去 detech change 了, 今天做了一个测试, 两个组件 a, b 然后通过一个 service 沟通

b 点击的时候调用 a 的 append component, 结果就是 template 出去后没有 detech change.

一直到 a component detech change 才有效。

更新: 2019-12-03

发现 cdkportal 使用了 ng-template 而不是 ng-container, 但指令内部依然是注入 ngContainerRef,有一点不好理解,因为我认为 ng-template 应该用于定义模板, 这里作为出口应该是 ng-container

angular2 学习笔记 ( Dynamic Component 动态组件)

然后自己玩了一下. div #target1 是不正确的. ng-template 和 ng-container 效果倒是一样的. 不懂原理是啥。先记入起来,以后遇到麻烦有个印象。

<ng-container #target1 ></ng-container>
<ng-template #target1 ></ng-template>
<div #target1 ></div>
  @ViewChild('target1', { read: ViewContainerRef, static: true })
  target1: ViewContainerRef;

更新: 2019-12-01

ng-template 和依赖注入

情况 1 xyz 可以注入 abc

<cp-abc>
<cp-xyz></cp-xyz>
</cp-abc>

情况 2, 动态的效果是一样的

<cp-abc>
<ng-container [ngComponentOutlet]="XyzComponent" ></ng-container>
</cp-abc>

情况 3,无法注入到 abc 了. 同时 content child 也是无法获取到 xyz,

<cp-abc>
<ng-container [ngTemplateOutlet]="template"></ng-container>
</cp-abc>
<ng-template #template>
<cp-xyz></cp-xyz>
</ng-template>

情况 4, 获取到了 abc 但是下面那一个,注意哦.

<cp-abc>
<ng-container [ngTemplateOutlet]="template"></ng-container>
</cp-abc>
<cp-abc>
<ng-template #template>
<cp-xyz></cp-xyz>
</ng-template>
</cp-abc>

结论:

组件在 append 之前不在任何的逻辑树里, 所以 append 后的地方就是它的家.

template 不同,它在 append 之前就已经在某个逻辑树里了, 所以 append 到任何一个地方都好, 它都不会搬家了. append 的地方并没有获取到 template 内容的能力. 要搞清楚哦。

更新: 2019-11-12 

ui 越写越多,源码就越挖越深... 我不是应该写写业务...忽悠忽悠赚大钱的吗...为什么这苦...

这篇写了蛮多东西, 但是源码有点旧了,怕 ivy 的世界不太一样. 所以还是自己玩玩看

https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f

一直想搞清楚, angular 里面这么多个 xxRef 到底是啥关系. 这里把我经常搞不清楚的都记入下来呗.

1. viewchild 获取 template vs container

angular2 学习笔记 ( Dynamic Component 动态组件)

component.ts

@ViewChild('template', { static: true })
template: TemplateRef<any>; @ViewChild('container', { static: true, read: ViewContainerRef })
container: ViewContainerRef; ngOnInit() {
console.log((this.template.elementRef.nativeElement as Comment).nodeName);
console.log((this.container.element.nativeElement as Comment).nodeName);
}

template 无需使用 read 也可以正确读出, container 如果没有表明的话, 获取到的是 ElementRef

template 和 container 的 element 都是 comment 但是值却不同哦

template 的是 'container'

container 的是 'ng-container'

2. ViewContainerRef, TemplateRef, EmbeddedViewRef, ViewRef, ComponentRef, RootViewRef, ChangeDetectorRef, ElementRef, ApplicationRef, LView, TView

ViewContainerRef 是用来 append template or component 的. 它里面有封装了创建 component 和 template 的方法, 但是这里我们不看这个,因为这个只是方便罢了. 它的主要功能是在 insert

abstract insert(viewRef: ViewRef, index?: number): ViewRef;

insert 需要一个 viewRef, 我们可以通过 viewchild 拿到 TemplateRef, 那我们还差一个 templateRef -> viewRef 的方式.

很简单, 调用 createEmbeddedView 就可以了

const templateContext = { passingData: 'data' };
const embeddedViewRef = this.template.createEmbeddedView(templateContext);

EmbeddedView 就是一种 ViewRef

export abstract class EmbeddedViewRef<C> extends ViewRef

那组件是如何变成 ViewRef insert 的呢?

注入 resolver, 然后丢组件类进去, 会得到工厂,然后叫工厂生产出 ComponentRef

const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
const componentRef = componentFactory.create(this.injector);

ComponentRef 有个属性叫 hostView, 就是 ViewRef 来的, 同时注意, hostView 和 changeDetectorRef 是一样的.

export class ComponentRef<T> extends viewEngine_ComponentRef<T> {
destroyCbs: (() => void)[]|null = [];
instance: T;
hostView: ViewRef<T>;
changeDetectorRef: ViewEngine_ChangeDetectorRef;
componentType: Type<T>; constructor(
componentType: Type<T>, instance: T, public location: viewEngine_ElementRef,
private _rootLView: LView,
private _tNode: TElementNode|TContainerNode|TElementContainerNode) {
super();
this.instance = instance;
this.hostView = this.changeDetectorRef = new RootViewRef<T>(_rootLView);
this.hostView._tViewNode = assignTViewNodeToLView(_rootLView[TVIEW], null, -1, _rootLView);
this.componentType = componentType;
}

所以 ViewRef 也是一种 ChangeDetectorRef

export abstract class ViewRef extends ChangeDetectorRef 

而 RootViewRef 依然是一种 ViewRef, 这个 RootViewRef 是专门给 ComponentRef.hostView 用的,其它地方都没见过.

export class RootViewRef<T> extends ViewRef<T>

平时我们 component 注入的 ChangeDetectorRef 其实就是 VIewRef 来的,并不是分开的 2 个类,现在懂了

ng 有逻辑树的概念

当我们使用 view container ref 插入组件或模板时,组件模板就跟着这个 container 了。这个是在逻辑书中. detech change 机制就是依据这个跑的.

然后呢,如果我们通过 body.appendChild(view.rootNode[0]) 把刚才渲染好的 element cut & paste 去另外一个地方。

这个时候逻辑上它依然是刚才的地方,只是渲染的地方却被换掉了.

我们在做任何事情的时候都是依据逻辑树而不是渲染树的. 这样要清楚哦.

做动态组件经常遇到一个难题,就是组件间的通讯.

<test>
<ng-template let-inside-data="insideData">
{{ insideData }}
{{ outsideData }}
</ng-template>
</test>

使用模板的时候,我们是通过像上面这样的方式来通讯的. 使用体验还算不错.

模板里汇集了 2 个资料源, 一个来自 "定义模板的组件",一个来自使用模板的组件, 非常灵活.

本来 ivy 有个 breaking change https://github.com/angular/angular/issues/33393

If a template is declared in one view but inserted into a different view, change detection will occur for that template only when its insertion point is checked

(previously, change detection would also run when its declaration point was checked).

这个直接就影响到了上面这种通讯方式,幸好最后是改回来了。不然肯定被骂死... ng team 有时候做决定还真恐怖...

但是使用组件的话就没有这种方式了, 组件没有什么"定义的组件的组件" 这回事儿, 所以调用组件的组件,需要传入所有数据, 一次性的数据还算容易,如果数据是动态的只能使用 rxjs 了

因为即使调用的组件 detech change, 动态组件也不会跟着 detech change .这和模板是不同的. 可以自己试试 mat dialog 传 template 和传 component

2 个修改 data, template 可以 detech change 但 component 不行。

更新 : 2019-10-25 

template 作用域小结 :

test.html

<app-abc>
<ng-template>
<app-xyz></app-xyz>
</ng-template>
</app-abc>

abc 的 content child

test 的 viewchild

都可以获取到 xyz

但是需要等到 template 被创建以后才可以获取到哦.

xyz 的 inject 可以获取到 abc 也可以获取到 test.

概念:

不管 template 最后被 append 去了哪里, 上面的这些是不变的. 所以 template 的作用域并不是依赖于它最终被 append 到了哪里,而是它是来自哪里. 它的 css 也是这样.

我们可以想象成它是在原来的地方生产出来,然后被丢到了另一个地方.

我们在动态创建 component 时可以指定 injector, 但创建 template 的时候是不行的.

更新: 2019-03-20 

今天遇到烦人的情况, 在做 carousel 幻灯片插件.

<carousel>
<item *ngFor="..." >
<img lazyload ...>
</item>
<carousel>

本来是想用 ng-content 让外部的组件负责 for loop 然后把图放入到组件里.

后来发现要有 looping 的体验, 这样就需要做 clone element, 本来嘛, clone 也不是什么大件事, 直接操作 dom 也就是了.

后来又要求图片需要 lazy load, 还好之前就有 lazyload 指令了.

结果跑不起来,因为 lazyload 指令的设计是通过 onInit 去寻找 parent 然后做 intersectionObserver.observe(element)

我 html clone 的 element 不跑 ng, lazyload 指令作废...

看来 clone 行不通,就改用动态组件吧. 外部穿 template 进来,里面不用 clone 直接 createEmbedView.

本以为 transclude 换成 ng-template 是很容易的

https://medium.com/@unrealprogrammer/angulars-content-projection-trap-and-why-you-should-consider-using-template-outlet-instead-cc3c4cad87c9

甚至有文章说多用 template 少用 transclude 呢.

所以呢,我发现了. 使用 transclude template 和 translude element 是差很多的

<carousel>
<ng-template>
<item>
<img lazyload ...>
</item>
</ng-template>
<carousel>

表面上看只是套了一层, 但是这让组件沟通变得很难.

之前我可以用 @contentchild 直接获取到所有 item

现在呢, 由于是外部只给了一个 template, 没有 ngfor 了, 反而是内部使用 template 生产 item

这就导致了 @contentchild 获取不到 item (或者说要等到下一次 life cycle after content checked 才能获取到)

另外我也间接体会到, 如果这个 template 不是用 transclude 的方式传递,而是用 input 的方式传递.. carousel 组件时完全没有方法可以获取到 item 的.

由此看来 template 的局限还是很大的.

相关问题 :

https://github.com/angular/angular/issues/14842

最后就是想说,这 2 个方式并不是互相替代的. 某一些极端情况下, 2 个都不好用 >"<

更新 : 2019-01-07

不错的文章

refer :

https://juejin.im/post/5ae00616f265da0b7e0bee78

https://juejin.im/post/5ab09a49518825557005d805

更新 2019 01-06:

有时候我们会想要把 component 缓存起来, 比如说当用户在切换页面的时候, 我们希望当用户切回同一页是, 能够使用缓存.

做法很多.

比如, 我们可以搞一个 state service, 所有页面上的交互状态都存起来, 当用户切回来的时候, 在把 state 输入 component 内.

这种做法在 redux 的情况下比较容易实现,但是如果没有使用 redux 呢?

总不能为了这个就搞 redux 吧, 小题大做了.

那还有一个办法是使用 reusestrategy

它的实现原理其实很简单.

动态创建 component insert to container, 在 remove 的时候, 我们不调用 remove, 而是调用 detech

这样它只是把 view 拿走并不清除掉, 留意哦, 这时 dom 虽然没有 component 了, 但我们的 component 依然存在的, 如果里面有 interval 依然会执行.

然后呢就是把它 inert 放回去就可以了.

  private cacheComponent: ComponentRef<AbcComponent>;

  add() {
let factory = this.componentFactoryResolver.resolveComponentFactory(AbcComponent);
const component = this.cacheComponent = factory.create(this.injector);
this.viewContainerRef.insert(component.hostView, 0);
} remove() {
this.viewContainerRef.detach();
} back() {
console.log('cache component value', this.cacheComponent.instance.value);
this.viewContainerRef.insert(this.cacheComponent.hostView);
}

通过这个方法, 我们可以直接保留整个 component 和 view, state 自然也保存了.

更新 2018-02-07

详细讲一下 TemplateRef 和 ViewContainerRef 的插入

refer :

https://segmentfault.com/a/1190000008672478

https://*.com/questions/45438304/insert-a-dynamic-component-as-child-of-a-container-in-the-dom-angular2

https://*.com/questions/46992280/viewcontainerref-index-parameter

<ng-template #temp >
<dynamic-dada></dynamic-dada>
</ng-template>

模板就是一个不会马上被渲染出来的东西,等我们想要的时候才去调用渲染它.

上面 ng-template 就表达了这是个模板,我们通过 #temp + viewchild 来获取它的指针来准备后续的调用.

  @ViewChild('temp', { read: TemplateRef })
temp: TemplateRef<any>

等到我们想调用的时候, 就调用 createEmbeddedView,

 let embeddedView = this.temp.createEmbeddedView(null); // 参数是 template context 可以看之前的文章有提到, 诸如 let-i=index 之类的变量

调用这个方法之后, 我们会得到 ViewRef, 这时 template 就已经渲染成 element 了, 不过呢它并还没有运行 detechChange

所以任何 binding 这时是无效的, 里面有组件的话也统统还没有执行 OnInit 等. element 的 parentNode 目前也是 null

这时如果我们调用  detechChange() 那么模板会开始执行binding 和 compoent 实例化, 实例化的 component 会接上当前的 component tree . 但是呢, 这样做是不真确的, 我们应该要先把 element 插入到我们的 document 里头, 才 detechChange

(不然会有意想不到的结果, 比如你的 parent viewchildrenQueryList 没有触发change 等等, 顺便一提. templateRef insert and detechChange 完全好了之后 parent 的viewchildQueryList.change 才会触发哦)

接着就要介绍一下 "家“ 了 viewContainerRef

ng 替我们封装了一个 viewContainerRef 来负责 element 的插入或移除.

首先我们说说如何获取到这个 viewContainerRef

方法一 : 在组件注入 host 的 containerRef

constructor(
private vcr: ViewContainerRef,
private cdr: ChangeDetectorRef
) { }

方法二 :

<ng-container #mycontainer ></ng-container>
<div #mycontainertwo > <span>child</span> </div>

然后通过 viewchild 去获取

  @ViewChild('mycontainer', { read : ViewContainerRef })
mycontainer : ViewContainerRef

这时我们就可以使用 viewcontainerref 来插入我们刚才的 viewref 了

  this.mycontainer.insert(embeddedView, 0);
embeddedView.detectChanges();

insert 的第2参数是位置.

这里有一个重要的概念要说

insert 之后 element 会在 container 的 sibling 而不是 child

因为在 ng 看来 container 其实只是一个标签. 并不是一个具体的 element

即使你写 <div #mycontainer></div>

insert 之后 element 也不会去到这个 div 的 child 里面而是 sibling

<div #mycontainer ></div>

<inserted-elem>

所以如果你使用刚才说的方法一,注入 viewcontainer

也是同样的原理 ”记住是 sibling 而不是 child

有了 TemplateRef + ViewContainerRef 我们就可以很容易做动态模板啦 ^^

时间 : 2017-01-25

一样这一篇最要讲概念而已.

refer :

http://blog.rangle.io/dynamically-creating-components-with-angular-2/ (例子)
https://www.ag-grid.com/ag-grid-angular-aot-dynamic-components/ (动态 entryComponents)

http://*.com/questions/40106480/what-are-projectable-nodes-in-angular2 (Projectable nodes, something like transclude)

http://*.com/questions/38888008/how-can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular ( dynamic Jit version )

https://medium.com/@isaacplmann/if-you-think-you-need-the-angular-2-runtime-jit-compiler-2ed4308f7515#.72ln3bcsy (many way Aot version)

https://www.youtube.com/watch?v=EMjTp12VbQ8 (youtube)

动态组件分 2 种

1. Jit

2. Aot

Jit 的情况下你可以动态的写组件模板, 最后 append 出去, 类似 ng1 的 $compile

Aot 的话, 模板是固定的, 我们只是可以动态创建 component 然后 append 出去

这一篇只会谈及 Aot

要知道的事项 :

1. 所有要动态的组件除了需要 declarations之外还要声明到 entryComponents 里头, 原因是 ng 是通过扫描模板来 import component class 的, 动态组件不会出现在模板上所以我们要用另一个 way 告诉 ng。

            providers: [
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: [AComponentClass, BComponentClass], multi: true}
]

要方便扩展的话可以用 provides, ng-router 也是用这个实现的.

2. 目前只有动态组件,没有动态指令 (呃..所以动态创建的组件就不能附带指令咯.../.\)

3. 例子

@Component({
selector: 'aaa',
template: ``
})
export class AAAComponent implements OnInit, AfterContentInit {
constructor(
private vcr: ViewContainerRef,
private cfr: ComponentFactoryResolver
) { } @ContentChildren("dynamic", { read: ElementRef }) elem: QueryList<ElementRef> //read 的作用是强转类型 ngOnInit() { } ngAfterContentInit() {
let providers = ReflectiveInjector.resolve([AbcService]); //为组件添加 providers
let injector = ReflectiveInjector.fromResolvedProviders(providers, this.vcr.parentInjector); //创建注入器给 component (记得要继承哦)
let factory = this.cfr.resolveComponentFactory(AbcComponent); //创建 component 工厂
let component = factory.create(injector,[[this.elem.first.nativeElement],[this.elem.last.nativeElement] ]); //创建 component, 这是就把注入器放进了, 后面的 array 是给 ng-content 用的
component.instance.name = "keatkeat"; // 对 input, output 做点东西
this.vcr.insert(component.hostView, 0); // 插入到模板中 0 是 position, 如果是 0 其实可以不用放. // 如果不需要设定 providers 的话,可以省略一些 :
// let factory = this.resolver.resolveComponentFactory(AbcComponent);
// let component = this.vcr.createComponent(factory, 0);
// component.instance.name = "keatkeat";
}
}

里头说的 ng-content, 就是 Projectable nodes , 我个人认为这个做法还不太理想,因为 ng-content 应该是可以通过 select 找到对应的 tranclude 的,不过这里的参数 array 已经固定了 tranclude 的位置.

所以目前, 如果你要做类似 tranclude 的事情, 改用 input 传递 templateRef 反而会比较容易控制.

类似这样

@Component({
template : `
<p>final</p>
<template [ngTemplateOutlet]="template" [ngOutletContext]="{ innerValue : 'huhu' }" ></template>
`,
selector : "final"
})
export class FinalComponent implements OnInit {
constructor(
) { } @Input()
template : TemplateRef<any> //传进来 ngOnInit() {
console.log(this.template);
}
}

4. 个人的想法

一个动态组件应该和平时的组件必须是一样的,意思是我们可以随时把任何一个组件改成动态调用的方式.

不过目前 ng 支持的不是很好

-input, output (支持)

-tranclude (Projectable nodes 显然和 ng-content配合不上)

-在 component 上放指令 (动态创建的 component, 没办法加上指令, 这导致了 dynamic accessor 很难写)

例子

@Component({
templateUrl : "./debugTwo.component.html"
})
export class DebugTwoComponent implements OnInit, AfterViewInit { constructor(
private fb : FormBuilder,
private cfr : ComponentFactoryResolver
) { } form : FormGroup
@ViewChild("target", { read : ViewContainerRef }) target : ViewContainerRef
ngOnInit() { this.form = this.fb.group({
age : [0]
});
} ngAfterViewInit()
{
let factory = this.cfr.resolveComponentFactory(AccessorComponent);
let component = this.target.createComponent(factory, 0);
let ctrl = this.form.controls["age"];
component.instance.writeValue(ctrl.value); //需要手动去调用 writeValue, registerOnChange, registerOnTouched, 如果可以直接加上 formControlName 指令,就方便多了.
component.instance.registerOnChange((v) => {
ctrl.setValue(v);
});
//component.instance.template = this.template;
}
}

5. 一些常用到的类

-ViewContainerRef : 好比一个 root div, 通常我们引用它目的就是 append element 进去.

常用 : createEmbeddedView, createComponent, insert

-TemplateRef : 指的是 <template> 里面的内容.

常用 : createEmbeddedView

-ElementRef : dom

常用 : nativeElement (获取 dom 对象引用)

 
 

angular2 学习笔记 ( Dynamic Component 动态组件)