ToolJet 表格组件服务端分页实战:用 limit/offset 分块加载 PostgreSQL 海量数据
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
导读
当 Table 组件需要展示数十万乃至百万级数据时,把全部记录一次性加载到浏览器会让应用卡顿甚至崩溃。本指南将基于 ToolJet 的 Table 组件,演示如何借助数据库原生的limit与offset语法实现服务端分页(Server-side Pagination):每次只从数据库拉取当前页数据,并配合pageIndex暴露变量、Total records server side与Page changed事件构建完整的分页闭环。阅读完本文,你将掌握在 ToolJet 中配置分页查询、总数查询、分页按钮启用条件、加载态与翻页事件处理器的完整方法,并理解其底层实现原理。
适用场景与核心思路
服务端分页适用于 MySQL、PostgreSQL、MSSQL、MongoDB 等支持按块(chunk)取数的数据库。与客户端分页(一次性加载全部数据、在浏览器内翻页)不同,服务端分页的核心是:
- 前端只持有当前页的数据;
- 每次翻页都向数据库发起一次新的查询,通过
limit控制每页行数、通过offset跳过前面页的数据; - 用一个独立的
COUNT(*)查询告知 Table 组件总记录数,用于渲染总页数和"下一页/上一页"按钮。
在 ToolJet 中,这一切都由 Table 组件暴露的pageIndex变量驱动:它的默认值为1(见 table.js 中的exposedVariables定义),每当用户翻页,该值就会更新,从而触发查询重新执行。
第一步:编写分页查询,按块加载 PostgreSQL 数据
以 PostgreSQL 的users表为例,编写如下查询:
SELECT * FROM users ORDER BY id LIMIT 100 OFFSET {{(components.table1.pageIndex-1)*100}};该查询每次只从users表拉取 100 行,实际返回的行数由 Table 组件中pageIndex(暴露变量)的当前值决定。例如:
- 第 1 页:
(1-1)*100 = 0,返回第 0~99 行; - 第 2 页:
(2-1)*100 = 100,返回第 100~199 行; - 第 3 页:
(3-1)*100 = 200,返回第 200~299 行。
对查询的逐段拆解如下:
| 子句 | 作用 |
|---|---|
ORDER BY id | 对结果集按id列排序,保证翻页时数据顺序稳定、不重复不遗漏 |
LIMIT 100 | 限制每次查询最多返回 100 行 |
OFFSET {{(components.table1.pageIndex-1)*100}} | 基于当前页码计算起始行号,实现翻页跳转 |
注意:
LIMIT 100中的每页行数应与后面 Table 组件的"每页行数"设置保持一致,否则会出现页数与数据错位的问题。
第二步:编写总数查询
要让 Table 组件正确计算总页数,还需要一个返回总记录数的查询:
SELECT COUNT(*) FROM users;该查询结果会被用作 Table 组件的Total records server side属性值。在 ToolJet 的查询结果中,COUNT(*)的返回值通常位于data[0].count,因此可直接以{{queries.<countquery>.data[0].count}}引用。
第三步:编辑 Table 组件属性
在 ToolJet 应用编辑器中,按以下步骤配置 Table 组件:
1. 绑定数据源
从组件库将 Table 组件拖入画布,在Data属性中填入分页查询的结果引用:
{{queries.<postgresquery>.data}}其中<postgresquery>替换为你实际创建的 PostgreSQL 分页查询名称。这样表格每页展示的就是查询返回的当前页数据。
2. 启用服务端分页
开启Server-side pagination选项。在组件定义中,该开关对应serverSidePagination属性,是一个「Client side / Server side」二选一开关(见 table.js)。开启后,Table 组件会把分页交由数据库处理,而不是在浏览器端对已加载数据做切片。
3. 配置「上一页」按钮启用条件
点击Enable previous page button旁边的Fx,输入以下表达式。该条件会在当前页为第 1 页时禁用"上一页"按钮:
{{components.table1.pageIndex >=2 ? true : false}}4. 配置「下一页」按钮启用条件
点击Enable next page button旁边的Fx,输入以下表达式。该条件会在当前页为最后一页时禁用"下一页"按钮:
{{components.table1.pageIndex < queries.<countquery>.data[0].count/100 ? true : false}}这里的100必须与分页查询中的LIMIT 100一致;queries.<countquery>.data[0].count是总数查询返回的总记录数,两者相除即总页数。当前页索引小于总页数时,"下一页"按钮保持可用。
提示:在启用服务端分页后,分页页脚对上一页/下一页按钮的可用性判断正是读取
enablePrevButton/enableNextButton这两个属性值(见 Pagination.jsx),所以上述两个Fx表达式的计算结果会直接控制按钮的禁用状态。
5. 设置服务端总记录数
在Total records server side属性中填入总数查询结果:
{{queries.<countquery>.data[0].count}}该值告诉 Table 组件数据集的总大小,组件据此计算出总页数并渲染分页按钮。从源码看,页脚在服务端分页模式下用Math.ceil(总记录数 / 每页行数)计算有效总页数(见 Pagination.jsx)。
下图展示了完成以上属性配置后的 Table 组件检查器面板:
6. 添加加载状态指示
为提升翻页体验,将 Table 组件的Loading state属性设置为分页查询的加载态,使查询执行期间表格显示加载指示器:
{{queries.<postgresquery>.isLoading}}7. 绑定「Page changed」事件
选中 Table 组件的Page changed事件,点击New event handler新建事件处理器,选择Run Query动作,并在Query下拉框中选择第一步创建的分页查询(即从 PostgreSQL 表分块取数的那条查询)。
事件配置示例如下图:
配置完成后,每当用户翻页,分页查询就会重新执行,从 PostgreSQL 表中按块拉取对应页的数据:
底层实现:pageIndex 如何驱动整个分页闭环
理解源码有助于排查问题和按需扩展。ToolJet 新表格组件基于 TanStack React Table 构建,其关键链路如下:
1. 手动分页模式接管
在 useTable.js 中,manualPagination: serverSidePagination一行把分页切换为"手动模式"——开启服务端分页后,表格不再对已加载的data做客户端切片,只维护pagination状态(pageIndex、pageSize),真正的数据切片由数据库完成。
2. 暴露变量与事件触发
在 TableExposedVariables.jsx 中,组件通过setExposedVariables({ pageIndex })把当前页码暴露出去——这正是分页 SQL 中{{(components.table1.pageIndex-1)*100}}能够动态求值的原因。同时,只有在用户通过页脚分页按钮/输入框翻页时才会触发onPageChanged事件(paginationBtnClicked标志位控制),从而避免程序化跳页造成重复请求。
3. 页脚渲染规则
在 Pagination.jsx 中可以看到服务端分页模式的完整渲染逻辑:
- 若同时拿到
serverSideRowsPerPage与totalRecords,则effectivePageCount = Math.ceil(totalRecords / serverSideRowsPerPage); - 若总记录数未知,则只显示当前页码,不显示跳页弹层(
showFirstLastBtns为 false); canGoToNextPage/canGoToPreviousPage在服务端模式下直接读取enableNextButton/enablePrevButton属性。
4. 每页行数的服务端取值
在 TableContainer.jsx 中,服务端分页开启时优先使用serverSideRowsPerPage(即属性面板里的 "Number of rows per page")作为pageSize,未设置时才回退到普通rowsPerPage。
5. 属性默认值
initSlice.js 中初始化了这些属性:serverSidePagination默认false、totalRecords默认10、enablePrevButton/enableNextButton默认true,并且只要启用了serverSidePagination就会自动打开enablePagination。
扩展到其他数据库
虽然本文示例使用 PostgreSQL,但分页公式本身是数据库无关的,可平移到其他支持LIMIT/OFFSET的数据库,例如 MySQL 与 MSSQL:
SELECT * FROM users ORDER BY id LIMIT 100 OFFSET {{(components.table1.pageIndex-1)*100}};对于 MongoDB 这类文档型数据库,虽然关键字不同(使用skip代替offset),但"当前页决定跳过多少条记录"的算法完全一致,同样可以套用(pageIndex-1) * 每页行数的思路实现分块取数。
配置核对清单与常见问题
完成配置后,可对照以下清单做一次自查:
- Data属性 =
{{queries.<postgresquery>.data}} - Server-side pagination已开启
- Enable previous page buttonFx =
{{components.table1.pageIndex >=2 ? true : false}} - Enable next page buttonFx =
{{components.table1.pageIndex < queries.<countquery>.data[0].count/100 ? true : false}} - Total records server side=
{{queries.<countquery>.data[0].count}} - Loading state=
{{queries.<postgresquery>.isLoading}} - Page changed事件绑定
Run Query,指向分页查询
常见问题排查方向:
- 总页数不准确:确认
COUNT(*)查询的返回字段路径确实为data[0].count,且 "下一页按钮" Fx 中的除数与LIMIT的行数一致; - 翻页后数据重复或漏行:确认分页查询包含
ORDER BY唯一性列(如id),否则数据库返回顺序不稳定会导致分页错位; - 按钮始终禁用:检查
enableNextButton/enablePrevButton的 Fx 表达式返回类型是否为布尔值,以及总数查询是否成功执行; - 未触发翻页请求:确认事件处理器选择的是Page changed事件,且目标 Query 是分页查询而非总数查询。
按照以上步骤配置后,即使数据集达到数十万行,Table 组件也只需承担当前页数据的渲染开销,应用性能与交互流畅度都会得到显著提升。
相关参考
- 组件属性与暴露变量定义:frontend/src/AppBuilder/WidgetManager/widgets/table.js
- 分页状态与手动分页开关:frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js
- 分页页脚渲染与总页数计算:frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/Pagination.jsx
- 暴露变量与事件触发逻辑:frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx
- 表格属性初始化与默认值:frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/initSlice.js
- 服务端分页配置界面截图:docs/static/img/how-to/server-side/pagination-v2.png
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考