Svelte 表格列分面(Column Faceting)完整指南:用 TanStack Svelte Table 构建高性能过滤界面
2026/9/21 18:36:18 网站建设 项目流程

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;如果使用客户端分面,还需在对应特性之后配置filteredRowModelfacetedRowModel——因为行模型槽位(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 展示了更完整的真实配置——它还额外注册了globalFilteringFeaturerowPaginationFeaturepaginatedRowModel,并只注册示例实际用到的两个过滤函数includesStringinNumberRange

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)回答:能从这些行计算出什么汇总值?

列分面如何响应过滤器

一个列的分面行模型包含「通过除该列自身过滤器之外所有适用过滤器」的行。这样做的目的是:当用户正在编辑某个分面时,该分面仍能继续展示备选选项。

考虑一张带RegionPlan过滤器的表格:

  1. 用户选择Region = Europe
  2. Plan分面应用区域过滤器,重新计算套餐计数;
  3. 用户选择Plan = Pro
  4. 表格只显示欧洲的 Pro 行;
  5. Plan分面仍然基于所有欧洲行计算选项,因为它排除了自己的Plan过滤器。

而其他分面会应用已选的 Plan 过滤器。例如Status分面此时只描述欧洲的 Pro 行——正是这种「互相收窄」的机制让多个分面可以协同工作。

客户端分面需要同时具备filteredRowModelfacetedRowModel才能实现上述行为。没有过滤行模型时,分面行模型会回退到过滤前的行,其值将不会响应其他列的过滤条件。这一回退逻辑在 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>类型描述「桶值 + 标签 + 判定函数」,lastLoginBucketsstorageBuckets两个数组分别覆盖「今天/昨天/本周/本月/更早」和「<1GB/1–10GB/10–100GB/100+GB」,再由getBucketcreateBucketFilter统一生成过滤函数。列的meta.facetOptions供过滤组件渲染勾选列表,filterVariant: 'facets' | 'text'则让同一个 ColumnFilter.svelte 组件按列元信息自动切换分面控件或普通文本输入。

客户端分面与性能

内置的客户端分面行模型是**记忆化(memoized)**的:它们只在其输入行或相关过滤状态变化时重新计算。但计算成本仍然取决于表格的行数、列数与唯一值数量。

针对唯一值很多的列,可以考虑以下方案:

  • 只渲染前若干个或最相关的值,而不是遍历整个 Map;
  • 渲染长列表之前,先让用户搜索可选值;
  • 将连续值或高基数值分桶为有意义的区间;
  • 当浏览器中没有完整数据集时,把分面移到服务端。

另外,避免在无关组件中反复排序或转换巨大的分面 Map。分面选项应在订阅相关过滤状态的组件附近派生并渲染——这正是「就近派生」原则:既减少不必要的重算,也让响应式依赖保持清晰。

从源码看,记忆化有两个层次:createFacetedUniqueValues/createFacetedMinMaxValues工厂内部用tableMemo缓存结果(依赖是分面行模型的flatRows),而columnFacetingFeature层的 API 本身不做额外记忆化——如 columnFacetingFeature.ts 注释所述,这避免了冻结那些数据独立变化的自定义工厂,自定义工厂的记忆化由开发者自行负责。

自定义服务端分面

当过滤在服务端执行时,浏览器中加载的行可能不足以计算完整的分面值或计数。此时应在服务端计算分面,并提供自定义的facetedUniqueValuesfacetedMinMaxValues工厂。

每个工厂接收表格与列 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 同时注册了globalFilteringFeaturecolumnFacetingFeature,顶部提供「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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询