TanStack Lit Table 全局过滤(Global Filtering)实战指南:从客户端筛选到服务端过滤
【免费下载链接】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/lit-table的全局过滤(Global Filtering)能力为核心,完整讲解如何在 Lit 自定义元素中基于TableController接入跨列搜索:包括特性注册顺序、客户端与服务端两种过滤模式的取舍、12 个内置过滤函数的选择、全局过滤状态的读取与控制、搜索输入框的 UI 接入,以及自定义过滤函数的写法。读完本文,你将能在一个 Lit 表格组件中实现"一个输入框搜索所有列"的完整方案,并理解其底层 row model 与状态切片机制。
快速开始:全局过滤的最小配置
全局过滤(Global Filtering)是作用于所有列的过滤方式,与逐列过滤(Column Filtering)相对。在 Lit 适配器中,全局过滤依赖列过滤特性,因此特性注册有严格顺序:先注册columnFilteringFeature,再注册globalFilteringFeature。如果使用客户端过滤,还需在tableFeatures的 row model 槽位上注册filteredRowModel——row model 槽位是类型检查的,缺少它会直接编译报错。
以下是最小可运行的全局过滤配置:
import { LitElement, html } from 'lit' import { customElement, state } from 'lit/decorators.js' import { TableController, tableFeatures, columnFilteringFeature, globalFilteringFeature, createFilteredRowModel, filterFn_includesString, } from '@tanstack/lit-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel(), // if using client-side filtering // manualFiltering: true, // if using manual server-side filtering filterFns: { includesString: filterFn_includesString }, }) @customElement('my-table') class MyTable extends LitElement { @state() private data = defaultData private tableController = new TableController(this) protected render() { const table = this.tableController.table({ features, columns, data: this.data, }) return html`...` } }其中tableFeatures({ ... })把特性、row model 槽位和过滤函数注册表打包成一个可复用的features对象,传入每次渲染中的this.tableController.table(options)。这与仓库中 filters 示例 的做法一致——该示例同时注册了columnFilteringFeature、rowPaginationFeature、createFilteredRowModel()和createPaginatedRowModel(),并只注册实际用到的四个过滤函数。
关于
filterFns注册表:上面的配置只列出了本表用到的内置过滤函数。虽然直接展开整个内置注册表(filterFns: { ...filterFns })也能工作,但会把全部内置过滤函数打进打包产物。建议只注册实际用到的函数,或者干脆把函数直接传给globalFilterFn选项而无需任何注册。从源码看,filterFns.ts 中的完整注册表导出本身就标注为@deprecated,官方推荐逐个导入filterFn_*以减小包体积。
客户端过滤还是服务端过滤
过滤应当与排序、分页基于同一份数据集。判断依据很简单:
- 浏览器持有完整数据集时,使用客户端过滤;
- 浏览器只拿到一页或一个子集时,应使用服务端过滤——除非你刻意只对已加载的行做过滤。
完整的决策框架、性能因素以及多种数据操作组合的指引,参见仓库根目录的 客户端与服务端数据操作指南。
此外,客户端filteredRowModel在全局过滤输入变化时会触发页码自动重置钩子。是否重置取决于autoResetPageIndex、autoResetAll和manualPagination选项的组合。若过滤是手动的且未注册(或被绕过)该 row model,全局过滤状态变化不会触发该钩子,此时需要在过滤变更处理器中手动重置服务端分页。
从实现上看,createFilteredRowModel在 createFilteredRowModel.ts 中把columnFilters与globalFilter两个状态原子作为 memo 依赖,并在更新后通过onAfterUpdate回调触发table_autoResetPageIndex——这正是"全局过滤输入变化 → 页码自动重置"这一行为的源码依据。
手动服务端全局过滤
如果决定不使用内置的客户端全局过滤,而是自己实现服务端过滤,做法如下:
服务端全局过滤不需要filteredRowModel。传给表格的data应当已经是过滤后的数据。不过,如果你在features里注册了filteredRowModel,可以通过设置manualFiltering: true让表格跳过它:
import { TableController, tableFeatures, columnFilteringFeature, globalFilteringFeature, } from '@tanstack/lit-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, }) const table = this.tableController.table({ features, data: this.data, columns, manualFiltering: true, })注意:使用手动全局过滤时,本文后面讨论的许多选项都不会生效。当manualFiltering为true,表格实例不会对传入的行应用任何全局过滤逻辑,而是假定行已被过滤,原样使用你传入的数据。
客户端全局过滤
使用内置客户端全局过滤时,把globalFilteringFeature(连同其前置依赖columnFilteringFeature)加入features,并把filteredRowModel加入 row model 槽位:
import { TableController, tableFeatures, columnFilteringFeature, globalFilteringFeature, createFilteredRowModel, filterFn_includesString, } from '@tanstack/lit-table' const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, filteredRowModel: createFilteredRowModel(), filterFns: { includesString: filterFn_includesString }, }) const table = this.tableController.table({ features, // other options... })客户端过滤的执行链路在源码中非常清晰:createFilteredRowModel会取出全局过滤函数table_getGlobalFilterFn,过滤出所有column_getCanGlobalFilter返回true的叶子列,把全局过滤值通过resolveFilterValue归一化后对每一行逐一求值;只要任意一列命中即保留该行(对应row.columnFilters.__global__ = true并break的逻辑),随后再与列过滤结果做水平合并过滤。这就是"全局过滤是 OR 语义、跨所有可过滤列"的底层实现,参见 createFilteredRowModel.ts。
选择全局过滤函数:globalFilterFn
globalFilterFn选项用于指定全局过滤使用的过滤函数。它可以是:
- 字符串,引用注册在
tableFeatures的filterFns槽位上的内置或自定义过滤函数; - 函数本身,直接传入。
const table = this.tableController.table({ features, data: this.data, columns, globalFilterFn: 'includesString', // built-in filter function })源码中的解析逻辑(见 globalFilteringFeature.utils.ts)表明:函数值直接返回;'auto'委托给默认的includesString(即table_getGlobalAutoFilterFn返回filterFn_includesString);字符串则从表格的过滤函数注册表中查找;在开发环境下查找不到会输出globalFilterFn 'xxx' is not registered的警告。
默认情况下有 12 个内置过滤函数可供选择:
| 过滤函数 | 语义 |
|---|---|
includesString | 不区分大小写的字符串包含 |
includesStringSensitive | 区分大小写的字符串包含 |
equalsString | 不区分大小写的字符串相等 |
equals | 严格相等=== |
weakEquals | 宽松相等== |
arrIncludes | 行的数组(或字符串)值包含至少一个过滤值 |
arrIncludesAll | 行的数组值包含全部过滤值 |
arrIncludesSome | 行的数组值包含至少一个过滤值 |
arrHas | 行的标量值等于至少一个过滤值 |
inNumberRange | 闭区间[min, max]数字范围(端点归一化,反序自动交换) |
between | 开区间 min/max(空白端点视为开区间) |
betweenInclusive | 闭区间 min/max(空白端点视为开区间) |
需要说明的是,这 12 个是文档针对全局过滤场景列出的常用集合;filterFns.ts 中的完整内置注册表还包含startsWith、endsWith、empty、notEmpty、equalsStringSensitive、inDateRange、greaterThan、greaterThanOrEqualTo、lessThan、lessThanOrEqualTo等函数,均可按需导入并注册后用于globalFilterFn。
你也可以自定义全局过滤函数并直接传给globalFilterFn,见下文 自定义全局过滤函数。
全局过滤状态:读取与控制
globalFilter状态切片保存当前全局过滤值,通常是一个搜索字符串(切片类型是any,以便自定义过滤函数接收其他形状的值)。
在render方法中读取时,使用table.state.globalFilter(这是经过传入tableController.table的选择器选取后的状态)。在事件处理器或其他非渲染代码中,可以用table.atoms.globalFilter.get()读取当前快照。
方式一:外部 Atom(v9 推荐)
如果需要在表格之外访问全局过滤状态,推荐用外部 atom 通过atoms表格选项来"拥有"这个切片。Atom 保持细粒度订阅,过滤值可以从任意模块读取或订阅(例如用作服务端过滤的 query key),无需经过持有表格的组件:
import { createAtom } from '@tanstack/store' // create a stable atom at module scope (or in a shared store module) const globalFilterAtom = createAtom<string>('') // inside your element's render method const table = this.tableController.table({ features, // other options... atoms: { globalFilter: globalFilterAtom, // table.setGlobalFilter now updates globalFilterAtom }, }) const globalFilter = globalFilterAtom.get() // read the atom wherever you need the value方式二:受控 state(v8 风格兼容)
v8 风格的state.globalFilter加onGlobalFilterChange模式仍然受支持。它适合简单集成或迁移 v8 代码,但粒度不如外部 atom 精细。更深入的对比见 表格状态指南。
@state() private globalFilter: string = '' const table = this.tableController.table({ features, // other options... state: { globalFilter: this.globalFilter, }, onGlobalFilterChange: (updater) => { this.globalFilter = typeof updater === 'function' ? updater(this.globalFilter) : updater }, })注意:在这个模式下,Lit 组件自身的@state()与表格状态双向联动,TableController在hostUpdated阶段通过table_publishExternalState把外部状态发布回表格 store——这也是 Lit 适配器与 table-core 状态系统对接的核心机制,见 TableController.ts。
在 UI 中添加全局过滤输入框
TanStack Table不会为你添加全局过滤的输入 UI,需要手动把输入框放进模板。一个典型做法是在表格上方放一个搜索输入框:用table.state.globalFilter读值,用table.setGlobalFilter更新:
html` <input type="text" .value=${String(table.state.globalFilter ?? '')} @input=${(e: InputEvent) => table.setGlobalFilter((e.target as HTMLInputElement).value)} placeholder="Search all columns..." /> `setGlobalFilter的 updater 支持"新值"或"以旧值为参数的函数"两种形式,源码中它直接路由到onGlobalFilterChange处理器(见 globalFilteringFeature.utils.ts)。由于全局过滤值是标量,store 自身的Object.is身份判断会自动抑制无操作写入。
自定义全局过滤函数
自定义函数只需接收(row, columnId, filterValue)三个参数,返回布尔值表示该行是否应保留:
const customFilterFn = (row, columnId, filterValue) => { return // true if the row should be included in the filtered rows } const table = this.tableController.table({ features, // other options... globalFilterFn: customFilterFn, })一个常见的思路是为全局过滤使用模糊匹配(fuzzy filtering)函数,这在 模糊过滤指南 中有专门讨论,对应的可运行示例见 filters-fuzzy 示例。
设置初始全局过滤状态
若想在表格初始化时设置全局过滤的初始值,可通过initialState传入。但如果你自己控制该切片(使用外部 atom 或响应式属性),则应改在 atom 或属性上设置起始值:
const table = this.tableController.table({ features, // other options... initialState: { globalFilter: 'search term', // if not controlling globalFilter state, set initial state here }, })不要同时使用
initialState.globalFilter和受控的globalFilter(通过atoms或state),因为受控值会覆盖initialState.globalFilter。
从源码看,globalFilteringFeature的getInitialState为切片提供默认值undefined,并把用户传入的initialState合并到其后(globalFilteringFeature.ts)。而table.resetGlobalFilter无参数时会把状态重置为table.initialState.globalFilter的克隆,传入true则忽略初始状态、重置为undefined。
禁用全局过滤
默认情况下,全局过滤对所有列启用。可以通过表格选项enableGlobalFilter对所有列禁用全局过滤,也可以设置enableFilters: false同时关闭列过滤与全局过滤。
禁用全局过滤会使对应列的column.getCanGlobalFilter返回false。
const columns = [ { header: () => 'Id', accessorKey: 'id', enableGlobalFilter: false, // disable global filtering for this column }, //... ] //... const table = this.tableController.table({ features, // other options... columns, enableGlobalFilter: false, // disable global filtering for all columns })column.getCanGlobalFilter的判定逻辑在 globalFilteringFeature.utils.ts 中一目了然:列定义上的enableGlobalFilter(默认true)、表格选项enableGlobalFilter(默认true)、表格选项enableFilters(默认true)、可选的getColumnCanGlobalFilter回调,以及该列必须存在 accessor(!!column.accessorFn),五者全部通过才返回true。此外特性默认的getColumnCanGlobalFilter还要求列值类型为字符串或数字(参见 globalFilteringFeature.ts)。
全局过滤 API 速查
以下 API 用于搭建全局过滤 UI 时非常有用:
table.setGlobalFilter— 设置全局过滤值。适合接入搜索输入框的@input处理器。table.resetGlobalFilter— 重置全局过滤值为初始状态;传入table.resetGlobalFilter(true)则清空。table.getGlobalFilterFn— 返回当前用于全局过滤的过滤函数。table.getGlobalAutoFilterFn— 返回默认全局过滤函数(当前为includesString)。column.getCanGlobalFilter— 返回某列是否参与全局过滤。调试哪些列会被搜索时很有用。
这些 API 由globalFilteringFeature通过assignTableAPIs与assignPrototypeAPIs统一挂载到表格实例和列原型上(见 globalFilteringFeature.ts),并且均从@tanstack/lit-table的入口直接导出(该包重新导出了@tanstack/table-core的全部内容,见 index.ts)。
小结
全局过滤为 Lit 表格提供了"一个搜索框过滤所有列"的能力,其使用要点可归纳为:
- 特性顺序:
columnFilteringFeature必须在globalFilteringFeature之前注册;客户端过滤还需filteredRowModel。 - 模式选择:完整数据集在浏览器端用客户端过滤;仅持有子集时用
manualFiltering: true配合服务端过滤。 - 函数注册:按需导入并注册
filterFn_*,避免整包filterFns打进产物;默认的globalFilterFn为includesString。 - 状态管理:渲染中读
table.state.globalFilter,事件中写table.setGlobalFilter;需要跨模块共享时优先用外部 atom。 - UI 自理:搜索输入框需手动添加,配合
initialState、enableGlobalFilter与重置 API 即可完成完整交互闭环。
仓库中的 filters 示例(含列过滤与全局过滤的组合、5 万行数据基准与百万行压力测试按钮)和 filters-fuzzy 示例(模糊全局搜索)是进一步动手实践的起点。
【免费下载链接】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),仅供参考