ARTICLE DETAIL

资讯详情

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

TanStack Table Octane 列分面(Column Faceting)实战指南:从行模型工厂到过滤界面

TanStack Table Octane 列分面(Column Faceting)实战指南:从行模型工厂到过滤界面 TanStack Table Octane 列分面Column Faceting实战指南从行模型工厂到过滤界面【免费下载链接】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/tableFaceting分面是构建筛选式数据表格的核心技术它不直接过滤数据而是为每个列推导出当前还有哪些可选值、每个值出现多少次、数值范围是多少从而驱动复选框、下拉框、范围滑杆与自动补全等过滤控件。本文以 TanStack Table 的 Octane 框架适配包tanstack/octane-table为主线完整讲解columnFacetingFeature的配置、三大分面 API、与过滤特征的协同机制、分桶bucketed分面、服务端自定义分面以及全局分面并结合packages/table-core的源码实现说明其底层行模型与记忆化原理最终你可以直接复用到自己的 Octane 表格项目中。分面功能的核心定位为过滤界面提供元数据在动手配置之前先明确分面在 TanStack Table 能力矩阵中的位置。分面faceting从表格行数据中推导出可用于构建过滤界面的信息对于给定的一列它可以回答以下四类问题当前数据中该列有哪些可选值每个值出现了多少次发生频次在当前可用行中该列的最小值和最大值是多少哪些行应该被用于自定义分面计算例如一个应用可以借助分面渲染出如下的套餐筛选器Plan ☐ Free 128 ☐ Pro 47 ☐ Enterprise 9其中的套餐名称与数量正是从表格的分面行模型faceted row model推导而来。当另一列的过滤器发生变化例如Region Europe时套餐数量会随之更新只描述该区域内的行。分面不负责应用过滤它只提供值、数量、范围或行数据供你构建过滤 UI真正持有过滤状态并决定哪些行匹配的是列过滤特征column filtering feature。分面与行聚合Row Aggregation的区别分面与行聚合都对数据进行汇总但用途完全不同分面产出的是过滤控件的元数据可选值、出现次数或数值范围行聚合row aggregation对一组行计算结果值例如求和、平均值、总计用于页脚或分组行展示。分面的计数不会创建聚合行也不使用列的aggregationFn。用一个简单的方式来区分三者过滤回答的是哪些行保留下来分面回答的是哪些过滤选项还可用行聚合回答的是能从这些行计算出什么汇总值Faceting Setup注册特征与行模型工厂在 Octane 表格中使用分面需要将columnFacetingFeature加入tableFeatures组合中并按需注册对应的行模型工厂。注意在 Octane 适配层中行模型插槽row model slots是类型检查的因此如果使用客户端分面必须在对应特征之后配置filteredRowModel与facetedRowModel。import { useTable, tableFeatures, columnFacetingFeature, columnFilteringFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues, createFilteredRowModel, filterFns, } from tanstack/octane-table const features tableFeatures({ columnFacetingFeature, columnFilteringFeature, filteredRowModel: createFilteredRowModel(), // if using client-side filtering // manualFiltering: true, // if using manual server-side filtering facetedRowModel: createFacetedRowModel(), // if using client-side faceting facetedUniqueValues: createFacetedUniqueValues(), facetedMinMaxValues: createFacetedMinMaxValues(), filterFns, }) const table useTable({ features, columns, data, })各工厂的职责如下工厂作用是否必需createFacetedRowModel()为每列计算应用了其他列过滤、排除了本列过滤的行模型客户端分面必需createFacetedUniqueValues()计算每个值的出现次数Map使用唯一值与计数时必需createFacetedMinMaxValues()计算数值列的最小/最大值使用数值范围时必需只注册你的表格实际会用到的工厂即可——上文完整的配置一次性注册了全部三个。仓库中的 Faceted Filters 示例 展示了与之等价的实际用法它在tableFeatures中同时注册了filteredRowModel、facetedRowModel、facetedMinMaxValues与facetedUniqueValues并配合rowPaginationFeature使用。列分面如何响应过滤排除自身过滤的巧妙设计一个列的分面行模型包含的是通过了除该列自身过滤之外所有适用过滤的行。这样设计的目的在于当用户正在编辑某个分面时该分面依然能展示其他的备选选项。考虑一个同时具备Region与Plan过滤器的表格用户选择Region EuropePlan分面应用区域过滤并重新计算其套餐计数用户选择Plan Pro表格只展示欧洲的 Pro 行由于Plan分面排除了自己的过滤它仍从全部欧洲行中计算选项。而其他分面则会应用已选定的套餐过滤——例如一个Status分面此时只会描述欧洲 Pro 行。这正是多个分面能够互相收窄的原因。缺少 filteredRowModel 时的行为客户端分面需要同时具备filteredRowModel与facetedRowModel才能实现上述效果。若没有已过滤的行模型分面行模型会回退到过滤前的行pre-filtered rows此时其值将无法响应其他列的过滤变化。从源码看这一排除自身逻辑实现在 createFacetedRowModel.ts 中工厂读取preRowModel、columnFilters与globalFilter后遍历过滤条件把不等于当前columnId的过滤列加入filterableIds再通过filterRows过滤出分面行模型对于全局上下文__global__它同样会把全局过滤本身排除在外与列分面排除自身过滤的语义完全一致。三大分面 API 的用法与适用场景根据你要构建的过滤控件类型选择对应的分面 APIAPI返回结果常见用途column.getFacetedRowModel()通过其他活跃过滤的行自定义分面计算column.getFacetedUniqueValues()值到出现次数的Map复选框、下拉菜单、自动补全建议column.getFacetedMinMaxValues()[min, max]元组或undefined数字输入框、范围滑杆这些 API 由 columnFacetingFeature.ts 统一挂载特征通过assignColumnPrototype将getFacetedRowModel、getFacetedUniqueValues、getFacetedMinMaxValues挂到列原型上通过constructTableAPIs将getGlobalFacetedRowModel、getGlobalFacetedUniqueValues、getGlobalFacetedMinMaxValues挂到表格上。这里有一个值得注意的实现细节源码注释亦有说明该层刻意不做记忆化内置的createFaceted*工厂在内部自行记忆化与其他内置行模型一致若在此处再加一层记忆反而会冻结那些数据独立于分面行模型变化的自定义工厂——因此自定义工厂的缓存由工厂自身负责。在 columnFacetingFeature.utils.ts 中可以看到这些 API 的惰性解析机制工厂按列 ID 解析一次并缓存在table._rowModels上而工厂返回的函数在每次读取时都会执行。如果未注册对应工厂则回退到安全默认值分面行模型回退为getPreFilteredRowModel()唯一值回退为同一个稳定空Map实例保证引用稳定性min/max 回退为undefined。唯一值与出现次数驱动复选框与下拉框column.getFacetedUniqueValues()返回一个Map键是分面值值是出现次数。你可以将其转换成排序列表用于自动补全或下拉控件const suggestions Array.from(column.getFacetedUniqueValues().entries()) .sort(([valueA], [valueB]) String(valueA).localeCompare(String(valueB))) .slice(0, 5_000)每个条目同时包含值与次数可直接渲染为选项return ( select {suggestions.map(([value, count]) ( option key{String(value)} value{String(value)} {String(value)} ({count}) /option ))} /select )对于标量列每行通常贡献一个值因此出现次数就等于行数。但如果通过列的getUniqueValues选项让一行贡献多个分面值计数描述的是出现次数其总和可能大于行数columnHelper.accessor(tags, { header: Tags, getUniqueValues: (row) row.tags, })如果你希望每个计数代表行数请确保getUniqueValues对每一行返回的值互不重复。底层的计数逻辑见 createFacetedUniqueValues.ts它遍历分面行模型的flatRows对每行调用row.getUniqueValues(columnId)后逐个累加进Map在__global__全局上下文中它会遍历所有getCanGlobalFilter为真的叶子列做聚合。Octane 中的响应式分面控件table.Subscribe分面值通常在过滤组件内部读取该组件接收稳定的column对象。为了让分面在其他分面变化时自动更新需要把组件订阅到过滤状态上——Octane 的table.Subscribe会把订阅局部化到渲染分面控件的组件内避免无关组件重渲染。function FacetOptions({ table, column }) { table.Subscribe selector{(state) state.columnFilters} {() ( FacetOptionList column{column} isSelected{isSelected} toggleValue{toggleValue} / )} /table.Subscribe } function FacetOptionList({ column, isSelected, toggleValue }) { const values Array.from(column.getFacetedUniqueValues().entries()) for (const [value, count] of values; key String(value)) { label input typecheckbox checked{isSelected(value)} onChange{() toggleValue(value)} / {String(value)} ({count}) /label } }列的过滤函数仍然决定选中的值如何匹配行。过滤函数与过滤状态的详细说明见 Column Filtering Guide完整实现可参考 Faceted Filters 示例。该示例中的Filter组件是一个很好的实战样板它通过column.columnDef.meta.filterVariant区分textdatalist自动补全、select下拉框与range双输入框三种过滤控件并全部基于getFacetedUniqueValues()与getFacetedMinMaxValues()动态生成选项。最小与最大值驱动数字输入与范围滑杆column.getFacetedMinMaxValues()返回在应用了其他活跃过滤后当前可用的数值范围当没有数值时返回undefined。典型的范围滑杆用法const [min, max] column.getFacetedMinMaxValues() ?? [0, 1] return ( input typerange min{min} max{max} value{currentValue} onChange{(event) column.setFilterValue(Number(event.currentTarget.value)) } / )注意这里的 min/max 描述的是过滤 UI 中可用的值而选中值或范围如何过滤行仍由列的过滤函数决定。实现上createFacetedMinMaxValues.ts 遍历分面行模型的flatRows用Number(row.getValue(columnId))扫描并维护最小值与最大值遇到Number.isNaN的值会跳过如果没有任何行或没有数值返回undefined。分桶分面为连续值构建有意义的分组原始唯一值并非总是有用——日期、文件大小、时长、价格、度量值等列可能产生成百上千个不同值。这些列在放入有意义的桶bucket后更容易过滤Last login ☐ Today ☐ Yesterday ☐ This week ☐ This month ☐ Older你可以利用列的getUniqueValues选项返回分桶键用于分面同时保留原始 accessor 值用于渲染和其他表格功能type StorageBucket under-1-gb | 1-to-10-gb | 10-to-100-gb | 100-gb-plus const GB 1024 ** 3 function getStorageBucket(value: number): StorageBucket { if (value GB) return under-1-gb if (value 10 * GB) return 1-to-10-gb if (value 100 * GB) return 10-to-100-gb return 100-gb-plus } const storageBucketFilter constructFilterFn({ resolveDataValue: (value) getStorageBucket(value as number), filter: (bucket, selected: ArrayStorageBucket) selected.includes(bucket), autoRemove: (selected: ArrayStorageBucket) selected.length 0, }) columnHelper.accessor(storageBytes, { header: Storage, getUniqueValues: (row) [getStorageBucket(row.storageBytes)], filterFn: storageBucketFilter, })关键实践是分面与过滤必须使用同一套分桶定义这样展示的计数才能与每个桶选中的行一致。列仍保留原始数值因此无需为了分面专门创建隐藏的派生列。完整的日期与存储分桶过滤实现见 Bucketed Faceted Filters 示例其main.tsrx同时包含按日期与按存储量分桶的过滤组件。客户端分面与性能内置的客户端分面行模型是记忆化的它们只在输入行或相关过滤状态变化时重新计算计算开销仍取决于表格的行数、列数与唯一值数量。对于唯一值很多的列可以考虑以下优化手段只渲染前若干个或最相关的值而不是渲染每个Map条目让用户在渲染长列表前先搜索可用值将连续值或高基数值分桶为有用的范围当完整数据集不在浏览器中时把分面移到服务端。此外应避免在无关组件中反复对大分面Map做排序或转换尽量在与订阅相关过滤状态的组件紧邻处派生并渲染分面选项。自定义服务端分面当过滤在服务端进行时浏览器中加载的行可能不足以计算完整的分面值或计数。此时应在服务端计算分面并提供自定义的facetedUniqueValues与facetedMinMaxValues工厂。每个工厂接收 table 与列 ID返回一个解析分面结果的函数。常规的列 API 会原样返回服务端提供的值。需要特别注意两点工厂每个 table 和列只解析一次但其返回的函数在每次读取时都会执行table 不会缓存其结果因此应在返回的函数内部读取实时值来自 signal、store 或table.options.meta这样更新后的服务端分面能立即生效如果计算昂贵可在工厂内部做记忆化。// Refreshed whenever the facet request resolves; read live below const facets { data: await fetchFacets(activeFilters) } const features tableFeatures({ columnFacetingFeature, // The returned functions run on every read and table.options stays in // sync with the latest render, so read live data through options.meta facetedUniqueValues: (table, columnId) () { const facetingData table.options.meta?.facetingData return new Mapstring, number(facetingData?.uniqueValues[columnId] ?? []) }, facetedMinMaxValues: (table, columnId) () { return table.options.meta?.facetingData?.minMaxValues[columnId] }, }) const table useTable({ features, columns, meta: { facetingData: facets.data }, data, })为了与内置列分面的行为保持一致针对某一列的服务端查询应当应用其他活跃过滤、但排除该列自身的过滤——这样当前分面始终保留备选选项同时分面之间仍能互相收窄。你也可以直接获取分面值并传给过滤组件完全不使用 TanStack Table 的分面 API。全局分面Global Faceting全局分面在所有可参与全局过滤的叶子列上推导值非常适合为全局过滤器提供自动补全建议或其他元数据。全局分面行模型会应用活跃的列过滤并排除全局过滤本身const globalFacetedRows table.getGlobalFacetedRowModel().flatRows const suggestions Array.from(table.getGlobalFacetedUniqueValues().entries()) const [min, max] table.getGlobalFacetedMinMaxValues() ?? [0, 1]如果表格使用全局过滤需要注册globalFilteringFeature让行过滤管线评估全局过滤列分面使用的同一组分面工厂同样为上述表格级 API 供电。在源码中全局上下文使用内部列 ID__global__请求工厂见 columnFacetingFeature.utils.ts 中的table_getGlobalFaceted*系列函数并且createFacetedUniqueValues与createFacetedMinMaxValues在__global__分支下会自动跨所有可全局过滤的叶子列聚合。当服务端对列分面与全局分面返回不同结果时可以在自定义工厂中根据该 ID 分支const features tableFeatures({ columnFacetingFeature, facetedUniqueValues: (_table, columnId) () { if (columnId __global__) { return new Map(globalFacets.uniqueValues) } return new Map(columnFacets[columnId]?.uniqueValues) }, })小结TanStack Table Octane 的列分面体系可以概括为一条清晰的链路columnFacetingFeature挂载 API →tableFeatures中注册的三个工厂facetedRowModel、facetedUniqueValues、facetedMinMaxValues提供数据 → 列/表格 API 惰性解析工厂并按需记忆化 → 过滤组件借助table.Subscribe订阅过滤状态把分面数据渲染为复选框、下拉框、范围滑杆或自动补全。理解分面行模型排除自身过滤这一核心语义是构建多分面联动过滤界面的关键而分桶与服务端分面则分别解决了高基数列的性能问题与大数据集的完整性问题。你可以直接以 Faceted Filters 与 Bucketed Faceted Filters 两个示例为起点把本文的配置与 API 组合复用到自己的表格中。【免费下载链接】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),仅供参考
返回列表