Angular 17+ 高级教程 – Angular 的局限 の Query Elements

前言

熟悉 Angular 的朋友都知道,Angular 有非常多的局限,许多事情它都做不好,打开 Github 一堆 2016 - 2017 的 Issues,时至今日都没有解决。

原因也很简单 -- Angular 团队的不作为😔。

通常我会把常见的 Angular 的局限记入在这篇 <<Angular 的局限和 Github Issues>>,但由于本篇要讲的问题篇幅比较大,所以特别把它分一篇出来。

本篇要讲的是 Angular Query Element 的局限。虽然我们已经在 <<Component 组件 の Query Elements>> 文章中,深入理解了 Angular 的 Query 机制。

但是!理解没有用啊。

如果它本来做的到,但由于我们不理解,所以以为做不到,那去理解它是对的。

但如果它本来就无能,我们即便理解了,也只是知道它为什么做不到,最终任然是做不到啊😔。

本篇,我们一起来看看 Angular 在 "Query Elements" 上有哪些局限,有没有什么方法可以去突破它。

 

不支持 viewChildren descendants

App Template 里有一个 Parent 组件

Parent Template 里有一个 Child 组件

结构

尝试在 App 组件里 query Child 组件

export class AppComponent {
  child = viewChild(ChildComponent);
  parent = viewChild(ParentComponent);

  constructor() {
    afterNextRender(() => {
      console.log(this.child());
      console.log(this.parent());
    })
  }
}
View Code

不奇怪,因为 Angular 也有 Shadow DOM 概念,组件是封闭的,上层无法直接 query 到 ShadowRoot 内的 elements。

by default 不行,OK!我可以接受,但如果我真的想要 query,你总要给我个方法吧?

Layer by layer query

方法是有,只是...

把 viewChild Child 移到 Parent 组件里

export class ParentComponent {
  child = viewChild(ChildComponent);
}

现在变成 App query Parent query Child 😂

如果有很多层,那每一层组件都需要添加这个 viewChild 逻辑...

虽然我们确实做到了,但这种写法在管理上是很有问题的。

假设,有 A 到 Z 组件 (一层一层,共 26 层)。

A 想 query Z。

那在管理上,这件事就不可以影响到除了 A 和 Z 以外的人。

但按照上述的方法,B 到 Y 组件都需要添加 viewChild 逻辑才能让 A query 到 Z,这显然已经影响到了许多不相干的人,管理直接不及格😡!

原生的 Web Component ShadowRoot 都没有这么糟糕

虽然也是一层一层往下 query,但至少 B - Y 组件内不需要增加不相干的代码。

Use inject instead of query

Angular query child element 有限制,但很神奇的 query parent 却没有限制。(这和 ShadowRoot 不一样,ShadowRoot query parent 和 child 都限制)

于是我们可以尝试反过来做。

App 组件

export class AppComponent {
  public child!: ChildComponent;

  constructor() {
    afterNextRender(() => {
      console.log(this.child);
    })
  }
}

准备一个空的 child 属性

接着在 Child 组件 inject App,然后填入 Child 实例。

export class ChildComponent {
  constructor() {
    const app = inject(AppComponent);
    app.child = this;
  }
}

这样 App 组件就 "query" 到 Child 了。

间中,Parent 组件完全不需要配合做任何事情。这个方案,管理...过👍

上难度 の 动态组件

看似我们好像是突破了 query 的局限,但其实不然...☹️

假设 Child 组件要支持动态插入。

首先,我们需要把 children 改成 signal (这样可以监听变化)

export class AppComponent {
  public children = signal<ChildComponent[]>([]);

  constructor() {
    effect(() => console.log(this.children())); // 监听变化
  }
}

接着

export class ChildComponent {
  constructor() {
    // 初始化时添加
    const app = inject(AppComponent);
    app.children.set([...app.children(), this]);

    // destroy 时删除
    const destroyRef = inject(DestroyRef);
    destroyRef.onDestroy(() => app.children.set(app.children().filter(c => c === this)))
  }
}

嗯...只是改一改,方案视乎还可以,没有翻车...😨

上难度 の maintain sequence

假设,Parent 组件里有 3 个 Child 组件 (代号 A, B, C)

@if (show()) {
  <app-child />
}
<app-child />
<app-child />

第一次 console.log 结果是 B, C,因为 A 被 @if hide 起来了。

接着我们 show = true。

