多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

DataTable搜索条件全攻略:全局搜索、列搜索与自定义过滤

DataTable搜索条件全攻略:全局搜索、列搜索与自定义过滤 后台系统里最磨人的从来不是复杂的业务逻辑而是那些看起来简单的列表页。表格要能搜、能筛、要能记住条件用户才愿意用。DataTable也就是常搜到的 datatable js能成为老牌表格插件很大一部分原因就是它把搜索条件这个环节设计得足够灵活全局搜索、单列搜索、自定义扩展搜索三种方式各有各的适用场景还能配合服务端接口做远程筛选。这篇文章我想把 DataTable 搜索条件从原理到实操串讲一遍把我日常项目里真正用到的写法、踩过的地方、以及怎么排查问题都分享出来。如果你是那种已经在项目里接入了 DataTable、但每次遇到搜索条件不对都要临时去查文档的人或者你正准备把一张新表格接入 DataTable、想一次把搜索功能做完整这篇文章应该能帮你省下不少时间。我会尽量避开纯 API 文档式的罗列而是从数据怎么流转的角度来讲理解了这个后面写任何搜索条件都不会心虚。1. 先搞清楚DataTable搜索条件的底层逻辑1.1 数据在表格里其实有三层很多人在 DataTable 上写搜索条件写不出来或者写错了根本原因是没搞清楚它内部的数据组织方式。DataTable 不会直接对你给的原始数组做过滤它在初始化时会维护三份不同用途的数据原始数据source data、过滤数据filtered/sorted data和显示数据display data。原始数据就是你通过data选项传进去的数组或者 ajax 拿回来的 JSON。过滤数据则是经过排序、搜索、自定义过滤之后的数据集合DataTable 的分页、统计信息都是基于这一层来计算的。显示数据是最终渲染到页面上的内容它会经过columns.render、columnDefs的处理可能跟原始数据长得完全不一样。搜索条件就是在原始数据 → 过滤数据这一层发生作用的。DataTable 默认会对每一行数据预先构建一个可搜索的文本串存在内部的_aFilterData里。全局搜索和列搜索本质上都是拿你输入的关键词去匹配这些文本串。这也是为什么很多莫名其妙的搜不到问题最后都出在你看到的单元格内容和实际参与搜索的内容不是一回事上——这个问题一会儿专门讲。1.2 全局搜索与列搜索两个互不干扰的维度DataTable 搜索条件里最常用的两个入口是table.search()和table.column(...).search()。前者负责全局搜索也就是默认右上角那个搜索框它会在所有列的可搜索文本里做匹配后者负责单独的某一列只在指定列里过滤。这两个维度是互相独立的而且默认是叠加关系不是替换关系。举个例子你全局搜索了北京同时又在第 2 列搜索了已完成最终展示的行必须同时满足这两个条件。这个叠加逻辑对大多数业务场景是对的比如订单列表页关键字 状态 日期范围的组合筛选本质上就是多个搜索条件的叠加。需要注意DataTable 默认的全局搜索是 smart 模式它会把你输入的关键词按空格拆成几段每一段都得在行数据的某个位置出现才算匹配。这个设计本意是让用户输入北京 已完成也能命中但很多人误以为它支持 AND 语法其实它是所有词都要出现的隐式规则。如果你想做真正的精确匹配可以给search()传参数关闭 smart后面会给出具体写法。1.3 自定义搜索函数第三种搜索条件除了全局和单列搜索DataTable 还留了一个非常强力的后门$.fn.dataTable.ext.search。这是一个全局的过滤函数数组你可以 push 进去任意多个函数每个函数接收这一行的数据返回true表示保留返回false表示过滤掉。这个 API 是处理复杂搜索条件的杀手锏。比如你要按金额区间 1000~5000筛选、按日期范围筛选、甚至要跨列做逻辑判断比如当类型为 A 时金额要大于 X类型为 B 时金额要大于 Y用全局搜索和列搜索都没法优雅表达但 ext.search 可以。它相当于把搜索条件从字符串匹配升级成了任意逻辑函数判断。要理解的是ext.search里的函数和内置搜索也是叠加关系。也就是说全局搜索、列搜索、自定义搜索函数三者同时生效时行要满足所有条件才会被展示。这个机制我们在写组合搜索页面时要充分利用而不是在一个地方把所有逻辑写死。2. 三种常见搜索条件的实现方式与代码细节2.1 全局搜索框重新定义提示文案与触发方式DataTable 默认会渲染一个搜索框位置由dom选项里的f控制文案默认是英文的 Search:。很多项目第一件事就是把文案改成中文。最简单的做法是配置language.search$(#myTable).DataTable({ language: { search: 筛选 } });但这里有个限制改文案容易改 placeholder 却不那么直接。DataTable 内置搜索框的 HTML 是固定的没办法直接给它加 placeholder 属性。我试过几种方案最干净的是自己干掉默认的f然后在外面自己放一个搜索框。$(#myTable).DataTable({ dom: lrtip // 注意去掉了 f }); // 外部自定义搜索框 $(#customSearch).on(keyup, function () { table.search(this.value).draw(); });用这个方案还有一个额外好处你可以完全控制触发的时机。默认的keyup触发有点太频繁尤其是表格数据量大的时候每敲一个字符就重新过滤一次虽然 DataTable 内部做了优化但页面还是会感觉卡。我一般会加一个简单的防抖let timer; $(#customSearch).on(input, function () { clearTimeout(timer); const keyword this.value; timer setTimeout(() { table.search(keyword).draw(); }, 400); });table.search()的完整签名是search(input, regex, smart, caseInsen)。如果你要做精确搜索第二和第三个参数要这么传// 按正则精确匹配整行文本 table.search(^ keyword $, true, false).draw();2.2 单列搜索footer输入框与独立筛选控件单列搜索最常见的场景是多列表格里的状态列城市列这类维度筛选。推荐的方式是在tfoot里给对应列放一个输入框监听输入事件然后用columns().every()遍历绑定。tfoot tr tdinput typetext placeholder按姓名搜索/td tdinput typetext placeholder按城市搜索/td td/td td/td /tr /tfootconst table $(#myTable).DataTable(); table.columns().every(function () { const column this; $(input, column.footer()).on(keyup change, function () { if (column.search() ! this.value) { column.search(this.value).draw(); } }); });这里有个很关键的细节column.footer()只有在页面里存在tfoot并且 DataTable 初始化时能读到tfoot才会生效。如果你初始化之后动态加的tfoot需要调用table.columns.adjust()重新计算事件绑定也可能失效。另外columns().every()绑定的触发条件是每个 footer 里都有输入框如果某列不需要筛选留空 td 就行绑定事件时$(input, column.footer())选不到元素也就不会绑定不会报错。还有一点我踩过坑column.search()是有状态的你输入abc之后清空输入框如果直接column.search().draw()它是能清掉的但很多人写的是column.search(this.value).draw()value 为空字符串时其实也能清空。真正的问题是当你用table.search()设置了全局搜索值之后再想单独清某一列的搜索需要column.search().draw()同时不能动全局搜索两者互不影响这点和上面讲的叠加关系是对应的。如果不想用 footer更灵活的方案是做成页面顶部的独立筛选区$(#statusFilter).on(change, function () { table.column(5).search(this.value).draw(); });2.3 ext.search多条件组合的自定义搜索当搜索条件超过在某列里包含某个词这个范围就需要$.fn.dataTable.ext.search登场了。它的用法很直接把你需要的过滤函数塞进这个全局数组然后调用draw()触发重绘。下面是一个经典的数字范围区间筛选示例我经常拿它当模板$.fn.dataTable.ext.search.push(function (settings, searchData, index, rowData) { const min parseInt($(#minAmount).val(), 10); const max parseInt($(#maxAmount).val(), 10); const amount parseFloat(searchData[3]) || 0; // 假设金额在第4列 // 如果两个输入框都为空不过滤 if (isNaN(min) isNaN(max)) { return true; } // 只有一个边界 if (isNaN(min) amount max) return true; if (isNaN(max) amount min) return true; // 两边都有值 return amount min amount max; }); // 在输入框变化后触发 $(#minAmount, #maxAmount).on(input, function () { table.draw(); });这个函数的四个参数里searchData是最常用的它是这一行在所有列上的过滤数据数组注意它不是渲染后的显示文本而是搜索用的那一份。index是这一行在原始数据中的位置rowData是原始的行数据对象如果你用对象数组作为数据源这里能拿到。用 ext.search 有个非常容易踩的坑它是全局的不会随着表格销毁自动清空。如果你的页面里有多张表或者你在 SPA 里反复初始化同一个容器旧的回调函数会残留。轻则搜索结果被上一个页面的条件影响重则控制台直接飘红。我现在的习惯是每次初始化表格之前先做一个清理动作$.fn.dataTable.ext.search.length 0;然后再 push 新的函数。这个清理动作基本无害但能避免你排查半天为什么条件不生效。3. 实战给订单表格加上组合搜索条件3.1 页面结构搜索区表格区怎么设计理论讲再多不如一个完整案例。我拿一个典型的订单管理页面来演示表格有订单号、客户姓名、城市、金额、状态、下单时间六列搜索区放在表格上方包含关键字输入框、城市下拉框、状态下拉框、金额区间、日期范围还带一个重置按钮。DOM 结构大概长这样div classorder-filter input typetext idkeyword placeholder订单号/客户姓名 select idcity option value全部城市/option option value上海上海/option option value北京北京/option option value广州广州/option /select select idstatus option value全部状态/option option value待支付待支付/option option value已支付已支付/option option value已发货已发货/option option value已完成已完成/option /select input typenumber idminAmount placeholder最小金额 input typenumber idmaxAmount placeholder最大金额 button idresetBtn重置/button /div table idorderTable classdisplay stylewidth:100% thead tr th订单号/th th客户姓名/th th城市/th th金额/th th状态/th th下单时间/th /tr /thead /table这里的设计思路是关键字用全局搜索城市和状态用column().search()金额区间用 ext.search。三种搜索条件各管各的互不侵入。这样做的好处是任何一个条件变化时你只需要调用table.draw()重新触发一次过滤不用关心其他条件是怎么设置的DataTable 会自动把三层条件叠加。3.2 初始化配置与搜索逻辑绑定初始化时我建议把dom、language一次性配好省得后面反复改。注意我这里故意去掉了内置搜索框的f因为关键字搜索已经放在顶部搜索区了表格右上角再放一个默认搜索框会显得多余。let table $(#orderTable).DataTable({ dom: lrtip, language: { lengthMenu: 每页 _MENU_ 条, search: 搜索, zeroRecords: 没有找到匹配的记录, info: 共 _TOTAL_ 条当前显示 _START_ 到 _END_ 条, infoFiltered: 从 _MAX_ 条中筛选 }, ajax: /api/orders, columns: [ { data: orderNo }, { data: customerName }, { data: city }, { data: amount }, { data: status }, { data: createTime } ] });如果用服务端模式ajax 是请求后端接口如果数据量不大、前端已经有全量数据可以直接用data传数组。两种模式下搜索条件的代码大体相同区别在于服务端模式会把全局搜索值、列搜索值作为请求参数发给后端ext.search 在前端不生效后面会专门讲。接下来是绑定三个搜索区的逻辑。我会把它们拆成独立的函数避免事件回调里堆一大坨// 关键字全局搜索 $(#keyword).on(input, function () { table.search(this.value).draw(); }); // 城市列第3列索引2 $(#city).on(change, function () { table.column(2).search(this.value).draw(); }); // 状态列第5列索引4 $(#status).on(change, function () { table.column(4).search(this.value).draw(); });金额区间走 ext.search。需要注意ext.search 的回调函数要在初始化之前或者初始化之后 push 都行关键是触发draw()的时候它已经在了$.fn.dataTable.ext.search.push(function (settings, searchData, index, rowData) { const min $(#minAmount).val(); const max $(#maxAmount).val(); const amount parseFloat(searchData[3]); if (amount undefined || isNaN(amount)) return true; if (min ! max ! ) { return amount parseFloat(min) amount parseFloat(max); } if (min ! ) { return amount parseFloat(min); } if (max ! ) { return amount parseFloat(max); } return true; });3.3 组合条件如何与分页、排序、清空配合组合搜索有一个小细节很容易忽略draw()之后DataTable 默认会重置到第一页。用户正在看第 5 页突然改了筛选条件页面跳回第 1 页体验上其实还行但反过来说有些场景我们希望重绘后保持当前页码这时可以传false给 drawtable.draw(false);但要注意如果搜索条件变了当前页码可能超出总页数DataTable 会自动修正页码所以大多数情况下draw(false)是安全的。我一般只在重置搜索条件这种操作时用普通的draw()让用户回到第一页符合预期。重置按钮的逻辑要清理三类搜索状态全局搜索、列搜索、自定义 ext.search。只清一个就会造成条件残留$(#resetBtn).on(click, function () { $(#keyword).val(); $(#city).val(); $(#status).val(); $(#minAmount).val(); $(#maxAmount).val(); table.search().draw(); // 清全局 table.column(2).search(); // 清城市列 table.column(4).search(); // 清状态列 $.fn.dataTable.ext.search.length 0; // 清自定义搜索 // 重新push金额过滤函数或者重置后不再需要过滤 table.draw(); });这里有个细节ext.search.length 0是清掉所有自定义搜索回调。如果你重置后还想保留金额过滤函数就需要在清理后重新 push 回去或者在 push 之前判断输入框是否为空再决定是否过滤。我建议把 ext.search 的 push 逻辑封装成一个函数例如initAmountSearch()重置时先清空数组再调用一次这样代码更清晰。3.4 细节打磨状态保存、空结果提示、URL同步搜索条件一旦多了用户就会希望我刷新页面后条件还在。DataTable 的stateSave: true默认能保存全局搜索值和列搜索值但它不保存 ext.search 自定义条件因为 DataTable 并不知道你自定义了哪些逻辑。我自己常用的方案是手动存 localStorage重置和初始化时都读取let table $(#orderTable).DataTable({ stateSave: true, stateSaveCallback: function (settings, data) { const searchState { keyword: $(#keyword).val(), city: $(#city).val(), status: $(#status).val(), minAmount: $(#minAmount).val(), maxAmount: $(#maxAmount).val() }; localStorage.setItem(orderTableState, JSON.stringify(searchState)); }, stateLoadCallback: function (settings) { return JSON.parse(localStorage.getItem(orderTableState) || {}); } }); // 页面加载后从state里回填搜索区 $(function () { const saved JSON.parse(localStorage.getItem(orderTableState) || {}); if (saved.keyword) $(#keyword).val(saved.keyword); if (saved.city) $(#city).val(saved.city); if (saved.status) $(#status).val(saved.status); if (saved.minAmount) $(#minAmount).val(saved.minAmount); if (saved.maxAmount) $(#maxAmount).val(saved.maxAmount); table.draw(); });这个做法比默认的 stateSave 更可控因为它把UI 上的搜索控件状态和表格内部搜索状态一起保存了。否则你只恢复了表格内部搜索搜索框里的值却是空的用户一眼就看到条件不对。另外还有个小体验关键字输入没有匹配结果时可以自定义 zeroRecords 提示这个在language里配置即可不需要额外的 JS。想要更醒目的空状态样式可以直接用initComplete回调监听draw事件在结果是空的时候显示一个自定义提示块。4. 搜索条件的常见问题与排查技巧4.1 搜索框输入了却没反应这个问题排在所有 DataTable 搜索问题里的第一位。常见原因有三个第一search()之后忘了调用draw()第二初始化时searching: false把搜索功能关了但代码还在调table.search()API 不会报错只是没有任何效果第三表格是用destroy: true重新初始化的但你持有的table变量还是旧实例调用的 search 方法作用在旧实例上页面上的新表格完全不理会。排查思路很直接先在控制台打印table.search()的返回值看是不是你期望的关键词。如果返回值是对的但页面上数据没变就检查是不是没draw()或者draw被某个异常提前中断了。如果返回空字符串多半是实例不对。我自己遇到最多的是旧实例问题。尤其在后台系统里一个 Tab 页反复打开关闭DataTable 容器被复用很容易出现多个实例叠加。我的习惯是每次初始化前先检查容器是否已有 DataTable 实例if ($.fn.DataTable.isDataTable(#orderTable)) { $(#orderTable).DataTable().destroy(); }这行代码能避免绝大多数搜索没反应和重复初始化的坑。4.2 render格式化列搜不到预期内容前面 1.1 节埋的那个坑这里详细展开。假设你的日期列原始数据是时间戳渲染成2025-06-01这种格式。用户在全局搜索框里输入2025-06-01会发现搜不到。原因是 DataTable 默认的搜索数据用的是原始数据而不是 render 之后的结果。时间戳字符串里并没有2025-06-01这几个字符自然匹配不到。解决办法是在列定义里给 render 增加 filter 类型让搜索也使用格式化后的文本{ data: createTime, render: { display: function (data) { return dayjs(data).format(YYYY-MM-DD); }, filter: function (data) { return dayjs(data).format(YYYY-MM-DD); } } }如果只是显示格式化、搜索允许按原始数据匹配那可以不写 filter。业务上怎么选取决于需求比如金额列显示为千分位用户搜索1,000还是1000我一般会把 filter 也定义成不含千分位的纯数字这样用户输入数字就能搜到输入带逗号的字符串也不会出错。排查这类问题有一个通用手段在 ext.search 回调里打印searchData看看这一列的过滤数据到底是什么。你能直接确认是原始值还是渲染值再决定要不要配 filter。4.3 正则搜索误伤与转义处理DataTable 的search()第二参数可以启用正则表达式模式这功能很强大但也很危险。用户输入1.5按字面意思应该是匹配包含1.5的文本但在正则里.是任意字符可能连1a5都匹配上。如果全局搜索框开放了正则模式不处理转义很容易出现莫名其妙的搜索结果。我自己一般不给用户开放正则搜索如果业务上确实需要也要在把用户输入传给search()之前做一次转义function escapeRegex(value) { return value.replace(/[.*?^${}()|[\]\\]/g, \\$); } table.search(escapeRegex(this.value), true).draw();这个函数可以存成一个公共方法。另外要注意如果同时设置了regex: trueDataTable 会自动把 smart 搜索关掉因为正则本身就是精确匹配逻辑两者叠加没有意义。这个行为我记得很清楚因为当初我以为 smart 还能辅助正则做分词结果发现它被忽略了后来看文档确认了。4.4 serverSide模式下的搜索条件处理与性能页面数据量一旦到几十万行前端过滤就扛不住了这时候会开serverSide: true。在这个模式下前端搜索条件的作用机制会发生变化全局搜索和列搜索会变成请求参数发送给服务器DataTable 不会在前端做任何过滤逻辑所以$.fn.dataTable.ext.search会直接失效。发出的请求参数大概是这样的约定search[value]关键字 search[regex]false columns[0][search][value] columns[0][search][regex]false columns[2][search][value]上海 columns[4][search][value]已支付后端需要自己去解析这些参数拼 SQL 或查询条件。这里我要提醒一个容易被忽略的点全局搜索的search[value]和后端接口之间的语义要对齐。有些后端只把search[value]拿去匹配某一个字段并不做全字段扫描导致用户以为全局搜索是搜整行结果只搜了订单号。这个偏差最好在前后端联调时明确下来。服务端模式下性能优化的关键参数是searchDelay。它本意是给搜索输入加一个内置的短延迟避免每次键盘敲击都立刻发请求。我一般设置为 400ms 到 500mslet table $(#orderTable).DataTable({ serverSide: true, ajax: /api/orders, searchDelay: 400, // ... });就算前端已经自己做了防抖searchDelay也建议留着因为 DataTable 内部还会因为排序、分页等操作触发重绘这个参数能让它与搜索输入之间的触发路径更平滑。实测下来300ms 以下在慢网环境下仍会出现一连串请求400ms 是个比较稳妥的值。4.5 搜索性能的其他优化思路非服务端模式下如果前端一次性加载了几万行搜索还是会明显变慢。除了开启searchDelay还可以从这几个方向优化一是尽量缩小表格列。DataTable 每个单元格都会参与构建过滤数据列越多、文本越长过滤开销越大。没必要参与搜索的列可以在列定义里通过searchable: false关掉。二是避免在回调里做一些昂贵操作。ext.search 会对每一行执行一次你在里面写$(#minAmount).val()其实问题不大但如果写$(.foo).each(...)这种会导致每行都重新查 DOM性能就完全失控了。应该把输入值先取好再 push 过滤函数。三是如果用stateSave保存了大量状态每次初始化都会额外耗时可以只保存真正需要的字段或者干脆手动去操作 localStorage不要用 DataTable 默认的stateSave: true。4.6 搜索条件与 Column visibility 联动的一个隐藏坑还有一个很隐蔽的问题当用column().search()搜索某一列时如果该列被columns.visible(false)隐藏了DataTable 默认会怎样答案是隐藏列不参与全局搜索但column().search()依然对该列生效。这就可能导致表格显示的数据和用户预期对不上——用户看不到这一列但他之前设置的条件还在过滤。我遇到过一个实际案例用户在某列上用了单列筛选然后通过列显示控制把这一列隐藏了之后表格一直在少数据排查半天才发现是隐藏列的条件还在生效。解决办法是监听列可见性变化隐藏时同步清空对应列的搜索值table.on(column-visibility.dt, function (e, settings, columnIdx, state) { if (!state) { table.column(columnIdx).search().draw(); } });这个细节不算高频但一旦遇到非常浪费时间。4.7 调试搜索状态的辅助工具排查 DataTable 搜索问题时我强烈建议先去控制台直接操作实例。用几个 API 就能看到全貌table.search(); // 当前全局搜索值 table.column(2).search(); // 某列搜索值 $.fn.dataTable.ext.search; // 自定义过滤函数数组这三个打印出来叠加关系一目了然。很多时候搜索条件不对不是 DataTable 的 bug而是你自己在某个地方遗留了搜索状态没清干净。把这三个值打出来对照需求基本能定位问题。也可以给表格绑定 draw 事件在每次重绘后打印当前过滤后的记录数table.on(draw.dt, function () { console.log(table.rows({ filter: applied }).count()); });这个数字如果和你手动数出来的行数对不上说明过滤逻辑比预想的复杂这时候再回头检查搜索条件不迟。4.8 搜索框的 IME 输入法兼容问题国内项目绕不开中文输入法的问题。用默认搜索框或者自己绑定的keyup事件时用户用拼音输入法打字keyup会触发了中间态的拼音串导致表格跟着拼音过滤体验很差。解决办法是监听compositionstart和compositionend事件在输入中文的组合过程中不触发搜索等组合结束再搜let isComposing false; $(#keyword).on(compositionstart, function () { isComposing true; }); $(#keyword).on(compositionend, function () { isComposing false; handleSearch(); }); $(#keyword).on(input, function () { if (!isComposing) { handleSearch(); } }); function handleSearch() { table.search($(#keyword).val()).draw(); }这个细节看起来小但做后台管理系统的人都知道搜索框对输入法不友好用户会直接认为功能坏了。加上 composition 判断后既能保留输入过程中的实时搜索体验又不会搜出拼音中间态。我个人在实际项目里的经验是DataTable 搜索条件的功能边界比很多人想象的大得多。它真正复杂的不是 API 调用本身而是三层数据的认知、三种搜索条件的叠加关系以及不同业务场景下怎么选型。做列表页的时候先想清楚到底用全局搜索还是单列搜索还是 ext.search再动手写代码往往比事后再补丁式地加条件要省事得多。最后再分享一个小技巧把 ext.search 的清理、字段转义、输入法防抖这些逻辑抽成公共函数放进团队的公共前端库里下次任何项目接 DataTable 搜索条件基本就是几行配置的事。
返回列表