李申山头像
关注

Angular 固定尺寸虚拟滚动实战:基于 @tanstack/angular-virtual 的行、列与网格示例

  • 前端
  • UI组件

【免费下载链接】virtual

🤖 Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte

项目地址: https://gitcode.com/gh_mirrors/vi/virtual
点击查看 免费下载

导读

本指南以仓库中的 Angular fixed 示例(Angular CLI 17.3.0 生成、当前升级至 Angular 20 系依赖)为核心,完整讲解从开发服务器启动、代码脚手架生成、生产构建到单元/端到端测试的全套 Angular 工程化工作流,并深入剖析示例中 injectVirtualizer 实现固定尺寸(fixed)行、列与网格三类虚拟列表的源码细节。读完本文,你将掌握在 Angular 应用中接入 @tanstack/angular-virtual、以固定尺寸渲染 1 万条数据的完整方案,以及如何从示例迁移到其他尺寸策略。

示例项目定位:什么是"固定尺寸"虚拟滚动

本示例位于 examples/angular/fixed,项目名称为 @tanstack/virtualExampleAngularFixed,其页面描述明确界定了"固定尺寸"的含义:

These components are using fixed sizes. This means that every element's dimensions are hard-coded to the same value and never change.

即每个条目的尺寸被硬编码为同一数值且永不改变,这是虚拟滚动最简单、最高效的场景:虚拟器只需要一次估算即可精确计算所有条目的偏移,无需在滚动过程中反复测量真实 DOM 尺寸。示例在单个页面中同时展示了三种形态:

形态组件文件方向条目数固定尺寸
行列表(Rows)row-virtualizer-fixed.component.ts纵向1000035px
列列表(Columns)column-virtualizer-fixed.component.ts横向10000100px
网格(Grid)grid-virtualizer-fixed.component.ts横纵双向10000 × 1000035px × 100px

三个组件统一由 app.component.ts 作为 standalone 根组件导入并渲染,且全部采用 ChangeDetectionStrategy.OnPush 策略,配合虚拟化的信号式响应更新。

环境准备与工程配置

依赖清单(package.json)

package.json 中声明了本示例的运行时与构建依赖:

  • 运行时核心:@tanstack/angular-virtual: ^6.0.6,即本仓库 packages/angular-virtual 发布的 Angular 适配包;
  • Angular 框架:@angular/core、@angular/common、@angular/forms、@angular/router、@angular/platform-browser、@angular/platform-browser-dynamic 等均使用 ^20.2.0;
  • 响应式与变更检测基础:rxjs: ^7.8.2、zone.js: 0.15.1、tslib: ^2.8.1;
  • 构建工具链:@angular-devkit/build-angular: ^20.2.0、@angular/cli: ^20.2.0、@angular/compiler-cli: ^20.2.0、typescript: 5.9.3。

scripts 字段定义了四个命令:ng、start(ng serve)、build(ng build)和 watch(ng build --watch --configuration development)。注意:本示例未配置 test 与 e2e 脚本,后面章节会说明如何补全。

构建配置(angular.json)

angular.json 中的关键构建选项:

  • 构建器采用 @angular-devkit/build-angular:application(现代 Angular 应用构建器);
  • outputPath 输出到 dist/tanstack/virtual-example-angular-fixed;
  • 入口:index 指向 src/index.html,browser 指向 src/main.ts,polyfills 仅含 zone.js;
  • 样式入口为 src/styles.css,该样式文件定义了列表边框 .list 与 list-item-even/list-item-odd 两种交替底色(用于视觉区分行/列/单元格);
  • 生产构建开启 outputHashing: "all";开发构建关闭优化(optimization: false)、关闭许可提取、开启 sourceMap: true,默认配置为生产模式。

此外,tsconfig.json 开启了 strict: true 以及 strictTemplates: true 等严格模板检查,确保 injectVirtualizer 的类型签名在编译期被完整校验。

启动开发服务器:ng serve

原 README 给出的开发流程:

Run ng serve for a dev server. Navigate to http://localhost:4200/. The application will automatically reload if you change any of the source files.

在示例目录下执行:

ng serve

然后访问 http://localhost:4200/。Angular CLI 会监听源码变化并自动热重载(文件修改后页面自动刷新),无需手动重启。若使用仓库的 pnpm 工作区(见根目录 pnpm-workspace.yaml),也可通过 pnpm start 触发 package.json 中定义的 start: ng serve 脚本。

启动后页面应呈现三个区域:Rows(400×200px 滚动容器)、Columns(400×100px 横向滚动容器)、Grid(500×500px 双向滚动容器),每条虚拟条目根据 row.index 奇偶切换 list-item-even/list-item-odd 背景色。

