import { Component } from '@angular/core'; @Component({ selector: 'wrapper', template: ` <div *ngFor="let item of items"> <ng-content></ng-content> </div> ` }) class Wrapper { items = [0, 0, 0]; }
以上代码运行后与我们使用多个 <ng-content> 的效果是一样的,只会显示一个计数器!为什么不按照我们的预期运行?
The explanation
<ng-content> 不会 "产生" 内容,它只是投影现有的内容。你可以认为它等价于 node.appendChild(el) 或 jQuery 中的 $(node).append(el) 方法:使用这些方法,节点不被克隆,它被简单地移动到它的新位置。因此,投影内容的生命周期将被绑定到它被声明的地方,而不是显示在地方。
这种行为有两个原因:期望一致性和性能。什么 "期望的一致性" 意味着作为开发人员,可以基于应用程序的代码,猜测其行为。假设我写了以下代码:
<div> <counter></counter> </div>
很显然计数器将被实例化一次,但现在假如我们使用第三方库的组件:
<third-party-wrapper> <counter></counter> </third-party-wrapper>
如果第三方库能够控制 counter 组件的生命周期,我将无法知道它被实例化了多少次。其中唯一方法就是查看第三方库的代码,了解它们的内部处理逻辑。将组件的生命周期被绑定到我们的应用程序组件而不是包装器的意义是,开发者可以掌控计数器只被实例化一次,而不用了解第三方库的内部代码。
性能的原因更为重要。因为 ng-content 只是移动元素,所以可以在编译时完成,而不是在运行时,这大大减少了实际应用程序的工作量。
The solution
为了让包装器能够控制其子元素的实例化,我们可以通过两种方式完成:在我们的内容周围使用 <ng-template> 元素,或者使用带有 "*" 语法的结构指令。为简单起见,我们将在示例中使用 <ng-template> 语法,我们的新应用程序如下所示:
<wrapper> <ng-template> <counter></counter> </ng-template> </wrapper>
包装器不再使用 <ng-content>,因为它接收到一个模板。我们需要使用 @ContentChild 访问模板,并使用ngTemplateOutlet 来显示它:
@Component({ selector: 'wrapper', template: ` <button (click)="show = !show"> {{ show ? 'Hide' : 'Show' }} </button> <div *ngIf="show"> <ng-container [ngTemplateOutlet]="template"></ng-container> </div> ` }) class Wrapper { show = true; @ContentChild(TemplateRef) template: TemplateRef; }
现在我们的 counter 组件,每当我们隐藏并重新显示时都正确递增!让我们再验证一下 *ngFor 指令:
@Component({ selector: 'wrapper', template: ` <div *ngFor="let item of items"> <ng-container [ngTemplateOutlet]="template"></ng-container> </div> ` }) class Wrapper { items = [0, 0, 0]; @ContentChild(TemplateRef) template: TemplateRef; }
上面代码成功运行后,每个盒子中有一个计数器,显示 1,2 和 3,这正是我们之前预期的结果。