微信小程序价格区间筛选实战:按钮交互与JS数组过滤

发布时间:2026/10/14 16:28:00
微信小程序价格区间筛选实战:按钮交互与JS数组过滤
1. 从一个价格筛选按钮说起为什么这个小功能值得单独写一篇做小程序开发的朋友大概都有过这种体验列表页的数据渲染跑通了样式也调得七七八八结果产品经理过来说“加个价格筛选吧用户想按价格区间看商品”。听起来简单但真动手写的时候问题一个接一个冒出来——按钮点击后数据怎么变原数组要不要保留过滤条件怎么组合多个按钮之间怎么互斥这一篇就围绕“用按钮实现对价格进行过滤”这个具体场景把从零到跑通的完整过程拆开讲。核心关键词是微信小程序、JS 数组过滤、按钮交互、价格区间筛选和数据绑定。适合刚学完小程序基础组件、想找一个完整小案例练手的人也适合已经能写页面但一遇到“数据变了视图没变”就卡住的开发者。我自己的习惯是学任何框架都不能只停留在“能跑”得找一个真实的小需求把它做透。价格过滤这个需求足够小小到一两个小时能搞定但又足够典型典型到它几乎覆盖了小程序开发中最核心的几个环节WXML 事件绑定、JS 数据处理、setData 更新机制、以及列表渲染的配合。把这一个案例吃透后面做搜索过滤、分类筛选、多条件组合查询思路都是通的。下面我会先讲清楚这个功能到底要解决什么问题再拆解数据流的设计然后一步步写代码最后重点讲几个我实际踩过的坑。文章里的代码都是可以直接复制到项目里跑的数据用模拟数据不依赖任何后端接口。2. 价格过滤到底在过滤什么需求拆解与数据流设计2.1 把“价格过滤”翻译成程序语言用户看到的是几个按钮全部、0-50、50-100、100以上。点一下列表里只剩下符合区间的商品。这个描述里藏着三个需要明确的东西。第一过滤的基准数据是什么。列表最初展示的是一份完整的商品数组这份数据必须原封不动地保留着。很多人第一次写的时候直接在原数组上做删除或截取结果点了一次“0-50”之后再点“全部”就回不去了因为原始数据已经被破坏。正确做法是准备两份数据一份是全量数据一份是当前展示数据。每次过滤都从全量数据出发而不是从上一次的结果出发。第二过滤条件怎么表示。价格区间本质上是一个最小值和一个最大值。比如“50-100”对应 min50、max100。“100以上”对应 min100、maxInfinity。“全部”对应 min0、maxInfinity。用两个数字就能统一描述所有按钮这样过滤函数只需要写一套逻辑。第三按钮状态怎么管理。用户需要知道当前选中的是哪个区间所以按钮要有选中态。这要求我们在数据里记录一个“当前激活的区间标识”点击时更新它WXML 里根据这个标识动态加 class。把这三件事想清楚整个功能的数据流就出来了点击按钮 → 更新激活标识 → 根据区间参数从全量数据过滤 → setData 更新展示数据。2.2 为什么不用后端过滤有人可能会问为什么不在后端做过滤前端只管展示在这个案例里数据量很小全部加载到前端再过滤响应速度是最快的点按钮的瞬间列表就变了没有任何等待。如果数据量很大比如上千条商品那确实应该把过滤条件传给后端。但作为学习案例前端过滤能让你完整体验“数据驱动视图”的整个过程这是小程序开发的基本功。而且实际项目中很多筛选场景就是前端做的。比如一个已经加载完的订单列表用户想按金额区间看没必要再请求一次接口。理解前端过滤的写法对后面做本地搜索、多条件组合筛选都有直接帮助。2.3 数据结构长什么样我准备了一份模拟数据每条商品有 id、name、price 三个字段。价格故意设置得分散一些覆盖各个区间方便测试边界情况。const allGoods [ { id: 1, name: 商品A, price: 12 }, { id: 2, name: 商品B, price: 35 }, { id: 3, name: 商品C, price: 48 }, { id: 4, name: 商品D, price: 50 }, { id: 5, name: 商品E, price: 68 }, { id: 6, name: 商品F, price: 99 }, { id: 7, name: 商品G, price: 100 }, { id: 8, name: 商品H, price: 156 }, { id: 9, name: 商品I, price: 230 } ];注意这里有几个边界值50、100 正好落在区间分界点上。过滤的时候到底包不包含边界需要提前想好。我的习惯是区间用“左闭右开”即 0-50 包含 0 不包含 5050-100 包含 50 不包含 100。这样区间之间不重叠用户不会觉得“怎么这个商品在两个区间都出现了”。当然你也可以用闭区间但一定要统一不能一个区间闭一个区间开。3. 页面骨架搭建WXML 里按钮和列表怎么写3.1 按钮组的布局与事件绑定按钮组用 flex 横向排列每个按钮绑定同一个点击事件通过>view classfilter-bar view wx:for{{filterOptions}} wx:keylabel classfilter-btn {{activeIndex index ? active : }} >view classgoods-list view classgoods-item wx:for{{showGoods}} wx:keyid text classgoods-name{{item.name}}/text text classgoods-price¥{{item.price}}/text /view /view view classempty-tip wx:if{{showGoods.length 0}} 当前价格区间没有商品 /view空状态这个细节很容易被忽略。如果某个区间过滤后没有数据页面就是一片空白用户会以为页面坏了。加一行提示体验会好很多。wx:if和wx:for可以同时用但要注意wx:for和wx:if在同一标签上时的优先级问题这里分开写更清晰。3.3 样式上的几个小考虑按钮的选中态要明显我用背景色和文字颜色同时变化来区分。按钮之间留一点间距手指点的时候不容易误触。列表项用卡片式布局价格右对齐视觉上更整齐。.filter-bar { display: flex; padding: 20rpx; gap: 16rpx; } .filter-btn { flex: 1; text-align: center; padding: 16rpx 0; border-radius: 8rpx; background: #f5f5f5; color: #333; font-size: 28rpx; } .filter-btn.active { background: #07c160; color: #fff; } .goods-item { display: flex; justify-content: space-between; padding: 24rpx; border-bottom: 1rpx solid #eee; } .goods-price { color: #e64340; font-weight: bold; } .empty-tip { text-align: center; color: #999; padding: 60rpx 0; font-size: 28rpx; }样式这部分没有太多技术难点但有一个经验按钮的点击区域要足够大。小程序的 rpx 单位在大部分手机上表现一致但按钮高度低于 60rpx 时手指点起来会有点费劲。我一般把可点击元素的最小高度设在 80rpx 左右。4. JS 逻辑核心过滤函数与 setData 的配合4.1 filterOptions 的设计把区间配置抽成一个数组放在 data 里。这样做的好处是 WXML 和 JS 共用同一份配置改区间只需要改一个地方。data: { filterOptions: [ { label: 全部, min: 0, max: Infinity }, { label: 0-50, min: 0, max: 50 }, { label: 50-100, min: 50, max: 100 }, { label: 100以上, min: 100, max: Infinity } ], activeIndex: 0, allGoods: [], showGoods: [] }注意Infinity在小程序的 data 里是可以正常序列化和使用的不用担心。用Infinity比用一个很大的数字比如 999999更语义化也不会出现“万一价格超过这个数”的尴尬。4.2 过滤函数的写法过滤函数接收 min 和 max返回新数组。用数组的filter方法一行就能搞定。filterByPrice(min, max) { return this.data.allGoods.filter(item { return item.price min item.price max; }); }这里用的是左闭右开区间。如果你想让 50-100 包含 100就把 max改成 max。但要注意如果两个相邻区间都包含边界值同一个商品会在两个区间里都出现。我建议统一用左闭右开最后一个区间用Infinity兜底这样所有商品都会被覆盖到且不重复。4.3 点击事件里做了什么点击事件的处理逻辑分三步拿到索引、更新激活状态、重新过滤并 setData。onFilterTap(e) { const index e.currentTarget.dataset.index; const option this.data.filterOptions[index]; const filtered this.filterByPrice(option.min, option.max); this.setData({ activeIndex: index, showGoods: filtered }); }看起来很简单但这里有一个非常关键的细节setData是异步的。也就是说你不能在setData之后立刻去读this.data.showGoods来获取更新后的值。在这个场景里因为过滤结果是提前算好的所以没问题。但如果你写成先setData({activeIndex: index})然后再在回调里根据this.data.activeIndex去过滤逻辑上也能跑但多了一次不必要的 setData。我习惯把能合并的 setData 合并成一次减少视图层的通信次数。4.4 页面加载时的初始化页面 onLoad 的时候把全量数据赋值给 allGoods同时把 showGoods 初始化为全量数据activeIndex 默认为 0。onLoad() { this.setData({ allGoods: allGoods, showGoods: allGoods }); }这里有一个容易犯的错误把allGoods和showGoods指向同一个数组引用。虽然后面过滤时filter会返回新数组不会修改原数组但为了保险起见我建议在赋值时用扩展运算符复制一份showGoods: [...allGoods]。这样即使后面某处不小心直接修改了showGoods也不会影响到allGoods。5. 实测中遇到的几个坑和排查过程5.1 点击按钮没反应dataset 取值的类型问题第一次写的时候我在 WXML 里写的是>applyFilters() { const { category, minPrice, maxPrice, keyword } this.data.filters; const result this.data.allGoods.filter(item { const matchCategory !category || item.category category; const matchPrice item.price minPrice item.price maxPrice; const matchKeyword !keyword || item.name.includes(keyword); return matchCategory matchPrice matchKeyword; }); this.setData({ showGoods: result }); }这种写法的好处是每个条件独立判断互不干扰。新增一个筛选维度只需要加一个判断不用改其他逻辑。6.2 排序与过滤的配合过滤之后往往还要排序。排序和过滤一样都应该基于全量数据操作而不是在上一次的结果上叠加。顺序是先过滤再排序因为过滤会减少数据量排序更快。const filtered this.filterByPrice(min, max); const sorted filtered.sort((a, b) a.price - b.price); this.setData({ showGoods: sorted });注意sort会修改原数组所以要先filter返回新数组再sort这样不会影响到allGoods。6.3 性能上的一个提醒如果数据量到了几百条以上每次点击都重新过滤全量数据虽然不至于卡顿但可以做一个简单的优化把过滤结果缓存起来。同一个区间第二次点击时直接取缓存不用重新计算。不过对于学习案例来说这个优化不是必须的先跑通再优化。7. 几个让代码更稳的实操习惯写这个小功能的过程中我总结了几个习惯后来在写更复杂的页面时也一直在用。第一个习惯是数据分层。全量数据、筛选条件、展示数据分开存放不要混在一起。全量数据是只读的筛选条件是用户操作的结果展示数据是计算出来的。三者职责清晰排查问题时很容易定位是哪一层出了错。第二个习惯是过滤函数保持纯粹。filterByPrice只接收参数、返回结果不修改任何外部变量也不调用 setData。这样这个函数可以单独测试也可以在其他地方复用。setData 只在事件处理函数里调用保持数据流向的单向性。第三个习惯是边界值单独测。价格过滤最容易出问题的就是边界值。我一般会专门准备几条价格正好等于区间端点的数据点一遍所有按钮确认每个商品只出现在该出现的区间里。这个测试花不了两分钟但能避免上线后被用户发现“怎么有个商品不见了”。第四个习惯是空状态一定要处理。不管你觉得某个区间多不可能为空都要加上空状态提示。用户的操作路径你永远猜不全加一行提示的成本极低但能避免很多困惑。这套价格过滤的写法说到底就是“全量数据 条件参数 过滤函数 setData”四件事。把这四件事的关系理顺后面做任何筛选功能都是在这个骨架上加东西。我自己在写的时候最大的收获不是学会了 filter 方法而是理解了小程序里“数据驱动视图”到底是什么意思——你不需要去操作 DOM只需要把数据算对视图自然就对了。