Svelte 表格列分面(Column Faceting)完整指南:用 TanStack Svelte Table 构建高性能过滤界面
【免费下载链接】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
本文是 TanStack Table 在 Svelte 适配器(@tanstack/svelte-table)下的列分面(Column Faceting)权威指南。分面技术用于从表格行数据中派生过滤界面所需的元信息——可选值、出现次数、数值区间与候选行集合,从而构建带实时计数的复选筛选、下拉联想、范围滑块等过滤 UI。读完本文,你将掌握分面特性(columnFacetingFeature)的注册方式、三个核心 API(getFacetedRowModel/getFacetedUniqueValues/getFacetedMinMaxValues)的用法、分面与过滤的协同规则、连续值的分桶策略、性能优化要点,以及服务端分面与全局分面的完整实现方案。
示例先行
想直接跳到完整实现?仓库中提供了两个开箱即用的 Svelte 示例:
- Faceted Filters(分面过滤示例):包含复选式分面过滤、下拉联想(datalist)、数值区间过滤与全局搜索,支持 100 万行压力测试。
- Bucketed Faceted Filters(分桶分面过滤示例):针对「上次登录时间」「存储容量」等高基数连续值字段的日期分桶与容量分桶过滤。
提示:向
createTable传入 Svelte 状态(如data)时,应使用 getter 形式(get data() { return data })以保持响应式。这一点在 svelte 官方指南 中反复强调,也是后续所有示例的基本写法。
Faceting 设置:注册特性与行模型工厂
在 Svelte 中使用分面功能,第一步是使用tableFeatures组合特性与行模型工厂,再传给createTable。添加columnFacetingFeature会启用分面相关 API;如果使用客户端分面,还需在对应特性之后配置filteredRowModel与facetedRowModel——因为行模型槽位(slot)是类型检查的,顺序错误会导致类型错误。
import { createTable, tableFeatures, columnFacetingFeature, columnFilteringFeature, createFacetedRowModel, createFacetedUniqueValues, createFacetedMinMaxValues, createFilteredRowModel, filterFns, } from '@tanstack/svelte-table' const features = tableFeatures({ columnFacetingFeature, columnFilteringFeature, filteredRowModel: createFilteredRowModel(), // 客户端过滤时需要 // manualFiltering: true, // 服务端手动过滤时开启 facetedRowModel: createFacetedRowModel(), // 客户端分面时需要 facetedUniqueValues: createFacetedUniqueValues(), facetedMinMaxValues: createFacetedMinMaxValues(), filterFns, }) const table = createTable({ features, columns, get data() { return data }, })仓库中的 filters-faceted 示例 features.ts 展示了更完整的真实配置——它还额外注册了globalFilteringFeature、rowPaginationFeature、paginatedRowModel,并只注册示例实际用到的两个过滤函数includesString与inNumberRange:
export const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, columnFacetingFeature, rowPaginationFeature, facetedRowModel: createFacetedRowModel(), facetedMinMaxValues: createFacetedMinMaxValues(), facetedUniqueValues: createFacetedUniqueValues(), filteredRowModel: createFilteredRowModel(), paginatedRowModel: createPaginatedRowModel(), filterFns: { includesString: filterFn_includesString, inNumberRange: filterFn_inNumberRange, }, })从源码看,columnFacetingFeature本身只负责把三个列级 API 挂到列原型、把三个全局 API 挂到表格实例上(见 columnFacetingFeature.ts)。实际的取值逻辑全部委托给你在tableFeatures中注册的工厂函数——这也是「注册哪些工厂,启用哪些能力」的底层原因。
什么是 Faceting?
分面(Faceting)用于派生构建过滤界面所需的信息。针对某一列,分面可以回答如下问题:
- 该列当前可选哪些值?
- 每个值出现的次数是多少?
- 在可用行中,该列的最小值和最大值是多少?
- 哪些行应当用于自定义的分面计算?
例如,一个应用可以用分面渲染这样的「套餐(Plan)」过滤器:
Plan ☐ Free 128 ☐ Pro 47 ☐ Enterprise 9套餐名称与计数都来自表格的分面行模型(faceted row model)。当其他列的过滤条件改变时(例如选择了Region = Europe),套餐计数会自动更新,只描述该区域内的行。
需要特别强调:分面本身不会过滤表格。它只提供值、计数、区间或行集合,供你构建过滤 UI。过滤状态的归属和「哪些行匹配所选值」的判断,由列过滤特性(columnFilteringFeature)负责。
Faceting 与行聚合(Row Aggregation)的区别
分面与行聚合都会汇总数据,但目的截然不同:
- 分面产出的是过滤控件的元数据——可选值、出现次数或数值区间;
- 行聚合(Row Aggregation)对一组行计算结果值——如求和、平均值、总数——用于页脚或分组行展示。
分面计数不会创建聚合行,也不使用列的aggregationFn。区分这两者的最简方式:
- 过滤(Filtering)回答:哪些行保留?
- 分面(Faceting)回答:哪些过滤选项仍然可选?
- 行聚合(Row Aggregation)回答:能从这些行计算出什么汇总值?
列分面如何响应过滤器
一个列的分面行模型包含「通过除该列自身过滤器之外所有适用过滤器」的行。这样做的目的是:当用户正在编辑某个分面时,该分面仍能继续展示备选选项。
考虑一张带Region与Plan过滤器的表格:
- 用户选择
Region = Europe; Plan分面应用区域过滤器,重新计算套餐计数;- 用户选择
Plan = Pro; - 表格只显示欧洲的 Pro 行;
Plan分面仍然基于所有欧洲行计算选项,因为它排除了自己的Plan过滤器。
而其他分面会应用已选的 Plan 过滤器。例如Status分面此时只描述欧洲的 Pro 行——正是这种「互相收窄」的机制让多个分面可以协同工作。
客户端分面需要同时具备filteredRowModel与facetedRowModel才能实现上述行为。没有过滤行模型时,分面行模型会回退到过滤前的行,其值将不会响应其他列的过滤条件。这一回退逻辑在 columnFacetingFeature.utils.ts 中有明确实现:当未注册工厂时,分面行模型直接返回getPreFilteredRowModel()的结果。
Faceting APIs 一览
根据你要构建的过滤界面形态,选择对应的分面 API:
| API | 结果 | 常见用途 |
|---|---|---|
column.getFacetedRowModel() | 通过其他活动过滤器的行 | 自定义分面计算 |
column.getFacetedUniqueValues() | 值到出现次数的Map | 复选框、下拉菜单、自动补全建议 |
column.getFacetedMinMaxValues() | [min, max]元组或undefined | 数字输入框、范围滑块 |
在tableFeatures中注册的行模型工厂启用这些 API:
createFacetedRowModel()—— 客户端分面的必需项;createFacetedUniqueValues()—— 唯一值与计数的必需项;createFacetedMinMaxValues()—— 数值最小值与最大值的必需项。
只注册你的表格实际用到的工厂即可。本指南开头给出的完整配置注册了全部三个。
从实现角度看,工厂的调用是惰性且按列缓存的:column_getFacetedUniqueValues首次读取某列时才会调用注册的工厂并缓存生成的函数(见 createFacetedUniqueValues.ts 与 columnFacetingFeature.utils.ts)。createFacetedUniqueValues内部使用tableMemo做记忆化:其依赖是分面行模型的flatRows,只有当输入行变化时才重新计算计数——这正是性能的基础。
唯一值与计数
column.getFacetedUniqueValues()返回一个Map,键是分面值,值是该值出现的次数。可以将其转换为排序列表,用于自动补全或下拉控件:
const suggestions = Array.from(column.getFacetedUniqueValues().entries()) .sort(([valueA], [valueB]) => String(valueA).localeCompare(String(valueB))) .slice(0, 5_000)每个条目同时包含值与计数:
<select> {#each suggestions as [value, count] (String(value))} <option value={String(value)}> {String(value)} ({count}) </option> {/each} </select>对于标量列,每行通常贡献一个值,因此出现次数等价于行数。若定义了列的getUniqueValues选项,一行可以贡献多个分面值,此时计数描述的是「出现次数」,总和可能大于行数:
columnHelper.accessor('tags', { header: 'Tags', getUniqueValues: (row) => row.tags, })如果你希望每个计数都代表行数,请确保getUniqueValues每行对每个值最多返回一次。计数聚合的逐行遍历逻辑在 createFacetedUniqueValues.ts 中清晰可见——外层循环行、内层循环列,再内层循环该行返回的值数组进行累加。
分面过滤示例的 ColumnFilter.svelte 提供了真实用法:对文本列取getFacetedUniqueValues().keys()排序后渲染进<datalist>,并在占位符中显示可选值总数column.getFacetedUniqueValues().size;对数值列则使用getFacetedMinMaxValues()约束输入框的 min/max。
在 Svelte 中构建响应式分面控件
在渲染控件的组件内部,从columnprop 派生出分面值。Svelte 会在适配器响应式表格状态变化时自动更新组件:
<script lang="ts"> let { column } = $props() const values = $derived(Array.from(column.getFacetedUniqueValues().entries())) </script> {#each values as [value, count] (String(value))} <label> <input type="checkbox" checked={isSelected(value)} onchange={() => toggleValue(value)} /> {String(value)} ({count}) </label> {/each}关键点在于$derived:分面值来自表格适配器的响应式状态,Svelte 的派生逻辑会建立依赖关系,一旦表格状态变化(例如其他列过滤条件改变导致分面行模型重算),组件自动重新渲染。示例中的 ColumnFilter.svelte 采用同样的模式,将column.getFacetedUniqueValues()用$derived包起来,并通过column.setFilterValue切换勾选值。
列的过滤函数仍然决定所选值如何匹配行。过滤函数与过滤状态详见 Column Filtering Guide,完整实现可参考 Faceted Filters 示例。
最小值与最大值
column.getFacetedMinMaxValues()返回应用其他活动过滤器后可用的数值区间,没有数值时返回undefined:
<script lang="ts"> const range = $derived(column.getFacetedMinMaxValues() ?? [0, 1]) </script> <input type="range" min={range[0]} max={range[1]} value={currentValue} oninput={(event) => column.setFilterValue(Number(event.currentTarget.value))} />最小值与最大值描述的是过滤 UI 可用的数值范围。至于某个选中值或区间如何过滤行,仍由列的过滤函数决定。在 filters-faceted 示例 中,数值列用getFacetedMinMaxValues()作为 Min/Max 两个防抖输入框的上下限与占位提示,配合inNumberRange过滤函数实现区间过滤。
面向连续值的分桶分面(Bucketed Faceting)
原始唯一值并不总是好用。日期、文件大小、时长、价格、测量值等字段可能产生成百上千个不同的值。这些列更适合放进有意义的桶(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: Array<StorageBucket>) => selected.includes(bucket), autoRemove: (selected: Array<StorageBucket>) => selected.length === 0, }) columnHelper.accessor('storageBytes', { header: 'Storage', getUniqueValues: (row) => [getStorageBucket(row.storageBytes)], filterFn: storageBucketFilter, })分面与过滤必须使用同一套桶定义,这样展示的计数才能与每个桶勾选后命中的行一致。列仍保留原始数值,因此无需为分面单独创建隐藏的派生列。完整的日期与容量分桶过滤实现见 Bucketed Faceted Filters 示例。
该示例的 buckets.ts 给出了工程化封装:用Bucket<TValue>类型描述「桶值 + 标签 + 判定函数」,lastLoginBuckets与storageBuckets两个数组分别覆盖「今天/昨天/本周/本月/更早」和「<1GB/1–10GB/10–100GB/100+GB」,再由getBucket与createBucketFilter统一生成过滤函数。列的meta.facetOptions供过滤组件渲染勾选列表,filterVariant: 'facets' | 'text'则让同一个 ColumnFilter.svelte 组件按列元信息自动切换分面控件或普通文本输入。
客户端分面与性能
内置的客户端分面行模型是**记忆化(memoized)**的:它们只在其输入行或相关过滤状态变化时重新计算。但计算成本仍然取决于表格的行数、列数与唯一值数量。
针对唯一值很多的列,可以考虑以下方案:
- 只渲染前若干个或最相关的值,而不是遍历整个 Map;
- 渲染长列表之前,先让用户搜索可选值;
- 将连续值或高基数值分桶为有意义的区间;
- 当浏览器中没有完整数据集时,把分面移到服务端。
另外,避免在无关组件中反复排序或转换巨大的分面 Map。分面选项应在订阅相关过滤状态的组件附近派生并渲染——这正是「就近派生」原则:既减少不必要的重算,也让响应式依赖保持清晰。
从源码看,记忆化有两个层次:createFacetedUniqueValues/createFacetedMinMaxValues工厂内部用tableMemo缓存结果(依赖是分面行模型的flatRows),而columnFacetingFeature层的 API 本身不做额外记忆化——如 columnFacetingFeature.ts 注释所述,这避免了冻结那些数据独立变化的自定义工厂,自定义工厂的记忆化由开发者自行负责。
自定义服务端分面
当过滤在服务端执行时,浏览器中加载的行可能不足以计算完整的分面值或计数。此时应在服务端计算分面,并提供自定义的facetedUniqueValues与facetedMinMaxValues工厂。
每个工厂接收表格与列 ID,返回一个解析分面结果的函数。常规列 API(getFacetedUniqueValues等)会返回服务端提供的值。
工厂按表格和列只解析一次,但工厂返回的函数每次读取都会运行——表格不会缓存其结果。因此要在返回的函数内读取实时值(来自 signal、store 或table.options.meta),让更新的服务端分面立即生效;如果计算开销大,则在工厂内部自行记忆化。
const facetingQuery = createQuery() const features = tableFeatures({ columnFacetingFeature, // 返回的函数每次读取都会运行,table.options 与最新渲染保持同步, // 因此要通过 options.meta 读取实时数据 facetedUniqueValues: (table, columnId) => () => { const serverFacets = table.options.meta?.serverFacets return new Map<string, number>(serverFacets?.uniqueValues[columnId] ?? []) }, facetedMinMaxValues: (table, columnId) => () => { return table.options.meta?.serverFacets?.minMaxValues[columnId] }, }) const table = createTable({ features, columns, get meta() { return { serverFacets: facetingQuery.data } }, get data() { return data }, })为了与内置列分面行为一致,针对某一列的服务端查询应该应用其他活动过滤器,但排除该列自身的过滤器。这样当前分面始终保留备选选项,同时各分面之间又能互相收窄。
也可以完全不使用 TanStack Table 的分面 API,直接获取分面值并传给自己的过滤组件——分面 API 只是便利设施,不是强制路径。
全局分面(Global Faceting)
全局分面跨所有「可参与全局过滤的叶子列」派生值,适合为全局搜索框提供自动补全建议或其他关联元数据。全局分面行模型应用活动的列过滤器,并排除全局过滤器自身。
如果表格使用全局过滤,需要注册globalFilteringFeature,让行过滤管线评估全局过滤器。列分面使用的同一组分面工厂也支撑以下表格级 API:
const globalFacetedRows = table.getGlobalFacetedRowModel().flatRows const suggestions = Array.from(table.getGlobalFacetedUniqueValues().entries()) const [min, max] = table.getGlobalFacetedMinMaxValues() ?? [0, 1]自定义分面工厂在处理全局请求时会收到内部列 ID__global__。当服务端对列分面与全局分面分别返回结果时,可以据此分支:
const features = tableFeatures({ columnFacetingFeature, facetedUniqueValues: (_table, columnId) => () => { if (columnId === '__global__') { return new Map(globalFacets.uniqueValues) } return new Map(columnFacets[columnId]?.uniqueValues) }, })从 createFacetedUniqueValues.ts 的实现可以印证:全局上下文会遍历getAllLeafColumns()中所有「可全局过滤」的列进行计数聚合,这与「跨所有可参与全局过滤的叶子列」的语义完全对应。filters-faceted 示例 的 App.svelte 同时注册了globalFilteringFeature与columnFacetingFeature,顶部提供「Search all columns...」防抖搜索框(table.setGlobalFilter),正是全局过滤与分面协同的完整落地。
小结
分面是构建专业过滤体验的核心数据管道:getFacetedRowModel提供候选行、getFacetedUniqueValues提供带计数的选项、getFacetedMinMaxValues提供数值区间,而「排除自身过滤器、应用其他过滤器」的规则让多个分面能够互相收窄。在 Svelte 中,用tableFeatures注册对应工厂、在组件内以$derived派生分面值,即可获得完全响应式的过滤 UI;面对高基数列,分桶是兼顾体验与性能的成熟方案;当数据量超出客户端承载能力时,可通过自定义工厂无缝切换到服务端分面。两个官方示例(filters-faceted、filters-faceted-bucketed)及 table-core 源码 可作为继续深入与二次开发的参照。
【免费下载链接】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),仅供参考