某财税系统高性能表格实践总结

2,852 阅读6分钟

【快来报名】2025年 1季度VisActor数据可视化创意编程大赛暨文档达人挑战赛

奖品丰厚,全程指导,参与开源,提升技能!


本文来自 @visactor/VTable 用户投稿,该用户获得VisActor奖励的精美礼品一份!


背景

我们项目采用了 AngularJS 作为主要的技术框架,团队自行开发的表格列表及扩展功能,存在性能问题,而且扩展能力差,难以实现丰富表格功能。

我们调研各种开源解决方案,希望解决如下问题:

  1. 高性能,期望在十万数据级别,能够快速渲染,交互无卡顿,无白屏。
  2. 扩展能力强,在单元格渲染上支持一定程度的自定义。同时提供默认可用的丰富可视化效果
  3. 支持多维数据分析与展示
  4. 可以很方便的同目前技术框架(AngularJS)融合。

同时希望新表格的应用能做到如下开发体验:

  1. 功能一致性: 替换后的表格与原表格功能基本一致。
  2. 交互体验: 交互功能和使用体验得到显著提升。

VTable 调研

我们调研了多个开源表格库,从实现方式上整体分为两类:

  1. 基于dom的表格。优点是自定义能力强,可以基于dom节点做灵活的自定义渲染和扩展;缺点是大数据渲染场景下,性能劣化明显。
  2. 基于Canvas的表格。优点是具有非常高的渲染性能;缺点是扩展能力普遍较弱,需要使用原生Canvas 绘图api进行自定义渲染。

