TanStack Alpine Table 排序(Sorting)完全指南:客户端排序、多列排序与服务端排序实战
【免费下载链接】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 在 Alpine 框架下的适配包@tanstack/alpine-table为对象,系统讲解其排序(Sorting)特性的完整实现路径:从启用rowSortingFeature与createSortedRowModel的初始化配置,到排序状态(state)的三种管理方式、6 个内置排序函数的取舍、自定义排序函数的编写,再到禁用排序、排序方向、多列排序(multi-sorting)、未定义值处理等全部可定制项。读完本篇,你将能独立为 Alpine 应用接入可点击表头的客户端排序,也能平滑切换到服务端手动排序(manualSorting)模式。
快速上手:Alpine 排序示例
仓库中提供了一个完整可运行的排序示例(含 1,000 行演示数据,并内置 "Stress Test (1M rows)" 压力测试按钮),可以直接对照阅读:
- Alpine Sorting 示例(入口为 index.html 与 src/main.ts)
- 示例还附带 Playwright 端到端冒烟测试 smoke.spec.ts,验证了表格渲染、表头可见性以及点击 "Regenerate Data" 后首行数据确实变化等关键行为。
一个重要的 Alpine 使用要点:创建表格时,请通过 getter 读取响应式输入(例如用Alpine.reactive作为数据后备),这样表格才能感知到数据更新。示例中正是这样实现的:
const local = Alpine.reactive({ data: makeData(1_000) }) const table = createTable({ features, columns, get data() { return local.data }, })排序功能启用:Sorting Setup
在 Alpine 中启用排序特性只需要在tableFeatures中注册特性与行模型:
import { createSortedRowModel, createTable, rowSortingFeature, sortFn_alphanumeric, sortFn_datetime, sortFn_text, tableFeatures, } from '@tanstack/alpine-table' const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), // if using client-side sorting // manualSorting: true, // if using manual server-side sorting sortFns: { alphanumeric: sortFn_alphanumeric, datetime: sortFn_datetime, text: sortFn_text, }, }) const table = createTable({ features, columns, get data() { return local.data }, })要点说明:
- 添加
rowSortingFeature后,排序相关的 API 与状态才会存在;如果需要客户端排序,还必须在其后配置sortedRowModel,因为行模型插槽(row model slots)是类型检查的。 - 在源码层面,
@tanstack/alpine-table只是对@tanstack/table-core的整体再导出(见 packages/alpine-table/src/index.ts),排序的核心实现位于 packages/table-core/src/features/row-sorting/,其中 rowSortingFeature.ts 定义了状态与 API,createSortedRowModel.ts 实现了排序行模型。
[!NOTE] 将整个内置注册表展开(
sortFns: { ...sortFns })仍然可用,但会把每个内置排序函数都打进你的打包产物。推荐只注册你用到的函数,或者直接把函数传给列的sortFn选项。默认的sortFn: 'auto'会根据列的数据类型,从注册表中解析为alphanumeric、text或datetime,所以请注册你的列真正依赖的那些函数。源码中 sortFns.ts 也明确标注:整体导出的sortFns注册表会破坏 tree-shaking,被标记为@deprecated。
排序状态(Sorting State)
排序状态被定义为对象数组,结构如下:
type ColumnSort = { id: string desc: boolean } type SortingState = ColumnSort[]由于排序状态是数组,因此可以同时对多列进行排序(详见下文多列排序)。
读取排序状态
表格的状态 atom 在 Alpine 中具有响应性。table.atoms.sorting.get()在 Alpine 绑定(x-text、x-html、:value、x-if、x-for、x-effect,或你Alpine.data对象上的 getter/方法)内是一次响应式读取;在事件处理器等未被跟踪的代码中,同样的调用只是返回当前值。table.store.get()则返回一份当前完整状态的快照,便于调试。
table.atoms.sorting.get() // reactive read inside Alpine bindings, plain read elsewhere不过,如果你需要在表格之外访问排序状态,可以按下面介绍的方式“控制(control)”它。
受控排序状态(Controlled Sorting State)
如果你需要在应用的其他部分方便地访问排序状态,可以自行拥有这段状态。v9 推荐的方式是通过atoms表格选项传入一个外部 atom。@tanstack/store本来就是@tanstack/alpine-table的依赖,所以createAtom开箱即用。这个 atom 可以在别处被读取、写入或订阅(比如用作服务端排序的 query key),而无需让表格依赖组件局部状态。
import { createAtom } from '@tanstack/store' const sortingAtom = createAtom<SortingState>([]) // can set initial sorting state here // subscribe to the atom wherever you need the value (e.g. for a query key) sortingAtom.subscribe(() => { // react to sorting changes }) const table = createTable({ features, columns, get data() { return local.data }, atoms: { sorting: sortingAtom, // table sorting APIs now update sortingAtom }, })此外,v8 风格的state.sorting加onSortingChange模式仍然受支持,适合简单集成或迁移 v8 代码。方式是在Alpine.reactive中持有状态切片:
const local = Alpine.reactive({ sorting: [] as SortingState }) const table = createTable({ features, columns, get data() { return local.data }, state: { get sorting() { return local.sorting // connect the reactive slice back down to the table }, }, onSortingChange: (updater) => { local.sorting = typeof updater === 'function' ? updater(local.sorting) : updater }, })两种受控方式的深入对比可参考 Table State 指南。
初始排序状态(Initial Sorting State)
如果你不需要在自己的状态管理或作用域内控制排序状态,但仍想设置初始排序,可以使用initialState表格选项,而不是state:
const table = createTable({ features, columns, get data() { return local.data }, initialState: { sorting: [ { id: 'name', desc: true, // sort by name in descending order by default }, ], }, })[!NOTE] 不要同时使用
initialState.sorting和state.sorting,因为受控的state.sorting值会覆盖initialState.sorting。
客户端排序与服务端排序
排序应与过滤、分页操作同一份数据集。如果服务端只返回一页或已过滤的子集,客户端排序只能对已加载的这些行排序,而不是完整数据集。完整的决策框架,以及哪些场景下刻意混用客户端与服务端操作是合理的,请参见 Client-Side vs Server-Side Guide。
另外要注意:客户端排序行模型在排序输入变化时会触发 page-index 自动重置钩子。页面索引是否重置取决于autoResetPageIndex、autoResetAll和manualPagination选项。如果排序是手动的,且该行模型被省略或绕过,排序状态变化不会触发该钩子——这时若需要重置服务端分页,请在排序变化处理器中自行处理。
手动服务端排序(Manual Server-Side Sorting)
如果你计划在后端逻辑中自行完成服务端排序,就不需要提供排序行模型。但如果你已经提供了排序行模型却想禁用它,可以使用manualSorting表格选项:
import { createAtom } from '@tanstack/store' const features = tableFeatures({ rowSortingFeature }) // feature needed for sorting state/APIs const sortingAtom = createAtom<SortingState>([]) const table = createTable({ features, columns, get data() { return local.data }, manualSorting: true, // use pre-sorted row model instead of sorted row model atoms: { sorting: sortingAtom, }, })将排序状态提升到自己的作用域(通过外部 atom 或state.sorting加onSortingChange模式)的方法,已在上文受控排序状态中介绍。此例中把外部 atom 订阅到 query key,即可在服务端数据变更后自动重新请求。
[!NOTE] 当
manualSorting为true时,表格会假设你提供的数据已经是排序好的,不会再对其应用任何排序。
客户端排序(Client-Side Sorting)
实现客户端排序,需要在 features 中添加rowSortingFeature和sortedRowModel工厂,并从@tanstack/alpine-table导入createSortedRowModel以及你要用的各个排序函数:
import { createSortedRowModel, createTable, rowSortingFeature, sortFn_alphanumeric, sortFn_datetime, sortFn_text, tableFeatures, } from '@tanstack/alpine-table' const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns: { alphanumeric: sortFn_alphanumeric, datetime: sortFn_datetime, text: sortFn_text, }, }) const table = createTable({ features, columns, get data() { return local.data }, })排序示例 src/main.ts 正是采用这套配置,并为 10 列数据分别应用了字符串列默认升序、数字列默认降序、sortUndefined: 'last'、invertSorting等典型列配置。
排序行模型函数(Sorting RowModelFns)
所有列的默认排序函数会根据列的数据类型自动推断。但为特定列精确指定排序函数通常很有用,尤其是当数据可空或不属于标准数据类型时。
可以用sortFn列选项为每一列指定自定义排序函数。默认情况下,有 6 个内置排序函数可供选择:
alphanumeric— 混合字母数字值排序,不区分大小写。较慢,但如果字符串中包含需要自然排序的数字则更准确(例如item2排在item10之前)。alphanumericCaseSensitive— 混合字母数字值排序,区分大小写。较慢,但含数字字符串时更准确。text— 文本/字符串值排序,不区分大小写。更快,但如果字符串中包含数字则不太准确。textCaseSensitive— 文本/字符串值排序,区分大小写。更快,但不适合含数字的字符串。datetime— 按时间排序,值类型为Date对象时使用。basic— 使用基本的a > b ? 1 : a < b ? -1 : 0比较。最快的排序函数,但可能不够准确。
从源码看,这些函数都通过constructSortFn构建,位于 packages/table-core/src/features/row-sorting/sortFns.ts:例如sortFn_datetime在比较前用resolveDataValue把Date转为getTime()时间戳(源码使用>和<而非==,因为 Date 对象即使时间相同也不相等);sortFn_alphanumeric则先把值转为小写字符串,再用分块算法逐块比较字符与数字(见compareAlphanumeric),这正是"自然排序"的实现来源。
你也可以定义自己的自定义排序函数,既可以内联作为sortFn列选项,也可以按名称注册到你传给createSortedRowModel的排序函数注册表中。
自定义排序函数(Custom Sorting Functions)
无论是注册到createSortedRowModel的注册表,还是直接作为sortFn列选项传递,自定义排序函数都应具有以下签名:
// optionally use the SortFn to infer the parameter types const myCustomSortFn: SortFn<TFeatures, TData> = ( rowA: Row<TFeatures, TData>, rowB: Row<TFeatures, TData>, columnId: string, ) => { return // -1, 0, or 1 - access any row data using rowA.original and rowB.original }[!NOTE] 比较函数不需要考虑列是降序还是升序,行模型会处理这部分逻辑。
sortFn只需要提供一致的比较结果。
每个排序函数接收两行和一个列 ID,预期用列 ID 比较两行并返回-1、0或1(升序语义)。对照速查表:
| 返回值 | 升序语义 |
|---|---|
-1 | a < b |
0 | a === b |
1 | a > b |
完整示例(同时演示按名称引用内置函数、按名称引用注册的自定义函数、直接内联自定义函数三种方式):
const columns = [ { header: () => 'Name', accessorKey: 'name', sortFn: 'alphanumeric', // use built-in sorting function by name }, { header: () => 'Age', accessorKey: 'age', sortFn: 'myCustomSortFn', // reference a custom sorting function registered with createSortedRowModel }, { header: () => 'Birthday', accessorKey: 'birthday', sortFn: 'datetime', // recommended for date columns }, { header: () => 'Profile', accessorKey: 'profile', // use custom sorting function directly sortFn: (rowA, rowB, columnId) => { return rowA.original.someProperty - rowB.original.someProperty }, }, ] //... const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns: { alphanumeric: sortFn_alphanumeric, datetime: sortFn_datetime, myCustomSortFn: (rowA, rowB, columnId) => rowA.original[columnId] > rowB.original[columnId] ? 1 : rowA.original[columnId] < rowB.original[columnId] ? -1 : 0, }, }) const table = createTable({ features, columns, get data() { return local.data }, })TypeScript 提示:要让
sortFn: 'myCustomSortFn'这样的字符串引用通过类型检查,请把函数注册到tableFeatures的sortFns插槽上(如上所示)。该插槽就是注册表,无需declare module增强。另一种做法是绕开注册表,直接把函数传给sortFn列选项。
示例仓库中的枚举列排序是一个很好的实战案例:main.ts 用sortStatusFn把status列的枚举值(single、complicated、relationship)按自定义的statusOrder顺序排序,而非字典序。
自定义排序函数行为(Customize Sorting Function Behavior)
排序函数支持一个可选的"挂载(hanging)"属性:
sortFn.resolveDataValue— 在比较两侧之前,先对每行的值做归一化。所有用constructSortFn辅助函数构建的排序函数(包括全部内置函数)都会尊重它。
constructSortFn用一个值级比较器(sort)加上可选解析器来构建排序函数。把比较逻辑留在sort、归一化留在resolveDataValue,意味着某个现有排序函数的变体只需替换解析器。定义会挂载到返回的函数上,所以你可以展开(spread)任何用constructSortFn构建的排序函数,只覆盖不同的部分——这正是 sortFns.ts 的实现方式。
例如,忽略变音符号(diacritics)的alphanumeric变体,让 "Éric Bernard" 排在 "Eric Brandon" 旁边,而不是排在 "Zak O'Sullivan" 后面:
const stripDiacritics = (value: string) => value.normalize('NFD').replace(/\p{Diacritic}/gu, '') const alphanumericIgnoreDiacritics = constructSortFn({ ...sortFn_alphanumeric, // reuse the comparator resolveDataValue: (value) => stripDiacritics(sortFn_alphanumeric.resolveDataValue!(value)), }) const features = tableFeatures({ rowSortingFeature, sortedRowModel: createSortedRowModel(), sortFns: { alphanumeric: sortFn_alphanumeric, alphanumericIgnoreDiacritics }, })同样的模式也适用于从零定义新的排序函数:
const byLastName = constructSortFn({ sort: (dataValueA, dataValueB) => dataValueA === dataValueB ? 0 : dataValueA > dataValueB ? 1 : -1, resolveDataValue: (value) => String(value ?? '') .split(' ') .at(-1) ?? '', })自定义排序行为(Customize Sorting)
表格和列有大量选项可以进一步定制排序的交互体验与行为。
禁用排序(Disable Sorting)
可以用enableSorting列选项或表格选项,禁用某一列或整张表的排序:
const columns = [ { header: () => 'ID', accessorKey: 'id', enableSorting: false, // disable sorting for this column }, { header: () => 'Name', accessorKey: 'name', }, //... ] //... const table = createTable({ features, columns, get data() { return local.data }, enableSorting: false, // disable sorting for the entire table })排序方向(Sorting Direction)
默认情况下,使用toggleSortingAPI 循环列排序时,字符串列第一次排序为升序,数字列第一次排序为降序。可以用sortDescFirst列选项或表格选项改变这一行为:
const columns = [ { header: () => 'Name', accessorKey: 'name', sortDescFirst: true, // sort by name in descending order first (default is ascending for string columns) }, { header: () => 'Age', accessorKey: 'age', sortDescFirst: false, // sort by age in ascending order first (default is descending for number columns) }, //... ] //... const table = createTable({ features, columns, get data() { return local.data }, sortDescFirst: true, // sort by all columns in descending order first (default is ascending for string columns and descending for number columns) })[!NOTE] 建议在任何包含可空值的列上显式设置
sortDescFirst列选项。如果列包含可空值,表格可能无法正确判断该列是数字还是字符串。
反转排序(Invert Sorting)
反转排序不同于改变默认排序方向。如果某列的invertSorting列选项为true,"desc/asc" 排序状态仍会正常循环,但行的实际排序会被反转。这对数值越小越好的倒置标度(如排名 1st、2nd、3rd,或高尔夫式计分)非常有用:
const columns = [ { header: () => 'Rank', accessorKey: 'rank', invertSorting: true, // invert the sorting for this column. 1st -> 2nd -> 3rd -> ... even if "desc" sorting is applied }, //... ]未定义值排序(Sort Undefined Values)
任何 undefined 值都会根据sortUndefined列选项或表格选项被排到列表的开头或结尾。如果不指定,sortUndefined的默认值是1,undefined 值按较低优先级(降序)排序,即升序时 undefined 出现在列表末尾。
'first'— Undefined 值被推到列表开头'last'— Undefined 值被推到列表末尾false— Undefined 值像其他值一样传给排序函数,不做特殊处理,由排序函数自己负责-1— Undefined 值按较高优先级(升序)排序(升序时 undefined 出现在列表开头)1— Undefined 值按较低优先级(降序)排序(升序时 undefined 出现在列表末尾)
[!NOTE]
'first'和'last'选项在 v9 中可用。
const columns = [ { header: () => 'Rank', accessorKey: 'rank', sortUndefined: -1, // 'first' | 'last' | 1 | -1 | false }, ]示例中 main.ts 对lastName与visits两列使用了sortUndefined: 'last',确保有 null 值时这些列仍能稳定排序。
移除排序(Sorting Removal)
默认情况下,在列上循环排序状态时可以移除排序。可以用enableSortingRemoval表格选项禁用此行为,这在你想确保至少有一列始终处于排序状态时很有用。
使用getToggleSortingHandler或toggleSortingAPI 时,默认的循环行为如下(第一个方向取决于列的数据类型与sortDescFirst选项,见上文排序方向;此处以字符串列为例):
'none' -> 'asc' -> 'desc' -> 'none' -> 'asc' -> 'desc' -> ...
如果禁用了排序移除,'none'状态在第一次排序后就会被跳过:
'none' -> 'asc' -> 'desc' -> 'asc' -> 'desc' -> ...
一旦某列已排序且enableSortingRemoval为false,在该列上切换排序永远不会移除排序。但如果用户排序了另一列且不是多排序事件,排序会从上一列移除并只应用到新列。
若想确保至少一列始终被排序,请将
enableSortingRemoval设为false。
const table = createTable({ features, columns, get data() { return local.data }, enableSortingRemoval: false, // disable the ability to remove sorting on columns (sorting can never return to 'none' once applied) })多列排序(Multi-Sorting)
如果使用column.getToggleSortingHandlerAPI,多列排序默认是启用的。用户按住Shift键点击列表头时,表格会在已排序的列基础上再对该列排序。如果使用column.toggleSortingAPI,则必须手动传入是否使用多列排序(column.toggleSorting(desc, multi))。
禁用多列排序
可以用enableMultiSort列选项或表格选项,为特定列或整张表禁用多列排序。为特定列禁用多列排序时,会用新列的排序替换所有现有排序:
const columns = [ { header: () => 'Created At', accessorKey: 'createdAt', enableMultiSort: false, // always sort by just this column if sorting by this column }, //... ] //... const table = createTable({ features, columns, get data() { return local.data }, enableMultiSort: false, // disable multi-sorting for the entire table })自定义多列排序触发键
默认使用Shift键触发多列排序。可以用isMultiSortEvent表格选项改变这一行为,甚至可以指定所有排序事件都触发多列排序(自定义函数返回true):
const table = createTable({ features, columns, get data() { return local.data }, isMultiSortEvent: (e) => true, // normal click triggers multi-sorting //or isMultiSortEvent: (e) => e.ctrlKey || e.shiftKey, // also use the `Ctrl` key to trigger multi-sorting })多列排序上限
默认情况下,同时排序的列数没有限制。可以用maxMultiSortColCount表格选项设置上限:
const table = createTable({ features, columns, get data() { return local.data }, maxMultiSortColCount: 3, // only allow 3 columns to be sorted at once })移除多列排序
默认情况下,移除多列排序是启用的。可以用enableMultiRemove表格选项禁用此行为:
const table = createTable({ features, columns, get data() { return local.data }, enableMultiRemove: false, // disable the ability to remove multi-sorts })接入排序 UI(Wiring up the sort UI)
由于 Alpine 不会在通过x-html设置的内容里初始化指令,表头内容要用x-html="FlexRender({ header })"渲染,但点击处理器要挂在它外面的真实元素上,并用事件调用getToggleSortingHandler返回的处理器:
<th> <template x-if="!header.isPlaceholder"> <div :style="header.column.getCanSort() ? 'cursor: pointer' : ''" @click="header.column.getToggleSortingHandler()?.($event)" > <span x-html="FlexRender({ header })"></span ><span x-text="({ asc: ' 🔼', desc: ' 🔽' })[header.column.getIsSorted()] ?? ''" ></span> </div> </template> </th>真实示例 index.html 中的实现与之对应:可排序列通过header.column.getCanSort()决定是否加sortable-header类与cursor: pointer样式,点击事件调用getToggleSortingHandler(),方向指示器则由sortIndicator(header.column.getIsSorted())返回的🔼/🔽文本渲染。
数据变化时重置排序(Reset Sorting When Data Changes)
默认情况下,data选项变化时排序状态会被保留。设置autoResetSorting: true可以在处理新的数据引用时重置排序。重置会恢复initialState.sorting,如果没有提供初始值则恢复为空排序状态。
该选项只对数据变化做出响应。改变排序、过滤或分组不会触发它。全局的autoResetAll选项在被显式设置时会覆盖autoResetSorting。
与手动/服务端排序组合使用时需小心:服务端响应通常会替换data,启用重置可能立刻清除请求该响应所用的排序状态。另外,如果与服务端分页配合,通常还应考虑autoResetPageIndex的取值(排序变化时是否跳回第一页)。
排序 API 一览(Sorting APIs)
排序相关的 API 非常丰富,以下列出全部排序 API 及其典型用途:
table.setSorting— 直接设置排序状态。table.resetSorting— 将排序状态重置为初始状态或清空。column.getCanSort— 用于为列启用/禁用排序 UI。column.getIsSorted— 用于为列显示视觉排序指示器。column.getToggleSortingHandler— 用于为列接入排序 UI。可以挂到排序箭头(图标按钮)、菜单项或整个列表头单元格上。该处理器会用正确的参数调用column.toggleSorting。column.toggleSorting— 用于为列接入排序 UI。如果用它代替column.getToggleSortingHandler,必须手动传入是否使用多列排序(column.toggleSorting(desc, multi))。column.clearSorting— 用于为特定列提供"清除排序"按钮或菜单项。column.getNextSortingOrder— 用于显示列下一次将按哪个方向排序(asc/desc/clear,可放在 tooltip、菜单项或 aria-label 中)。column.getFirstSortDir— 用于显示列第一次将按哪个方向排序(asc/desc,可放在 tooltip、菜单项或 aria-label 中)。column.getAutoSortDir— 决定列第一次排序方向是升序还是降序。column.getAutoSortFn— 内部使用,当列未指定排序函数时查找默认排序函数。column.getSortFn— 返回列当前实际使用的排序函数。column.getCanMultiSort— 用于启用/禁用列的多列排序 UI。column.getSortIndex— 用于在多列排序场景中显示列的排序序号(第一、第二、第三……个被排序的列),例如徽标或指示器。
小结
TanStack Alpine Table 的排序能力覆盖了从"开箱即用的客户端排序"到"完全自定义的服务端手动排序"的完整光谱:通过rowSortingFeature加createSortedRowModel一行即可启用;SortingState的数组结构天然支持多列排序;6 个内置sortFn_*函数配合sortFn列选项与constructSortFn辅助函数,既能按列精确选型,也能以极小成本扩展出"忽略变音符号""按姓氏排序"等自定义变体。在 Alpine 场景下,务必记住两点:数据通过 getter 响应式读取;表头点击 UI 用真实元素包裹x-html渲染的内容。结合 Alpine Sorting 示例 与源码 row-sorting 模块,你可以快速在 Alpine 应用中落地一整套专业、可扩展的表格排序体验。
【免费下载链接】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),仅供参考