做后台系统,基本绕不开表格。
订单、库存、财务流水、项目台账、客户资料和操作日志,最后大多要用表格展示。只看最简单的例子,DataGrid 好像就是接收一组列和一个数组,然后画出表头、行和滚动条。
业务页面当然没有这么简单。固定列、排序、筛选、分组、汇总、复制、键盘操作、单元格编辑、输入校验、Lookup 和大数据滚动经常出现在同一张表里。功能叠加以后,既要保证状态一致,也要控制滚动和输入时的开销。
这里主要关心两件事。数据量增加后,首帧呈现、滚动和交互不能明显变慢;用户开始录入后,键盘导航、单元格编辑、Popup 选择和校验提交也不能互相打断。
ds-ui 的 DataGrid 同时处理这两类问题。绘制部分面向较大的数据集,编辑部分则参考了 DevExpress WinForms GridControl 等成熟桌面端 DataGrid 的使用方式,尤其是连续录入、键盘导航和单元格编辑。
上一篇文章讲了从 WinForms 到 Vue:我为什么决定在 Web 上重做一套完整的 GUI 框架。这篇先只看 ds-ui 的 DataGrid:数据怎样进入视口,一次滚动怎样变成下一帧画面,鼠标怎样找到单元格,以及编辑会话怎样稳定地接管输入、焦点、Popup 和校验。
文末使用十万行数据做了一次压力测试,目的是放大性能问题,观察每帧工作量。实际业务仍然要根据数据来源和查询方式选择分页或服务端查询。
事件、绘制和图层合成会顺带提到,GUI Runtime 留到下一篇再展开。
项目官网:hleditor.com
DataGrid 在线演示:打开 Live Example
这个 Showcase 把固定列、筛选、范围选择、编辑、Lookup、分组汇总和大数据压力场景放在了同一张表里。
一、业务代码只关心列、数据和规则
先看一个普通的业务数据模型:
ts
interface BusinessRecord {
id: number;
name: string;
category: string;
status: "active" | "review" | "paused";
owner: string;
department: string;
amount: number;
progress: number;
updatedAt: string;
}
创建 DataGrid 时,业务代码只需要说明数据、列和需要开启的功能:
ts
const grid = new RenderDataGrid<BusinessRecord>({
rowKey: "id",
columns,
rows,
selectionMode: "range",
editable: true,
sortable: true,
resizableColumns: true,
reorderableColumns: true,
onCellChange: (row, key, value) => {
syncBusinessState(row.id, key, value);
},
});
DataGrid 会先完成有效值的写回,再触发 onCellChange,因此这个回调更适合用来同步外部状态、记录待保存变更或驱动字段联动。后续筛选、选择、新增和编辑,都可以通过 setFilters()、selectRange()、addRows()、beginEdit() 等 API 完成。状态颜色和编辑权限则交给 resolveCellStyle()、resolveCellEditPolicy()。
列配置还描述了操作规则:哪些单元格可编辑、哪些字段只读或禁用、Tab 应该停在哪里、输入何时有效,以及 Lookup 应该显示哪些候选项。DataGrid 在编辑和键盘导航时读取这些规则。
至于下面这些事情,应该由 DataGrid 自己处理,而不是每个页面重新写一遍:
- 当前单元格怎样进入编辑,提交后怎样继续移动到下一个业务字段;
editable、readonly、disabled和tabStop怎样共同决定录入路径;- 文本、数字、Lookup、日期、树和树表编辑器怎样遵守同一套提交规则;
- 排序、筛选和分组以后,当前记录究竟位于第几行;
- 滚动到任意位置时,本帧应该处理哪些单元格;
- 固定列和普通列怎样共享同一个纵向视口;
- 鼠标坐标对应哪一行、哪一列;
- 键盘焦点、当前单元格和选择范围怎样协同;
- 一次 hover、滚动或输入,是需要重新布局,还是只重画一帧。
二、一张表格,内部有几套分工
业务代码只会接触 RenderDataGrid,但组件内部并不是一个类包办所有事情。数据处理、视口、选择、编辑和绘制各有自己的模块:
对外只有一个 DataGrid,对内则按数据、视口、交互、编辑和绘制分工。
拆开以后,各部分的边界更清楚。数据投影、键盘导航可以分别测试,绘制模块也能单独做性能优化。
DataGrid 内部实际上同时运行着两条链路:
- 渲染链路:源数据 → 数据投影 → 可视范围 → 单元格绘制;
- 编辑链路:当前单元格 → 编辑会话 → 输入桥或 Popup → 校验 → 提交 → 下一个 Tab 停靠点。
两条链路共享行身份、单元格几何、焦点状态和组件生命周期。从滚动切换到输入时,不需要在两套 UI 模型之间交接状态。
比如滚动时,视口模块(Viewport)只更新偏移量,绘制模块(Painter)根据新的可视范围重画,交互模块(Interaction)继续保存当前选择。如果正在编辑,编辑模块还要检查编辑器是否仍在屏幕里。一次滚轮事件不需要把这些逻辑全塞进同一个函数。
三、数组里的第 20 行,不一定还是屏幕上的第 20 行
表格内部至少要区分三种位置:
- 源数据索引:记录在原始
rows数组中的位置。 - 可视数据索引:排序、筛选后的记录位置。
- 可视项目索引:加入分组标题后的屏幕项目位置,分组标题本身不是业务记录。
排序、筛选等规则变化时更新投影;普通滚动只换一段可视范围。
ds-ui 不会为了当前视图复制一遍所有业务对象。它保存的是原始记录和当前投影之间的索引关系,并缓存排序、筛选、分组和汇总结果。只有数据或规则改变时,相关缓存才会重算。
所以选择和编辑不能只记住"第 20 行"。一旦用户重新排序,这条记录可能已经跑到别的位置。要想继续找到它,必须使用业务对象或稳定的 rowKey。
虚拟化只减少屏幕附近的布局和绘制,不能让全量排序、筛选消失。十万条记录仍然在内存里,客户端排序也仍然要处理十万条。数据再大一些,还是要考虑分页、服务端查询和输入防抖。
四、没有单元格 DOM,改变的不只是滚动
现在很多 Web 表格都有虚拟滚动,但"都只渲染可视区域"不代表它们的实现方式相同。
下面比较的是常见的 DOM 虚拟列表或 DOM 虚拟表格。Canvas 和 WebGL 当然也属于 Web 技术,这里只是为了方便区分两种实现。
成熟桌面 DataGrid 的共同思路,是由 Grid 统一管理当前单元格、键盘导航、in-place editor 与提交生命周期,而不是给每个单元格长期放置一个输入控件。ds-ui 在浏览器中采用了相近的控件思路:当前单元格首先由逻辑行列身份、状态与几何定义,而不是由一个单元格 DOM 节点定义。
典型 DOM 方案以限制可视节点为主要边界;ds-ui 则直接从索引、几何、命中和绘制阶段控制本帧工作。
DOM 虚拟表格:保留少量行节点
普通 DOM 表格直接渲染十万行,会产生大量行节点和单元格节点,创建、样式计算、布局和内存压力都会上升。
典型虚拟滚动会使用占位层、位移层或分段滚动空间表达逻辑总高度,同时只挂载视口附近的一批行。滚动时,框架创建、复用或移动这些 DOM 节点。
这样可以把 DOM 数量从与总数据量相关,压缩到主要与视口和 overscan 相关。可视行仍然是真实节点,所以更容易建立在浏览器的文本布局、CSS、原生滚动和可访问性基础设施之上。
ds-ui DataGrid:没有行节点
ds-ui 的 Canvas DataGrid 不会给每一行创建 DOM。不可见行没有节点,可见行也没有节点,屏幕上的单元格只是这一帧的绘制结果。
完整内容高度只是一个数值:
text
totalHeight = visibleItemCount × rowHeight
当前滚动位置也是 DataGrid 自己维护的视口状态。固定行高下,可视范围可以直接通过索引计算得到:
text
visibleStart = floor(scrollY / rowHeight)
visibleEnd = ceil((scrollY + viewportHeight) / rowHeight) - 1
组件会在可视范围前后多留几行缓冲,然后只计算和绘制这一小段内容。行在屏幕上的位置也能直接算出来:
text
rowY = bodyY + rowIndex × rowHeight - scrollY
在行和单元格层面,没有旧 DOM 节点需要搬动。DataGrid 只是在下一帧换一段索引,再把新内容画到既有的 Canvas 图层中。
鼠标命中也靠计算:先用鼠标坐标、滚动偏移和行高算出行,再根据列宽算出列,不需要去 DOM 树里找单元格元素。
overscan 不是两者的主要区别
两种方案都可以使用固定行高、可视范围和 overscan,也都能把滚动工作限制在视口附近。典型 DOM 虚拟化以限制已挂载节点为主要手段,并由此缩小组件渲染、样式、布局与绘制范围;ds-ui 不创建行和单元格节点,直接控制本帧的数据解析、索引计算、命中测试和绘制工作。
| 对比项 | 典型 DOM 虚拟表格 | ds-ui Canvas DataGrid |
|---|---|---|
| 主要 UI 载体 | 视口内的行、单元格 DOM 与组件实例 | 既有 Canvas 图层中的绘制结果 |
| 可视范围 | 主要限制已挂载节点及相关组件工作 | 直接限制索引、数据解析、命中与绘制指令 |
| 总高度表达 | 内部数值状态加浏览器滚动空间或占位层 | Runtime 数值状态加自绘滚动条 |
| 行定位 | 几何计算后写入节点位置或 transform |
几何计算后直接生成绘制坐标 |
| 单元格识别 | 可利用事件目标或委托,也可结合坐标几何 | 统一由坐标映射到行列索引 |
| 编辑核心 | 活动编辑组件与逻辑编辑状态协同 | 逻辑编辑会话与共享输入、Popup 服务协同 |
| 键盘焦点 | Grid 状态协调单元格和编辑器的 DOM 焦点 | Runtime 协调逻辑焦点与共享输入桥 |
| Popup 编辑 | Grid 或 Popup 服务协调 Portal、锚点、外部点击和滚动 | Popup 服务直接复用 Grid 几何和生命周期 |
| 浏览器工作 | 可视节点仍参与样式、布局和绘制 | Canvas 元素参与页面布局,内部单元格由框架绘制 |
| 原生语义 | 更容易利用文本、CSS 与可访问性基础设施 | 需要框架额外承接输入、复制和可访问性 |
ds-ui 在行和单元格层面没有 DOM 节点需要回收。本帧要处理哪些数据、命中哪些单元格、生成多少绘制指令,都由索引范围直接决定。
进入编辑后,典型 DOM DataGrid 还要协调虚拟节点、活动编辑组件、浏览器焦点、Popup Portal 和滚动位置。ds-ui 的浏览、命中和编辑不依赖单元格 DOM 的生命周期,相关状态都由 Runtime 管理。
AG Grid 和 MUI X 都能在 DOM 架构下提供成熟的单元格编辑、键盘导航和自定义编辑能力。它们通过编辑组件、焦点规则和 Portal 协议处理这些关系;ds-ui 使用自己的 Runtime、逻辑焦点和单元格几何处理。
Canvas 并不会自动带来更高性能。JavaScript、文本测量、Canvas 绘制和浏览器最终显示都要花时间;如果每一帧都重新排序或创建大量临时对象,照样会卡。
DOM 表格也有很明显的优势。数据量不大、行高不固定、单元格里有复杂富文本,或者项目很重视原生可访问性时,成熟的 DOM 表格通常更省事。选哪一种,应该看页面需求,而不是只比一句"谁更快"。
五、一次滚动如何变成屏幕上的下一帧
DataGrid 虽然自己管理滚动,但鼠标输入、事件分发和帧调度仍然由 GUI Runtime 统一处理。
一次滚轮或触控板操作,大致会经过下面几步:
有输入并且状态发生变化时,运行时才安排新一帧。
在 60 Hz 屏幕上,一帧预算约为 16.7 ms;到了 120 Hz,预算则只有约 8.3 ms。刷新率并不要求组件树持续重画。页面没有变化时不安排新帧;滚动、hover、输入或动画触发的多次绘制请求,也会合并到同一帧。
滚动通常只修改视口偏移。表格尺寸、列宽、行高或父布局约束发生变化时,才重新计算布局。
本文里的 composite,指 ds-ui 把主内容、Popup、Window、Overlay 等图层合到最终 Canvas 的过程。它不等于浏览器开发者工具里完整的 GPU 合成阶段。
事件捕获、焦点路由、脏布局传播和输入法桥接,留到下一篇 GUI Runtime 再讲。
六、只绘制可视行还不够
可视范围是大表格性能的基础,但不是全部。
1. 固定行高让计算更简单
当前 DataGrid 使用统一的 rowHeight。代价是不适合大量动态行高内容,好处是坐标关系简单而稳定。
只要知道 scrollY、rowHeight 和 viewportHeight,就能直接算出可视行范围,也能把鼠标坐标换算成行索引。程序跳转到某条记录时,同样不需要逐行测量。
2. 显示回调只处理可视单元格
金额格式、状态颜色、错误边框和只读样式,最后都要转换成单元格的显示结果。
getCellDisplayText()、resolveRowStyle()、resolveCellStyle() 和 resolveCellEditPolicy() 只对当前可视区及少量 overscan 单元格执行。业务回调要尽量轻量,不要在里面请求网络、遍历大对象或重建整份数据。
这些回调会在滚动绘制时频繁调用。一次数组查找看起来很快,但乘上可视行数、列数和帧数后,成本就可能很明显。
3. 固定列和普通列共用同一段可视行
左固定列、普通列和右固定列使用不同的横向区域,但共享同一段可视行。固定列不会导致全量行绘制,只是同一行要在几个裁剪区域里分别画。
横向滚动时,完全看不见的普通列不会参与单元格解析和绘制。实际工作量仍然跟当前视口有关,而不是"总行数 × 总列数"。
4. 排序、过滤、分组和 Lookup 都需要缓存
虚拟化解决的是绘制范围,数据模型还要避免每次 paint 都重新排序、过滤和分组。
DataGrid 会缓存当前投影。数据或规则改变时才重算;普通 hover 和滚动不会重新处理十万条数据。
Lookup 列也会建立值到候选记录的索引,避免显示、复制和筛选时反复扫描整个候选数组。
5. 自动列宽不能扫描全部数据
如果为了计算"最合适"的列宽而测量十万行文本,虚拟化节省下来的成本很快会被抵消。
大数据表格最好给出明确列宽。需要自动列宽时,应限制采样数量,并避免在滚动、输入或 hover 时反复测量。长文本可以用省略号和 Tooltip,不必为了少数内容无限拉宽列。
6. 诊断不能只看平均 FPS
平均 FPS 很容易藏住偶发卡顿。一百帧里即使九十九帧都很快,只要有一帧卡住一百毫秒,用户就能感觉到。
更有价值的指标包括:
- 本帧实际绘制了多少行、多少单元格;
- 可视范围和 overscan 范围是多少;
- 当前
scrollY和总内容高度是多少; - 本帧是否执行了 layout;
- layout、主内容 paint、临时图层 paint 和 composite 分别用了多久;
- 排序、筛选或分组缓存为什么失效;
- 长时间滚动和反复编辑后,编辑会话是否正确结束,临时对象是否被释放。
这些数据比一句"看起来很流畅"更能说明问题出在哪里。
七、流畅不只发生在滚动时
高频录入会暴露浏览状态下看不到的问题:编辑器切换是否跳动,Tab 会不会中断,Popup 滚动后是否还对得上单元格,校验失败后能不能留在原处继续修改。
ds-ui 参考了 DevExpress WinForms GridControl 等桌面 DataGrid 的操作方式,由 Grid 管理当前单元格、编辑会话、焦点路径、编辑器和提交规则。成熟 Web DataGrid 也能采用这种组织方式。ds-ui 的具体选择,是让这些功能与绘制、Popup、输入桥和 GUI Runtime 共用状态与几何。
1. 编辑会话不依附于单元格 DOM
普通单元格只保存数据和显示状态,不会各自持有一个输入控件或编辑器对象。每个 DataGrid 持有统一的编辑控制器,并按类别维护可复用的 Popup 宿主。
用户双击单元格或按下编辑快捷键后,文本、数字和 Popup 类编辑器会建立逻辑编辑会话,关联当前行列身份、原始值和草稿;单元格几何与校验规则在提交时按需解析。复选框没有持续会话,切换后直接进入提交管线。编辑结束时清理临时状态,编辑控制器和 Popup 宿主留给下一个单元格使用。Lookup 等复杂 Popup 的内部子视图仍会按打开会话创建和释放。
很多基于 DOM 或组件框架的表格,会在单元格进入编辑状态时挂载活动编辑组件,退出时再卸载。成熟实现会保存编辑状态、复用实例,并处理虚拟化后的重建。ds-ui 没有这层单元格 DOM 生命周期,编辑会话直接由 Runtime 持有。
共享编辑器并非 Canvas 特有。以 Windows Forms DataGridView 为例,微软官方文档说明它一次只承载一个 editing control,并在连续编辑同类型单元格时复用。ds-ui 也使用共享编辑模型,并把它接入 Canvas 几何、逻辑焦点、输入桥和 Popup Runtime。
2. textarea 只负责输入,编辑外观仍由 DataGrid 绘制
DataGrid 没有在 Canvas 上覆盖一个带样式的可见 DOM 输入框。Runtime 共享一个透明 textarea,用它接收浏览器键盘、剪贴板和中文 IME。为保证输入法候选框位置正确,textarea 会跟随自绘光标更新坐标,但不参与视觉呈现。
编辑文本或数字时,单元格背景、边框、文字、选区、闪烁光标、IME 预编辑文本、数字步进按钮和校验状态都由 ds-ui 绘制。下拉框、Lookup、日期、树和树表选择器使用相同的组件绘制和 Popup 管理机制,进入编辑时不会切换到另一套 DOM 样式和盒模型。
浏览态和编辑态使用同一份单元格几何。滚动、固定列或列宽变化时,文字、光标、命中区域和 Popup 锚点一起更新,不需要再同步一个可见 DOM 输入框的位置。
3. 连续键盘录入由 Grid 统一管理
高频录入很依赖键盘路径。ds-ui 目前支持:
Enter或F2进入当前单元格编辑,复选框可以用Space切换;- 编辑中按
Enter提交,按Escape放弃草稿; Tab/Shift+Tab在校验和提交后移动到下一个或上一个有效停靠点;readonly单元格可以按业务规则参与 Tab 路径但不能修改,disabled单元格默认跳过;- 数字编辑器可以用
ArrowUp/ArrowDown按精度和边界步进; - Popup 打开后,方向键、确认和取消优先交给当前 Popup。
文本、数字和 Popup 类编辑器有持续会话及提交、取消过程,复选框则即时切换。它们都经过相同的权限、值标准化、校验和写回流程:
text:单行文本编辑;number:带最小值、最大值、步长和小数精度的数字编辑;checkbox:布尔值切换;select:固定值域的下拉选择;lookup:可搜索、多字段展示的表格查询选择;date:日期选择;drop-tree:树形数据选择;drop-tree-grid:带层级和多列信息的树表选择。
Lookup 查询还能读取当前外层单元格和业务行。同一列可以根据当前记录过滤候选项,而不需要为每一行创建不同的编辑器。
当前编辑单元格的文字、光标、选区和边框同样由 Canvas 绘制,textarea 只负责接收文本输入。
固定值域直接使用简单的单选列表。
Lookup 可以同时显示姓名、团队和人员编码,并支持搜索。
日期选择器贴着当前单元格打开,确认后再写回数据。
4. 草稿、校验和提交
文本和数字输入先写入编辑草稿,不会每敲一个字符就修改业务数据。用户按下 Enter、Tab、点击其他单元格或者显式调用 commitEdit() 时,DataGrid 执行值标准化、单元格校验和提交。
校验失败时,当前编辑会话和草稿会被保留,焦点不会跳到下一个单元格,也不会触发 onCellChange;错误边框和原因提示由 DataGrid 绘制。校验通过后,值才写回当前记录。需要提交整张业务表单时,还可以显式调用 validate() 检查全部源数据行并执行跨字段的行级校验;校验不通过时,再调用 focusFirstError() 定位第一个可见错误。
resolveCellEditPolicy() 决定每个单元格是 editable、readonly 还是 disabled,是否参加 Tab 路径,以及为什么不可编辑。鼠标、键盘、复选框、Popup、粘贴和最终提交都会读取这份策略。
5. 编辑会话跟随行身份和单元格几何
一个很常见的业务动作是新增记录后直接编辑第一项:
ts
grid.addRows([newRecord]);
grid.beginEdit(newRecord, "itemName");
这时不能只是把 editing 改成 true。
新记录可能在当前纵向视口之外,也可能因为排序出现在别的位置。DataGrid 要先在当前数据投影中找到它,再把目标行纵向滚入视口,最后才能定位编辑器:
新增行不在屏幕里时,先把目标行纵向滚入视口,再让对应编辑器接管该单元格。
如果记录已经可见,nearest 不会多滚一下;如果记录被筛选条件隐藏,beginEdit() 会返回失败,而不是偷偷清掉用户的筛选条件。当前 API 只保证目标行纵向可见,不会主动改变横向滚动位置。
排序、筛选或行投影变化后,只要目标记录仍在当前投影中,编辑会话就会根据行对象或稳定的 rowKey 重新定位,而不是继续依赖旧的可视行号;目标记录被筛选排除时,这次编辑会话会结束。滚动和布局变化时,文本编辑位置与 Popup 锚点使用同一份单元格几何更新;Popup 目标离开视口后,DataGrid 会按明确的生命周期结束这次 Popup 编辑,而不是留下一个已经失去目标的浮层。
编辑结束后,输入状态和 Popup 会话都要正确结束,临时子视图、事件监听和定时器也要及时释放;可复用的控制器和 Popup 宿主则继续保留。反复编辑几千次后,如果临时对象越积越多,表格一样会慢下来。
给单元格设置 editable:true 只打开了编辑入口。定位、输入、校验、提交、Popup 和下一个 Tab 停靠点都要接得上,用户才能沿着表格一直操作下去。
八、压力验证:把数据放大到十万行之后
下面把数据放大到 100,000 行做压力测试。这个规模不代表常规项目的数据交付方式,主要用来观察滚动工作量、全量数据操作,以及编辑会话接管当前单元格时的启动开销。
测试数据来自本文 Showcase 的生产构建。测试机器是 M1 Pro / 16 GB,浏览器为 Chromium 150,DPR 为 2,Canvas 尺寸是 1280 × 720。表格包含 100,000 行、10 列。
每轮先滚动 12 帧预热,再采集 100 个滚动帧;连续测试 3 轮,共记录 300 帧。测试时关闭可视性能浮层,只保留后台计时,避免图表本身参与绘制和合成。触发测试动作后,探测器会先等待并记录被测帧,再读取诊断状态,避免提前计算表格汇总并预热缓存。
表中的 P50 可以理解为中位数,P95 和 P99 用来观察较慢的那部分帧。
| 指标 | 结果 |
|---|---|
| 工厂创建至首帧可见 | P50 48.5 ms |
| 首帧总耗时 | P50 19.0 ms |
| 滚动整帧 | P50 2.8 ms / P95 3.2 ms / P99 3.9 ms |
| 主内容绘制 | P50 2.4 ms / P95 2.7 ms |
| 10 万行排序 | P50 18.8 ms |
| 开始编辑到画面稳定 | P50 7.9 ms |
| 全表快速筛选 | P50 833.8 ms |
首帧和滚动指标观察视口绘制,排序和快速筛选观察全量数据操作。"开始编辑到画面稳定"只测量一次编辑会话的启动成本;7.9 ms 不代表完整的连续录入过程。Tab 提交、Popup 打开和长时间反复编辑还需要单独采样。
首帧总耗时是一次性的初始化样本,不代表稳定滚动帧。持续交互能否落入帧预算,应主要观察滚动整帧的 P95 和 P99。
视口里大约能看到 15 行,加上边界情况和前后缓冲,本次采样每帧实际处理约 16~18 行,也就是约 160~180 个单元格。滚动一帧大约 2.8 ms,说明滚动时的工作量已经控制在视口附近。
快速筛选则不同。它要检查十万条记录,所以仍然需要约 0.83 秒。虚拟滚动帮不了这部分工作。实际项目中应加输入防抖、缩小查询字段,数据再大时改用服务端查询。
正式数据只包含隐藏浮层时的后台采样,不把性能图表本身的绘制成本算进 DataGrid 帧耗时。浮层用于开发阶段观察趋势,不能和业务内容一起参与正式基准。
性能浮层可以在开发时查看整帧、布局、主内容绘制和合成耗时。上面的正式数据使用后台采样,测试时没有显示这个浮层。
这组结果用来观察框架的工作量边界。实际页面仍应根据查询模式选择分页、服务端筛选和输入防抖。
九、哪些页面适合使用它
ds-ui DataGrid 自己管理视口、命中测试、绘制、焦点和编辑器。这样做控制力更强,但也意味着框架要承担更多工作。
比较适合的场景有:
- 高信息密度、长时间驻留、接近 DevExpress WinForms GridControl 等成熟桌面 DataGrid 操作方式的业务工作台;
- 医嘱、订单、费用明细、物料、字典和配置项等需要连续 Tab 录入、复杂 Lookup 与严格校验的页面;
- 大量固定结构数据的连续浏览;
- 强键盘导航、范围选择和复制;
- 需要单元格编辑、校验和 Popup 选择器协同的页面;
- 希望表格的输入、弹层和其他 GUI 组件保持一致的项目。
它也有明确的使用边界:
- 当前所有数据行共用统一的
rowHeight,不支持按行动态高度、合并单元格或跨行布局。需要自适应多行排版时,应选择更合适的表格或文档组件; - 单元格适合结构化文本、状态和字段编辑,不以承载任意 DOM 结构或复杂富文本为目标;
- 当前编辑体系包含八类内置单元格编辑器和同步校验,暂不支持撤销重做、整行编辑事务、任意自定义编辑器和 Excel 式矩形粘贴;
- 键盘输入、复制、粘贴和焦点已经由框架接管,但 Canvas 不能直接继承逐单元格的 DOM 语义。项目有严格的屏幕阅读器要求时,还需要单独评估可访问性适配;
- 内置排序、筛选、分组和汇总在客户端执行。数据量很大或来自远程接口时,应由业务层分页、查询并向 DataGrid 提供当前结果集;
- 少量静态数据当然也可以使用 DataGrid,只是虚拟滚动、复杂选择和编辑体系的价值不明显,此时轻量表格通常更省事。
文中以 DevExpress WinForms GridControl 为参照,参照的是桌面 DataGrid 的交互原则,并不表示两者的功能已经对等。浏览、定位、输入、校验、选择器和键盘导航由一个控件统一协调,ds-ui 用 Canvas GUI Runtime 实现了类似的组织方式。
数据投影、视口、焦点、Popup 和校验都放在 DataGrid 内部管理。数据量变大时控制每帧绘制范围,进入编辑后保持操作路径连续,这是当前实现的两个重点。