中文输入法期间怎样触发搜索?composition 与请求竞态如何一起处理?
从输入拼音时搜索框频繁请求的现象出发,解释组合输入、最终值、防抖和请求身份,给出原生输入框实现并说明不同设备的事件边界。
简答
- 输入法组合期间(compositionstart 到 compositionend)不应提交搜索查询。
- 组合开始时让旧请求失效,组合结束后再按最终值防抖搜索。
- 每次请求要带身份标识,只接受最新请求的结果,避免竞态覆盖。
- Enter 在组合期间可能用于确认候选词;要用真实输入法和设备测试。
用户输入拼音准备选择“前端”,搜索框却在每个中间音节都请求接口,甚至返回结果后重置输入框,导致候选词丢失。输入法组合阶段的文本是编辑过程的一部分,不一定是用户确认的最终查询。
compositionstart 表示开始组合,compositionend 表示结束;InputEvent.isComposing 可以说明 input 事件是否处于组合阶段。不能只监听 keyup,因为粘贴、语音输入、移动键盘和输入法不一定与物理按键一一对应。
保留输入,不在组合期间提交查询
下面的函数接收已有 input 和 render 回调。render 负责用安全的文本或组件展示结果;搜索端点约定为同源 /api/search?q=...,返回 JSON。它展示的是输入与请求生命周期,不实现完整搜索建议的键盘选择模式。
function bindImeSearch(input, render) {
let composing = false;
let timer;
let request;
let revision = 0;
let disposed = false;
let lastScheduled = null;
function invalidate() {
clearTimeout(timer);
request?.abort();
revision += 1;
}
function schedule() {
if (disposed || composing) return;
const query = input.value.trim();
// compositionend 后可能紧接相同最终值的 input,避免重复安排。
if (query === lastScheduled) return;
lastScheduled = query;
invalidate();
const current = revision;
if (!query) {
render({ state: 'empty', items: [] });
return;
}
timer = setTimeout(async () => {
const controller = new AbortController();
request = controller;
try {
const response = await fetch('/api/search?' + new URLSearchParams({ q: query }), {
signal: controller.signal,
});
if (!response.ok) throw new Error(`搜索失败:HTTP ${response.status}`);
const data = await response.json();
if (!disposed && current === revision) render({ state: 'ready', items: data });
} catch (error) {
if (!disposed && current === revision && !controller.signal.aborted) {
lastScheduled = null;
render({ state: 'error' });
}
}
}, 200);
}
function onStart() {
composing = true;
lastScheduled = null;
invalidate();
}
function onEnd() {
composing = false;
schedule();
}
function onInput(event) {
if (!event.isComposing && !composing) schedule();
}
input.addEventListener('compositionstart', onStart);
input.addEventListener('compositionend', onEnd);
input.addEventListener('input', onInput);
return () => {
disposed = true;
invalidate();
input.removeEventListener('compositionstart', onStart);
input.removeEventListener('compositionend', onEnd);
input.removeEventListener('input', onInput);
};
}
输入框自己的值保持由用户编辑,不在组合期间把它替换成格式化字符串。框架受控输入也要遵守这个边界,不能为了“过滤非法字符”在每次中间输入时重新写值、移动选区或重新挂载节点。
200 毫秒只是教学防抖值,不是输入法协议要求。lastScheduled 用于避免相同查询重复安排;正式 UI 应另提供显式重试按钮,不要求用户改一个字符才能重试。返回 JSON 在实际项目中还应按响应契约校验,不能把未知对象直接当作可渲染列表。
为什么开始组合就要让旧请求失效
用户先搜“前”,随后开始新的组合输入。如果旧请求此时返回,不能用旧结果覆盖新输入阶段的界面。invalidate 在组合开始时就增加 revision,并取消已有请求,而不是等到新词确认后才更新身份。
AbortController 减少无用网络工作,revision 则防止已经完成网络传输、仍在解析中的旧结果写回。两者职责不同,不能只保留一个。卸载后还要清理防抖计时器与监听器,避免晚到结果更新已不存在的组件。
Enter 可能用于确认候选词
搜索框按 Enter 提交时,必须先确认不在组合输入中。不能在候选词确认过程中既提交表单又结束组合,也不要无条件 preventDefault 阻断输入法操作。KeyboardEvent.isComposing 与内部组合状态可以提供判断,但不同操作系统、输入法和浏览器的最终事件顺序仍需实测。
不要用“出现 keyCode 229 就一定是什么状态”作为唯一长期协议。旧环境的兼容分支可以存在,但需要说明对应目标与回归用例。快捷键、按键提交与 input 查询应分别测试。
测试哪些真实操作
纯逻辑测试可人工派发组合事件和 input,验证组合中不请求、最终值只安排一次、旧结果不覆盖、清空与卸载正确。这样的事件是程序模拟,不能证明真实输入法候选窗口、选区与平台顺序正常。
真实设备还应覆盖中文拼音、日文转换、移动键盘、粘贴、撤销、语音输入和 Enter 确认。搜索建议若能选择条目,需要另行实现组合框的键盘与无障碍契约;不应因为输入法处理正确就声称整个搜索控件已无障碍。
搜索参数进入 URL 前使用编码 API,服务端限制长度、频率并校验权限;组合输入处理不是安全过滤器。敏感输入也不应作为分析事件或错误日志原样上报。