接了个中后台项目列表页要支持点击表头排序我第一反应就是用 jEasyUI 的 DataGrid 把列的 sortable 打开前后端配合一下就能跑起来。jEasyUI 这里说的其实是 jQuery EasyUI 这套组件库在传统管理系统里出镜率很高尤其是那些要快速搭后台、又不愿意在前端工程化上投入太多时间的场景。它的排序功能看着简单真正用起来还是有不少细节讲究比如本地排序和远程排序怎么选、sortName 和 sortable 之间什么关系、数字列为什么经常排成 1、10、2。这篇就按我实际做过的项目来拆一拆把 jEasyUI 设置排序的完整玩法、参数逻辑和踩坑点都理清楚。1. 动手之前先搞清楚 jEasyUI 排序到底在排什么很多人一上来就在 columns 里加sortable: true结果发现表头确实能点数据却纹丝不动。这不是 bug而是没理解 jEasyUI 排序的机制。DataGrid 的排序本质上是一个状态管理过程点击表头组件内部更新 sortName 和 sortOrder 这两个值然后要么走本地排序要么重新请求远程接口。所以设置排序不是一个属性的事而是一整套配合流程。1.1 本地排序还是远程排序这是第一个选择题本地排序的意思是数据一次性加载到前端后表头点击时直接在浏览器里对已加载的数据做排序不需要再请求后端。这种模式适合数据量小的场景比如配置表、字典表、单页报表撑死几百行到一千行。jEasyUI 默认的排序模式就是本地排序它对当前页的数据数组排序好处是响应快、不依赖网络、实现成本最低。缺点是只排当前加载的数据如果你用了分页而且每页 20 条那本地排序只是把这 20 条排一下整个数据集的排序并不生效用户会看到每翻一页排序都是“乱的”这就是本地排序和分页天然冲突的地方。远程排序则是把排序参数提交给后端由数据库或服务端处理后再返回完整排序结果。DataGrid 在加载数据时会把sort和order两个参数自动带到请求里服务端根据这两个参数拼接 ORDER BY 语句查询后再把数据返回整页刷新。远程排序适用于数据量大、需要配合后端分页的场景也是我在实际项目中用得最多的模式因为后台管理系统的列表几乎都是上万条起步。我见过很多刚接触 jEasyUI 的人把url指向接口后分页、排序都开了结果发现排序只对当页生效。原因就是没有设置remoteSort: true。所以选型时要提前判断如果列表有分页且总数据量预期超过一页能承载的量就一定要走远程排序开头把这个定了后面能省很多事。1.2 点击一次表头背后发生了什么理解排序机制最好的方式是把“点击表头”拆成几步来看。第一步jEasyUI 检测到用户点击了某个设置了sortable: true的列这个点击的目标列对应一个 field 名。第二步组件内部判断当前排序状态如果此前没有排序列就设置 sortName 为当前列 fieldsortOrder 默认为 ascending 或你在初始化时定义的值如果此前已经按这一列排序了就切换方向asc 变 descdesc 变 asc如果配置了sortStable点击第三下会恢复到无排序状态。第三步判断remoteSort的状态为 true 时重组查询参数触发 load 或 reload请求带上 sort、order 参数为 false 时调用本地排序逻辑按指定字段对 rows 重新排序。第四步重新渲染表格把排序状态的箭头或样式显示在对应表头上。这四步看下来你会发现排序方向是自动切换的不需要自己维护“上次点了哪列”。但自动切换也有坑jEasyUI 内部只认 field不认列的显示文本所以如果你在多个列上用了相同的 field排序逻辑就会“串台”点哪一列都是排同一个字段。另一个坑是某些情况下初始化时就设置了sortName比如sortName: createTime那么表格首次加载就会按这个条件排序然后用户点击其他列时sortName 会被新点击的列覆盖。这意味着如果你希望默认按创建时间倒序但又允许用户切到按名称排序初始化时设定 sortName 是正确姿势但也要确保后端接口支持这组默认参数的接收。2. 核心参数逐个拆解sortable、sortName、sortOrder、remoteSortjEasyUI 排序相关的配置项其实就这几个但它们组合起来能玩出的效果和踩出来的坑一点也不少。我先把四个核心参数拆开讲清楚再补几个平时容易忽略的相关配置。2.1 四个参数各自的职责和配合关系先说属性层面的配置整理成一张表来看最直观参数配置位置作用关键点sortablecolumns 列配置控制该列是否允许点击表头排序必须配合 field 使用没有 field 的列即使 true 也没用sortNamedatagrid 初始化参数默认排序列的 field 名首次加载就按这个字段排序优先级高于用户点击前的默认状态sortOrderdatagrid 初始化参数默认排序方向asc 或 desc与 sortName 成对出现remoteSortdatagrid 初始化参数指定排序发生在远程还是本地true 走后端接口false 在本地排序默认 falsesortName 和 sortOrder 是初始化层面的配置它们和 columns 里的 sortable 不是一回事。sortName 的值为字段名对应列配置中的 fieldsortOrder 决定首次加载是升序还是降序。举个例子你的表有个“创建时间”字段列配置里 field 写的 createTimesortable 开了 true那么初始化时写sortName: createTime, sortOrder: desc页面第一次加载出来的数据就是按创建时间倒序的表头上也会带一个下降箭头。这样做的实际意义是排序状态和接口加载是同一个时机而不是表格加载完后再触发一次额外排序避免了首屏出现“先看一遍正序再自动切到倒序”的闪烁。remoteSort 则更像一个全局开关。它决定排序走本地还是远程并且这个配置不能和分页的本地模式混着用。曾经我接手过一个项目前端开了 remoteSort: true接口也接了 sort 和 order 参数但分页用的是本地分页也就是说接口把所有数据一次性返回来前端只切页显示。这时候 remoteSort 配合本地分页是矛盾的每次点击表头都重新请求接口但分页只是在本地切两套逻辑互相冲突表现为排序偶尔正常偶尔失效。后来统一成接口分页加远程排序问题才消失。如果你不想改接口就要把 remoteSort 关掉让本地排序和本地分页搭配起来。2.2 关于 sortable 列的两个隐藏约束sortable 这个列属性看起来只是开个开关实际还有两个隐藏约束。第一个约束是列必须设置 field没 field 的列比如那种“操作”列、复选框列jEasyUI 根本不知道点击后要排哪个字段点上去毫无反应。很多项目里的“序号”列、纯展示列都没有 field这种天然就不能排序不算故障。第二个约束是如果列设置了 formatter 格式化显示排序时排的是原始数据的字段值而不是格式化后的显示文本。这反而是一件好事因为 formatter 常用来做状态标签、日期格式化如果排序排的是文本状态和时间全按字符串比了结果肯定乱套。所以 jEasyUI 排序动作发生在 formatter 之前的较底层拿到的始终是字段原始值。我碰到过一个比较难受的情况是某列表里的“金额”字段为了显示好看在 formatter 里加了千分位逗号比如 1,234.56。列配置里 executable 是没问题但如果你把这个列设置成本地排序jEasyUI 拿到的排序值其实是字符串 1,234.56 而不是 1234.56于是排序结果就是 1000.00、1,234.56、900.00 这种诡异顺序。查了半天才发现是 formatter 输出的文本参与排序了。这其实不怪 formatter要怪列配置里的排序字段名写错了或者 local 模式下 formatter 影响了排序值。总之记住一条经验排序列不要为了展示方便和排序字段混用一个字段如果非要混用把排序逻辑放在后端或自定义 sorter 里处理。2.3 多列排序和单列排序边界在哪jEasyUI 官方虽然支持多列排序但那是通过几次点击操作叠加上去的不是常见的多关键字排序。我理解很多用户其实想要的是“先按类型排再按时间排”这样的二级排序jEasyUI 原生并没有把这个能力直接暴露出来。它一次只维护一个 sortName 和 sortOrder新点击的列会覆盖之前的排序状态。如果你的业务确实需要多关键字排序常见的做法有两种。第一种是在后端接口里自行处理前端只传一个主排字段服务端在 ORDER BY 里写死次要排序规则例如ORDER BY ${sort} ${order}, create_time DESC这是最简单也不影响用户体验的做法。第二种是自定义工具栏做一个“组合排序”弹窗用户选择第一关键字段、第二关键字段然后前端拼一个复合排序字符串比如type asc, createTime desc后端解析这个字符串来拼 SQL。这个方法灵活度最高但工作量也大适合数据仓库或报表类的后台。至于 DataGrid 的 sortable 点击就当它是单列排序的入口不要强行扩展为多列机制。// 多关键字复合排序参数示例配合后端解析 let multiSort type asc, createTime desc; $(#dg).datagrid(load, { multiSort: multiSort });3. 实操从零到一搭一套能用的排序列表前面原理讲了一堆现在上真操作。我会用一套前后端配合的完整示例从页面的 HTML 初始化开始到后端接口接收参数再到自定义排序规则把整个链路串起来。这套流程我在不同项目里反复用过照抄基本都能跑。3.1 基础初始化一个带默认排序的本地排序示例如果你只是做个内部小工具、数据量不过百本地排序最快。HTML 部分定义一个表格容器然后用 jQuery 初始化 DataGridtable iddg/table$(#dg).datagrid({ url: api/list-data, // 如果不走接口也可以不写 url直接用 data 属性 method: get, remoteSort: false, // 明确走本地排序 pagination: false, rownumbers: true, singleSelect: true, columns: [[ { field: id, title: ID, width: 60, sortable: true }, { field: name, title: 名称, width: 120, sortable: true }, { field: price, title: 价格, width: 100, sortable: true, align: right } ]], sortName: id, sortOrder: desc });这里remoteSort: false能写就写上不然默认 false 虽然也一样但显式写出来后来的人一看就知道这页是前端排序。sortName 配了 idsortOrder 配了 desc页面加载后 ID 就会倒序展示。用户再点“名称”列表格会在本地把 name 字段做一次字符串排序点第二次就反向整体体验很顺。如果你完全不想走后端接口数据是写死的那就别用 url直接在初始化里指定 data 属性$(#dg).datagrid({ data: staticRows, remoteSort: false, columns: [[ { field: name, title: 名称, sortable: true } ]] });本地排序时要特别注意排序范围只是当前 datagrid 的 rows 数组。如果你不启用分页那自然所有数据都能排到一旦启用分页本地排序就只对当前页生效。这也是我刚才反复强调的小数据量可以直接本地大数据量直接上远程别折中。3.2 切换远程排序后端怎么接住参数数据量大、有分页、要全员排序的项目远程排序是标配。做法不复杂前端先把remoteSort: true打开然后正常情况下DataGrid 会在 load 时自动发送sort和order参数到 url 对应的接口。如果你用的是默认请求方式接口里直接取这两个参数拼 SQL 就行。后端接口我用一个简单的 PHP 示例说明其他语言思路都一样无非是把请求参数映射进 SQL$sort isset($_GET[sort]) ? $_GET[sort] : id; $order isset($_GET[order]) ? $_GET[order] : desc; // 白名单校验防止恶意传字段名 $allowSortField [id, name, price, create_time]; if (!in_array($sort, $allowSortField)) { $sort id; } $order strtoupper($order) ASC ? ASC : DESC; $sql SELECT * FROM product ORDER BY {$sort} {$order} LIMIT {$offset}, {$pageSize};需要注意两个安全细节。第一千万别直接拼用户传进来的 sort 字段否则就是 SQL 注入。白名单校验是底线只允许代码里预设的字段名通过其他一律回退到默认值。第二order 参数也要做同样的处理只接受 asc 和 desc 两个值其余全按 desc 处理。网上有很多现成框架比如 Spring Boot 的 PageHelper、PHP 的 ThinkPHP都有排序参数的安全封装但道理是一样的。有个容易忽略的点DataGrid 翻页时也会把 sort 和 order 一起带上。你可能没主动设置分页参数但 DataGrid 的默认分页参数是 page 和 rows也就是页码和每页行数。后端分页查询一般用这两个参数算 offset 和 limit再把总条数 total 和当前页数据 rows 返回给前端。只要 total 和 rows 是 jEasyUI 认识的格式翻页和排序就能无缝联动。{ total: 256, rows: [ { id: 1, name: 苹果, price: 3.5 } ] }这种数据格式是 jEasyUI 的默认约定。如果你后端返回的数组直接是一个 list没有 total 字段翻页控件就不显示总页数排序也可能出问题。最简单的处理是把 total 设为 list 的长度虽然分页总数不准但至少接口格式能让 DataGrid 正常工作。3.3 自定义 sorter字符串和版本号的高级玩法jEasyUI 的列配置里还有一个sorter属性它允许你传入一个自定义排序函数。这个 sorter 在本地排序时会被调用格式是function(a, b)a、b 是两个字段原始值返回值小于 0 表示 a 在前大于 0 表示 b 在前等于 0 则保持不变。sortable 没它也能排但处理特殊数据格式就有它没它两回事了。最常见的场景是版本号排序比如 1.2.10、1.2.9 这种字符串直接按字典序排会得到 1.2.10 排在 1.2.9 前面因为字符串比较是从左到右逐字符比较所以 1.2.10 的第四段 10 是 1 开头而 1.2.9 第四段是 9字典序里字符 1 小于 9结果完全不对。解决办法是把版本号拆成数字数组再逐个比较function versionSorter(a, b) { const pa String(a).split(.).map(num parseInt(num, 10)); const pb String(b).split(.).map(num parseInt(num, 10)); for (let i 0; i Math.max(pa.length, pb.length); i) { const na pa[i] || 0; const nb pb[i] || 0; if (na ! nb) return na - nb; } return 0; } $(#dg).datagrid({ columns: [[ { field: version, title: 版本号, sortable: true, sorter: versionSorter } ]] });另一个常见场景是混合中文和数字的排序比如“第1章”“第2章”“第10章”。默认字符串排序会把第10章排在第2章前面。这种可以用一个简单的自然排序函数把字符串里的连续数字提取出来参与比较网上有很多 natCompare 的实现也可以直接在版本号的 sorter 思路上扩展。自定义 sorter 还有一个“隐藏功能”你可以在本地排序模式下模拟远程排序行为。这叫“假远程排序”本质是借用本地 sortable 的表头点击事件但实际不依赖本地数组排序而是自己拦截 onSortColumn 事件然后调接口。但如果接口都写了你还不如直接 remoteSort: true 来得干净。所以我的建议是能用 remoteSort 就用远程sorter 只留给那些确实无法在后端处理、数据量又小的场景。3.4 默认排序和点击记忆的联动问题很多时候用户希望“上次选了哪列排序下次进来还记住”。这个需求其实要打通两个环节页面加载时设置 sortName 和 sortOrder然后在每次排序变化时把这两个值存下来。存哪呢localStorage 最方便key 可以按页面路径加前缀区分。function loadSortPref(pageKey) { const pref localStorage.getItem(sortPref_ pageKey); return pref ? JSON.parse(pref) : null; } function saveSortPref(pageKey, sortName, sortOrder) { localStorage.setItem(sortPref_ pageKey, JSON.stringify({ sortName: sortName, sortOrder: sortOrder })); }初始化时读出偏好赋给 sortName 和 sortOrderlet pref loadSortPref(product_list) || { sortName: createTime, sortOrder: desc }; $(#dg).datagrid({ url: api/product/list, remoteSort: true, sortName: pref.sortName, sortOrder: pref.sortOrder, onSortColumn: function(sort, order) { saveSortPref(product_list, sort, order); } });这个方案写一次后面所有列表页都能复用。要注意的是onSortColumn 在初始化加载时也会触发一次所以第一次进页面可能马上写入一次偏好不影响逻辑但如果你需要严格区分“用户主动点击”和“初始化加载”可以在 onSortColumn 里加一个标志变量判断。真实项目里个人觉得不必那么严格因为写入相同的值没有副作用。4. 排序实战中的五大问题逐个排查给你看排序功能跑起来不难但实际项目里总会冒出各种莫名其妙的现象。我把这些年碰到的高频问题列出来每个都配上原因和排查方向基本覆盖了 jEasyUI 排序的绝大多数坑。4.1 表头点击没反应到底是哪一步断了现象是列标题配置了 sortable: true但点击表头完全没有排序变化也没有新的请求发出。排查时优先看三点列有没有设置 field列是不是被当成 frozen 列但漏配以及页面有没有报 JS 错误。frozen 列是指固定列它虽然出现在左侧区域但数据绑定机制和普通列没有本质区别frozen 列同样可以设置 sortable但如果列在 frozen true 列组里且字段值不在原始 rows 数据中排序自然失效。我遇到最离谱的情境是页面引用了两个版本的 jQuery 或者多个 EasyUI 脚本组件初始化就报错了点表头自然没反应打开浏览器控制台能看到 ReferenceError 或类似错误。这种问题最容易被忽略建议任何表格类问题都先开控制台看一眼。另外还有一种情况是同一页面初始化了多次 datagrid比如弹窗里的表格和页面表格共用了一个 id 选择器初始化时选中了已存在的那个。排查办法是打印一下$(#dg).datagrid(options)看当前 options 里的字段对不对一目了然。4.2 数字列排序变成 1、10、2 的字符串序这个问题出现的频率极高几乎每个用自动排序的人都遇到过。现象是“价格”“年龄”“数量”这种数值列点击排序后 10 排在 2 前面。原因在于本地排序默认按 JavaScript 的字符串比较规则处理数字被转成字符串字典序下 10 小于 2 是正常现象。解决方案有几种。如果走远程排序让后端用数据库数值字段排序就没有这个问题因为数据库列类型是数值类型数据库知道按大小比。如果是本地排序用数字类型的字段值比如确保 rows 数据里的 price 是 number 而不是 string。如果你从接口拿到的 price 本身就是字符串在前端做一次转型比如在 load 成功回调里parseFloat或者在 column 上配一个自定义 sorter{ field: price, title: 价格, sortable: true, sorter: function(a, b) { return parseFloat(a) - parseFloat(b); } }这样写之后排序就按数值走了。之所以强调“转型”而不是“直接减”是因为有些数据里塞了单位或逗号比如 “1,200 元”parseFloat 会直接变成 NaN。这种时候要先把非数字字符去掉再转数值function numberSorter(a, b) { const cleanA parseFloat(String(a).replace(/[^\d.-]/g, )); const cleanB parseFloat(String(b).replace(/[^\d.-]/g, )); return cleanA - cleanB; }4.3 日期列排序不对看起来是按字符串排的日期列排序不对的根因和数字列类似如果日期字段以字符串形式参与排序比如 “2024-01-02” 和 “2024-1-2”字符串比较结果就不稳定如果格式是 “2024/01/02” 和 “2024-01-02” 混着来更是直接乱掉。解决办法是统一日期格式在前端处理时把所有日期字段转成时间戳再进行本地排序或者直接让后端的日期字段按时间类型排序。我习惯的做法是后端接口把日期 ISO 格式统一返回比如2024-01-02 10:00:00前端排序时用Date.parse把字符串转时间戳function dateSorter(a, b) { return Date.parse(String(a)) - Date.parse(String(b)); }这套方案在主流浏览器里都有效但要注意时区问题如果是2024-01-02T10:00:00Z这种带 Z 后缀的 ISO 字符串Date.parse 会按 UTC 处理而显示时又按本地时区排序结果不会错没必要太纠结。更坑的是 Excel 导出的数据里有 “2024/1/2 上午10:00” 这种本地化格式Date.parse 在各浏览器表现不一致还是提前和后端约定好统一格式一劳永逸。4.4 开启了 remoteSort 但自定义 sorter 完全没执行jEasyUI 文档对 sorter 的说明主要针对本地排序当remoteSort: true时点击表头实际上会触发一次远程加载排序交给后端前端 sorter 函数不会被调用。如果你在后端已经写了排序逻辑这不影响什么但如果你误以为 sorter 会在远程模式下作用于提交给后端的参数或者对返回数据再次排序就会出现“我定了 sorter 但一点用都没有”的困惑。这种场景下正确的做法是把数据排序逻辑挪到后端。如果后端是别人写的、不好改前端还想保留自定义排序就只能在 onSortColumn 里拦截排序事件手动调整参数或本地排序加一个分支把 remoteSort 临时置为 false。这种做法比较 hack不是长久之计但确实能应付一些“接口暂时不改成”的过渡阶段。更常见的是另一个变体remoteSort: true 时我用 onSortColumn 想在提交前修改 sort 参数但修改无效。原因是 DataGrid 内部用参数对象保存了 sort 和 orderonSortColumn 触发的时机是排序状态已更新之后你改 options 里的 sortName 已经来不及直接作用于本次请求。要在请求前改参数应该用 queryParams 配置项或者重写 loader而不是依赖 onSortColumn 的返回值。$(#dg).datagrid({ remoteSort: true, queryParams: { // 固定附加参数每次请求都会带上 extraParam: xxx }, onSortColumn: function(sort, order) { // 这里 sort 和 order 已确定但如果你想改本次请求参数 const opts $(#dg).datagrid(options); $(#dg).datagrid(load, $.extend({}, opts.queryParams, { sort: sort, order: order, mySort: 自定义 sort })); } });这属于“手动与自动并存”的过渡方案。我自己的习惯是能改后端就改后端前端拦截越少越好否则以后维护的人看代码会疯掉。4.5 排序参数字段名与后端字段名不一致这也是个非常隐蔽的问题。比如前端列配置的 field 是createTime驼峰数据库字段是create_time下划线。前端点击表头时jEasyUI 发送的sortcreateTime后端如果直接拿这个值拼 SQL数据库会报字段不存在或者由于白名单校验被拦截回退到默认排序表现为某些列排序正常某些列排序没变化。解决办法是在前端 at 列配置里额外加一个field用于显示但在onSortColumn事件里把前端 field 映射为后端字段再重新拼接请求参数或者在后端做字段映射表把createTime映射成create_time。相比之下我更倾向后端做映射因为前端涉及多语言、按钮等复杂场景时列配置的调整会影响很多地方而接口层做一次映射一劳永逸。关于 jEasyUI 的排序设置说到底是一套前后端协作的流程。本地排序适合轻量场景远程排序适合正经管理系统排序参数的安全和字段对齐是基本盘自定义 sorter 是补充手段。真正影响体验的从来不是“能不能排序”而是数据量大时的稳定性、默认排序是否合理、排序状态是否和分页联动正常。这部分我踩过的坑也最多尤其是 remoteSort 和本地分页混用、字段名驼峰和下划线不匹配这两个问题几乎是每个项目都会遇到。设排序前先理清数据从哪来、要排谁、后端要不要参与jEasyUI 这套组件就能很舒服地融入你的系统。
