手把手实现高效的多条件搜索功能优化

艺菲 Dev 交互 阅读 1,574
赞 25 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近做完一个后台管理项目,核心功能是数据列表页加一个多条件搜索。本来以为就是几个输入框加个查询按钮的事儿,结果上线前一周被产品拉着改了三轮交互,差点没把我干趴下。

手把手实现高效的多条件搜索功能优化

一开始我图省事,直接用表单收集所有筛选项,每次 change 就更新 state,点击“搜索”才调接口。后来产品说要支持“实时筛选”,也就是选一个条件就自动请求——这下可好,用户点个下拉框都得发请求,后端报警了。

最后我们折中:部分字段(比如状态、类型)支持“即时搜索”,输入框类的还是手动触发。技术上用了 React + Ant Design 的 Form 和 Table 组件,算是老搭档了,熟悉套路也少踩坑。

最大的坑:性能问题

你以为改个触发时机就完事儿了?too young。很快我们就发现,用户在快速连续操作时,比如连点几个筛选项,接口会乱序返回。

举个例子:先点“未处理” → 接口A发出;紧接着点“紧急” → 接口B发出。但因为网络波动,A比B慢,结果页面显示的是“未处理”的数据,尽管用户最后选的是“紧急”。这种体验简直灾难。

折腾了半天,发现解决方案其实也不复杂:加个请求锁 + 取消上一次未完成的请求。Axios 自带 CancelToken(虽然现在推荐用 AbortController),于是上了这个:

let abortController = null;

const fetchData = async (filters) => {
  // 取消上一次请求
  if (abortController) {
    abortController.abort();
  }

  abortController = new AbortController();

  try {
    const res = await fetch('https://jztheme.com/api/list', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(filters),
      signal: abortController.signal,
    });
    const data = await res.json();
    setData(data.list);
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('请求已取消');
    } else {
      console.error('请求失败:', err);
    }
  }
};

这里注意我踩过好几次坑:第一次忘了判断 err.name === 'AbortError',导致每次切换都报错到监控平台,半夜被钉钉叫醒……第二次是没把 abortController 放到组件外层或 useRef 里,导致根本没法取消。

还有个问题是 debounce 没设对。最开始给所有字段都加了300ms防抖,结果输入框输一半就触发了,用户体验很差。后来调整成:文本类输入框用300ms防抖,下拉选择类直接无延迟发送(配合上面的 abort 机制)。

表单状态管理有点乱

随着条件越来越多,form 表单的状态也开始变得难以维护。一开始是每个字段单独 useState,写着写着发现 reset 的时候要一个个清,麻了。

后来改成用 useForm(Ant Design 的 Hook),统一管理:

const [form] = Form.useForm();

// 提交时获取值
const handleSearch = () => {
  const values = form.getFieldsValue();
  fetchData(values);
};

// 重置并刷新
const handleReset = () => {
  form.resetFields();
  fetchData({});
};

但这带来新问题:有些字段需要默认值,比如“最近7天”。我在 initialValues 里设了,但 reset 后确实回到了7天,可接口没传这个参数,默认查的是全部时间……这里卡了我一下午才意识到:form.resetFields() 是重置到 initialValues,但我得主动触发一次查询。

所以最后写成了:

const handleReset = () => {
  form.resetFields();
  const defaultValues = form.getFieldsValue(); // 获取重置后的值
  fetchData(defaultValues);
};

看似 trivial,但在多条件组合下很容易遗漏。建议大家只要用了默认值,reset 后一定要重新查一次,别指望页面自己刷新。

高级筛选收起展开的玄学问题

界面空间有限,我们把部分冷门条件做成“高级筛选”,点击展开更多。结构大概是这样:

<Form form={form}>
  <Row>
    <!-- 常规条件 -->
    <Col span={8}><Form.Item name="status">...</Form.Item></Col>
    <Col span={8}><Form.Item name="type">...</Form.Item></Col>
    <Col span={8}>
      <Button onClick={() => setShowAdvanced(!showAdvanced)}>
        {showAdvanced ? '收起' : '展开高级筛选'}
      </Button>
    </Col>
  </Row>
  {showAdvanced && (
    <Row>
      <Col span={8}><Form.Item name="createTime">...</Form.Item></Col>
      <Col span={8}><Form.Item name="operator">...</Form.Item></Col>
    </Row>
  )}
  <Row>
    <Col span={24} style={{ textAlign: 'right' }}>
      <Button type="primary" onClick={handleSearch}>搜索</Button>
      <Button onClick={handleReset}>重置</Button>
    </Col>
  </Row>
</Form>

问题来了:当高级筛选关闭时,那些字段的值还在 form 里存着!也就是说,即使你看不见 createTime,它依然可能带着上次的值参与查询。

这到底是 bug 还是 feature?产品说是 bug。用户点了“收起”,就该当成这些条件不存在。

解决办法有两个方向:

  • 隐藏时清空字段值(太暴力,用户体验差)
  • 提交时不包含隐藏字段(更合理)

我选了后者。改造了一下 handleSearch:

const handleSearch = () => {
  const allValues = form.getFieldsValue();
  const activeValues = { ...allValues };

  // 如果高级筛选未展开,剔除相关字段
  if (!showAdvanced) {
    delete activeValues.createTime;
    delete activeValues.operator;
  }

  fetchData(activeValues);
};

简单粗暴有效。虽然代码看起来不优雅,但满足需求又不容易出错。有时候我觉得,比起设计模式,能跑通才是第一位的。

回顾与反思

做完回头看,这个功能总共花了大概6天,其中4天在修各种边界情况和交互细节。核心逻辑其实很简单,真正难的是“用户怎么用才不懵”。

做得好的地方:

  • 请求中断机制稳住了性能,没再出现数据错乱
  • reset + 默认值的联动处理完整
  • 高级筛选的状态隔离让用户感知清晰

还能优化的地方:

  • URL 参数同步没做。用户复制链接分享,别人打不开当前筛选结果。本来计划用 query-string 序列化 form 值,但排期紧就没上,靠 sessionStorage 缓存了一波,勉强过关。
  • 表单项太多后,加载时 skeleton 显示不自然。应该按条件分组渐进展示,现在是一股脑全出来。
  • 移动端适配基本没搞,折叠逻辑在小屏上有点崩。不过目前主要使用场景是 PC 端,暂时不管了。

说实话,到现在还有一个小问题没彻底解决:当用户极速切换多个下拉选项时,偶尔会出现“空白一秒”的现象,是因为 abort 后旧数据要不要保留的问题。我试过保留旧数据加 loading 遮罩,但产品嫌不明显;也试过加骨架屏,又太重。最后妥协方案是:只在首次加载时显示 loading,后续更新不提示。影响不大,但心里总觉得欠了点什么。

以上是我踩坑后的总结,希望对你有帮助

多条件搜索看着简单,真要做顺滑并不容易。尤其是状态管理 + 请求控制 + 用户体验这三者之间,得反复权衡。

以上是我个人对这个功能的完整实践记录,有更优的实现方式欢迎评论区交流。这类“不起眼但巨麻烦”的功能我还会继续分享,毕竟日常搬砖就是这样,一边骂一边修。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论