第二次的 console.log 结果是 B, C, A。

注意不是 A, B, C 而是 B, C, A。

因为在 Child 初始化的时候,Child instance 是被 "push" 到 query results 里。

这就导致了 sequence 不一致。

为什么用 push?换成 unshift, splice 可以吗?

可以,但是没有用,因为 Child 不可能知道自己的位置,所以它也无法确定要用 push, unshift 还是 splice。

是的...这个方案翻车了😢,这种时候,我们还得用回 layer by layer query 方案...

好,这个问题我们先隔着,继续往下看看其它 Query Elements 的问题。

 

不支持 viewChildren <ng-container /> 和 <ng-content />

相关 Github Issue: 

Github – ContentChildren doesn't get children created with NgTemplateOutlet (2017)

Github – Accessing @ViewChild and @ViewChildren of a template declared in a different component than the one where it is host (2021)

App Template

<ng-template #template>
  <app-child />
</ng-template>

<ng-container #container />

一个 ng-template 和一个 <ng-container />

App 组件

export class AppComponent implements OnInit {
  readonly templateRef = viewChild.required('template', { read: TemplateRef });
  readonly viewContainerRef = viewChild.required('container', { read: ViewContainerRef });
  readonly child = viewChild(ChildComponent);
  
  constructor() {
    // 2. 查看是否可以 query 到 Child 组件
    afterNextRender(() => console.log(this.child()))
  }

  ngOnInit() {
    // 1. create <ng-template> and insert to <ng-container />
    this.viewContainerRef().createEmbeddedView(this.templateRef());
  }
}
View Code

App 组件成功 query 到 Child 组件。

好,我们改一改结构

App Template

<app-parent>
  <ng-template #template>
    <app-child />
  </ng-template>
</app-parent>

把 ng-template transclude 给 Parent 组件

Parent Template

<p>parent works!</p>
<ng-container #container />

Parent 组件

export class ParentComponent implements AfterContentInit {
  readonly templateRef = contentChild.required(TemplateRef);
  readonly viewContainerRef = viewChild.required('container', { read: ViewContainerRef });

  readonly child = viewChild(ChildComponent);

  constructor() { 
    // 2. 查看是否可以 query 到 Child 组件
    afterNextRender(() => console.log('Parent query Child succeeded?', this.child())); // undefined
  }

  ngAfterContentInit() {
    // 1. create <ng-template> and insert to <ng-container />
    this.viewContainerRef().createEmbeddedView(this.templateRef());
  }
}
View Code

Parent 组件无法 query 到 Child 组件。

但是 App 组件任然可以 query 到 Child 组件。

Why?!

因为 Angular 的 query 机制是 based on declare 的地方,而不是 insert 的方法。

我们可以 query <ng-template>,但是不可以 query <ng-container />。

我们经常有一种错误,觉得 <ng-container /> 可以被 query。

<ng-container *ngIf="true">
  <app-child />
</ng-container>

这是因为被 *ngIf,*ngFor 误导了。

上面 *ngIf 只是语法糖,它的正确解读是 

<ng-template [ngIf]="true">
  <ng-container>
    <app-child />
  </ng-container>
</ng-template>

我们之所以能 query 到 Child 是因为,它在 ng-template 里面,

另外上面这段代码里, <ng-container /> 只是一个摆设而已,正真作为 ViewContainerRef 的 element 也是 ng-template (任何一种 element 都可以成为 ViewContainerRef,不一定是 <ng-container />,<ng-template> 也可以的)。

除了 <ng-container /> 不能被 query,<ng-content /> 也不能被 query。

viewChildren ng-template 顺序的问题

相关 Github Issue – QueryList not sorted according to the actual state

我在 <<Angular 高级教程 – 学以致用>> 文章中详细讲解过这个问题了,这里不再复述。

How to solve?

<app-parent>
  <ng-template #template>
    <app-child />
  </ng-template>
</app-parent>

上面这种情况,虽然 Parent 无法 viewChild Child 组件,但是可以 contentChild Child 组件。

另外 Child 也可以 inject 到 Parent,所以也可以像上一 part 那样 use inject instead of query。

但这些方式也都有局限。假如 ng-template 被丢到千里之外的 <ng-container /> 那 contentChild 和 inject 都可能连不上它们,这样就真的无计可施了。

 

Direct DOM query 方案

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

即便是 ShadowRoot,我也可以一层一层的往里面进。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章