代码脚手架:ng generate

原 README 说明的脚手架能力:

Run ng generate component component-name to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module.

例如新增一个纵向虚拟列表组件:

ng generate component row-virtualizer-fixed

生成的组件可作为 standalone 组件直接在模板中按 <row-virtualizer-fixed /> 使用(与本示例三个组件的用法一致),也可以在 @Component({ imports: [...] }) 中声明后嵌入其他组件模板。该命令同样支持指令(directive)、管道(pipe)、服务(service)、类(class)、守卫(guard)、接口(interface)、枚举(enum)与模块(module)等生成目标。

生产构建:ng build

原 README 说明:

Run ng build to build the project. The build artifacts will be stored in the dist/ directory.

执行:

ng build

产物输出至 dist/tanstack/virtual-example-angular-fixed/(对应 angular.json 的 outputPath 配置)。如需开发模式持续构建,可运行:

ng build --watch --configuration development

即 package.json 中 watch 脚本的内容:以 development 配置(不优化、带 sourceMap)进入 watch 模式。开发/生产两种配置的差异(optimization、sourceMap、outputHashing 等)已在上一节列出,可在 angular.json 中逐项核对。

单元测试与端到端测试

单元测试(ng test)

原 README 提到:

Run ng test to execute the unit tests via Karma.

需要说明的是,本示例的 package.json 并未声明 test 脚本与 Karma 依赖,因此直接执行前需先补充相应配置(Angular CLI 会在配置缺失时给出提示)。Karma 是 Angular CLI 默认的单元测试运行器;若希望看到虚拟化核心逻辑的单元测试,仓库中 packages/virtual-core/tests/index.test.ts 提供了对 Virtualizer 核心行为的直接验证,可作为理解 @tanstack/angular-virtual 底层行为的参考。

端到端测试(ng e2e)

原 README 说明:

Run ng e2e to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.

即 ng e2e 是一个"需要先安装具备端到端能力的包"的通用命令。本仓库的 Angular 适配包 packages/angular-virtual 通过 playwright.config.ts 配置了 Playwright 驱动端到端测试,e2e/app 下存放对应测试应用;本示例若需接入,可参考该配置结构。

核心原理:injectVirtualizer 如何实现固定尺寸渲染

从示例到实现

三个组件都通过 @tanstack/angular-virtual 导出的 injectVirtualizer 创建虚拟器实例,例如 row-virtualizer-fixed.component.ts:

virtualizer = injectVirtualizer(() => ({
  scrollElement: this.scrollElement(),   // 通过 viewChild 获取的滚动容器
  count: 10000,                         // 条目总数
  estimateSize: () => 35,               // 固定行高估算
  overscan: 5,                          // 视口外额外渲染 5 条
}))

其实现位于 packages/angular-virtual/src/index.ts。injectVirtualizer 会自动补齐三项基础设施:

  • observeElementRect:用 ResizeObserver 观察滚动容器尺寸;
  • observeElementOffset:监听滚动偏移;
  • scrollToFn: elementScroll:以 scrollTop/scrollLeft 实现程序化滚动。

scrollElement 传入的是 viewChild 信号返回的 ElementRef,injectVirtualizer 内部通过 isElementRef 判断后取 nativeElement 作为真正的滚动节点,因此模板中只需 #scrollElement 标记滚动容器即可完成绑定。

信号驱动的响应式更新

从 packages/angular-virtual/src/index.ts 可以看清完整的信号化封装链路:

  • computed 懒初始化 Virtualizer 实例,避免在输入信号初始化前创建实例;
  • linkedSignal 维护 reactiveVirtualizer,保证选项变更时重建实例引用;
  • afterRenderEffect 分别调用 _didMount() 与 _willUpdate(),与虚拟核心的渲染生命周期对齐;
  • 模板侧可安全读取的 getVirtualItems()、getTotalSize() 被暴露为计算信号,range、scrollOffset、isScrolling 等状态也被转换为信号;
  • 选项中的 useApplicationRefTick(默认 true)会在虚拟核心更新后通过 ApplicationRef.tick() 主动刷新变更检测,确保模板绑定在下一帧滚动协调读取 scrollHeight 之前落到 DOM,这解释了示例组件全部使用 OnPush 策略仍能正确渲染的原因。

固定尺寸下为何无需测量

固定尺寸场景下,estimateSize 返回常量,虚拟核心据此直接算出每个条目的 start(偏移起点)与 size,模板再用绝对定位 + translateY/translateX 摆放条目(见下方实现解析)。正因为"尺寸永不改变",无需 measureElement 反复校正,这也是 fixed 示例相对 dynamic 示例(见 examples/angular/dynamic)更省开销的原因。

