搜索联想(自动补全)组件要处理哪些问题?
输入防抖减少请求,用 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 个字符,减少宽泛查询。