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

这篇具有很好参考价值的文章主要介绍了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" 上有哪些局限,有没有什么方法可以去突破它。

 文章来源地址https://www.toymoban.com/news/detail-856771.html

不支持 viewChildren descendants

App Template 里有一个 Parent 组件

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

Parent Template 里有一个 Child 组件

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

结构

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

尝试在 App 组件里 query Child 组件

Angular 17+ 高级教程 – Angular 的局限 の Query ElementsAngular 17+ 高级教程 – Angular 的局限 の Query Elements
export class AppComponent {
  child = viewChild(ChildComponent);
  parent = viewChild(ParentComponent);

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

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

不奇怪,因为 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 😂

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

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

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

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

A 想 query Z。

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

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

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

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

虽然也是一层一层往下 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 了。

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

间中,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 组件

Angular 17+ 高级教程 – Angular 的局限 の Query ElementsAngular 17+ 高级教程 – Angular 的局限 の Query Elements
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

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

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 组件

Angular 17+ 高级教程 – Angular 的局限 の Query ElementsAngular 17+ 高级教程 – Angular 的局限 の Query Elements
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

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

Parent 组件无法 query 到 Child 组件。

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

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

Why?!

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

因为 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 都可能连不上它们,这样就真的无计可施了。

 

问题小总结

问题:

  1. viewChild 不能 descendants

  2. query 不到 inside <ng-container /> 和 <ng-content />

  3. query ng-template 顺序和 ViewContainerRef 不一致

这些问题归根究底都是因为 Angular 的奇葩 query 机制。

Angular query child 查找的是 Logical View Tree,而不是 DOM Tree。

Angular query parent 查找的是 Injector Tree,而不是 DOM Tree。

Angular query 机制对静态友好,对动态不友好 (一旦有动态组件,它要嘛完全 query 不到,要嘛顺序不对...😔)

安全范围

怎么能不掉坑呢?

  1. viewChild 控制在 1 layer

    App 组件 viewChild 就只 query App Template 上的元素

  2. contentChild 控制在 1 layer

    <app-child>
      <ng-content />
    </app-child>

    像上面这样就 multilayer 了,Child 组件无法 contentChild 到任何东西。

  3. ng-template 作为 ViewContainerRef

    要 query ng-template 内容,最好把 ng-template insert 到相同位置,让 ng-template 自己作为 ViewContainerRef。

    只有这样才能确保 query 的顺序是正确的。

 

Direct DOM query 方案

既然我们心里想的是 query DOM,而 Angular 又不是 query based on DOM Tree,那是不是一开始方向就错了呢?

是的,我们完全可以换一个思路,我们就 query DOM,然后再想办法让这个 DOM element 关联上 Angular。

App Template 里有一个 Parent 组件

<app-parent />

Parent 组件里有三个 Child 组件

<p>parent works!</p>

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

第一个还是动态输出的,三秒后 show 会从 false 转为 true

export class ParentComponent {
  show = signal(false);

  constructor() {
    window.setTimeout(() => {
      this.show.set(true);
    }, 3000);
  }
}

我们的需求是,从 App 组件 query 出 Child 组件实例。

首先,我们做一个 Root Level Service

@Injectable({
  providedIn: 'root'
})
export class ChildCollector {
  private childComponentMap = new Map<HTMLElement, ChildComponent>();

  getChild(childElement: HTMLElement): ChildComponent {
    return this.childComponentMap.get(childElement)!;
  }

  addChild(childElement: HTMLElement, childInstance: ChildComponent) {
    this.childComponentMap.set(childElement, childInstance);
  }

  removeChild(childElement: HTMLElement) {
    this.childComponentMap.delete(childElement);
  }
}

它的职责是收集 Child 组件实例,顺序不重要,只要把实例和 element 关联起来就可以了。

在 Child 组件初始化做添加,在 destroy 时做移除

export class ChildComponent {
  constructor() {
    const childCollector = inject(ChildCollector);
    const host: HTMLElement = inject(ElementRef).nativeElement;
    childCollector.addChild(host, this);

    const destroyRef = inject(DestroyRef);
    destroyRef.onDestroy(() => childCollector.removeChild(host));
  }
}

App 组件

export class AppComponent {
  constructor() {
    const host: HTMLElement = inject(ElementRef).nativeElement;
    const childCollector = inject(ChildCollector);
    const destroyRef = inject(DestroyRef);
    const injector = inject(Injector);

    afterNextRender(() => {

      const childInstances$ = new Observable<ChildComponent[]>(subscriber => {
        // 1. 利用 MutationObserver 监听 DOM 变化
        const mo = new MutationObserver(() => emitQuery());
        mo.observe(host, { childList: true, subtree: true });
        emitQuery();
        return () => mo.disconnect();

        function emitQuery() {
          // 2. 用原生 DOM querySelectorAll 做 query
          const childElements =  Array.from(host.querySelectorAll<HTMLElement>('app-child'));
          // 3. 把 element 替换成 Child 组件实例
          const childInstances = childElements.map(childElement => childCollector.getChild(childElement));
          subscriber.next(childInstances);
        }

      }).pipe(distinctUntilChanged((prev, curr) => prev.length === curr.length && prev.every((p, i) => p === curr[i])), takeUntilDestroyed(destroyRef));

      const childInstances = toSignal(childInstances$, { injector, requireSync: true });
      
      effect(() => console.log(childInstances()), { injector });
    })
  }
}

最关键的是有注释的那三句,其它的只是 RxJS Obserable 和 Signal 的包装。

我们直接用原生的 DOM querySelectorAll 作为 query,这样就完全突破 Angular 的限制了。

接着把 query 到的 Child element 替换成 Child 组件实例,这样就链接会 Angular 了。

效果

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

这个方案不仅仅可用于 query child,想用于 query parent 也是相同思路,只要把 querySelectorAll 换成 parentElement 或者 cloest 向上查找就行了。(注:Angular CDK Scrolling 里也使用了这个方案)

DOM query 方案要注意的事项

直接操作 DOM 通常是不顺风水的,容易掉坑。但如果你熟悉 Angular 底层机制的话,一切是可以 under control 的,不必担心。

我个人建议:

  1. 首选 Angular way -- viewChild, contentChild 

  2. 次选 "use inject instead of query" 方案 

  3. 真的摆不平,才出杀手锏 -- DOM query 方案

 

总结

本篇介绍了一些 Angular 常见的 query 局限,并提供了一些粗糙的解决方案,happy coding 💻😊

  

 

到了这里,关于Angular 17+ 高级教程 – Angular 的局限 の Query Elements的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Angular 17+ 高级教程 – Signals

    在上一篇 Change Detection 中, 我们有提到 MVVM 监听 ViewModel 变化的难题. 当年 AngularJS 和 Knockout.js (下面简称 KO) 各自选了不同的道路. 但如今, 事过境迁, Angular 最终也走向了 KO 的道路. 这就是这篇的主角 Signal。   在 JavaScript, 值类型 variable 无法被监听, Signal 的做法是把它们都变

    2024年03月09日
    浏览(68)
  • Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

    我在 初识 Angular 文章里有提到 Angular 目前的断层问题。 大部分的 Angular 用户都停留在 v9.0 版本。 v9.0 是一个里程碑版本,Angular 从 v4.0 稳定版推出后,好几年都没有什么动静,直到 v9.0 推出了 Ivy rendering engine。 本以为 v9.0 以后 Angular 会大爆发,结果迎来的是 Angular 团队搞内

    2024年04月22日
    浏览(37)
  • Angular 17+ 高级教程 – Change Detection

    虽然 Angular 正在把大部分 Change Detection 概念换成 Signal,但是最快也要 1 年后,所以还是有必要认真学习一下的。   MVVM 框架的开发方式是这样的: 写 HTML 写 ViewModel 在 HTML 里加入 binding syntax。 在 HTML 里加入 listening syntax,在事件发生时修改 ViewModel。 MVVM 的宗旨是 \\\"不要直接

    2024年03月09日
    浏览(42)
  • Angular 17+ 高级教程 – 学以致用

    读这么多原理,到底为了什么?真实项目中真的会用得到吗? 你正在疑惑 \\\"知识的力量\\\" 吗? 本篇会给一个非常非常好的案例,让你感悟 -- 知识如何用于实战。 记住,我的目的是让你感悟,而不是要你盲目相信知识。   下面是我在 2020-11-06 记入的一个问题。 一模一样的问题

    2024年04月22日
    浏览(39)
  • Angular 17+ 高级教程 – Dependency Injection 依赖注入

    本来是想先介绍 Angular Component 的,但 Component 里面会涉及到一些 Dependency Injection (简称 DI) 的概念,所以还是先介绍 DI 吧。 温馨提醒:如果你对 JS class、prototype 不太熟悉的话,建议你先看这篇 JavaScript – 理解 Object, Class, This, Prototype, Function, Mixins   首先我们有一个 class Ser

    2024年03月09日
    浏览(64)
  • Angular 17+ 高级教程 – Component 组件 の Control Flow

      Control Flow 是 Angular v17 版本后推出的新模板语法,用来取代 NgIf、NgForOf、NgSwitch 这 3 个 Structure Directive。 Structure Directive 的好处是比较灵活,原理简单,但是即便用了微语法,它看上去还是相当繁琐,而且不够优雅。 Conrol Flow 的好处是它的语法够美,缺点是不必 Structure Di

    2024年03月11日
    浏览(53)
  • Angular 17+ 高级教程 – Component 组件 の ng-template

    上一篇 Dynamic Component 我们有提到,作为 MVVM 框架的 Angular 需要有方法替代掉 2 个 DOM Manipulation: document.createElement  template.content.clone Dynamic Component 便是替代 document.createElement 的方案。 而这篇我们要讲的 ng-template 则是 template.content.clone 的替代方案。   我们从浅入深,一个一

    2024年03月09日
    浏览(52)
  • Angular 17+ 高级教程 – Component 组件 の Dependency Injection & NodeInjector

    在 Dependency Injection 依赖注入 文章中,我们学习了 50% 的 Angular DI 知识,由于当时还不具备组件知识,所以我们无法完成另外 50% 的学习。 经过了几篇组件教程后,现在我们已经具备了基础的组件知识,那这一篇我们便来完成 Angular DI 所有内容吧。   Angular in Depth – A Deep

    2024年03月09日
    浏览(44)
  • Angular 17+ 高级教程 – Component 组件 の 生命周期钩子 (Lifecycle Hooks)

    之前在 Component 组件 の Angular Component vs Custom Elements 文章中,我们有学习过几个基础的 Lifecycle Hooks。 比如 OnChanges、OnInit、AfterViewInit、OnDestroy,但那篇只是微微带过而已。 这篇让我们来深入理解 Angular 的 Lifecycle Hooks。   在 Component 组件 の Dependency Injection NodeInjector 文章中

    2024年03月09日
    浏览(52)
  • Angular 17+ 高级教程 – Component 组件 の Structural Directive (结构型指令) & Syntax Reference (微语法)

    在 Attribute Directives 属性型指令 文章中,我们学习过了指令。指令是没有 HTML 和 CSS 的组件,它单纯用于封装 JS 的部分。 这一篇我们将继续学习另一种指令 -- Structural Directive 结构型指令。 就代码而言,Structural Directive 和 Attribute Directives 是完全一样的,只是用途不同,因此

    2024年03月10日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包