[译]在公共领域构建一个速度极快的缓冲数据网格。

原文:itnext.io/building-a-...

缓冲单元格和行、OMT(多线程)、实时更新、高级拖放、对角线滚动

1. 市场概述

实际上有相当多的供应商,而且他们中的大多数提供的解决方案都很糟糕。让我们从一个非常糟糕的开始。

1. Sencha

题外话:Sencha 在 13 年前是一家非常有前途的用户界面框架公司,但犯了严重的错误。快到最后时,它被出售了,买家解雇了几乎所有员工,试图将开发工作外包到海外。显然这没有成功。这是一个很好的例子,说明了人的重要性。

  • ➕ 行缓冲(但不是单元格)
  • ➕ 一组不错的功能
  • ➕ 移动设备上的对角线滚动(只有一个滚动容器)
  • ➖ 产品网站上的公然谎言和夸大其词
  • ➖ 在我看来,列出的公司中可能很少(如果有的话)还在使用它
  • ➖ 在过去 7 年中没有有意义的产品改进
  • ➖ 感觉好像已经没有人能理解代码库了
  • ➖ 代码库太过时了,甚至都不是基于 ECMAScript 6 的
  • ➖ 一个糟糕的数据包(模型定义是记录的基类,然后使用custom字段实例,占用你的内存)
  • ➖ 没有适当的可访问性规则
  • ➖ 单线程

简直每个人,甚至他们的猫都声称自己拥有市场上最快的网格。

(Bryntum 基准测试证明并非如此。)

2. AG 网格

Demo - Performance Grid

  • ➕ 设计看起来不错
  • ➕ 丰富的功能集
  • ➕ 开源基线版本(不过功能非常有限)
  • ➕ 行和列的缓冲
  • ➕ 在 DOM 级别上有适当的可访问性标签
  • ➖ 水平滚动性能较差
  • ➖ 深度嵌套的 DOM 结构
  • ➖ GitHub 上的代码库充其量看起来很普通
  • ➖ 至少这些演示在移动设备上表现很差
  • ➖ 单线程(这就是为什么金融演示不流畅的原因)

我不认同。它可能适用于带有精美动画的小网格,但重点在于设计而非提供企业级用户体验。

我给公司的建议是先聘请一些专家级工程师,打造一个坚实的核心基础,然后再"胡乱添加"更多功能。

3. MUI 数据网格专业版

我实际上很惊讶地发现一个基于 React 的实现。

这感觉就像幼儿园里的小孩在玩耍: "我有一把锤子,我可以用它把螺丝钉进墙里!"

ini 复制代码
export const DataGridPro = React.memo(DataGridProRaw as DataGridProComponent);

而且,对所有内容进行顶级记忆化并不是防止疯狂重新渲染的万能"解决一切"的方法。


React Data Grid component - MUI X

  • ➕ 单元格和行的缓冲
  • ➕ 移动端的对角线滚动
  • ➕ 虽然花了数年时间,但在 React 方面取得这样的进展值得称赞!
  • ➕ 在 DOM 级别上有适当的无障碍标签
  • ➖ 代码库遵循 "React 最佳实践" => 充其量也就是一般。带有状态提供器标签的 JSX。顶级属性如 hideFooterRowCount 。架构非常值得怀疑。
  • ➖ 重新渲染次数过多。值得使用 React Scan 工具进行检查。
  • ➖ 使用 Code Sandbox 单独运行演示是个好主意,但那里的演示会抛出很多 JS 错误,而且性能也不一样。见视频。
  • ➖ 单元格包含 aria-rowspan="1" 。根据规范,只有在它不是 1 的情况下才应该使用(很容易修复,去做吧)

4. Bryntum

我找到的迄今为止最好的商业解决方案是 Bryntum:

