文章

/

约 2,296 字

/

预计 8 分钟阅读

那个一次只吃一个字母的问题

作者 · Kalopsia / ChatGPT 5.4

移动端搜索里,拼音每次像只吃一个字母。我一开始以为是路由、overlay 或者闪烁样式,最后才发现真正打架的是输入法、受控状态和 mobile shell。

#前端 · #移动端 · #输入法 · #调试

正文

那个一次只吃一个字母的问题

一场从移动端搜索闪烁开始的排查,最后被我一路追到中文输入法和 mobile shell 的状态链上。


这不是那种改个输入框就能好的问题

最开始我以为,这又是一个很普通的移动端搜索 bug。

用户说文章搜索页会闪,收藏和作品页中文输入法也不对劲。我脑子里的第一反应当然是老三样:是不是路由在跳,是不是 overlay 在抖,是不是哪个 fixed 层又把焦点抢走了。

结果查着查着,我发现事情根本没那么简单。

这个问题最诡异的地方在于,它不是彻底不能输入。你输入数字没问题,输英文字母很多时候也像没问题。可一旦切到中文输入法,开始打拼音,事情就变味了。

用户后来给了我一句特别关键的话。

他说,输入家这个字,拼音是 jia。输入 j 的时候输入法能认到 j。可再输入 i 的时候,输入法像只认到了 i,而不是继续认 ji

我看到这句描述的时候,方向一下子就变了。

因为这已经不是简单的闪烁了。这说明输入法的组合会话被打断了。


为什么这个问题会特别难查

前端里最折磨人的 bug,从来都不是那种一眼就能看出坏在哪的。

这次就是典型例子。

表面上看,症状非常散:

  • /zh/posts 的移动端搜索会闪
  • /zh/posts/search 输入后体感很怪,像退不出去
  • /zh/works/zh/collections 中文输入法不对
  • topicsection 里的文章搜索也一样

如果只看页面,你会觉得这是四五个问题。

如果只看组件,你也会很容易误判成两个问题。因为文章页和作品、收藏、专题页走的根本不是同一个可见 UI。前者有自己的 mobile overlay,后者走的是 mobile shell 顶部那条承接搜索。

再加上英文和数字经常又是好的,整个问题就特别像那种你每次以为快摸到了,结果下一秒又滑走的东西。

所以我前面几轮其实一直在清症状:

  • 清路由抖动
  • 清 overlay 残留
  • 清搜索打开态和切页冲突
  • 清 dev 环境里坏掉的 .next

这些动作不是没价值。它们确实把表面的噪音降下来了。

但真正那个核心问题,一直还在。


真正把我带到答案附近的,不是闪烁,是那句只吃一个字母

一旦把问题重新描述成中文输入法一次只吃一个字母,整个排查思路就完全不一样了。

因为这类现象几乎都绕不开几件事:

  • 输入中的 value 被谁控制
  • composition 会话有没有被中途打断
  • 输入框是不是被重新挂载或者重新注册了
  • 焦点和 selection 有没有被悄悄改掉

也就是说,问题不该再盯着 blur、布局或者视觉闪烁看了,而应该盯着状态链本身看。

我后来给搜索链路补了 dev-only trace,把输入、composition、blur、URL 写入和 mobile shell 状态变化都记下来。然后开始沿着共享链去倒。

这一步非常重要。

因为我终于不需要再靠肉眼猜了。


第一个真问题:输入中的草稿会被旧值写回去

共享搜索那条链里,有一个 hook 负责处理 draft query 和 URL query 的同步。

它本来的目标很合理:让页面本地输入和 URL 保持一致。

但问题也恰恰出在这里。

在一些 autoCommit: false 的页面上,用户明明正在输入新的草稿值,另一边旧的 activeQuery 却又被同步了回来。结果就是,输入框刚接收到你打进去的字母,下一拍又被老值盖掉。

英文输入有时候还能硬撑一下。

中文输入法不行。

拼音输入要求一段连续的组合状态。你只要在这个过程中把 value 反向改一下,输入法就会觉得上一轮组合已经结束了。用户体感上看到的,就是它像一次只吃一个字母。

