我的 Angular 偏好:使用 ngTemplateOutlet 代替 ng-content

203 阅读3分钟

我们知道,可以使用ng-content将内容注入到组件内部。但同样地,我们也可以用ngTemplateOutlet来实现这一点。最终,在大多数情况下,由于类型安全、简洁性以及ng-content在扩展性方面的问题,实际案例使我倾向于默认使用ngTemplateOutlet

card为例。假设我们需要支持自定义的card-titlecard-body

类型安全与简洁性

我用ng-content实现了card-title,而用ngTemplateOutlet实现了card-body。类型安全版本如下:

const CARD_TITLE_SELECTOR = "card-title";

@Component({
  selector: CARD_TITLE_SELECTOR,
  imports: [],
  template: `<ng-content></ng-content> `,
  styles: ``,
})
export class CardTitleComponent {}

@Component({
  selector: "card",
  imports: [NgTemplateOutlet],
  template: `
    <ng-content [select]="cardTitleSelector"></ng-content>
    <div class="card-divider"></div>
    <ng-container *ngTemplateOutlet="cardBody"></ng-container>
  `,
  styles: ``,
})
export class CardComponent {
  @Input({ required: true }) cardBody!: TemplateRef<any>;
  cardTitleSelector = CARD_TITLE_SELECTOR;
}

使用方式如下:

<card [cardBody]="cardBody">
  <card-title>card title</card-title>
  <ng-template #cardBody> card body </ng-template>
</card>

如你所见,它们确实很相似。

关于简洁性,ngTemplateOutlet版本更胜一筹,因为ng-content版本看起来稍微冗长一些,需要声明CardTitleComponentCARD_TITLE_SELECTOR

关于类型安全,ngTemplateOutlet版本也更优,因为如果未提供cardBody,编辑器会显示错误,因为它是一个必需的输入项。错误信息类似于:

必须指定来自CardComponent组件的必需输入'cardBody'

扩展性

条件内容投影

不建议在条件内容投影的情况下使用ng-content,这可以在Angular的主页上找到相关信息。

不应该有条件地包含带有@if, @for或@switch的<ng-content>。即使该<ng-content>占位符被隐藏,Angular总是实例化并为渲染到<ng-content>占位符的内容创建DOM节点。有关组件内容的条件渲染,请参阅模板片段。

例如,给定以下代码:

<card [cardBody]="cardBody">
  <card-title>card title</card-title>
  <ng-template #cardBody> card body </ng-template>
</card>
@Component({
  selector: CARD_TITLE_SELECTOR,
  imports: [],
  template: `<ng-content></ng-content> `,
  styles: ``,
})
export class CardTitleComponent {
  constructor() {
    alert("card title");
  }
}

@Component({
  selector: "card",
  imports: [NgTemplateOutlet, NgIf],
  template: `
    <ng-container *ngIf="false">
      <ng-content [select]="cardTitleSelector"></ng-content>
    </ng-container>

    <div class="card-divider"></div>
    <ng-container *ngTemplateOutlet="cardBody"></ng-container>
  `,
  styles: ``,
})
export class CardComponent {
  @Input({ required: true }) cardBody!: TemplateRef<any>;
  cardTitleSelector = CARD_TITLE_SELECTOR;
}

即使card组件中带有*ngIf="false"cardTitleSelectorcard-title组件仍然会被渲染,并且你会看到警告框。

在实际情况中,我们可能需要根据内部组件状态的变化来隐藏或显示某些东西。在这种情况下,尽管我们可以使其工作,但不推荐使用ng-content

复杂情况下的上下文

对于与组件状态交互的情况,ngTemplateOutlet支持context,这对于复杂情况非常有用。例如,

@Component({
  selector: "card",
  imports: [NgTemplateOutlet, NgIf],
  template: `
    <ng-container *ngIf="false">
      <ng-content [select]="cardTitleSelector"></ng-content>
    </ng-container>

    <div class="card-divider"></div>
    <ng-container
      *ngTemplateOutlet="cardBody; context: { isActive: false}"
    ></ng-container>
  `,
  styles: ``,
})
export class CardComponent {
  @Input({ required: true }) cardBody!: TemplateRef<any>;
  cardTitleSelector = CARD_TITLE_SELECTOR;
}
<card [cardBody]="cardBody">
  <card-title>card title</card-title>
  <ng-template #cardBody let-isActive="isActive">
    card body is active? {{ isActive }}
  </ng-template>
</card>

有时,context可以用Output事件替代,但有时候这样做并不容易。

实际上,有一个开放的问题作用域插槽(类似Vue),针对ng-content中的类似功能。

最后的话

不得不说,我认为ng-content更加直观,这也是我在使用Angular时的初始选择。我以为它与Vue中的Slots API相似(我之前使用过Vue)。

然而,上述原因使我倾向于在开发过程中使用ngTemplateOutlet。我不想首先实现直观的ng-content,然后有一天由于context或条件内容投影限制而切换到ngTemplateOutlet。或者添加一些非直观的技巧来使ng-content继续工作也不是我想要的。

实际上,对于这两个不同的API,Vue只提供了一个。我真的不知道Angular团队是怎么考虑的。