Bryntum Grid

  • ➕ 设计看起来不错
  • ➕ 我找到的最丰富的功能集
  • ➕ 出色的滚动性能
  • ➕ 在 DOM 级别有适当的可访问性标签
  • ➕ 由一个自行编写的微框架提供支持
  • ➖ 仅对行进行缓冲(缓冲单元格是未来规划项目)
  • ➖ 没有可用的开源基准
  • ➖ 单线程

如果我必须从现有的付费解决方案中选择,那就是它了。我强烈建议去体验一下演示。

2. 市场总结

虽然市场上有很多供应商,但似乎只有极少数能做对。

对我来说这并不意外,但我还没有找到一个基于 Angular 或 React 的出色网格实现。

这里有一个值得吸取的宝贵教训: Angular 和 React 都不足以作为构建企业级复杂组件的基准。 这就引出了一个问题,即使用它们来构建组件库或应用是否有意义,但这是另一篇博客文章的主题了。

如果你正在评估不同的选项,一定要关注滚动性能。许多供应商试图创建自己的虚拟滚动。想象一下,你在移动设备上拖动进行滚动,一旦你结束拖动操作,custom JavaScript 逻辑会尝试模拟你的设备接下来会做什么(例如,以逐渐减小的加速度继续滚动)。

这是个糟糕的主意。虽然对于某一种设备类型(例如 iOS)你可能会比较接近,但在其他设备(例如安卓)上,custom滚动会感觉非常奇怪,因为它们有着完全不同的滚动用户体验。

→ 坚持默认的浏览器滚动行为绝对至关重要。

你完全可以忽略所有基于表格标签的"网格"。表格根本不是用来缓冲行和单元格的。

3. 我们需要新网格的理由

  • 主线程外(OMT)

    特别是对于数据科学和其他对性能要求苛刻的领域,所有与网格相关的操作都应该在专用工作线程共享工作线程内进行。如果你还不熟悉这个主题,请查看这篇文章

  • 整洁架构

    我们需要严格分离视图层和数据层,并与逻辑和 DOM 解耦。

  • 用户体验很重要

    虽然网格需要进行虚拟化以处理大量单元格,但在其中进行导航/操作仍应感觉自然。

  • 坚实的基于开源的核心基础

    仅仅在一个混乱的代码库(纸牌屋范式)中"硬塞"更多功能是没有意义的。这不是一项容易的任务,而且需要花费大量时间:创建一个针对此类任务设计的框架。

  • 创建完全基于网络标准的代码

    没有隐藏的魔法,一个无需构建或转译就能运行的开发模式。

4. 高级列排序拖拽(Drag & Drop)

可能新网格实现当前的亮点在于基于拖放的列重新排序是如何工作的:

这一功能的实现并非易事,但我对结果非常满意。用户体验感觉很自然。

移动整列是可选的。如果你更喜欢一种更类似于在 Chrome 中重新排列浏览器标签的用户体验 => 只移动标签标题按钮并根据需要滑动其他按钮:只需一行配置更改就能实现。

5. 实时客户端过滤

虽然我为这个演示创建了一个加载蒙版,但实际上我们并不需要它。我们可以轻松地即时过滤 50000 条记录。

我们可以在类或实例定义中设置过滤器,但它们是响应式的,适用于运行时更改。

js 复制代码
changeFilters() {
    // The store.filters config is reactive => you can change it at run-time
    this.grid.store.filters = [{
        property: 'firstname',
        operator: 'like',
        value   : null
    }, {
        property: 'lastname',
        operator: 'like',
        value   : null
    }],
}

onFilterFieldChange(data) {
    // The configs of a filter are reactive too => just assign a new value
    this.grid.store.getFilter('firstname').value = data.value
}

ControlsContainer.mjs

6. 对角线滚动

让我们快速看看移动设备上表格标签的用户体验:

在线演示

这种对角线滚动的用户体验简直完美。到目前为止,我发现很少有网格解决方案能做到这一点,这让我有点惊讶。

