今天做了什么
这两天在梦想城镇小程序的列表页上花了不少时间。这个列表页是镇长互助社区的核心入口:用户发求助帖、按关键词搜自己缺的卡牌或产品,再进详情帮别人。问题有两个,用户反馈得很直接——搜得很不准,而且不少早就过期的求助还挂在「进行中」里能点。
我把它当成一次典型的「查询质量」重构来做,而不是头痛医头。下面把过程、坑和方法论都记下来,也方便以后面试时拿出来讲。
搜索为什么不准
最初的 buildWhere 只匹配了 title 和 gameId 两个字段。可真实数据里,卡牌名藏在 haveCards[].name / wantCards[].name,产品物品名在 items[].name,正文在 content。搜「火车票」「小麦」这些词,压根没进索引,自然空手而归。
修法第一步是扩展字段,用云开发的 _.or 一并匹配:
const kw = escapeRegexp(keyword.value.trim())
buildWhere = _.or([
{ title: db.RegExp({ regexp: kw, options: 'i' }) },
{ content: db.RegExp({ regexp: kw, options: 'i' }) },
{ gameId: db.RegExp({ regexp: kw, options: 'i' }) },
{ 'items.name': db.RegExp({ regexp: kw, options: 'i' }) },
{ 'haveCards.name': db.RegExp({ regexp: kw, options: 'i' }) },
{ 'wantCards.name': db.RegExp({ regexp: kw, options: 'i' }) },
])
第二步是正则转义。原来的 database.RegExp({ regexp: keyword }) 没转义,用户输入 (、*、[、? 这类字符,云库查询直接报错。加一行兜底:
function escapeRegexp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
第三步是触发方式,这个来回改了两轮。一开始做成「输入即搜」防抖 300ms,用户反馈输入过程中列表乱跳;回退成「回车才搜、清空才刷新」;后来用户又要求改回输入即搜。最终版是输入停顿 300ms 才查库,清空关键词立刻回全部,回车则立即搜并清掉防抖计时器——既避免每敲一字就跳,又不让回车被防抖串扰。
一个很隐蔽的 Taro 4 坑
这里踩到一个容易忽略的坑:Taro 4 的小程序 input 组件,如果同时写 v-model 和显式 @input,keyword 会不同步。表现就是输入「小」字,列表闪一下却显示全部——因为 buildWhere 读到的是空 kw,直接回了全部。
修复办法是模板去掉 v-model,改成 :value="keyword",在 onSearchInput(e) 里显式取值:
onSearchInput(e) {
keyword.value = (e.detail.value) || ''
}
小程序 input 的值路径是 e.detail.value,不是 e.target.value。这是 Taro 4 + Vue3 小程序受控输入的可靠写法,以后这类 input 一律这么写,别依赖 v-model。
过期帖过滤与可选字段兜底
再说过期帖。原来「进行中」只按 status:'open' 过滤,可产品帖过期后 status 还是 open,于是继续挂在列表里还能帮。我用 conditions 数组统一拼装:
if (filterStatus === 'open') {
conditions.push({ status: 'open' })
conditions.push(_.or([
{ expiryAt: _.exists(false) }, // 旧数据缺字段
{ expiryAt: _.eq(0) }, // 卡牌帖永久有效
{ expiryAt: _.gt(Date.now()) }, // 产品帖未过期
]))
}
这里出了一次回归。第一版我只写了 _.eq(0) 和 _.gt(now),结果卡牌帖从「进行中」整类消失。排查发现,「过期时长」功能上线前发布的卡牌帖,以及部分未设时长的旧产品帖,数据库里根本没有 expiryAt 字段,值是 undefined。_.eq(0) 匹配不到 undefined,_.gt(now) 对 undefined 也无从比较,于是被一并过滤。补 _.exists(false) 后,语义变成「字段不存在 / 等于 0 / 大于 now」三者任一都算有效,只排除「产品帖 expiryAt 落在 (0, now]」的区间。微信云开发的 db.command.exists 原生支持。
白屏事故
还有个白屏:groupByTheme 重构时漏加到 setup 的 return 里,模板直接调用拿到 undefined,整页白屏。教训很朴素:模板里直接调用的纯函数,必须在 setup return 显式导出,import 不够。
方法论沉淀
这几件事看着零散,背后是同一套工程素养:
- 列表查询的质量,取决于是否真实覆盖字段分布和数据演进(旧数据可能缺字段)。
- 查询条件集中到
buildWhere用conditions数组拼装,比到处散写稳得多,也方便统一加正则转义与状态过滤。 - 对可选字段永远假设它可能缺失,用
_.exists(false)兜底,是低成本高收益的写法。 - Taro 4 + Vue3 小程序里
input受控一律:value+@input显式同步,不碰v-model。
顺带一句,镇长互助这个模块后来因为微信社区类目下架了,但这套列表查询和筛选逻辑是通用的,换个项目照样能复用。
如果你想看更多这类工程复盘,可以来我的博客 https://blog.alleria.cn 翻翻,或者去 GitHub 仓库翻我的技术笔记。