三种固定尺寸组件实现解析

行虚拟化(Rows)

row-virtualizer-fixed.component.ts 的模板结构是纵向虚拟列表的标准范式:

  • 外层 #scrollElement 为滚动容器(height: 200px; overflow: auto);
  • 内层相对定位的 "总尺寸容器" 通过 [style.height.px]="virtualizer.getTotalSize()" 撑起完整滚动高度;
  • 每个虚拟项绝对定位在顶部,并用 [style.transform]="'translateY(' + row.start + 'px)'" 移动到正确偏移;
  • track row.index 保证按索引高效复用 DOM。
<div #scrollElement class="list scroll-container">
  <div style="position: relative; width: 100%;"
       [style.height.px]="virtualizer.getTotalSize()">
    @for (row of virtualizer.getVirtualItems(); track row.index) {
      <div [attr.data-index]="row.index"
           [class.list-item-even]="row.index % 2 === 0"
           [class.list-item-odd]="row.index % 2 !== 0"
           style="position: absolute; top: 0; left: 0; width: 100%;"
           [style.height.px]="row.size"
           [style.transform]="'translateY(' + row.start + 'px)'">
        Row {{ row.index }}
      </div>
    }
  </div>
</div>

列虚拟化(Columns)

column-virtualizer-fixed.component.ts 与行版本几乎对称,仅需在选项中开启 horizontal: true,虚拟核心便会切换到横向坐标系:

virtualizer = injectVirtualizer(() => ({
  horizontal: true,
  scrollElement: this.scrollElement(),
  count: 10000,
  estimateSize: () => 100,
  overscan: 5,
}))

模板相应变为:内层容器用 [style.width.px]="virtualizer.getTotalSize()" 撑宽、条目 [style.transform]="'translateX(' + col.start + 'px)'" 横向平移,滚动容器固定为 width: 400px; height: 100px; overflow: auto。

网格虚拟化(Grid)

grid-virtualizer-fixed.component.ts 展示了"双虚拟器叠加"方案:一个纵向虚拟器(行)+ 一个 horizontal: true 虚拟器(列),共用同一滚动容器:

rowVirtualizer = injectVirtualizer(() => ({
  scrollElement: this.scrollElement(),
  count: 10000,
  estimateSize: () => 35,
  overscan: 5,
}))

columnVirtualizer = injectVirtualizer(() => ({
  horizontal: true,
  scrollElement: this.scrollElement(),
  count: 10000,
  estimateSize: () => 100,
  overscan: 5,
}))

模板用两层 @for 嵌套遍历行、列虚拟项,每个单元格的 height 取 row.size、width 取 col.size,位移由 col.start(X)与 row.start(Y)拼接而成:

<div style="position: relative; height: 100%;"
     [style.width.px]="columnVirtualizer.getTotalSize()"
     [style.height.px]="rowVirtualizer.getTotalSize()">
  @for (row of rowVirtualizer.getVirtualItems(); track row.index; let rowEven = $even) {
    @for (col of columnVirtualizer.getVirtualItems(); track col.index; let colEven = $even) {
      <div [style.height.px]="row.size"
           [style.width.px]="col.size"
           [style.transform]="'translateX(' + col.start + 'px)' + 'translateY(' + row.start + 'px)'">
        Cell {{ row.index }}, {{ col.index }}
      </div>
    }
  }
</div>

从源码结构看,这个嵌套写法意味着网格总渲染量为"可见行数 × 可见列数",例如 500×500px 容器、35px 行高与 100px 列宽下,单屏约渲染 15 × 5 个单元格,而无需为 1 亿个逻辑单元格创建 DOM。

从 fixed 到更多场景的扩展路径

本示例是 examples/angular 系列中"最简场景"的起点,仓库内其他 Angular 示例与文档提供了明确的进阶路线:

结语

本示例虽由 Angular CLI 模板生成,但其价值在于把标准 Angular 工程化流程(ng serve/ng generate/ng build/ng test/ng e2e)与 @tanstack/angular-virtual 的固定尺寸虚拟化实践结合成了可直接运行的样板:10000 行/列的虚拟列表配合 overscan: 5 与 OnPush 策略,页面仅渲染视口附近的少量 DOM。理解 injectVirtualizer 的信号化封装 后,你既能照搬本示例的三种形态,也能按需修改 estimateSize、count、overscan 与 horizontal 选项,快速过渡到动态测量、窗口滚动等更复杂的虚拟化场景。

  • 前端
  • UI组件

【免费下载链接】virtual

🤖 Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte

项目地址: https://gitcode.com/gh_mirrors/vi/virtual
点击查看 免费下载

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/gitblog_00347/article/details/155485131

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--