有两种方法可供选择:

  1. 一个滚动容器 => 可以免费实现对角线滚动(Material-UI、Sencha),但这会使单元格和行的动画重新排序变得更加棘手
  2. 两个滚动容器 => 单元格和行采用绝对定位。对滚动和缓冲区有更精细的控制。动画很简单(AG Grid、Bryntum、Neo.mjs)

从技术角度来看,使用两个单独的容器(div)来实现对角线滚动确实有点棘手。我们需要一个水平滚动容器,它包括列标题工具栏和视图主体(否则我们最终会像 AG Grid 那样),以及一个用于视图主体(滚动行)的垂直滚动容器。

现在想想看:浏览器在给定时间只能处理一个滚动容器内的滚动。水平容器只会收到滚动事件的 scrollLeft ,垂直滚动容器只会收到 scrollTop 。不过还是可以处理的。

在线演示

7. 缓冲单元格和行

显然,任何缓冲网格实现的核心部分就是缓冲本身。意思是:我们只想绘制屏幕内可见的单元格。还可以选择多绘制几行和几个单元格(缓冲范围),以微调滚动体验。

在演示中,你可以处理多达 500 万个单元格(50000 行,每行 100 列)。我添加了控制台日志来测量数据和记录创建时间。

这是对浏览器/CPU 的纯粹压力测试。我的假设是,一次性创建这么多记录比"市场上最好的数据包"(Sencha)快 10 倍。不过还需要进行基准测试。

对于实际应用程序,你需要从后端获取数据范围,并根据需要填充客户端存储。我计划添加更多演示来展示这一点,包括基于 WebSocket 的实时更新。通过这种设置,我们可以轻松地在视图层中处理远超过 500 万个单元格的数据。

8. 在运行时更改数据

网格的一个重要用例不仅是可视化静态数据,还能以最少的 DOM 操作实时更新数据。

如果你想尝试我在视频中所做的事情:

Neo.config.logDeltaUpdates = true 在主线程中,控制台会记录应用的整个最小 DOM 更新流。

将控制台切换到应用工作线程作用域。

输入: Neo.get('neo-grid-container-1').store.getAt(0) 以获取第一行的 Record

9. 选择模型

进行中:

我们已经有 6 种不同的选择模型,但需要对其进行增强,以处理导航到未绘制区域的情况。这是高优先级路线图项目。

10. 剧透:内联单元格编辑

预 Alpha 版 & 进行中:

11. 在线演示

大数据性能演示(适用于桌面端和移动端)如下:

neomjs.com/examples/gr...

压缩版(初始渲染更快):

neomjs.com/dist/produc...

附注:所有演示都遵循相同的 URL 结构 → 您可以在 URL 中添加/删除 dist/production 部分,以在真实代码和生产环境之间切换。

使用对话框编辑网格单元格(记录):

neomjs.com/examples/gr...

选择模型:

neomjs.com/examples/gr...

12. 代码链接

整个代码库遵循 MIT 许可协议。

100%基于网络标准,并且对于给定的范围来说相当简短。

包含网格代码的文件夹:

src/grid

重新排列列标题的代码:

src/draggable/grid/header/toolbar/SortZone.mjs

选择模型(正在开发中):

src/selection/grid

示例:

examples/grid

公开构建。要跟踪开发情况:

github.com/neomjs/neo/...

github.com/neomjs/neo/...

虽然网格代码具有挑战性,但使用起来非常简单:

js 复制代码
import CellModel     from '../../../src/selection/grid/CellModel.mjs';
import GridContainer from '../../../src/grid/Container.mjs';
import MainStore     from './MainStore.mjs';

Neo.create({
    module: GridContainer,
    store : MainStore,

    columnDefaults: {
        width: 200
    },

    columns: [
        {dataField: 'firstname', text: 'Firstname'},
        {dataField: 'lastname',  text: 'Lastname'},
        {dataField: 'githubId',  text: 'Github Id'},
        {dataField: 'country',   text: 'Country'}
    ],

    viewConfig: {
        selectionModel: CellModel
    },

    wrapperStyle: {
        height: '250px'
    }
})

