如何在Angular中实现点击父项懒加载API数据的层级树列表
Angular 实现懒加载层级嵌套树列表最优方案
核心最优实现为自引用递归组件 + 单节点独立状态管理,无需依赖重量型第三方树组件,逻辑完全可控,天然支持无限层级嵌套与按需加载。
核心实现逻辑
- 用可自引用的递归组件承接节点渲染,无需提前写死嵌套层级,任意深度的子节点都能自动适配
- 每个节点独立维护加载状态、展开状态、子节点数据,仅在用户点击对应节点时触发API请求,不会提前拉取未展开节点的数据
- 配合
trackBy做列表渲染优化,避免同层级节点变动时整树重绘,大数量级下性能表现稳定
具体实现步骤
1. 定义通用树节点数据结构
// tree-node.interface.ts export interface TreeNode { id: string | number; label: string; children?: TreeNode[]; isLoading?: boolean; isExpanded?: boolean; hasChildren?: boolean; // 由接口返回,标识当前节点是否存在子节点,避免无效请求 }
2. 开发递归树节点组件(核心)
组件可在自身模板内引用自身选择器,天然支持无限层级渲染:
// tree-node.component.ts import { Component, Input } from '@angular/core'; import { TreeNode } from './tree-node.interface'; import { DataService } from './data.service'; // 替换为项目自身的API请求服务 @Component({ selector: 'app-tree-node', template: ` <div class="tree-node-item" (click)="handleNodeClick(node)"> <span class="expand-icon">{{ node.isExpanded ? '▼' : node.hasChildren ? '▶' : '•' }}</span> <span class="node-label">{{ node.label }}</span> <span *ngIf="node.isLoading" class="loading-tip">加载中...</span> </div> <!-- 节点展开且已加载子节点时,递归渲染当前组件 --> <div class="children-wrapper" *ngIf="node.isExpanded && node.children?.length"> <app-tree-node *ngFor="let child of node.children; trackBy: trackByNodeId" [node]="child"> </app-tree-node> </div> `, styles: [` .tree-node-item { padding: 8px 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; } .tree-node-item:hover { background: #f5f5f5; } .children-wrapper { padding-left: 24px; } .loading-tip { font-size: 12px; color: #999; } `] }) export class TreeNodeComponent { @Input() node!: TreeNode; constructor(private dataService: DataService) {} handleNodeClick(node: TreeNode) { // 无子节点的项直接跳过逻辑 if (!node.hasChildren) return; // 已加载过子节点的项直接切换展开/收起,不重复发请求 if (node.children?.length) { node.isExpanded = !node.isExpanded; return; } // 触发子节点加载 node.isLoading = true; this.dataService.getChildrenByNodeId(node.id).subscribe({ next: (children) => { node.children = children; node.isExpanded = true; node.isLoading = false; }, error: () => { node.isLoading = false; // 可在此处自定义错误提示逻辑 } }) } trackByNodeId(index: number, node: TreeNode) { return node.id; } }
3. 根节点初始化
根组件仅需加载第一层列表(即需求中提到的list1-list4),后续层级由递归组件自动处理:
// app.component.ts import { Component, OnInit } from '@angular/core'; import { TreeNode } from './tree-node.interface'; import { DataService } from './data.service'; @Component({ selector: 'app-root', template: ` <div class="tree-container"> <app-tree-node *ngFor="let rootNode of rootNodeList; trackBy: trackByNodeId" [node]="rootNode"> </app-tree-node> </div> ` }) export class AppComponent implements OnInit { rootNodeList: TreeNode[] = []; constructor(private dataService: DataService) {} ngOnInit() { // 初始化加载第一层根节点 this.dataService.getRootNodes().subscribe(nodes => { this.rootNodeList = nodes; }) } trackByNodeId(index: number, node: TreeNode) { return node.id; } }
方案注意事项
记得在
@NgModule的declarations数组中声明TreeNodeComponent,Angular原生支持组件在自身模板内递归引用,无需额外配置。如果单层级节点量级超过1000条,可配合Angular CDK的虚拟滚动做性能优化,交互流畅度会明显提升。
方案优势
- 无额外第三方依赖,对包体积几乎无影响,逻辑完全可控
- 天然支持无限层级嵌套,无论子节点深度是4层还是10层都无需修改核心代码
- 懒加载逻辑精准,仅用户点击的节点会触发API请求,首屏加载速度快,接口压力小
- 节点状态完全独立,展开、加载、收起操作互不干扰
内容的提问来源于stack exchange,提问作者Janus Karthik
相关产品推荐
相关产品推荐

