前情提要:AntV 2021 年度公布了新产品多维穿插剖析表格 S2,能够点这里理解详情。
导读
在蚂蚁的大数据研发平台中,数据表格是一类重要的业务组件。咱们须要晦涩的展现 SQL 查问进去的上万条后果,并对后果做筛选、排序、搜寻、复制、框选、聚合剖析等操作。同时也存在数据手工录入的场景,须要表格有可编辑的能力。所以咱们最终须要的是一种 JavaScript 版的电子表格,相似一个简略的 Excel 工作簿。
本文记录的是,在开源软件不足以满足需要,同时商业软件价格高、定制难的状况下,咱们如何基于开源 Canvas 表格渲染库 AntV S2 来实现领有诸多性能的 React 大数据表格组件,并且解决了之前商业软件版本实现的性能问题和拓展性问题。
业务场景介绍
Dataphin 是蚂蚁外部的大数据研发平台,同时也有云上版本对外售卖。Dataphin 一站式提供数据采、建、管、用全生命周期的大数据能力。在 Dataphin 中有很多场景都用到了大数据表格。最典型的就是研发模块中,用来展现计算工作的运行后果:
以下是数据表格罕用性能的演示:
行列解冻
搜寻
筛选 & 排序
框选 & 复制
技术选型:为什么应用 AntV S2 ?
对于复交互的电子表格类组件来说,基于 Canvas 实现会比 DOM 实现有着更大的劣势。从性能上说,DOM 渲染须要通过如下的渲染流程,所有的 UI 改变都会波及外部数据结构的构建、款式的解析、布局的计算。最终才渲染进去:
<div align=’center’>
<img src=”https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1b56cc1e4d8242b4879718605312a2df~tplv-k3u1fbpfcp-watermark.image?” width=”80%” />
</div>
而 Canvas 的渲染管线就比较简单,Canvas 的 API 间接和 Skia 这样的底层 2D 图形调用绝对应,免去了 DOM 的解析和布局流程。所以基于 Canvas 的图形利用,有着更高的性能下限。特地是在富交互,大数据量的场景下。同时基于 Canvas 的利用也更容易编写简单的图形交互,因为图形不再以矩形作为最小单元,而是能够任意绘制。所以咱们在选型时次要思考基于 Canvas 的产品。上面是对市场上已有的类 Excel 前端表格组件的一些剖析:
产品名称 | 技术栈 | 开发商 | 费用 | 论断 |
---|---|---|---|---|
SpreadJS | Canvas | 公司 | 免费 | 市场上最弱小的 Excel 组件,但须要比拟高的受权费用,同时在多实例、大数据量的场景下,性能比拟差,并且很难拓展和定制 UI |
LuckySheet、x-spreadsheet | Canvas | 集体 | 开源 | 开源的类 Excel 组件。都是集体作品,由社区力量保护,投入的持续性、稳定性无奈保障,在产品细节体验上也不如 SpreadJS |
Handsontable | DOM | 公司 | 免费 | DOM 实现。同时须要免费。 |
语雀、钉钉等表格服务 | Canvas | 公司 | / | 通过服务化的 SDK 提供前后端一体的服务,而不是单纯的前端组件模式。同时拓展性也不如开源产品。 |
在市场上无奈找到适合的抉择之后,咱们蚂蚁数据智能前端团队抉择将外部的穿插表组件孵化为开源的 AntV S2 表格渲染引擎。
S2 是 AntV 团队推出的 数据表可视化引擎,旨在提供高性能、易扩大、好看、易用的多维表格。不仅有丰盛的剖析表格状态,还内置丰盛的交互能力,帮忙用户更好地看数和做决策。
S 取自于 SpredSheet 的两个 S,2 也代表了透视表中的行列两个维度。S2 不仅有穿插表模式,还反对明细表模式。
明细表就是一般的表格。相似 Antd Table 组件。实用于明细数据的展现。比方咱们一开始说到的 SQL 查问后果的预览等等。明细表组件晦涩在反对 10w+ 条大数据展现的根底上,还反对丰盛的交互:单选、刷选、复制到 Excel、快捷键多选、列宽高拖拽调节等等。不便用户疾速的对数据做抉择和操作。
表格性能实现
S2 的明细表为咱们提供了一个很好的根底底座,对于明细表的性能,大家能够参考文档。接下来就聊聊如何在 明细表的根底上,利用 S2 的高拓展性设计,实现一款功能丰富的 React 大数据表格组件。其中一些罕用性能内置到了 S2 中,比方行列解冻、复制等。另外还揭秘了一些 S2 的外部实现,比方虚构滚动的原理。
性能大图
首先看一下大数据表格组件的性能大图:
上图的能力中,蕴含了数据展现、检索、导出、编辑等外围能力。其中剖析方面的公式、聚合等能力是将来布局的。接下去会讲讲目前实现的这些外围性能的设计与实现。
交互
单元格编辑
单元格编辑是大数据表格在交互上遇到的最外围诉求。用户能够间接的编辑数据,录入数据。在手工维度表、调试录入 Mock 数据等等场景有着宽泛的应用。因为 S2 的定位是剖析表、明细表外围库,所以单元格编辑的能力并没有内置。这个能力须要大数据表格组件来扩大实现。
首先须要思考的问题便是技术实现上,编辑能力是应用 DOM 还是 Canvas 实现?在性能开发之前,咱们调研了业界各类主体基于 Canvas 的电子表格库,后果大抵如下:
库 | 单元格编辑实现 |
---|---|
SpreadJS | DOM |
语雀表格 | DOM |
x-spreadsheet | DOM |
能够看到,支流的实现都抉择了 DOM 作为单元格编辑的载体。思考到光标绘制,文本选中,换行逻辑等等一系列在 DOM 中都将由浏览器实现,在 Canvas 中则须要从新实现,应用 Canvas 上笼罩 DOM 节点做文本编辑是性价比最高的计划。所以咱们最终决定应用 DOM 来实现,成果如下:
上面简要介绍实现的过程:
1. 计算 DOM 遮罩大小和坐标
编辑态其实相似一种 Modality。须要用户的 Focus,在编辑时须要阻断和底部表格的交互。所以咱们须要挂载一个 DOM 遮罩容器节点,大小和 Canvas 保持一致,用于搁置 TextArea。元素的定位应用到了目前的窗口滚动的 Offset 和 Canvas 容器的坐标值。
const EditableMask = () => {const { left, top, width, height} = useMemo(() => {const rect = (spreadsheet?.container.cfg.container as HTMLElement).getBoundingClientRect();
const modified = {
left: window.scrollX + rect.left,
top: window.scrollY + rect.top,
width: rect.width,
height: rect.height,
};
return modified;
}, [spreadsheet?.container.cfg.container]);
return (
<div
className="editable-mask"
style={{
zIndex: 500,
position: 'absolute',
left,
top,
width,
height,
}}
/>
);
};
EditableMask 组件会被 ReactDOM.render 渲染到 body 中,作为顶层的子元素:
2. 注册事件并渲染 TextArea 元素
编辑操作个别会由双击来触发,而 S2 为咱们提供了 DATA_CELL_DOUBLE_CLICK
事件。而后来看看 TextArea 的渲染逻辑,咱们须要拿到单元格的定位和以后数据值。在触发事件时,从被双击的 DataCell 对象中能够拿到 x/y/width/height/value
这几个外围数据。须要留神,这里的 x 和 y 针对的是整个表格,而不是以后的 ViewPort,因而咱们须要应用 spreadsheet.facet.getScrollOffset()
获取表格外部的滚动状态并对 x/y 做针对性的批改。
外围逻辑如下:
// 从事件回调拿到 Cell 对象
const cell: S2Cell = event.target.cfg.parent;
// 计算定位和宽高
const {
x: cellLeft,
y: cellTop,
width: cellWidth,
height: cellHeight,
} = useMemo(() => {const scroll = spreadsheet.facet.getScrollOffset();
const cellMeta = _.pick(cell.getMeta(), ['x', 'y', 'width', 'height']);
// 减去滚动值,获取绝对于 ViewPort 的定位
cellMeta.x -= scroll.scrollX || 0;
cellMeta.y -=
(scroll.scrollY || 0) -
(spreadsheet.getColumnNodes()[0] || {height: 0}).height;
return cellMeta;
}, [cell, spreadsheet]);
// 获取以后单元格数值并存到 state 中
const [inputVal, setinputVal] = useState(cell.getMeta().fieldValue);
接着咱们应用 x/y/width/height
把 TextArea 渲染到 DOM 遮罩中,填充单元格以后的值,并手动 .focus()
,不便用户间接开始编辑操作。
3. 编辑实现后清理逻辑
在用户触发了 onBlur
事件或者输出回车的时候,咱们销毁 TextArea,并将批改后的值回填到spreadsheet.originData
中。并且抛出对应的事件,告诉使用者。
刷选
在 Excel 中,刷选是一种重要的交互。用户能够自在的批量框选单元格,并且在鼠标挪动到画布外时,画布会主动滚动,同时更新框选区域:
S2 中内置了刷选的交互。上面咱们来看这个交互的实现形式。首先明确一些概念:
- StartBrushPoint 刷选开始点。MouseDown 事件时记录。
- EndBrushPoint 刷选完结点。MouseMove 事件时更新。
- BrushRange 刷选范畴。蕴含了开始点的行列 Index 和完结点的行列 Index。
刷选就是监听鼠标拖动事件,而后将开始和完结点之间的格子设置为高亮状态。但这只是开始,咱们须要反对刷选的主动滚动,外围流程如下:
首先在滚动触发阶段,监听 MouseMove 事件时,须要减少判断,如果鼠标不在画布范畴内,就进入主动滚动流程,并把 EndBrushPoint 限度在画布上。如图:
MouseMove 事件触发的点的坐标是在画布之外的,所以咱们会把这个点,垂直投射到画布的边缘。失去最终的 EndBrushPoint。
同时在这个过程中,咱们还能够失去滚动的方向。咱们把滚动方向分为 Leading 和 Trailing 两种(头部和尾部)。同时又分 X 轴和 Y 轴两个方向。这样就有了 8 种滚动的可能方向。依据 MouseMove 坐标所在的地位,就能够计算出须要滚动的方向。如下图所示:
在晓得滚动方向之后,就能够触发主动滚动了。
还有一些细节问题须要思考。比方在滚动中,每个格子的宽和高都有可能是不同的,如果滚动的间隔是一个恒定的值,那在遇到很高或者很宽的格子时,就会呈现龟速滚动几次能力滚完一个格子的状况。所以每次滚动的间隔,必须是一个动静的值。理论落地的计划是这样的:拿向右滚动举例,滚动的 Offset 会定位到下一个格子的右侧边缘。比方这样:
保障滚动后,下一个格子会残缺的进入视口内。
在循环滚动时,滚动的频率也是一个须要留神的细节。不同的用户对于滚动频率有不同的诉求。所以滚动频率也不应该是恒定的。一种计划是将滚动速度和滚动来到画布的间隔关联起来。往下面拉的越多,滚动就越快,直到一个最大值。比方 Excel 中的实现:
滚动持续时间的大抵计算逻辑:
const MAX_SCROLL_DURATION = 300;
const MIN_SCROLL_DURATION = 16;
let ratio = 3;
// x 轴滚动速度慢
if (config.x.scroll) {ratio = 1;}
this.spreadsheet.facet.scrollWithAnimation(
offsetCfg,
Math.max(MIN_SCROLL_DURATION, MAX_SCROLL_DURATION - this.mouseMoveDistanceFromCanvas * ratio),
this.onScrollAnimationComplete,
);
外面的一些参数,是依据理论的用户体感来确定的。须要通过一直的优化迭代,达到一个比拟现实的状态。
渲染
虚构滚动
在大数据表格场景中,如何高性能的渲染大量数据始终都是最重要的问题之一。咱们能够把大数据表格看成一个长列表,对于长列表,最常见的优化策略就是只渲染可见区域的内容。在滚动事件触发后,依据滚动 Offset 调整相应渲染的内容即可。在用户看来,还是一个残缺的长列表。
DOM 的虚构列表实现须要思考的货色比拟多,也有 React-Virtualized 这样的库能够间接应用。基于 Canvas 的长列表优化形式,咱们叫虚构滚动。因为基于 Canvas 的利用每次渲染都会重绘整个界面。咱们应用 requestAnimationFrame
来定时 Schedule 一次渲染,让频繁的滚动事件落到浏览器的渲染周期中。而后在每个 animationFrame 的回调中,只须要计算出以后视口内的元素范畴而后渲染这些格子就能够了。
上面讲讲具体的实现流程:
第一步:计算可视区坐标范畴,得出可视区内的格子列表
首先依据行列信息和以后滚动 Offset 计算出可视区的范畴,取得一个数组,包含 [xMin, xMax, yMin, yMax]
。也就是行和列的 index 范畴。如下图所示:
第二步:和上一次格子列表做比照,失去 Diff
这一步中,咱们将上一次渲染的可视区 Index 和以后进行 Diff,拿到须要新增和删除的格子:
export const diffPanelIndexes = (
sourceIndexes: PanelIndexes,
targetIndexes: PanelIndexes,
): Diff => {const allAdd = [];
const allRemove = [];
Object.keys(targetIndexes).forEach((key) => {const { add, remove} = diffIndexes(sourceIndexes?.[key] || [],
targetIndexes[key],
);
allAdd.push(...add);
allRemove.push(...remove);
});
return {
add: allAdd,
remove: allRemove,
};
};
第三步:别离对 Diff 后果做 add 和 remove 操作
这一步中,咱们通过 AntV/G 的 API 对 Canvas 对象树做操作,把格子实例化并增加 / 删除。理论的成果相似以下的动画演示(图中的 add 操作做了提早解决,让成果更显著):
这样就实现了虚构滚动,让每次滚动渲染的工夫都只和视口的大小无关,而不是线性增长。
这个官网例子展现了明细表在 100W 数据下晦涩渲染的体现。
自定义列头
在咱们的业务中,在数据的列头之外,还须要实现相似 Excel 的序号列头,就像这样:
数据列头上还须要反对筛选、排序、脱敏的展现,有着泛滥不同的状态:
对于这样的需要,S2 能够轻松满足。因为 S2 的底层绘制引擎是 AntV/G,所以咱们不须要去应用最底层的 Canvas API,升高了门槛和保护老本。最重要的是 S2 反对自定义 Cell。咱们只须要继承内置的 Cell 类,重写相应的办法,而后把新的 Class 传给 S2 即可。
S2 的 Options 中提供了诸多 API 来自定义单元格的渲染:
对于上述的场景,咱们能够自定义 dataCell。通过重写 drawActionIcon 办法来实现齐全自定义的 Icon 绘制:
import {TableDataCell} from '@antv/s2';
class S2Cell extends TableDataCell {private drawActionIcon() {
// 绘制逻辑,因为 TableDataCell 继承了 AntV/G 的 Group 对象
// 所以这里间接应用 addShape 这样的属性就能够做 Canvas 的绘制
}
}
export default S2Cell;
在上述代码中,用户能够通过 S2 内置的 renderIcon
工具办法,来绘制 Icon,并且依据筛选和排序的状态来管制 Icon 的款式。同时还能够注册自定义的事件,来定义 Icon 点击时的行为。这些绘图 API 底层都是 AntV/G。所以在自定义 S2 的单元格时,咱们只须要简略学习 G 的 API 就能够上手。
通过自定义角头,还能够实现相似 Excel 的三角形角头成果:
import {TableCornerCell} from '@antv/s2';
export default class CornerCell extends TableCornerCell {protected drawTextShape() {
this.textShape = this.addShape('polygon', {// 图形属性});
}
}
布局
行列解冻
前端的表格个别都反对左侧或者右侧肯定数量列的固定,比方 Antd。除了列的固定,基于 Canvas 的 SpreadJS,还反对行的固定。这也是 Excel 的罕用性能之一:
固定行列个别是比拟重要的参考信息,比方 id、名称等。在列数量较多的时候,信息固定能够不便用户在查看信息的同时,疾速理解每个行列的上下文。
解冻的意思就是,不论表格的内容如何滚动,解冻的行始终显示在视图上方或者下方,解冻的列会始终显示在视图的左侧或者右侧,也就是说,解冻的行的 y 坐标在滚动时放弃不变,解冻的列的 x 坐标在滚动时放弃不变。
在 S2 中,咱们能够通过设置这些 Options 实现行列解冻:
上面聊聊如何实现行列解冻,因为咱们须要在滚动时对解冻的局部做非凡解决。所以首先须要对做明细表的内容区域做分组:
咱们首先分出 frozenRowGroup、frozenColGroup、frozenTrailingRowGroup 和 frozenTrailingColGroup 来别离对应上下左右四种解冻方向,把每个方向须要解冻的格子放到这些分组。而后还须要一个 panelScrollGroup 分组寄存一般格子,也就是会追随 x、y 两个方向自在滚动的格子。
除了这几个惯例的分组之外,咱们还留神到,四个解冻分组在四个角上是有穿插的,这些穿插区域的格子,在 x、y 两个方向都不须要滚动,所以这些格子须要专门放到一个分组里,并做相似 postion: fixed 的非凡定位解决。对这些格子,咱们放入 frozenTopGroup 和 frozenBottomGroup 两个分组。
同时,对于列的固定,不仅仅是数据格子须要固定,列头也要做固定。所以咱们把列头区域也分为三个分组,别离是 frozenColGroup、scrollGroup 和 frozenTrailingColGroup。
所以接下去的思路是,首先在渲染时,确定格子属于哪个区域,而后退出对应的分组中。而后在 translate 时,对不同分组做不同的 translate 操作。比方固定列只须要在 y 方向上滚动,固定行只须要在 x 方向滚动。
新分组概览
接下来咱们须要对渲染链路做革新:
第一步:渲染穿插解冻区域格子
对于穿插解冻区的格子,也就是四个角上的区域:frozenTopGroup/frozenBottomGroup 两个分组。这些格子在表的渲染周期中只须要增加一次,所以能够和 header 一样,在 S2 初始化的时候把节点插入即可。
第二步:计算可视区域格子坐标范畴
S2 中用的是上文介绍的虚构滚动,因而在渲染时会通过 scrollX 和 scrollY 计算出以后可视区域内的格子有哪些。之前内容区只有一个分组,计算出的格子坐标范畴是这样的构造:
export type Indexes = [number, number, number, number]; // x Min, x Max, y Min, y Max
坐标在这个矩形范畴内的格子会被渲染。
在反对行列解冻之后,这段逻辑就须要做批改,不仅要计算出滚动区域的格子范畴,还须要计算出 frozenRow、frozenCol、frozenTrailingRow、frozenTrailingCol 这四个解冻区域的格子范畴。因为行列解冻区域是反对单向滚动的,所以也须要做虚构滚动。可视区坐标计算结果须要改成如下构造:
export type PanelIndexes = {
center: Indexes;
frozenRow: Indexes;
frozenCol: Indexes;
frozenTrailingRow: Indexes;
frozenTrailingCol: Indexes;
};
第三步:对格子分组渲染
这一步很简略,对于可视区的每个格子,判断格子属于那一个分组,而后把格子退出这个分组即可。
第四步:滚动时做 translate
在 translate 时,对于行列解冻的分组,须要做单向滚动。
- 其中 frozenRow、frozenTrailingRow 须要追随 X 方向的滚动。
- 其中 frozenCol、frozenTrailingCol 须要追随 Y 方向的滚动。
ColHeader 革新
和 Panel 区统一,ColHeader 也要做分组渲染革新。在 layout 时,把 ColHeader 分为 frozenColGroup(左侧固定列头),scrollGroup(非固定的一般列头),frozenTrailingColGroup(右侧固定列头)。而后在 ColHeader 做 offset 操作时,只对 scrollGroup 做偏移即可。
须要留神的是,列头是能够做 Resize 操作的。因而还绘制了 Resize 热区。对于解冻的列,咱们也须要固定热区的绘制地位。所以对于解冻列的 Resizer,是须要专门画到一个分组外面的,相似列头格子的 scrollGroup。同时对于非解冻列的热区,要做一个 Clip 操作,避免热区溢出到解冻列的区域。
工具栏操作
筛选 & 排序
筛选是用户应用的高频操作之一。目前咱们曾经实现的筛选模式有两种:数值筛选和表达式筛选。上面就别离聊聊两者的设计和实现:
数值筛选
数值筛选通过 S2 的 DataCfg 的 filterParams
参数设置:
export interface FilterParam {
filterKey: string; // 要筛选的列的 key
filteredValues?: unknown[]; // 被筛选(去掉)的值}
在大数据表格中实现的次要是帮忙用户可视化编辑要过滤的值。难点如下:
一、筛选细节逻辑须要对齐 Excel,保障用户习惯不变
- 搜寻文本筛选:搜素框有筛选值时,点确定的时候以筛选框里的值为基数(不在筛选范畴内的值会被全副过滤掉)。
- 筛选值联动:当其余列曾经有筛选条件且以后项没有筛选条件的状况时,数值筛选的勾选框中会暗藏曾经被其余行的筛选条件筛选掉的值。此时如果再做筛选,所有设置过筛选参数的列的筛选值会独特起作用。相似一个多级联动的筛选。
二、筛选器列表在大数据量下的卡顿问题
- 因为 Checkbox 是采纳 DOM 渲染的,而大数据表格组件会经常性的承载 1w+ 的数据量,导致用户在筛选时会卡顿,影响用户体验。解决方案是将数值筛选的 UI 组件整体做一层虚构滚动,比方应用 react-virtualized。只渲染可视区域内的 DOM 元素。
表达式筛选
在表达式筛选中,反对用户通过配置表单的形式,应用由字段、操作符、数值形成的表达式做筛选。同时两个表达式能够通过 AND 和 OR 两种逻辑条件进行组合。
表达式是一个构建 DSL 的过程,在不斗争将来扩大能力的状况下,咱们在实现中反对了一元运算符和二元运算符两种状态:
interface Serializable<T> {
// 序列化,反对从 string 重建办法
serialize(): ExpressionOf<T>;}
// 组合类型
export enum JoinType {
OR = 'OR',
AND = 'AND',
}
// 一元运算符
type UnitExpressionOf<T> = {
key: FilterFunctionTypes;
operand: T;
};
// 二元运算符
type JoinExpressionOf<T> = {
type: JoinType;
funcA: ExpressionOf<T>;
funcB: ExpressionOf<T>;
};
export type ExpressionOf<T> = UnitExpressionOf<T> | JoinExpressionOf<T>;
export abstract class FilterFunction<T> implements Serializable<T>, Filterable {filter(value: unknown): boolean {throw new Error('Method not implemented.');
}
key: FilterFunctionTypes;
private _operand: T;
// 表达式另一侧的值:e.g., value > 2,则 2 是这个值
public get operand(): T {return this._operand;}
public set operand(value: T) {if (isValidNumber(value)) this._operand = Number(value) as unknown as T;
else this._operand = value;
}
public serialize() {
return {
key: this.key,
operand: this.operand,
};
}
}
继承 FilterFunction
,实现.filter()
办法,咱们就取得了能在业务组件中应用的筛选表达式类型,并且反对任意原子能力的 AND 和 OR:
排序
排序应用 S2 的 DataCfg 的 sortParams 实现。
排序的实现和筛选根本相似,有一个须要留神的点:筛选能够在多个数据列中同时进行,而排序是排它的,因而在其余列已有排序的状况下设置当前列的排序,会导致其余列的排序条件被笼罩。
搜寻
搜寻的实现流程:
- 对数据集做遍历,找出所有匹配关键词的单元格
- 用户抉择搜寻后果,对下一个搜到的单元格做 Focus 操作
难点次要在 Focus 操作,须要把不在视口内的格子滚动进入视口内。比方这样:
S2 提供了 facet.scrollWithAnimation
API。参数是滚动的 Offset。所以问题最终能够归结到如何计算滚动的 Offset,从而让格子进去视口内。
外围逻辑就是应用之前提到的 calculateInViewIndexes
拿到以后可视区域的范畴。而后判断以后格子在 X 和 Y 两个方向上处于以后视口的哪个方向,并计算出相应的滚动 Offset。还有一点就是行列解冻区域的宽高须要从 Offset 中减去,这样能力让格子从行列解冻的区域之下“露”进去。
列展现管制
列展现管制能够让用户通过勾选的形式抉择局部列做展现,有助于列数据较多时,让用户专一在以后关注的几列上:
实现方面,S2 提供了便捷的 API 来管制表格的状态,比方 setDataCfg
来更新数据、setOptions
来更新表格选项。咱们通过管制 dataCfg 的 columns
传入的值即可不便的管制列的展现和暗藏。咱们能够在 S2 内部减少一个表单组件,这个组件将须要暗藏的列存在 hiddenCols
数组中。在用户更新列展现设置后,咱们只须要执行上面的逻辑就能够管制 S2 的列展现:
const hiddenCols = [] // 用户抉择的要暗藏的列
const cols = [] // 所有的列
s2.setDataCfg({
fields: {
// 筛掉在 hiddenCols 数组中的列
columns: cols.map((col) => hiddenCols.indexOf(col) === -1)
}
})
只有批改 hiddenCols 而后从新渲染,即可做到内部 UI 管制的暗藏列性能。
复制
S2 实现了页面表格到 Excel 的保留单元格的复制性能。上面聊聊实现这个性能要留神的一些点:
1 数据内容的获取
S2 应用虚构滚动的模式,所以咱们不能间接从 DataCell 实例下来获取数据。比方在整行整列抉择的时候,不可见的区域内的 Cell 实际上都是未渲染的,也就不能从这里去获取数据。为了解决这个问题,咱们对 S2 做了革新,在选中交互时,S2 提供的是 Cell Meta(Cell 的元数据,比方格子类型、行列 index)。在复制时,咱们最终是通过 Cell Meta 从源数据中选出被选中格子的信息,再从新拼接成须要复制的内容。
2 数据内容的拼接
获取到数据内容后,咱们须要把这些内容拼合在一起,使之合乎表格复制的标准。咱们晓得剪切板中的数据最终是一个字符串,那 Excel 又是如何辨认不同的单元格呢?
答案就是制表符 '\t'
和换行符 '\r\n'
。每一行的数据,通过换行符 '\r\n'
做辨别。每行中每个格子的数据后,须要跟一个制表符'\t'
。
有一个状况须要特地思考,就是单元格外部的换行。行内和整体换行的标记都是 '\r\n'
,那么如何区别这两种状况呢?在实际操作时,只须要在单元格的内容外包一层双引号,通知表格这是一个单元格外面的内容,那么就能够实现行内换行了。理论代码如下:
export const convertString = (v: string) => {if (/\n/.test(v)) {
// 如果单元格内容里有换行符,在外面包一个引号
return '"'+ v +'"';
}
return v;
};
3 复制 API 的抉择
之前支流的复制 API 是 document.execCommand('copy')
。但这个 API 在数据量大的状况下(数万),会有卡顿的状况。所以在有条件的浏览器中,咱们应用 navigator.clipboard
API 做复制。工具函数 copyToClipboard 的大抵逻辑如下:
export const copyToClipboard = (str: string, sync = false): Promise<void> => {if (!navigator.clipboard || sync) {return copyToClipboardByExecCommand(str);
}
return copyToClipboardByClipboard(str);
};
export const copyToClipboardByClipboard = (str: string): Promise<void> => {return navigator.clipboard.writeText(str).catch(() => {return copyToClipboardByExecCommand(str);
});
};
export const copyToClipboardByExecCommand = (str: string): Promise<void> => {return new Promise((resolve, reject) => {const textarea = document.createElement('textarea');
textarea.value = str;
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {resolve();
} else {reject();
}
});
};
成绩:成倍性能晋升、完满自定义 UI,顺利替换商业表格产品
下面咱们介绍了如何基于 AntV/S2 实现全功能大数据表格。当初来看看这个新的表格解决了哪些问题。之前咱们的线上的大数据表格应用 SpreadJS(v13),存在着以下的问题:
基于 SpreadJS 的老组件的问题
- 内置组件款式定制难
SpreadJS 的内置筛选、排序等 UI 组件是原生 JS 编写的。无奈应用 React 组件做拓展或者替换。也无奈定制 DOM 构造。所以内置的图标、布局都很难批改。根本是一锤子买卖。同时 Modal 呈现的地位也无奈定制,限度在表格外部,常常会呈现遮挡表格内容的状况。
- 单元格渲染定制难
SpreadJS 的单元格实践上能够拓展,然而只能应用 Canvas 原生的 API 去绘制。并且短少文档和源码反对。整体来说渲染定制十分艰难。比方加一个脱敏的 icon,就须要消耗数天工夫,并且实现比拟勉强。
- 应用简单,常常有未知的 Bug
SpreadJS 和 React 一起应用时,是须要自行封装 React 组件的。这就是额定的老本。同时在应用过程中咱们也发现了一些偶发的渲染异样,但没有残缺源码,所以排查比拟艰难。
- 体量大,数据模型简单,导致性能较差
同时关上 10 个数据后果 Tab(10 个表格实例),每个展现 1000 条数据。此时切换有显著的卡顿。切换工夫大抵在 500ms 左右。用户感知显著。
在大数据量场景下,预览用户上传的 5 万条数据文件,SpreadJS 须要加载 8 秒工夫能力失常展现。
基于 S2 的大数据表格如何应答这些问题
拓展性 & 组件化:解决内置组件款式定制 & 单元格渲染定制和上手难问题
利用 S2 内置的筛选和排序等 API,还有自定义单元格渲染的能力,咱们只须要封装 React 组件,就能够自在的定制组件的 UI。比方在业务中,咱们应用了 Antd 作为 UI 根底库,来封装合乎业务格调的筛选 & 排序 Modal。咱们能够借助 Antd 的能力,而不必反复建设 Modal 能力。同时 Modal 的整体的布局和内容都是自定义的,有着很大的自由度:
同时 S2 提供的内置 SheetComponent 组件也让咱们能够免去封装纯 JS 库到 React 组件这样的过程。在 React 中上手只须要复制一下 Demo 代码就能够跑起来。
开源 & 自研:解决疑难杂症
在商业软件中,咱们很难在购买到的编译混同后的代码中做 Debug。而在开源软件中,有源码在手,所有疑难问题都不是问题,都能够看源码,排查问题失去解决。同时借助开源社区的力量,Bug 也会很快失去修复。通过一段时间的进化和磨难之后,S2 会变的更加弱小和欠缺。
轻量级数据模型 & 虚构滚动:解决大数据量下性能问题
S2 定位是一个轻量级的表格渲染外围,所以没有 Excel/SpreadJS 那样简单的性能和数据模型。在大数据表格组件这个场景下,其实不须要那么重的数据模型,S2 的轻量级设计是更适合的。在性能体现上,10 个 Tab 切换时,切换耗时为 80ms,是老计划的 6 倍。在 5 万条数据的状况下,S2 渲染和初始化只须要 1 秒。是老计划的 8 倍。
同时 S2 也能够依据理论状况做拓展,将来咱们也会推出基于 S2 的类 Excel 电子表格场景组件,提供插件化、场景化的能力,用户能够按需应用,也能够通过插件化的形式引入公式等等高级性能。
结语
感激你看到了这里,对于如何应用 AntV/S2 打造大数据表格组件的故事曾经讲完了。但 AntV/S2 才刚刚起步。接下来咱们会把大数据表格场景的单元格编辑、搜寻、高级排序等等能力积淀到 s2-react 中,让整个社区都能够应用到。同时也会继续加强 S2 明细表的底层能力。反对多层级列头、聚合计算等能力,对大分辨率下的渲染性能做优化。也欢送社区的同学和咱们一起共建 AntV/S2,打造最强的开源大数据表格引擎。如果看完这篇文章你有所播种,欢送给咱们的仓库 Star⭐️⭐️⭐️⭐️⭐️ 激励。
S2 的相干链接:
- Github
- 官网
- 核心层: @antv/s2 V1.11.0
- 组件层: @antv/s2-react V1.11.0