MainContainer.mjs

通常你不会使用 Neo.create() ,而是将描述对象放入容器的 items 配置中。除非你想将其放入特定的 DOM 节点,例如放入 Angular 或 React 应用中。

你可以在运行时更改每个配置属性。甚至可以在开发工具控制台中进行更改。

13. 路线图

在新的网格核心基础之上,我们可以添加许多不同的项目。利用这个机会告诉我你最需要什么/最希望看到什么,以便对此产生影响。

  • 选择模型:导航到未绘制区域(最高优先级)
  • 单元格编辑:正在进行中
  • 列类型
    • 索引列
    • 小部件列(渲染任何类型的组件)
    • 日期列
    • 国家/地区列(例如旗帜和名称)
    • 树形列
  • 更多的示例
  • 样式设置学习部分中的指南
  • 有意义的基准测试
  • 多窗口网格 (*)
  • 样式设置
  • 列标题分组
  • 行的动画排序(仅在数据有限时才有意义)
  • 展开行以显示更多内容
  • 锁定列
  • 透视
  • CSV 导出
  • Excel 导出

(*) 由于 Neo.mjs 使得将应用程序和组件轻松扩展到多个窗口变得轻而易举,我正在考虑一个想法,即可以选择将锁定列移动到单独的浏览器窗口中。当使滚动状态和选择模型同步时,这将是一个很棒的演示。不确定是否有实际用例,更像是研发。对此有什么想法吗?

14. 最终想法

在"退网"(抱歉,忍不住用了这个双关语)了相当一段时间后,我非常高兴能与您分享这个故事。

说实话,我只花了 7 周时间就取得了这样的进展(甚至还不是全职工作)。

现在您可能会问:"这怎么可能,其他公司有大型开发团队花了数年时间都没做到?!"

答案很简单。网格基于 Neo.mjs:

github.com/neomjs/neo

我已经打下了坚实的基础:

  • OMT 工作线程 setup
  • 细粒度的增量 DOM 更新
  • 拖放支持(鼠标和触摸板)
  • 选择模型
  • 响应式数据包
  • 一个表格实现

使用 Neo.mjs 创建应用程序和组件至少将我的工作效率提高了 25 倍。

这是我想和你分享的: 迈向前端开发的新高度。

我们的每周工作坊仍有空位(免费),如果您感兴趣,欢迎加入我们的 Slack 频道

自从我离开 ACN 的高级Manager职位后,我有一些免费资源可供使用。如果您的团队在专业前端开发方面需要帮助,欢迎与我联系。

致以最诚挚的问候,祝您编码愉快,

托拜厄斯

相关推荐
是程序喵呀4 分钟前
uni-app使用web-view组件APP实现返回上一页
前端·uni-app
Joker Zxc1 小时前
【前端基础】9、CSS的动态伪类(hover、visited、hover、active、focus)【注:本文只有几个粗略说明】
前端·css
2401_837088501 小时前
CSS flex:1
前端·css
发呆小天才yy5 小时前
uniapp 微信小程序使用图表
前端·微信小程序·uni-app·echarts
@PHARAOH7 小时前
HOW - 在 Mac 上的 Chrome 浏览器中调试 Windows 场景下的前端页面
前端·chrome·macos
月月大王8 小时前
easyexcel导出动态写入标题和数据
java·服务器·前端
JC_You_Know9 小时前
多语言网站的 UX 陷阱与国际化实践陷阱清单
前端·ux
Python智慧行囊9 小时前
前端三大件---CSS
前端·css
Jinuss10 小时前
源码分析之Leaflet中Marker
前端·leaflet
成都渲染101云渲染666610 小时前
blender云渲染指南2025版
前端·javascript·网络·blender·maya