ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TanStack Table 客户端与服务器端数据处理选型指南:manual 选项、行模型与 Query 集成实战

TanStack Table 客户端与服务器端数据处理选型指南:manual 选项、行模型与 Query 集成实战 TanStack Table 客户端与服务器端数据处理选型指南manual 选项、行模型与 Query 集成实战【免费下载链接】table Headless UI for building powerful tables datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/tableTanStack Table 同时支持客户端与服务器端两种行处理方式既可在浏览器内通过行模型Row Model完成过滤、分组、排序、展开、聚合、分面与分页也可只管理这些功能的状态、把真正的数据处理交给后端。本篇指南面向需要在表格性能与实现复杂度之间做出取舍的开发者读完你将掌握两种方案的适用边界、manual*选项的准确含义、全手动服务器端数据流的完整落地步骤以及基于 TanStack Query 的页索引分页与游标分页两种实战写法。从最简单的方案开始客户端处理客户端处理通常是最简单的选择把完整数据集一次性交给 TanStack Table启用需要的行模型过滤、排序、分页即可在本地即时生效无需再发起任何请求。TanStack Table 的所有特性过滤、排序、分页、分组等都通过行模型Row Model在后台对原始数据做变换。行模型是 v9 版本的核心抽象工厂函数挂在tableFeatures()创建的 features 对象上核心行模型始终自动包含其余行模型按需注册详见 行模型指南import { tableFeatures, useTable, columnFilteringFeature, rowSortingFeature, rowPaginationFeature, createFilteredRowModel, createSortedRowModel, createPaginatedRowModel, filterFn_includesString, sortFn_alphanumeric, sortFn_text, } from tanstack/react-table const features tableFeatures({ columnFilteringFeature, rowSortingFeature, rowPaginationFeature, filteredRowModel: createFilteredRowModel(), sortedRowModel: createSortedRowModel(), paginatedRowModel: createPaginatedRowModel(), filterFns: { includesString: filterFn_includesString }, sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text }, }) const table useTable({ features, columns, data, })关于行模型的规模一个容易产生的误区是数据量大就必须上服务器端。实际上浏览器中处理几千行数据往往非常可行仓库官方文档声明所有 TanStack Table 特性都在100 万行客户端数据下做过压力测试并且期望保持可用性能。旧版本在约 100 万行时开始出现内存问题但在 Object Prototypes Refactor 重构之后官方宣称可轻松支持到1500 万行客户端数据。当然真实性能取决于列数、每行数据的大小与形态、访问器accessor与特性函数的开销以及用户设备的性能——请务必用具有代表性的数据和目标硬件实测而不是仅凭行数做判断。什么情况下选择服务器端处理服务器端处理通常在以下场景更合适获取完整数据集缓慢、昂贵或内存开销过大浏览器只接收一页或其他子集行查询、权限或业务规则必须由后端强制执行数据变化频繁完整下载很快会过期后端可以更高效地执行索引搜索、排序、分组或聚合。在评估取舍时请量化四个维度服务器查询完整数据集所需的时间与成本传输的载荷大小不仅是行数浏览器内存占用与行模型计算耗时每次状态变更后等待请求的交互成本。如果两种方案当前都可行从客户端开始可以让数据流更小同时由你掌控相关表格状态Table State会使之后把处理迁移到服务器端更加容易。保持数据集级操作的一致性过滤、分组、排序、分页通常是对同一数据集的同一条流水线。如果服务器只发送了数据集的一部分客户端操作就只能处理已加载的行——这会导致误导性结果。例如服务器端分页之后再在客户端排序排序的只是当前页而不是所有匹配行服务器端分页之后在客户端过滤可能隐藏当前页的行却无法找到其他页上的匹配行。因此有一条重要规则当服务器拥有分页时它也应拥有任何必须作用于完整结果集的过滤、分组、排序或聚合。只有当更小的作用域是有意为之例如只对服务器返回的每个分组内的行做排名时混合方案才成立并且要在 UI 中明确说明这一作用域。分面Faceting也需要同样的考量基于服务器返回页计算的分面计数只代表那一页。当分面需要代表完整过滤数据集时应在服务器端计算分面。Manual 到底是什么意思TanStack Table 把服务器端数据处理称为 manual手动因为表格不再执行该变换。一个manual*选项不会替你抓取或变换数据它的含义是告诉表格你提供的数据在该特性上已经处理完毕。操作Manual 选项客户端行模型或实现列过滤与全局过滤manualFilteringfilteredRowModel分组manualGroupinggroupedRowModel聚合值manualAggregationaggregationFn本地回退排序manualSortingsortedRowModel展开manualExpandingexpandedRowModel分页manualPaginationpaginatedRowModel分面Faceting同样支持服务器端提供的结果但它使用自定义工厂而非manual*选项具体见 聚合指南 与 列分面指南自定义服务器端分面。你可以省略未使用的客户端行模型如果共享的表格配置中包含某个行模型对应的manual*选项会告诉表格绕过它。该特性本身仍可保持启用表格依然提供其状态与 API。从源码看manual*选项在行模型流水线中确实扮演旁路开关的角色。在 coreRowModelsFeature.utils.ts 中分页行模型的解析逻辑清晰体现了这一点当manualPagination开启或未注册paginatedRowModel工厂时直接返回 pre-paginated 行模型因为分页被认为发生在数据到达表格之前if (table.options.manualPagination || !table._rowModels.paginatedRowModel) { return table.getPrePaginatedRowModel() }类似地展开、过滤、排序、分组等阶段在对应的manual*开启时都会回退到各自的getPre*RowModel见 行模型指南 中列出的执行顺序getCoreRowModel→getFilteredRowModel→getGroupedRowModel→getSortedRowModel→getExpandedRowModel→getPaginatedRowModel→getRowModel。一个典型的服务器端数据流服务器端处理意味着过滤、分组、排序、聚合、分页逻辑要写在你的后端语言、SQL 查询层、数据库 API 或服务架构中。TanStack Table 同样不负责抓取数据你的应用负责把表格状态发送给后端并把返回的行交给表格。任何数据抓取方案都可以TanStack Query 是很棒的搭档——声明式抓取、缓存、加载状态、请求生命周期管理一应俱全且与受控controlled的 TanStack Table 状态天然组合。服务器端处理的完整步骤掌控相关的过滤、分组、排序、分页状态让你的数据抓取代码能读到它们把所有服务器端拥有的状态值放进请求或 query key启用对应的manual*选项把服务器返回的处理后的行传给data手动分页时在已知的情况下额外提供rowCount或pageCount当过滤、分组、排序或页大小变化时重置或校验 page index有意识地保留旧结果或展示加载状态并防止慢的过期响应覆盖新结果。关于 autoResetPageIndex 的关键行为manualPagination会默认禁用autoResetPageIndex。更一般地说自动重置副作用只在调用它们的行模型重算时运行。客户端过滤、分组、排序行模型通常会在相关状态变化后触发页索引重置而在省略了这些行模型的完全手动服务器端配置中受控状态的变化不会运行这些重置钩子——因此要在对应的 change 回调中手动重置依赖状态示例如下。核心数据变化在返回数据被处理时仍可能触发某些自动重置行为。源码中的 table_autoResetPageIndex 确认了这一逻辑当autoResetAll、autoResetPageIndex显式设置时按显式值执行否则在!table.options.manualPagination时执行手动分页下只有显式 opt-in 才重置。表格继续管理状态并暴露事件处理器你的应用负责把状态接到数据抓取层。完整的模式参考各框架的 Table State 指南与 With TanStack Query 示例ReactTable State、With TanStack QueryPreactTable State、With TanStack QueryOctaneTable StateVueTable State、With TanStack QuerySolidTable State、With TanStack QuerySvelteTable State、With TanStack QueryAngularTable State、With TanStack QueryEmberTable StateLitTable StateAlpineTable StateVanillaTable State当选中、展开或其他行状态必须跨请求存活时请用稳定的后端标识符配合getRowId。页相对的行索引无法在不同服务器响应间可靠地标识同一条记录——在 with-tanstack-query 示例 中可以看到getRowId: (row) String(row.id)的用法。为什么没有内置的服务器端行模型TanStack Table 是一个同步的表格状态管理器。它的客户端行模型同步变换已经在内存中的数据而状态与 API 描述用户想要的过滤、排序、分页抓取数据与执行后端查询发生在表格之外。前后端之间的契约有无数种合理设计REST 查询参数、GraphQL 输入、RPC 调用、SQL 构建器、数据库 SDK、流式响应等等。一个内置的服务器端行模型必然会对这些选择强加观点TanStack Table 刻意把这个边界留给你的应用从而可以兼容任何后端技术栈或 API 设计。在实践中TanStack Query 通常扮演服务器端行处理的客户端协调者角色你自己的抓取层也可以承担同样的职责观察受控的表格状态通过你的 API 契约发送该状态缓存结果再把返回的行交回表格。实战一useQuery实现页索引分页当后端能返回过滤后的总行数时页索引分页最直接。以下为完整代码来自 main.tsx 的 Offset 部分const features tableFeatures({ columnFilteringFeature, globalFilteringFeature, rowPaginationFeature, rowSortingFeature, // Client-side filtering, sorting, and pagination row models are not // required because those operations are handled manually on the server. // Omitting the filtered and sorted row models also omits their page-reset // hooks, so pagination is reset in the change handlers below. }) const [sorting, setSorting] useStateSortingState([]) const [globalFilter, setGlobalFilter] useState() const [pagination, setPagination] useStatePaginationState({ pageIndex: 0, pageSize: 10, }) const dataQuery useQuery({ queryKey: [people, { sorting, globalFilter, pagination }], queryFn: () fetchPeople({ sorting, globalFilter, pagination }), placeholderData: keepPreviousData, }) const table useTable( { features, columns, data: dataQuery.data?.rows ?? [], rowCount: dataQuery.data?.rowCount, state: { sorting, globalFilter, pagination }, onSortingChange: (updater) { setSorting(updater) setPagination((previous) ({ ...previous, pageIndex: 0 })) // reset page index when sorting changes }, onGlobalFilterChange: (updater) { setGlobalFilter(updater) setPagination((previous) ({ ...previous, pageIndex: 0 })) // reset page index when global filter changes }, onPaginationChange: setPagination, manualFiltering: true, manualSorting: true, manualPagination: true, }, (state) state, )这里fetchPeople定义了你的前后端契约。后端负责按正确顺序执行操作并返回请求的行加上过滤后的总rowCount。注意几个关键点query key 包含全部服务器端状态sorting、globalFilter、pagination状态一变就会触发新请求没有注册filteredRowModel、sortedRowModel等客户端行模型因此页重置钩子也不存在改为在onSortingChange、onGlobalFilterChange中手动把pageIndex归零rowCount由后端返回驱动分页栏的总页数显示。源码中 table_getPageCount 表明手动分页下options.pageCount优先否则才由table_getRowCount与pageSize计算而table_getRowCount同文件 L432-L437在手动分页下以options.rowCount为准。useQuery契约的参考实现配套的 fetchData.ts 展示了后端契约的最小形态——先过滤、再排序、最后切片分页并返回rows与rowCountexport async function fetchData(options: DataQuery) { // Simulate some network latency await new Promise((r) setTimeout(r, 500)) const sortedData getFilteredAndSortedData(options) const { pageIndex, pageSize } options.pagination const pageStart pageSize Infinity ? 0 : pageIndex * pageSize return { rows: sortedData.slice(pageStart, pageStart pageSize), rowCount: sortedData.length, } }真实后端用 SQLWHERE、ORDER BY、LIMIT/OFFSET对应这三步即可同时注意排序需有确定性的次级键示例中以id兜底保证跨请求分页稳定。实战二useInfiniteQuery实现游标分页当计算总行数或支持任意页跳转代价高昂时游标分页是很好的替代方案。后端不接受页索引而是接受一个游标返回当前行、nextCursor以及是否还有下一页。生产环境的游标通常是不透明的当 ID 唯一且后端排序确定时稳定的最后一行 ID 也可以充当游标。TanStack Query 的useInfiniteQuery会缓存整条游标链。TanStack Table 可以用pagination.pageIndex从缓存中选择某一页只有当用户越过缓存边界时才发起新请求const dataQuery useInfiniteQuery({ queryKey: [people, cursor, pagination.pageSize, sorting, globalFilter], queryFn: ({ pageParam }) fetchPeople({ cursor: pageParam, pageSize: pagination.pageSize, sorting, globalFilter, }), initialPageParam: null, getNextPageParam: (lastPage) lastPage.nextCursor, }) const currentPage dataQuery.data?.pages[pagination.pageIndex] const hasCachedNextPage Boolean( dataQuery.data?.pages[pagination.pageIndex 1], ) const canNextPage hasCachedNextPage || Boolean(currentPage?.hasNextPage) const table useTable( { features, columns, data: currentPage?.rows ?? [], pageCount: -1, // the cursor API does not expose a total page count state: { sorting, globalFilter, pagination }, onSortingChange: (updater) { setSorting(updater) setPagination((previous) ({ ...previous, pageIndex: 0 })) }, onGlobalFilterChange: (updater) { setGlobalFilter(updater) setPagination((previous) ({ ...previous, pageIndex: 0 })) }, onPaginationChange: setPagination, manualFiltering: true, manualSorting: true, manualPagination: true, }, (state) state, ) async function goToNextPage() { const nextPageIndex pagination.pageIndex 1 if (!dataQuery.data?.pages[nextPageIndex]) { const result await dataQuery.fetchNextPage() if (!result.data?.pages[nextPageIndex]) return } table.nextPage() }游标分页的注意点Next 按钮使用canNextPage缓存中已有下一页或当前页声明hasNextPagepageCount: -1表示总页数未知。源码中 table_getCanNextPage 对pageCount -1直接返回true即无法知道是否到底table_getCanLastPage 则因为总数未知而对getCanLastPage()返回falselastPage()没有可跳转的有限页已抓取的页可以无请求回溯排序、过滤或页大小变化时把pageIndex重置为0让新查询从初始游标开始。渲染是另一个独立的决策数据处理与渲染解决的是不同的问题分页限制同时出现的行数可以在客户端或服务器端执行**虚拟化Virtualization**只渲染浏览器中已加载行的可见部分。虚拟化可以让大型客户端数据集渲染起来很廉价但它不会减少抓取的数据量也不会减少过滤、排序这些数据集所需的工作量。如果完整数据集大到无法加载请使用服务器端操作或增量抓取如果加载后的结果仍然大到渲染昂贵再叠加虚拟化。相关实现可参考各框架的virtualized-rows、virtualized-columns、virtualized-infinite-scrolling示例如 React 虚拟化行示例。如何最终选择方案客户端处理当浏览器能合理抓取并保留完整数据集且即时本地交互很有价值时使用服务器端处理当完整数据集不应或无法加载或后端必须定义权威结果时使用。选择服务器端处理意味着自带实现TanStack Table 只提供描述用户意图的状态与 API你的后端代码或 SQL 必须处理这些状态你的数据抓取代码必须负责请求与响应的传输。整个过程不涉及任何 TanStack Table 的服务器端模型或内置抓取层。这种分离让 TanStack Table 兼容任何后端TanStack Query 是可选的搭档用于帮助你管理抓取与缓存层。最后无论选择哪种方案都要用真实数据测试完整流水线网络传输、浏览器内存、行模型计算、DOM 渲染、后端查询成本是各自独立的瓶颈最佳边界是让所有这些维度对你的用户都可接受的那个边界。【免费下载链接】table Headless UI for building powerful tables datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表