前端知库

搜索联想(自动补全)组件要处理哪些问题?

输入防抖减少请求,用 AbortController 取消旧请求并校验返回对应的关键词以避免竞态;按关键词缓存结果;按 WAI-ARIA combobox 模式实现方向键、Enter、Escape 操作和 aria-expanded、aria-activedescendant 等属性。

参考答案

简答

  • 输入防抖:停止输入一段时间(如 200~300ms)后再请求,减少无意义请求;中文输入法组字期间不应触发搜索。
  • 竞态:新请求发出时用 AbortController 取消旧请求,并在结果返回时核对它对应的关键词仍是当前输入,否则丢弃。
  • 缓存:按规范化后的关键词缓存结果,回删时直接命中;缓存要有容量上限和过期时间。
  • 键盘与无障碍:按 combobox 模式实现,DOM 焦点留在输入框,用 aria-activedescendant 指向高亮项,方向键切换、Enter 选中、Escape 关闭。

防抖与输入法

JavaScript
let timer;
input.addEventListener('input', (event) => {
  if (event.isComposing) return; // 组字中不搜索,compositionend 后再处理
  clearTimeout(timer);
  timer = setTimeout(() => search(input.value), 250);
});

防抖的实现见《怎样实现只在停止调用后执行的防抖函数?》,输入法与竞态的组合处理见《中文输入法期间怎样触发搜索?composition 与请求竞态如何一起处理?》。

竞态与取消

JavaScript
let controller;
const cache = new Map();

const normalize = (value) => value.trim().toLowerCase();

async function search(raw) {
  const keyword = normalize(raw);
  if (!keyword) return render([]);
  if (cache.has(keyword)) return render(cache.get(keyword));

  controller?.abort();
  controller = new AbortController();
  try {
    const res = await fetch(`/api/suggest?q=${encodeURIComponent(keyword)}`, {
      signal: controller.signal,
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`); // 错误响应不进缓存
    const list = await res.json();
    cache.set(keyword, list);
    if (keyword === normalize(input.value)) render(list); // 只渲染当前关键词的结果
  } catch (error) {
    if (error.name !== 'AbortError') renderError();
  }
}

只靠取消不够:请求可能在 abort() 之前已经返回并进入解析阶段,所以渲染前仍要核对关键词。AbortController 只能让前端不再等待,服务端可能已经处理完请求,见《AbortController 能取消什么,不能取消什么?》。

关键词放进 URL 时用 encodeURIComponent 编码;渲染建议项时用文本节点或框架的默认转义,高亮匹配片段也不要拼接 HTML 字符串,避免 XSS。

缓存

  • 键用规范化后的关键词(去首尾空格、统一大小写),避免同义输入重复请求。
  • 用 Map 按插入顺序淘汰最旧项,设置上限(如 50 条)。
  • 数据会变化的场景给缓存设置过期时间。

键盘与 ARIA

HTML
<label for="city">城市</label>
<input id="city" role="combobox" aria-expanded="true" aria-controls="city-list"
  aria-autocomplete="list" aria-activedescendant="city-opt-2" />
<ul id="city-list" role="listbox">
  <li id="city-opt-1" role="option">北京</li>
  <li id="city-opt-2" role="option" aria-selected="true">上海</li>
</ul>
  • 弹层显示时 aria-expanded="true",隐藏时为 false。
  • aria-controls 指向 listbox;aria-autocomplete="list" 表示建议项与输入内容匹配。
  • 方向键移动高亮时更新 aria-activedescendant 和对应选项的 aria-selected,DOM 焦点不离开输入框,用户可以继续输入。
  • Enter 接受高亮项,Escape 关闭弹层;不要拦截输入框原有的文本编辑按键。
  • 结果数量变化可以用 aria-live 区域播报,例如"共 5 条建议"。

常见追问

  • 鼠标点击选项时为什么输入框先失焦导致弹层关闭?在选项的 mousedown 上 preventDefault() 保持焦点,或延迟处理 blur。
  • 空结果怎么显示?显示"无匹配结果",并保持 aria-expanded 与实际是否显示弹层一致。
  • 要不要设置最少输入字符数?结果集很大时可以要求至少 1~2 个字符,减少宽泛查询。