关于javascript:使用-AntV-S2-打造大数据表格组件

54次阅读

共计 14363 个字符,预计需要花费 36 分钟才能阅读完成。

前情提要: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 实现。

排序的实现和筛选根本相似,有一个须要留神的点:筛选能够在多个数据列中同时进行,而排序是排它的,因而在其余列已有排序的状况下设置当前列的排序,会导致其余列的排序条件被笼罩。

搜寻

搜寻的实现流程:

  1. 对数据集做遍历,找出所有匹配关键词的单元格
  2. 用户抉择搜寻后果,对下一个搜到的单元格做 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 的老组件的问题

  1. 内置组件款式定制难

SpreadJS 的内置筛选、排序等 UI 组件是原生 JS 编写的。无奈应用 React 组件做拓展或者替换。也无奈定制 DOM 构造。所以内置的图标、布局都很难批改。根本是一锤子买卖。同时 Modal 呈现的地位也无奈定制,限度在表格外部,常常会呈现遮挡表格内容的状况。

  1. 单元格渲染定制难

SpreadJS 的单元格实践上能够拓展,然而只能应用 Canvas 原生的 API 去绘制。并且短少文档和源码反对。整体来说渲染定制十分艰难。比方加一个脱敏的 icon,就须要消耗数天工夫,并且实现比拟勉强。

  1. 应用简单,常常有未知的 Bug

SpreadJS 和 React 一起应用时,是须要自行封装 React 组件的。这就是额定的老本。同时在应用过程中咱们也发现了一些偶发的渲染异样,但没有残缺源码,所以排查比拟艰难。

  1. 体量大,数据模型简单,导致性能较差

同时关上 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

正文完
 0