前端知库

中文输入法期间怎样触发搜索?composition 与请求竞态如何一起处理?

从输入拼音时搜索框频繁请求的现象出发,解释组合输入、最终值、防抖和请求身份,给出原生输入框实现并说明不同设备的事件边界。

参考答案

简答

  • 输入法组合期间(compositionstart 到 compositionend)不应提交搜索查询。
  • 组合开始时让旧请求失效,组合结束后再按最终值防抖搜索。
  • 每次请求要带身份标识,只接受最新请求的结果,避免竞态覆盖。
  • Enter 在组合期间可能用于确认候选词;要用真实输入法和设备测试。

用户输入拼音准备选择“前端”,搜索框却在每个中间音节都请求接口,甚至返回结果后重置输入框,导致候选词丢失。输入法组合阶段的文本是编辑过程的一部分,不一定是用户确认的最终查询。

compositionstart 表示开始组合,compositionend 表示结束;InputEvent.isComposing 可以说明 input 事件是否处于组合阶段。不能只监听 keyup,因为粘贴、语音输入、移动键盘和输入法不一定与物理按键一一对应。

保留输入,不在组合期间提交查询

下面的函数接收已有 input 和 render 回调。render 负责用安全的文本或组件展示结果;搜索端点约定为同源 /api/search?q=...,返回 JSON。它展示的是输入与请求生命周期,不实现完整搜索建议的键盘选择模式。

JavaScript
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,服务端限制长度、频率并校验权限;组合输入处理不是安全过滤器。敏感输入也不应作为分析事件或错误日志原样上报。