完整指南:用 TanStack Svelte Table 构建高性能过滤界面)
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的注册方式、三个核心 APIgetFacetedRowModel/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与inNumberRangeexport 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 EuropePlan分面应用区域过滤器重新计算套餐计数用户选择Plan Pro表格只显示欧洲的 Pro 行Plan分面仍然基于所有欧洲行计算选项因为它排除了自己的Plan过滤器。而其他分面会应用已选的 Plan 过滤器。例如Status分面此时只描述欧洲的 Pro 行——正是这种「互相收窄」的机制让多个分面可以协同工作。客户端分面需要同时具备filteredRowModel与facetedRowModel才能实现上述行为。没有过滤行模型时分面行模型会回退到过滤前的行其值将不会响应其他列的过滤条件。这一回退逻辑在 columnFacetingFeature.utils.ts 中有明确实现当未注册工厂时分面行模型直接返回getPreFilteredRowModel()的结果。Faceting APIs 一览根据你要构建的过滤界面形态选择对应的分面 APIAPI结果常见用途column.getFacetedRowModel()通过其他活动过滤器的行自定义分面计算column.getFacetedUniqueValues()值到出现次数的Map复选框、下拉菜单、自动补全建议column.getFacetedMinMaxValues()[min, max]元组或undefined数字输入框、范围滑块在tableFeatures中注册的行模型工厂启用这些 APIcreateFacetedRowModel()—— 客户端分面的必需项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 langts let { column } $props() const values $derived(Array.from(column.getFacetedUniqueValues().entries())) /script {#each values as [value, count] (String(value))} label input typecheckbox 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()返回应用其他活动过滤器后可用的数值区间没有数值时返回undefinedscript langts const range $derived(column.getFacetedMinMaxValues() ?? [0, 1]) /script input typerange 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: 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 示例。该示例的 buckets.ts 给出了工程化封装用BucketTValue类型描述「桶值 标签 判定函数」lastLoginBuckets与storageBuckets两个数组分别覆盖「今天/昨天/本周/本月/更早」和「1GB/1–10GB/10–100GB/100GB」再由getBucket与createBucketFilter统一生成过滤函数。列的meta.facetOptions供过滤组件渲染勾选列表filterVariant: facets | text则让同一个 ColumnFilter.svelte 组件按列元信息自动切换分面控件或普通文本输入。客户端分面与性能内置的客户端分面行模型是**记忆化memoized**的它们只在其输入行或相关过滤状态变化时重新计算。但计算成本仍然取决于表格的行数、列数与唯一值数量。针对唯一值很多的列可以考虑以下方案只渲染前若干个或最相关的值而不是遍历整个 Map渲染长列表之前先让用户搜索可选值将连续值或高基数值分桶为有意义的区间当浏览器中没有完整数据集时把分面移到服务端。另外避免在无关组件中反复排序或转换巨大的分面 Map。分面选项应在订阅相关过滤状态的组件附近派生并渲染——这正是「就近派生」原则既减少不必要的重算也让响应式依赖保持清晰。从源码看记忆化有两个层次createFacetedUniqueValues/createFacetedMinMaxValues工厂内部用tableMemo缓存结果依赖是分面行模型的flatRows而columnFacetingFeature层的 API 本身不做额外记忆化——如 columnFacetingFeature.ts 注释所述这避免了冻结那些数据独立变化的自定义工厂自定义工厂的记忆化由开发者自行负责。自定义服务端分面当过滤在服务端执行时浏览器中加载的行可能不足以计算完整的分面值或计数。此时应在服务端计算分面并提供自定义的facetedUniqueValues与facetedMinMaxValues工厂。每个工厂接收表格与列 ID返回一个解析分面结果的函数。常规列 APIgetFacetedUniqueValues等会返回服务端提供的值。工厂按表格和列只解析一次但工厂返回的函数每次读取都会运行——表格不会缓存其结果。因此要在返回的函数内读取实时值来自 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 Mapstring, 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让行过滤管线评估全局过滤器。列分面使用的同一组分面工厂也支撑以下表格级 APIconst 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),仅供参考