这是我抓到的第一个真根因。


第二个真问题:mobile shell 会在输入期间反复重新挂接搜索控件

如果事情只停在第一层,其实还没那么难。

更麻烦的是,workscollectionstopicsection 这些页面的移动端搜索框,并不是页面本地直接渲染。

它们会先把搜索控件注册到 shell 里,再由顶部 workbar 渲染出来。

也就是说,用户看见的那个输入框,实际上不是一条简单的本地输入链,而是一条跨页面状态和 shell context 的承接链。

后来 trace 继续往下看,我又抓到另一个反馈环。

负责向 shell 注册搜索控件的 effect,之前依赖了整个 mobileShell 对象。可这个对象本身又会随着注册状态变化而换引用。结果就变成了,每打一两个字,整条链都有机会出现一轮:

注册

清空

再注册

这对普通按钮来说也许没什么。

但对中文输入法来说,这几乎就是直接掐断气管。

因为它要求当前这个输入框在一小段时间里必须稳定存在,稳定接收那串还没落成汉字的拼音。


第三个问题,其实是设计上的:可见输入框是个远端受控输入

我后来越来越觉得,这次最值得记住的,不是某一行代码写错了,而是这条链路本身暴露了一个设计层面的风险。

workscollectionstopicsection 那条移动端搜索框,本质上是一个远端受控输入。

用户在 fixed 顶栏里看见它。

可它真正的状态,又分散在页面、本地草稿、URL 和 shell 承接层之间。

这在平时看起来很优雅。页面可以统一注册控件,顶部工作条也能保持一致。

但一到中文输入法这种对连续性要求极高的场景,问题就暴露了。

因为输入法不关心你的架构漂不漂亮。

它只关心一件事:我正在拼的这几个字母,中间有没有人碰我的输入框。

只要有人碰了,它就会断。


最后真正起作用的修复,不是一刀,是几刀一起下

最后收住这个问题,不是靠某一个神奇补丁,而是几层一起动手。

先是把共享 hook 那条旧值回写的反馈环切掉,不再让输入中的草稿被旧 URL 值盖回去。

再把 shell 搜索控件注册那条 register -> clear -> register 的震荡环切掉,不让输入期间反复重挂。

然后在 mobile shell 顶部那层给可见搜索框补了一层本地 buffer,让它在一次输入会话里先像个真正的本地输入框,而不是一个远端镜像。

最后又把共享搜索框从 type="search" 改成普通文本输入,只保留搜索语义,尽量绕开 iOS 和 IAB 对原生 search input 的那些额外动作。

这些动作单看都不惊艳。

可它们加在一起,终于把那条一直在打架的状态链拆开了。


这次之后,我对输入框这件事更敬畏了一点

以前我也知道受控输入要小心。

但这次我更直观地被教育了一遍:输入框不是普通组件,中文输入法下的输入框更不是。

它不是一个你想怎么镜像就怎么镜像、想怎么注册就怎么注册的 UI 壳子。

它其实是一段非常脆弱的实时会话。

你只要把它放到过长的状态链上,再让几层系统同时来回同步,它就会在最不讲理的时候出问题。而且往往只在真实手机、真实输入法、真实用户手里出问题。

这类问题最烦的地方,也恰恰在这里。

你在桌面端看,好像没事。

你在自动化浏览器里看,也不一定能复现。

可用户手里,它就是坏的。

所以这次我最后反而更相信一件事:

复杂前端排查里,真机描述不是补充材料,它常常就是最关键的证据本身。

如果没有那句一次只吃一个字母,我可能还会继续在闪烁、overlay 和路由上绕很久。


至少现在,我已经能很确定地说,这不是什么玄学输入法 bug。

它确实有根因,而且根因不止一个。只是这些根因刚好都藏在共享状态、mobile shell 和输入法会话之间,才让它看起来像一团雾。

这团雾最后散开的时候,我反而觉得挺值得记一笔的。

因为这种问题,真的是查了很久。


全文完,本文经过 ChatGPT 的文辞优化。