You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将现有ng-container递归树适配为cdk虚拟滚动树?

解决递归树形结构结合CDK虚拟滚动的问题

这问题我之前也碰到过!核心问题是递归模板里的cdkVirtualFor找不到CdkVirtualScrollViewport的实例——因为默认情况下cdkVirtualFor会从当前组件上下文注入viewport,但递归调用的模板上下文里没有这个引用,所以才会报No provider for CdkVirtualScrollViewport的错误。另外展开/折叠的逻辑也要适配虚拟滚动的特性,下面给你两种可行的解决方案:

方案一:保留递归DOM结构,传递Viewport引用

这种方案能保留你原有的树形DOM结构,只需要把viewport引用通过模板上下文传递给递归模板即可:

步骤1:确保导入ScrollerModule

首先确认你的NgModule里已经导入了ScrollerModule(这是基础,很多人容易漏):

import { ScrollerModule } from '@angular/cdk/scrolling';

@NgModule({
  imports: [
    // ...其他模块
    ScrollerModule
  ]
})
export class YourModule { }

步骤2:组件中获取Viewport引用

在组件类里通过@ViewChild获取虚拟滚动容器的引用:

import { Component, ViewChild } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';

@Component({
  // ...组件元数据
})
export class YourTreeComponent {
  @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport;
  root = [{ id: "aec04ef4-fc6a-481f-a12d-0ce987d494b3", expanded: true, children: [{ id: "cbf9e862-e932-410f-ad84-ed0a5ec1917a", expanded: false, children: [] }] }];

  trackById(index: number, item: any) {
    return item.id;
  }
}

步骤3:修改递归模板传递Viewport

把viewport引用通过模板上下文传递给递归的ngTemplateOutlet,并在cdkVirtualFor中明确指定使用该viewport:

<cdk-virtual-scroll-viewport style="height: 750px" itemSize="50" #viewport>
  <ng-template #recursiveList let-root let-viewport="viewport">
    <li *cdkVirtualFor="let item of root; trackBy: trackById; viewport: viewport">
      <!-- 展开/折叠按钮 -->
      <button (click)="item.expanded = !item.expanded">
        {{ item.expanded ? '-' : '+' }} {{ item.id }}
      </button>

      <!-- 递归渲染子节点,传递viewport引用 -->
      <ul [class.hide]="!item.expanded" *ngIf="item.children?.length > 0">
        <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: item.children, viewport: viewport }"></ng-container>
      </ul>
    </li>
  </ng-template>

  <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: root, viewport: viewport }"></ng-container>
</cdk-virtual-scroll-viewport>

补充CSS

.hide {
  display: none;
}

这种方案的优势是完全保留你原有的DOM结构,不需要修改数据格式,适合需要复杂子节点布局的场景。


方案二:扁平化树形数据,单虚拟滚动容器(性能更优)

如果你的树形结构有数千个节点,这种方案的性能会更好——因为只需要一个虚拟滚动容器,避免了递归嵌套带来的额外开销。核心思路是把树形数据扁平化,通过CSS缩进模拟层级,展开/折叠时重新生成扁平化数组:

步骤1:组件中实现数据扁平化和展开逻辑

import { Component } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';

@Component({
  // ...组件元数据
})
export class YourTreeComponent {
  root = [{ id: "aec04ef4-fc6a-481f-a12d-0ce987d494b3", expanded: true, children: [{ id: "cbf9e862-e932-410f-ad84-ed0a5ec1917a", expanded: false, children: [] }] }];
  flattenedTree = this.flattenTree(this.root);

  trackById(index: number, item: any) {
    return item.id;
  }

  // 递归扁平化树形数据,添加level字段表示层级
  flattenTree(nodes: any[], level = 0): any[] {
    return nodes.reduce((acc, node) => {
      acc.push({ ...node, level });
      // 只添加展开状态的子节点
      if (node.expanded && node.children?.length) {
        acc.push(...this.flattenTree(node.children, level + 1));
      }
      return acc;
    }, []);
  }

  // 切换节点展开状态,重新生成扁平化数组
  toggleExpand(item: any) {
    item.expanded = !item.expanded;
    this.flattenedTree = this.flattenTree(this.root);
  }
}

步骤2:简化模板,单虚拟滚动容器

<cdk-virtual-scroll-viewport style="height: 750px" itemSize="50">
  <li *cdkVirtualFor="let item of flattenedTree; trackBy: trackById" 
      [style.padding-left.px]="item.level * 20">
    <button (click)="toggleExpand(item)">
      {{ item.expanded ? '-' : '+' }} {{ item.id }}
    </button>
  </li>
</cdk-virtual-scroll-viewport>

这种方案的优势是性能更高,适合大量节点的场景,缺点是需要修改数据格式,子节点的布局样式需要通过层级来控制。


内容的提问来源于stack exchange,提问作者MortenMoulder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:05:12