1. 排序需求从哪来DataGrid 排序的底层逻辑做前端维护的老手应该都有印象EasyUI 这套组件库在后台管理系统里出镜率极高尤其是 jEasyUI 的 DataGrid 表格组件。只要业务数据一多点击表头排序就成了几乎必提的需求。运营要看金额最高的订单、客服要按时间倒序查工单、财务要把报销记录按金额排个序用户永远觉得数据按我想要的顺序出现是理所当然的。但 EasyUI 这个排序说简单是真简单——列上加上sortable: true就能点表头了说坑也是真坑——数字排序错乱、中文排序不对、日期排序无效、翻页之后排序失效踩过的人应该深有体会。今天这篇文章我就把 jEasyUI 的排序功能从配置到底层原理、从本地排序到服务端排序、从常见坑到排查思路一次性梳理清楚。先搞清楚一件事DataGrid 的排序机制到底做了什么。其实它的核心逻辑并不复杂表格组件本身只负责管理排序状态和触发排序事件真正把数据排好序的动作发生在两个地方要么是 DataGrid 内部的本地排序器要么是你后端的 SQL 查询语句。这对应了remoteSort这个关键属性的两种取值。如果你把remoteSort设为falseDataGrid 就启用本地排序——它会把当前加载到前端的数据按照你指定的规则在浏览器端重新排列。注意这里的限定词当前加载到前端的数据。如果数据是分页加载的本地排序只能排当前这一页翻到第二页顺序又乱了。如果你把remoteSort设为trueDataGrid 就不再自己排序而是点击表头的时候自动向服务器重新发起一次请求并把当前点击的是哪一列、升序还是降序这两个参数带给后端。后端根据参数重新查询、重新排序、重新返回数据。排序目标变了排序后的数据是一致的、全局的。这两种模式对应了不同的典型场景。数据量小几百条以内、一次性全量加载的本地排序就够了简单快捷不用来回发请求。数据量大、用了分页的就必须用服务端排序否则排序结果毫无意义。这个选择至关重要很多人排序功能做完了但总觉得不对劲根源就是模式选错了。2. 快速上手DataGrid 排序的三个核心配置这一节直接看代码目标是五分钟内跑通一个带排序功能的表格。2.1 列定义中开启排序在 DataGrid 的列的配置里sortable: true是你在每一列上决定这一列允不允许排序的开关。可以放在列的部分也可以写在一个单独定义的 columns 数组里。最常见的写法是这样的$(#dg).datagrid({ url: /api/user/list, columns: [[ { field: id, title: ID, sortable: true }, { field: name, title: 姓名, sortable: true }, { field: age, title: 年龄, sortable: true }, { field: createTime, title: 创建时间, sortable: true } ]] });这里有个很容易忽略的点sortable的判定只认列字段的真实属性名也就是field对应的那个值而不是你在表格里看到的那一列的显示内容。比如createTime这一列显示的是格式化好的 2024-01-15 14:30:00但 DataGrid 排序时用的是原始数据里的createTime字段的值。这个说法在本地排序时尤其重要后文会专门展开。2.2 全局默认排序sortName 和 sortOrder如果需求是表格一打开就默认按某个字段排序那就需要设置sortName和sortOrder这两个属性。sortName指定默认排序的字段sortOrder指定排序方向取值只有asc和desc两个。$(#dg).datagrid({ url: /api/user/list, sortName: createTime, sortOrder: desc, columns: [[ { field: id, title: ID, sortable: true }, { field: name, title: 姓名, sortable: true }, // 其他列 ]] });这里要提醒一个常见误区很多人以为设置了sortName之后点其他列表头再点回来默认排序就永远生效。其实不是这样的。sortName只是表格第一次加载时使用的排序字段一旦用户手动点击了某个表头DataGrid 的排序状态就被用户的操作覆盖了。后端需要自己保存当前排序字段的话得另外传参或者自行维护状态。2.3 触发排序的时机onSortStart 与 onSortEnd除了排序本身DataGrid 还给了两个和排序相关的生命周期事件onSortStart开始排序前触发和onSortEnd排序完成后触发。这在开发中很有用比如你希望在排序开始前显示一个 loading 遮罩或者在排序完成后重新渲染某个状态栏。$(#dg).datagrid({ // ...其他配置 onSortStart: function(sort, order) { // sort 是点击的字段名order 是 asc 或 desc // 这里可以显示加载提示 }, onSortEnd: function(sort, order) { // 排序结束后的处理 } });这两个回调在本地排序时实际意义更大因为服务端排序时表格重新load数据的流程本身就会触发 loading 状态。在实际项目中我更常用的其实是监听load事件来感知排序后的数据刷新但也不排除有做额外业务的场景知道有这么个钩子总比用到时到处搜强。3. 服务端排序参数传递与 SQL 处理3.1 点击表头前端到底发什么请求当remoteSort设为true时用户点击表头DataGrid 会重新加载数据并自动在请求参数中带上sort和order两个字段。sort是你点击列的field值order是asc或desc。如果你用的是 jQuery 的load方法手动触发刷新这两个参数同样会被带上。服务端收到的请求参数大致长这样GET /api/user/list?page1rows10sortnameorderasc这就意味着后端接口在写的时候就要预留好sort和order这两个参数的接收逻辑。无论你用 Java、PHP、Python 还是 Node.js这俩参数都是固定的套路。比如用 Spring MVC 接收GetMapping(/api/user/list) public Result list( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int rows, RequestParam(required false) String sort, RequestParam(required false) String order) { String orderBy sort order; // 拼排序条件 // 调用 service 层做分页查询... }3.2 MySQL 排序的 SQL 写法与防注入服务端拿到sort和order之后最直接的做法是把它们拼到 SQL 的ORDER BY子句里。在 MySQL 里典型写法是这样的SELECT * FROM user ORDER BY create_time DESC LIMIT 0, 10;对应到动态拼 SQL 的场景SELECT * FROM user ORDER BY ${sort} ${order} LIMIT ${offset}, ${rows};但这里有个绝对不能省略的安全措施排序字段白名单校验。因为sort和order是用户可控的参数如果直接拼进 SQL就是典型的 SQL 注入点。攻击者完全可以构造一个sort字段比如传id; DROP TABLE user; --后果不堪设想。网上流传的很多 EasyUI 示例代码都没做校验照抄是要出事的。正确的做法是在后端维护一个允许排序的字段白名单只接受白名单里的字段名映射然后再拼接。比如private static final MapString, String SORT_COLUMN_MAP new HashMap(); static { SORT_COLUMN_MAP.put(id, id); SORT_COLUMN_MAP.put(name, name); SORT_COLUMN_MAP.put(age, age); SORT_COLUMN_MAP.put(createTime, create_time); } // 使用白名单映射后拼接 String column SORT_COLUMN_MAP.get(sort); if (column null) { column id; // 默认排序字段 } String direction asc.equalsIgnoreCase(order) ? asc : desc; String orderBy column direction;这样既解决了前端字段名和数据库列名不一致的问题比如前端叫createTime数据库叫create_time又堵住了注入漏洞。order参数也必须做白名单限制只允许asc或desc其他一律回退到默认值。3.3 多条件排序与特殊排序需求有时候业务不满足于单列排序。比如用户想先按部门排序再按入职时间倒序排这就需要支持多字段排序。DataGrid 本身的默认交互只支持单列排序要实现多字段常见的做法是在表格底部或工具栏加一个高级排序的面板把多个字段和方向组合好通过自定义参数传给后端。后端 SQL 上就体现为多个排序条件SELECT * FROM employee ORDER BY department_id ASC, hire_date DESC LIMIT 0, 10;还有一类特殊需求是分组内排序比如 SQL Server 里配合窗口函数使用ROW_NUMBER()实现组内排序。如果 EasyUI 表格展示的是这种分组后的数据排序逻辑就要在后端 SQL 层面提前处理好。这类需求我在实际项目中也遇到过比如某个报表要求在同一个产品类目内部按销量倒序展示这时候 DataGrid 的排序只能作为辅助真正的组内顺序必须靠 SQL 计算好。另外用 ORM 框架的项目里也有专门的门道。以 Node.js 的 Sequelize 为例排序参数order的写法需要注意别名问题const users await User.findAll({ order: [[ sequelize.col(department.name), ASC ]], include: [{ model: Department, as: department }] });这里如果不写sequelize.col(department.name)直接写字符串department.nameSequelize 在拼接 SQL 的时候会加反引号引起来反而导致排序失效。这类坑属于典型的普通功能在 ORM 框架里踩深入坑。4. 本地排序自定义排序器和 JS 数组排序原理4.1 本地排序时会遇到的经典坑如果你的数据量不大全量加载后直接用默认的本地排序最简单的配置就是remoteSort: false列上sortable: true。在数据量小的情况下这个方案是真的省事。但坑也随之而来。DataGrid 默认的本地排序规则是把要排序的值都转成字符串再按字符串规则比较。这意味着数字列1, 2, 10, 20按字符串排序会排成1, 10, 2, 20日期列2024-01-01, 2024-09-01, 2023-12-31按字符串排序可能碰巧是对的因为日期格式设计的就是字典序等于时间序但如果加了时间、或者日期格式不统一就会乱套。这是所有深入用 EasyUI 的人都会撞上的第一堵墙。4.2 自定义排序器 sortable 函数解决办法是给列配置自定义的sortable函数。DataGrid 的列配置里sortable除了可以写成true还可以写成一个函数这个函数接收两个参数a和b分别代表两行数据的字段值返回值规则和 JavaScript 的Array.sort一致负数表示a排在b前面正数表示a排在b后面0 表示相等。要理解这个机制就得懂一点 JavaScript 数组排序算法的底层逻辑。DataGrid 的本地排序最终是通过Array.sort来实现的而Array.sort在 V8 引擎里用的是 TimSort 算法一种稳定的归并排序变种。你传入sortable函数就相当于传给了Array.sort一个比较函数。既然是交给Array.sort执行的那这个函数就必须严格遵循比较函数的规范始终一致、可传递、不产生副作用。如果你写的比较函数一会儿返回正数一会儿返回负数排序结果会变得不可预测。一个针对数字列的排序器可以这么写$(#dg).datagrid({ remoteSort: false, columns: [[ { field: age, title: 年龄, sortable: function(a, b) { return a.age - b.age; } } ]] });注意这里函数收到的a和b是整行数据对象不是这一列的值。所以要用a.age - b.age来比较而不是a - b。这个细节很容易写错。同理当列数据是格式化后的日期字符串时就需要转成时间戳再比较{ field: createTime, title: 创建时间, sortable: function(a, b) { return new Date(a.createTime).getTime() - new Date(b.createTime).getTime(); } }4.3 字符串排序中文排序和 localeCompare字符串排序是另一类被高频使用的场景尤其是中文列。DataGrid 默认的字符串比较是按 JavaScript 字符串的 Unicode 码点排序的它并不是按拼音或按笔画排。如果你期望姓名列按拼音排默认行为给不了你。这时候还需要借助localeCompare{ field: name, title: 姓名, sortable: function(a, b) { return a.name.localeCompare(b.name, zh-CN); } }localeCompare在支持良好的浏览器里会把中文按拼音的近似规则比较。需要注意不同浏览器对localeCompare的语言环境处理不完全一致生产环境使用前最好做一轮明确的浏览器兼容测试。另外如果数据量很大频繁调用localeCompare会有明显的性能开销实测下来几千行数据排序时能感觉到卡顿这时候建议考虑服务端排序。4.4 当列数据被 formatter 格式化时很多表格列会设置formatter把原始值包装成带颜色、带单位、带图标的富文本。比如金额列显示为1,234.56 元状态列显示为已完成/待处理。这种时候如果在列上直接开启默认sortable: true排序依据的是formatter之后的展示值还是原始值答案是原始值。DataGrid 的排序逻辑发生在格式化渲染之前它内部比较的是原始数据的字段值。这个设计其实很合理。因为展示值往往是给人看的原始值才是机器可比较的。但也因此带来一个问题如果你希望在formatter显示的是A、B、C这种状态文案而排序依据应该是状态对应的数字权重光靠默认的字符串比较就做不到了。这就要用到自定义排序函数来处理原始字段的映射关系。5. 常见问题与排查技巧实录这一节整理我在实际项目中遇到过的、以及周围同行频繁踩过的排序相关问题附带排查思路和解决方案。建议收藏后直接当速查表用。5.1 问题速查表现象根本原因解决方案点击表头无任何反应没有排序箭头列上没加sortable: true检查列定义给需要排序的列配置sortable有排序箭头但数据顺序不变remoteSort为 true但后端忽略sort/order参数后端接口补充排序处理逻辑数字列排序乱序1、10、2默认按字符串排序配置自定义sortable函数用数值相减日期列排序完全不对格式化后的日期字符串默认可比性差自定义sortable转成时间戳比较翻页后排序失效本地排序只作用于当前页改用服务端排序数据全量交给 SQL排序后表格 loading 一直转圈服务端排序接口异常或超时打开浏览器 Network 面板检查请求响应自定义列formatter 列排序值和显示不一致sorting 用的是原始字段值自定义排序函数按业务映射比较默认排序第一次加载生效之后失效sortName只在初次加载时使用每次请求由后端额外保存排序参数5.2 排查思路先看 Network 再谈代码兜底排查的大原则是先确认请求是否正常发出参数、响应是否正确再去看代码逻辑。很多人一遇到排序没生效就直接翻 JS 文件找 bug效率极低。正确的排查顺序是打开浏览器开发者工具切到 Network点击表头排序观察是否发起了新的请求。如果完全没发请求说明remoteSort处于false走的是本地排序。请求参数里有没有sort和order。如果没带说明触发逻辑有问题检查列配置或某些自定义代码是否覆盖了默认行为。后端响应返回的数据顺序是否符合预期。如果接口返回顺序已经正确但表格展示不对可能是前端二次处理数据时有自己的排序逻辑覆盖了请求结果。如果是本地排序没生效直接在控制台执行$(#dg).datagrid(getData).rows查看当前数据再用自定义排序函数手动验证。这个流程走下来绝大多数问题都能定位到具体层级。5.3 几个我踩得比较深的具体问题有一个很隐蔽的坑是表格开启remoteSort后又用了sortName作为默认排序字段。此时如果后端在没有任何用户点击动作的情况下第一次加载数据时接收到了前端传来的sort参数来自sortName而你的后端恰好只处理了用户点击传参不处理初次加载传参就会导致第一次打开表格的顺序不对点一次表头才正常。解决办法有两种要么后端统一处理不管第一次加载还是后续点击都接收sort和order参数并应用到查询要么前端第一次加载时通过queryParams手动传排序参数避免依赖 DataGrid 的自动补参逻辑。还有一个更隐蔽的场景表格用了load方法自定参数比如$(#dg).datagrid(load, { keyword: 张三, deptId: 12 });这种情况下如果 DataGrid 的排序状态已经存在它仍然会把sort和order追加进最终请求参数里。这会和你手动传的参数合并如果后者里恰好也有sort就会覆盖掉 DataGrid 自动追加的参数排序也跟着错乱。排查这一类问题直接看 Network 里的实际请求参数是最有效的方法。5.4 日期排序的进阶处理日期字段是排序问题的高发区尤其是日期格式不统一的时候。前端展示层收到的数据可能是2024/12/01、2024-12-01、2024年12月01日、甚至时间戳字符串各种格式混在一起。如果使用默认的字符串排序结果根本没法看。正确做法是自定义排序器统一转成时间戳{ field: createTime, title: 创建时间, sortable: function(a, b) { var timeA new Date(a.createTime).getTime(); var timeB new Date(b.createTime).getTime(); if (isNaN(timeA)) timeA 0; if (isNaN(timeB)) timeB 0; return timeA - timeB; } }这里加isNaN判断是经验之谈。真实接口返回的数据里偶尔会混入个别脏数据直接new Date(...).getTime()会得到NaN而NaN参与比较运算结果永远是false最终排序结果无法预料。提前做异常兜底能避免线上环境出现偶发的排序错乱。6. 服务端排序与本地排序的代码组织实践6.1 两种模式的选择标准我整理一个简单的决策表项目迭代时照着选就行判断维度本地排序remoteSort: false服务端排序remoteSort: true数据量小于 1000 条大于 1000 条或不确定分页方式一次性全量加载服务端分页硬件/性能预算前端资源有限数据库承担排序压力排序复杂度简单列数字/日期多字段、跨表、组内排序并发体验快一次加载后本地响应每次点击都请求后端这个选择没有绝对的对错主要是看你的表格是不是分页的。只要分页就必须服务端排序不然就是自欺欺人。反过来几十条数据的配置项表格非要折腾一套后端排序接口纯属浪费时间。6.2 一个完整的前后端配合示例以最常见的 Java MySQL 组合为例完整跑通一遍。前端表格定义$(#dg).datagrid({ url: /api/order/list, method: get, remoteSort: true, sortName: createTime, sortOrder: desc, pagination: true, pageSize: 20, columns: [[ { field: id, title: 订单号, sortable: true }, { field: amount, title: 金额, sortable: true }, { field: status, title: 状态, sortable: true }, { field: createTime, title: 下单时间, sortable: true } ]] });后端接口逻辑Spring Boot 风格GetMapping(/api/order/list) public Result listOrders( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int rows, RequestParam(defaultValue createTime) String sort, RequestParam(defaultValue desc) String order) { String safeSort sortColumnMap.getOrDefault(sort, create_time); String safeOrder asc.equalsIgnoreCase(order) ? asc : desc; ListOrder list orderMapper.selectPage((page - 1) * rows, rows, safeSort, safeOrder); Long total orderMapper.countAll(); MapString, Object result new HashMap(); result.put(total, total); result.put(rows, list); return Result.success(result); }对应的 MyBatis XMLselect idselectPage resultTypeOrder SELECT * FROM order ORDER BY ${sortColumn} ${sortDirection} LIMIT #{offset}, #{limit} /select这里order表名关键字要加反引号转义sortColumn和sortDirection只能用白名单校验后的值因为$ {}是直接字符串拼接不走预编译。这是整个链路里最需要谨慎的地方。6.3 排序参数写到日志里这是一个我自己养成的习惯在后端接收到排序参数时把它打印到日志里。log.info(Order sort: {} {}, sort, order);这样做的好处是当前端排序行为异常时可以对照后端日志和前端 Network 请求参数快速判断是前端没传对还是后端处理错了。排查效率能提升一个档次。7. 关于排序机制的一些个人体会写了这么长最后分享几个我在实际项目里沉淀下来的认知谈不上标准答案但都是踩坑踩出来的。第一个认知是排序功能看起来简单但它本质上是一个前后端协作的契约问题。前端负责表达用户想要什么序后端负责实现数据到底怎么排中间通过sort和order两个参数串联。脱离任何一方排序都是不完整的。所以在一个需要排序的项目里前后端开发最好提前对齐哪些字段允许排序、字段命名映射是什么、服务端还是本地排序省得做完了再返工。第二个认知是默认行为永远不够用。DataGrid 自带的排序器最多只能覆盖最朴素的场景。一旦碰上格式化列、别名 ORM、多字段排序、脏数据就必须手写自定义排序逻辑。很多人项目上线之后才发现排序某个列是错的就是因为前期想当然用了默认配置。第三个认知是不要把排序和搜索分得太开。很多时候排序表面上是排序问题实际是查询条件需要动态追加。比如用户要按状态分组内按时间倒序没有 SQL 层面的计算前端再怎么写排序函数也没用。遇到这类需求时机合适就把它归到后端能力前端保持简单。如果你现在正在维护一个老项目正在为某个顽固的排序问题头疼我的建议是先打开 Network 面板确认请求长什么样再去看后端能不能正确消费这两个参数最后再检查前端是不是被formatter或者remoteSort配置误导了。按这个顺序排查九成问题都能落地解决。最后再送一个小技巧如果你拿不准某个排序函数的行为可以在浏览器控制台里直接把数据拉出来模拟Array.sort跑一遍自定义比较函数亲眼确认结果符合预期后再更新代码。这个习惯帮我避免过至少五次线上返工。
企业数字化 ERP 产品动态
相关推荐
Modbus RTU实战调试:波形、时序与CRC三要素深度解析 1. 这不是教科书,是我在产线调了73次Modbus RTU后撕下来的波形纸Modbus RTU不是协议栈里一段可配置的代码,它是AB线之间真实存在的电压跳变、是示波器上抖动的毛刺、是PLC扫描周期里被掐住喉咙的0.5毫秒、是CRC校验失败时PLC面板上那个不声不响却让整条灌… · 2026/9/25 22:42:05
OpenLess云同步完整指南:词典、风格包跨设备一键备份,同时守住隐私与凭据边界 OpenLess云同步完整指南:词典、风格包跨设备一键备份,同时守住隐私与凭据边界 【免费下载链接】openless Hold a key, speak, release — AI-polished text appears at your cursor in any app. Open-source voice input for macOS & Windows. (按住… · 2026/9/25 23:20:39
SharpPcap实战:C#零权限抓包与工业协议解析 简介:这是一份面向C#开发者与网络编程学习者的实用型抓包工具资源,聚焦网络诊断、协议分析与安全监控场景,特别适合初学者理解数据包捕获原理,也便于进阶者快速集成SharpPcap库到实际项目中。压缩包共17个文件,含3个程… · 2026/9/25 23:20:20
dy算法Go源码开源3.0:打造可自研的短视频推荐主链路 简介:dy算法Go版3.0是一份面向Go开发者、算法研究与逆向学习场景的开源代码包,侧重高并发请求下的协议处理与数据加密算法实现。压缩包含32个文件,共1.28MB,主要以Go源码为主(24个.go),并带有pr… · 2026/9/25 23:20:13
Unity 2D交互水效果实战:顶点扰动Shader与多点浮力实现 简介:面向2D游戏开发者的Unity水效果资源包,围绕BuoyancyEffector2D这一内置2D物理效果器,解决物体在水中的浮力、波浪、阻力与接触检测等问题,适合想要为横板、平台跳跃、模拟经营等2D项目快速加入真实水体交互的中初级开发者。包… · 2026/9/25 23:20:06
LSTM多变量预测实战:用Keras构建学生成绩预测模型 简介:面向LSTM时序预测的Python代码资源包,聚焦多变量预测、单变量预测与多步预测三大方向,适合正在学习循环神经网络、研究时间序列分析,或需要借助Keras/TensorFlow快速搭建预测模型的开发者。压缩包为RAR格式,共33个… · 2026/9/25 23:19:41
DeskcommCRM深度解析:从销售管道到自动化规则,让CRM真正驱动业务 做CRM实施和产品研究这几年,我听过最多的一个说法是:“我们公司买了套CRM,结果用成了Excel。”这话听起来像笑话,但背后是非常真实的行业现状——很多系统上线三个月就哑火,销售继续用私人表格记客户,管理者… · 2026/9/25 23:19:16
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37