为彻底解决我们的性能问题,我们决定采用Canvas 表格,在为数不多的Canvas表格中,我们最终选择了VTable,原因如下:

  1. 超高的性能表现,支持百万级数据的渲染与流程交互。

  1. 较好的扩展能力,支持JSX自定义单元格;React-VTable 支持自定义React组件的接入(www.visactor.io/vtable/guid…
  2. 和VChart 完美融合,支持在一张画布上渲染成百上千个图表,极大提升可视化能力及渲染性能(单元格显示图表

另外VTable虽然是开源项目,但是提供了多种沟通渠道,可以直接和开发者进行需求沟通,快速响应,确保我们项目的上线周期。

功能实现

由于VTable默认功能比较完善,整个升级过程中基本没有技术上的障碍,下面将我们开发过程中自定义的几个点,简单总结一下。

全局基础配置

项目中使用表格的场景很多,但是基本风格一致,我们定义了一份全局配置,用来规定项目整体表格的一个基础风格,每个列表可以单独写个性化的配置,通过复制基础配置信息产生一个新的配置。

基础配置大致如下:

vtableOption = {
    columns: [],
    records: [],
    frozenColCount: 5, //冻结列数
    //rightFrozenColCount: 右侧冻结列数,默认为 0。
    //bottomFrozenRowCount: 底部冻结行,默认为 0。
    //autoWrapText: boolean:全局配置是否允许自动换行,默认值为false。如果设置为true,则当前列的单元格会根据其内容自动换行。
    showFrozenIcon: false, // 是否显示固定列图标
    widthMode: 'standard', //表格列宽度的计算模式,可以是 'standard'(标准模式)、'adaptive'(自适应容器宽度模式)或 'autoWidth'(自动宽度模式),默认为 'standard'。
    dragHeaderMode: 'none', //拖拽表头换位置  'all' 所有表头均可换位 'none' 不可换位 'column' 只有换列表头可换位 'row' 只有换行表头可换位
    // heightMode: 'autoHeight', //'standard'(标准模式)、'adaptive'(自适应容器高度模式)或 'autoHeight'(自动行高模式),默认为 'standard'。
    // autoWrapText: true, //是否自动换行 heightMode需要设置为autoHeight
    emptyTip: {
        text: '数据为空,请重新设置搜索条件',
        icon: {
            width: 205,
            height: 113,
            image: '../assets/images/Group.png',
        },
    },
    tooltip: {
        //省略提示
        isShowOverflowTextTooltip: true,
    },
    keyboardOptions: {
        copySelected: true,
    },
    hover: {
        highlightMode: 'cross', //四种hover交互模式,分别为:十字交叉('cross')、整('column')、整行('row')和单个单元格('cell')
    },
    menu: {
        contextMenuItems: ['复制'],
    },
    theme: VTable.themes.DEFAULT.extends({
        headerStyle: {
            fontSize: 14,
            color: '#666',
            fontWeight: 'normal',
        },
        bodyStyle: {
            color: '#666',
        },
        scrollStyle: {
            visible: 'always',
        },
        checkboxStyle: {
            size: 18,
        },
    }),
    select: {
        disableSelect: true,
    },
}

下拉 菜单自定义

VTable默认提供了下拉菜单。

但是个人感觉不够灵活,得益于VTable提供了灵活的扩展能力,于是自己写了一个自定义下拉菜单。

首先自定义下拉菜单的icon,核心代码如下:

{
    field: 'dropdown_menu',
    title: '操作',
    width: 60,
    disableHeaderSelect: true,
    disableSelect: true,
    disableColumnResize: true,
    customLayout: (args) => {
        const { table, row, col, rect } = args
        var _rect
        var _ref = (_rect = rect) !== null && _rect !== void 0 ? _rect : table.getCellRect(col, row)
        const { height, width } = _ref
        const container = new VTable.CustomLayout.Group({
            height,
            width,
            display: 'flex',
            flexDirection: 'row',
            flexWrap: 'nowrap',
            alignItems: 'center',
            justifyContent: 'center',
        })
        const containerCenter = new VTable.CustomLayout.Group({
            height: 20,
            width: 20,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
        })
        container.add(containerCenter)
        const bloggerName = new VTable.CustomLayout.Image({
            height: 16,
            width: 16,
            image: '../../../../assets/images/dropdown.png',
        })
        containerCenter.add(bloggerName)
        return {
            rootContainer: container,
        }
    },
}

上面代码中,图标是通过自定义渲染customLayout方式实现的(好像通过VTable本身icon能力实现会比较简单)。

最后通过点击单元格,获取单元格位置的方式,将组件定位到相应位置,进而展现整个菜单,菜单是用html实现的,这里就不列详细代码了。

tableInstance.on('click_cell', (args) => {
    const { col, row, field } = args
    let currentRowData = tableInstance.getRecordByCell(col, row) //获取当前行数据
                   if (currentRowData && field === 'dropdown_menu') {
        let rect = tableInstance.getCellRelativeRect(col, row)
        //获取单元格坐标,展示下拉菜单
        // 显示 自定义的菜单
})

最终效果如下:

右键复制单元格信息功能

右键复制单元格信息功能,与官网上demo实现的功能有所区别(www.visactor.io/vtable/demo…

官网demo没有禁用单元格选中,需要选中单元格才能复制单元格内的信息,好处是可以拖拽选中多个单元格进行复制,不灵活的地方是,单个单元格也必须要先单击选中,才能右键复制内容。我的项目禁用了单元格选中功能,通过监听右键点击事件,直接进行单元格内容的复制,不能拖拽选中多个单元格进行复制。应该可以有兼容两种方式的处理方式,个人觉得处理交互太麻烦了,就暂时简单粗暴的处理了。

基本思路是通过VTable实例获取要复制的数据。

let copyData = tableInstance.getCellValue(col, row)

然后将数据写入剪贴板,如果浏览器支持clipboard接口,直接写入:

if (navigator.clipboard && window.isSecureContext) {
              // navigator clipboard 向剪贴板写文本
              return navigator.clipboard.writeText(copyData)
          }

如果不支持,则通过创建隐藏textArea的方式,将内容先写入textArea,

 let textArea = document.createElement('textarea')
              textArea.value = copyData

然后通过 copy 命令写入剪贴板。

 textArea.focus()
              textArea.select()
              return new Promise((res, rej) => {
                  // 执行复制命令并移除文本框
                  document.execCommand('copy') ? res() : rej()
                  textArea.remove()
              })

完整代码如下:

  tableInstance.on('dropdown_menu_click', (args) => {
      const { col, row } = args
      if (args.menuKey === '复制') {
          let copyData = tableInstance.getCellValue(col, row)
          if (navigator.clipboard && window.isSecureContext) {
              // navigator clipboard 向剪贴板写文本
              return navigator.clipboard.writeText(copyData)
          } else {
              // 创建text area
              let textArea = document.createElement('textarea')
              textArea.value = copyData
              // 使text area不在viewport,同时设置不可见
              textArea.style.position = 'absolute'
              textArea.style.opacity = 0
              textArea.style.left = '-999999px'
              textArea.style.top = '-999999px'
              document.body.appendChild(textArea)
              textArea.focus()
              textArea.select()
              return new Promise((res, rej) => {
                  // 执行复制命令并移除文本框
                  document.execCommand('copy') ? res() : rej()
                  textArea.remove()
              })
          }
      }
  })

最终效果

最终使用VTable 还原了原有表格功能:

基本表格样式:

下拉菜单:

性能得到了极大提升:

原表格:

新表格:

欢迎交流

欢迎更多使用VisActor的用户联系我们,给我们投稿,交流业务场景,提建议,贡献代码,谢谢大家!

VChartVChart 官网VChart Github(感谢 Star)

VTableVTable 官网VTable Github(感谢 Star)

VMindVMind 官网VMind Github(感谢 Star)

官方网站:www.visactor.io/

Discord:discord.gg/3wPyxVyH6m

飞书群:打开链接扫码

微信公众号:打开链接扫码

Twiter:twitter.com/xuanhun1

github:github.com/VisActor